fix: Transform relative URLs in srcset attributes to absolute URLs (#190)

pull/230/head
Toufic Mouallem 5 years ago committed by Adam Pash
parent 15a5229998
commit bb6ad2682b

@ -14,8 +14,37 @@ function absolutize($, rootUrl, attr, $content) {
});
}
function absolutizeSet($, rootUrl, $content) {
$('[srcset]', $content).each((_, node) => {
const attrs = getAttrs(node);
const urlSet = attrs.srcset;
if (urlSet) {
// a comma should be considered part of the candidate URL unless preceded by a descriptor
// descriptors can only contain positive numbers followed immediately by either 'w' or 'x'
// space characters inside the URL should be encoded (%20 or +)
const candidates = urlSet.match(
/(?:\s*)(\S+(?:\s*[\d.]+[wx])?)(?:\s*,\s*)?/g
);
const absoluteCandidates = candidates.map(candidate => {
// a candidate URL cannot start or end with a comma
// descriptors are separated from the URLs by unescaped whitespace
const parts = candidate
.trim()
.replace(/,$/, '')
.split(/\s+/);
parts[0] = URL.resolve(rootUrl, parts[0]);
return parts.join(' ');
});
const absoluteUrlSet = [...new Set(absoluteCandidates)].join(', ');
setAttr(node, 'srcset', absoluteUrlSet);
}
});
}
export default function makeLinksAbsolute($content, $, url) {
['href', 'src'].forEach(attr => absolutize($, url, attr, $content));
absolutizeSet($, url, $content);
return $content;
}

@ -56,4 +56,169 @@ describe('makeLinksAbsolute($)', () => {
assert.equal(result, '<div><img src="http://example.com/#foo"></div>');
});
describe('makes relative srcsets absolute', () => {
/**
* The spec for srcset requires a space character following the comma separating character
* when a candidate that doesn't contain a descriptor needs to be followed by another candidate,
* otherwise, that candidate would be treated as part of the succeeding candidate filename.
* This requirement allows for URLs that contain comma character but no un-encoded spaces.
*
* srcset candidates that all contain descriptors, can safely be separated by commas
* without the need for a space character.
*/
it('handles invalid srcsets as per their invalid implementation', () => {
/**
* The following srcset values would be interpreted (by browsers, and pasrsed similarly)
* as follows:
* (1) the first source tag - one candidate:
* assets/images/rhythm/076.jpg,assets/images/rhythm/076@2x.jpg 2x
* (2) the second source tag - two candidates:
* assets/images/rhythm/120@2x.jpg 2x
* assets/images/rhythm/120.jpg,assets/images/rhythm/120@3x.jpg 3x
* (3) the third source tag - two candidates:
* assets/images/rhythm/240.jpg,assets/images/rhythm/240@2x.jpg 2x,
* assets/images/rhythm/240@3x.jpg 3x
*/
const html = `<div>
<picture>
<source srcset="assets/images/rhythm/076.jpg,assets/images/rhythm/076@2x.jpg 2x" media="(max-width: 450px)">
<source srcset="assets/images/rhythm/120@2x.jpg 2x, assets/images/rhythm/120.jpg,assets/images/rhythm/120@3x.jpg 3x" media="(max-width: 900px)">
<source srcset="assets/images/rhythm/240.jpg,assets/images/rhythm/240@2x.jpg 2x,assets/images/rhythm/240@3x.jpg 3x" media="(min-width: 901px)">
<img src="assets/images/rhythm/120.jpg" alt="Vertical and horizontal rhythm">
</picture>
</div>`;
const $ = cheerio.load(html);
const $content = $('*').first();
const result = $.html(
makeLinksAbsolute($content, $, 'http://example.com')
);
assert.equal(
result,
`<div>
<picture>
<source srcset="http://example.com/assets/images/rhythm/076.jpg,assets/images/rhythm/076@2x.jpg 2x" media="(max-width: 450px)">
<source srcset="http://example.com/assets/images/rhythm/120@2x.jpg 2x, http://example.com/assets/images/rhythm/120.jpg,assets/images/rhythm/120@3x.jpg 3x" media="(max-width: 900px)">
<source srcset="http://example.com/assets/images/rhythm/240.jpg,assets/images/rhythm/240@2x.jpg 2x, http://example.com/assets/images/rhythm/240@3x.jpg 3x" media="(min-width: 901px)">
<img src="http://example.com/assets/images/rhythm/120.jpg" alt="Vertical and horizontal rhythm">
</picture>
</div>`
);
});
it('handles comma separated (with whitespace) srcset files with device-pixel-ratio descriptors', () => {
const html = `<div>
<picture>
<source srcset="assets/images/rhythm/076.jpg 2x, assets/images/rhythm/076.jpg" media="(max-width: 450px)">
<source srcset="assets/images/rhythm/076@2x.jpg 2x, assets/images/rhythm/076.jpg">
<img src="http://example.com/assets/images/rhythm/076.jpg" alt="Vertical and horizontal rhythm">
</picture>
</div>`;
const $ = cheerio.load(html);
const $content = $('*').first();
const result = $.html(
makeLinksAbsolute($content, $, 'http://example.com')
);
assert.equal(
result,
`<div>
<picture>
<source srcset="http://example.com/assets/images/rhythm/076.jpg 2x, http://example.com/assets/images/rhythm/076.jpg" media="(max-width: 450px)">
<source srcset="http://example.com/assets/images/rhythm/076@2x.jpg 2x, http://example.com/assets/images/rhythm/076.jpg">
<img src="http://example.com/assets/images/rhythm/076.jpg" alt="Vertical and horizontal rhythm">
</picture>
</div>`
);
});
it('handles comma separated (without whitespace) srcset files with device-pixel-ratio descriptors', () => {
const html = `<div>
<picture>
<source srcset="assets/images/rhythm/076.jpg 2x,assets/images/rhythm/076.jpg" media="(max-width: 450px)">
<source srcset="assets/images/rhythm/076@2x.jpg 2x,assets/images/rhythm/076.jpg">
<img src="http://example.com/assets/images/rhythm/076.jpg" alt="Vertical and horizontal rhythm">
</picture>
</div>`;
const $ = cheerio.load(html);
const $content = $('*').first();
const result = $.html(
makeLinksAbsolute($content, $, 'http://example.com')
);
assert.equal(
result,
`<div>
<picture>
<source srcset="http://example.com/assets/images/rhythm/076.jpg 2x, http://example.com/assets/images/rhythm/076.jpg" media="(max-width: 450px)">
<source srcset="http://example.com/assets/images/rhythm/076@2x.jpg 2x, http://example.com/assets/images/rhythm/076.jpg">
<img src="http://example.com/assets/images/rhythm/076.jpg" alt="Vertical and horizontal rhythm">
</picture>
</div>`
);
});
it('handles comma separated (with whitespace) srcset files with width descriptors', () => {
const html = `<div>
<img srcset="elva-fairy-320w.jpg 320w, elva-fairy-480w.jpg 480w, elva-fairy-800w.jpg 800w" sizes="(max-width: 320px) 280px, (max-width: 480px) 440px, 800px" src="elva-fairy-800w.jpg" alt="Elva dressed as a fairy">
</div>`;
const $ = cheerio.load(html);
const $content = $('*').first();
const result = $.html(
makeLinksAbsolute($content, $, 'http://example.com')
);
assert.equal(
result,
`<div>
<img srcset="http://example.com/elva-fairy-320w.jpg 320w, http://example.com/elva-fairy-480w.jpg 480w, http://example.com/elva-fairy-800w.jpg 800w" sizes="(max-width: 320px) 280px, (max-width: 480px) 440px, 800px" src="http://example.com/elva-fairy-800w.jpg" alt="Elva dressed as a fairy">
</div>`
);
});
it('handles multiline comma separated srcset files with width descriptors', () => {
const html = `<div>
<img srcset="elva-fairy-320w.jpg 320w,
elva-fairy-480w.jpg 480w,
elva-fairy-800w.jpg 800w" sizes="(max-width: 320px) 280px, (max-width: 480px) 440px, 800px" src="elva-fairy-800w.jpg" alt="Elva dressed as a fairy">
</div>`;
const $ = cheerio.load(html);
const $content = $('*').first();
const result = $.html(
makeLinksAbsolute($content, $, 'http://example.com')
);
assert.equal(
result,
`<div>
<img srcset="http://example.com/elva-fairy-320w.jpg 320w, http://example.com/elva-fairy-480w.jpg 480w, http://example.com/elva-fairy-800w.jpg 800w" sizes="(max-width: 320px) 280px, (max-width: 480px) 440px, 800px" src="http://example.com/elva-fairy-800w.jpg" alt="Elva dressed as a fairy">
</div>`
);
});
it('handles URLs that contain a comma', () => {
const html = `<div>
<picture><source media="(min-width: 768px)" srcset="cartoons/5bbfca021e40b62d6cc418ea/master/w_280,c_limit/181022_a22232.jpg, cartoons/5bbfca021e40b62d6cc418ea/master/w_560,c_limit/181022_a22232.jpg 2x"/><source srcset="cartoons/5bbfca021e40b62d6cc418ea/master/w_727,c_limit/181022_a22232.jpg, cartoons/5bbfca021e40b62d6cc418ea/master/w_1454,c_limit/181022_a22232.jpg 2x"/><img src="cartoons/5bbfca021e40b62d6cc418ea/master/w_727,c_limit/181022_a22232.jpg" /></picture>
</div>`;
const $ = cheerio.load(html);
const $content = $('*').first();
const result = $.html(
makeLinksAbsolute($content, $, 'https://media.newyorker.com/')
);
assert.equal(
result,
`<div>
<picture><source media="(min-width: 768px)" srcset="https://media.newyorker.com/cartoons/5bbfca021e40b62d6cc418ea/master/w_280,c_limit/181022_a22232.jpg, https://media.newyorker.com/cartoons/5bbfca021e40b62d6cc418ea/master/w_560,c_limit/181022_a22232.jpg 2x"><source srcset="https://media.newyorker.com/cartoons/5bbfca021e40b62d6cc418ea/master/w_727,c_limit/181022_a22232.jpg, https://media.newyorker.com/cartoons/5bbfca021e40b62d6cc418ea/master/w_1454,c_limit/181022_a22232.jpg 2x"><img src="https://media.newyorker.com/cartoons/5bbfca021e40b62d6cc418ea/master/w_727,c_limit/181022_a22232.jpg"></picture>
</div>`
);
});
});
});

Loading…
Cancel
Save