Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix parsing of srcset without whitespaces #11

Merged
merged 4 commits into from
Apr 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 43 additions & 26 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
'use strict';

const integerRegex = /^-?\d+$/;
/**
This regex represents a loose rule of an “image candidate string”.

@see https://html.spec.whatwg.org/multipage/images.html#srcset-attribute

An “image candidate string” roughly consists of the following:
1. Zero or more whitespace characters.
2. A non-empty URL that does not start or end with `,`.
3. Zero or more whitespace characters.
4. An optional “descriptor” that starts with a whitespace character.
5. Zero or more whitespace characters.
6. Each image candidate string is separated by a `,`.

We intentionally implement a loose rule here so that we can perform more aggressive error handling and reporting in the below code.
*/
const imageCandidateRegex = /\s*([^,]\S*[^,](?:\s+[^,]+)?)\s*(?:,|$)/;

function deepUnique(array) {
return array.sort().filter((element, index) => {
Expand All @@ -10,46 +25,48 @@ function deepUnique(array) {

exports.parse = string => {
return deepUnique(
string.split(/,\s+/).map(part => {
const result = {};

part
.trim()
.split(/\s+/)
.forEach((element, index) => {
if (index === 0) {
result.url = element;
return;
}
string.split(imageCandidateRegex)
.filter((part, index) => index % 2 === 1)
.map(part => {
const [url, ...elements] = part.trim().split(/\s+/);

const result = {url};

const value = element.slice(0, -1);
const postfix = element[element.length - 1];
const integerValue = Number.parseInt(value, 10);
const floatValue = Number.parseFloat(value);
const descriptors = elements.length > 0 ? elements : ['1x'];

if (postfix === 'w' && integerRegex.test(value)) {
if (integerValue <= 0) {
for (const descriptor of descriptors) {
const postfix = descriptor[descriptor.length - 1];
const value = Number.parseFloat(descriptor.slice(0, -1));

if (Number.isNaN(value)) {
throw new TypeError(`${descriptor.slice(0, -1)} is not a valid number`);
}

if (postfix === 'w') {
if (value <= 0) {
throw new Error('Width descriptor must be greater than zero');
} else if (!Number.isInteger(value)) {
throw new TypeError('Width descriptor must be an integer');
}

result.width = integerValue;
} else if (postfix === 'x' && !Number.isNaN(floatValue)) {
if (floatValue <= 0) {
result.width = value;
} else if (postfix === 'x') {
if (value <= 0) {
throw new Error('Pixel density descriptor must be greater than zero');
}

result.density = floatValue;
result.density = value;
} else {
throw new Error(`Invalid srcset descriptor: ${element}`);
throw new Error(`Invalid srcset descriptor: ${descriptor}`);
}

if (result.width && result.density) {
throw new Error('Image candidate string cannot have both width descriptor and pixel density descriptor');
}
});
}

return result;
})
return result;
})
);
};

Expand Down
23 changes: 21 additions & 2 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import test from 'ava';
import srcset from '.';

test('.parse() should parse srcset', t => {
const fixture = ' banner-HD.jpeg 2x, banner-HD.jpeg 2x, banner-HD.jpeg 2x, banner-phone.jpeg 100w, http://site.com/image.jpg?foo=bar,lorem 1x ';
const fixture = ' banner-HD.jpeg 2x, banner-HD.jpeg 2x, banner-HD.jpeg 2x, banner-phone.jpeg 100w, http://site.com/image.jpg?foo=bar,lorem 3x ,banner.jpeg ';

t.deepEqual(srcset.parse(fixture), [
{url: 'banner-HD.jpeg', density: 2},
{url: 'banner-phone.jpeg', width: 100},
{url: 'http://site.com/image.jpg?foo=bar,lorem', density: 1}
{url: 'http://site.com/image.jpg?foo=bar,lorem', density: 3},
{url: 'banner.jpeg', density: 1}
]);
});

Expand Down Expand Up @@ -42,6 +43,24 @@ test('.parse() should not parse invalid srcset', t => {
t.throws(() => {
srcset.parse('banner-phone-HD.jpeg -2x');
});

t.throws(() => {
srcset.parse('banner-phone-HD.jpeg 100.5w');
});

t.throws(() => {
srcset.parse('banner-phone-HD.jpeg xxx');
});
});

test('.parse() should parse srcset separated without whitespaces', t => {
const fixture = 'banner-HD.jpeg 2x,banner-HD.jpeg 2x,banner-HD.jpeg 2x,banner-phone.jpeg 100w,http://site.com/image.jpg?foo=100w,lorem 1x';

t.deepEqual(srcset.parse(fixture), [
{url: 'banner-HD.jpeg', density: 2},
{url: 'banner-phone.jpeg', width: 100},
{url: 'http://site.com/image.jpg?foo=100w,lorem', density: 1}
]);
});

test('.stringify() should stringify srcset', t => {
Expand Down