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 <neil@stably.ai>
This commit is contained in:
Blue Daydreaming
2026-11-01 04:15:42 -08:00
committed by GitHub
co-authored by Neil
parent 0dcc811e07
commit 0e25b0e22b
2 changed files with 79 additions and 4 deletions
+37 -4
View File
@@ -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
@@ -126,6 +126,48 @@ describe('README local link check', () => {
])
})
it('preserves commas inside srcset URLs', () => {
const root = makeFixture({
...validReadmes,
'README.md': '<source SrcSet=" ,,\t docs/Hero,wide.png 1x,, \n docs/Hero,tall.png,, ">',
'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': '<source srcset="docs/Missing,wide.png 1x">',
'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': '<source srcset="data:image/png;base64,AAAA 1x, docs/missing.png 2x">'
})
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': '<source srcset="resources/build/icon.png future(1,2), docs/missing.png 2x">'
})
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'))