187 lines
4.4 KiB
JavaScript
187 lines
4.4 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
/**
|
||
|
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*(?:,|$)/;
|
||
|
|
||
|
const duplicateDescriptorCheck = (allDescriptors, value, postfix) => {
|
||
|
allDescriptors[postfix] = allDescriptors[postfix] || {};
|
||
|
if (allDescriptors[postfix][value]) {
|
||
|
throw new Error(`No more than one image candidate is allowed for a given descriptor: ${value}${postfix}`);
|
||
|
}
|
||
|
|
||
|
allDescriptors[postfix][value] = true;
|
||
|
};
|
||
|
|
||
|
const fallbackDescriptorDuplicateCheck = allDescriptors => {
|
||
|
if (allDescriptors.fallback) {
|
||
|
throw new Error('Only one fallback image candidate is allowed');
|
||
|
}
|
||
|
|
||
|
if (allDescriptors.x['1']) {
|
||
|
throw new Error('A fallback image is equivalent to a 1x descriptor, providing both is invalid.');
|
||
|
}
|
||
|
|
||
|
allDescriptors.fallback = true;
|
||
|
};
|
||
|
|
||
|
const descriptorCountCheck = (allDescriptors, currentDescriptors) => {
|
||
|
if (currentDescriptors.length === 0) {
|
||
|
fallbackDescriptorDuplicateCheck(allDescriptors);
|
||
|
} else if (currentDescriptors.length > 1) {
|
||
|
throw new Error(`Image candidate may have no more than one descriptor, found ${currentDescriptors.length}: ${currentDescriptors.join(' ')}`);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const validDescriptorCheck = (value, postfix, descriptor) => {
|
||
|
if (Number.isNaN(value)) {
|
||
|
throw new TypeError(`${descriptor || value} is not a valid number`);
|
||
|
}
|
||
|
|
||
|
switch (postfix) {
|
||
|
case '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');
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
case 'x': {
|
||
|
if (value <= 0) {
|
||
|
throw new Error('Pixel density descriptor must be greater than zero');
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
case 'h': {
|
||
|
throw new Error('Height descriptor is no longer allowed');
|
||
|
}
|
||
|
|
||
|
default: {
|
||
|
throw new Error(`Invalid srcset descriptor: ${descriptor}`);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
exports.parse = (string, {strict = false} = {}) => {
|
||
|
const allDescriptors = strict ? {} : undefined;
|
||
|
return string.split(imageCandidateRegex)
|
||
|
.filter((part, index) => index % 2 === 1)
|
||
|
.map(part => {
|
||
|
const [url, ...descriptors] = part.trim().split(/\s+/);
|
||
|
|
||
|
const result = {url};
|
||
|
|
||
|
if (strict) {
|
||
|
descriptorCountCheck(allDescriptors, descriptors);
|
||
|
}
|
||
|
|
||
|
for (const descriptor of descriptors) {
|
||
|
const postfix = descriptor[descriptor.length - 1];
|
||
|
const value = Number.parseFloat(descriptor.slice(0, -1));
|
||
|
|
||
|
if (strict) {
|
||
|
validDescriptorCheck(value, postfix, descriptor);
|
||
|
duplicateDescriptorCheck(allDescriptors, value, postfix);
|
||
|
}
|
||
|
|
||
|
switch (postfix) {
|
||
|
case 'w': {
|
||
|
result.width = value;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
case 'h': {
|
||
|
result.height = value;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
case 'x': {
|
||
|
result.density = value;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// No default
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
const knownDescriptors = new Set(['width', 'height', 'density']);
|
||
|
|
||
|
exports.stringify = (array, {strict = false} = {}) => {
|
||
|
const allDescriptors = strict ? {} : null;
|
||
|
return array.map(element => {
|
||
|
if (!element.url) {
|
||
|
if (strict) {
|
||
|
throw new Error('URL is required');
|
||
|
}
|
||
|
|
||
|
return '';
|
||
|
}
|
||
|
|
||
|
const descriptorKeys = Object.keys(element).filter(key => knownDescriptors.has(key));
|
||
|
|
||
|
if (strict) {
|
||
|
descriptorCountCheck(allDescriptors, descriptorKeys);
|
||
|
}
|
||
|
|
||
|
const result = [element.url];
|
||
|
|
||
|
for (const descriptorKey of descriptorKeys) {
|
||
|
const value = element[descriptorKey];
|
||
|
let postfix;
|
||
|
switch (descriptorKey) {
|
||
|
case 'width': {
|
||
|
postfix = 'w';
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
case 'height': {
|
||
|
postfix = 'h';
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
case 'density': {
|
||
|
postfix = 'x';
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
// No default
|
||
|
}
|
||
|
|
||
|
const descriptor = `${value}${postfix}`;
|
||
|
|
||
|
if (strict) {
|
||
|
validDescriptorCheck(value, postfix);
|
||
|
duplicateDescriptorCheck(allDescriptors, value, postfix);
|
||
|
}
|
||
|
|
||
|
result.push(descriptor);
|
||
|
}
|
||
|
|
||
|
return result.join(' ');
|
||
|
}).join(', ');
|
||
|
};
|