From 0e25b0e22bf331fda82e470585df58eeef848d6b Mon Sep 17 00:00:00 2001 From: Blue Daydreaming <26271654+blueDaydreaming725@users.noreply.github.com> Date: Sun, 1 Nov 2026 20:15:42 +0800 Subject: [PATCH] fix(readme-links): preserve commas in srcset URLs (#36957) Tokenize srcset like the HTML parser: read each URL up to ASCII whitespace, strip trailing separator commas, and skip descriptors (including parenthesized commas) before the next candidate. Fixes #36955 Co-authored-by: Neil --- config/scripts/check-readme-local-links.mjs | 41 ++++++++++++++++-- .../scripts/check-readme-local-links.test.mjs | 42 +++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/config/scripts/check-readme-local-links.mjs b/config/scripts/check-readme-local-links.mjs index 70ff58e2d3a..51a130bb432 100644 --- a/config/scripts/check-readme-local-links.mjs +++ b/config/scripts/check-readme-local-links.mjs @@ -31,14 +31,47 @@ function trackedFiles(root, candidates) { return new Set(stdout.split('\0').filter(Boolean)) } +function* srcsetTargets(value) { + let position = 0 + while (position < value.length) { + while (position < value.length && /[\t\n\f\r ,]/.test(value[position])) { + position++ + } + if (position === value.length) { + return + } + const start = position + while (position < value.length && !/[\t\n\f\r ]/.test(value[position])) { + position++ + } + let end = position + while (value[end - 1] === ',') { + end-- + } + // HTML URL tokens can contain commas; trailing commas end a URL-only candidate. + yield value.slice(start, end) + if (end < position) { + continue + } + let inParentheses = false + while (position < value.length) { + const character = value[position++] + if (character === '(') { + inParentheses = true + } else if (character === ')') { + inParentheses = false + } else if (character === ',' && !inParentheses) { + break + } + } + } +} + function* localTargets(markdown) { for (const [, attribute, doubleQuoted, singleQuoted] of markdown.matchAll(HTML_ATTRIBUTE)) { const value = doubleQuoted ?? singleQuoted // Only srcset lists candidates; each begins with its URL. src/href hold one whole URL. - const targets = - attribute.toLowerCase() === 'srcset' - ? value.split(',').map((candidate) => candidate.trim().split(/\s+/)[0]) - : [value.trim()] + const targets = attribute.toLowerCase() === 'srcset' ? srcsetTargets(value) : [value.trim()] for (const target of targets) { if (target) { yield target diff --git a/config/scripts/check-readme-local-links.test.mjs b/config/scripts/check-readme-local-links.test.mjs index 3f802c8e86b..a347927141b 100644 --- a/config/scripts/check-readme-local-links.test.mjs +++ b/config/scripts/check-readme-local-links.test.mjs @@ -126,6 +126,48 @@ describe('README local link check', () => { ]) }) + it('preserves commas inside srcset URLs', () => { + const root = makeFixture({ + ...validReadmes, + 'README.md': '', + 'docs/Hero,wide.png': 'png', + 'docs/Hero,tall.png': 'png' + }) + + expect(findBrokenReadmeLinks(root)).toEqual([]) + }) + + it('rejects a missing srcset URL even when its comma-separated fragments are tracked', () => { + const root = makeFixture({ + ...validReadmes, + 'README.md': '', + 'docs/Missing': 'fragment', + 'wide.png': 'fragment' + }) + + expect(findBrokenReadmeLinks(root).map((link) => link.target)).toEqual([ + 'docs/Missing,wide.png' + ]) + }) + + it('ignores a srcset data URL while checking the following local candidate', () => { + const root = makeFixture({ + ...validReadmes, + 'README.md': '' + }) + + expect(findBrokenReadmeLinks(root).map((link) => link.target)).toEqual(['docs/missing.png']) + }) + + it('does not treat commas inside srcset descriptors as new candidates', () => { + const root = makeFixture({ + ...validReadmes, + 'README.md': '' + }) + + expect(findBrokenReadmeLinks(root).map((link) => link.target)).toEqual(['docs/missing.png']) + }) + it('checks tracked media outside the detector sparse checkout', () => { const root = makeFixture(validReadmes) const workflow = parse(readFileSync(path.join(projectDir, '.github/workflows/pr.yml'), 'utf8'))