diff --git a/config/scripts/check-readme-local-links.mjs b/config/scripts/check-readme-local-links.mjs
index 4781adcc5da..5d16461b236 100644
--- a/config/scripts/check-readme-local-links.mjs
+++ b/config/scripts/check-readme-local-links.mjs
@@ -7,7 +7,7 @@ import { pathToFileURL } from 'node:url'
// Docs-only CI skips some media owners; check index paths because GitHub cannot render uncommitted docs-site/feature-wall assets.
const TRANSLATED_README_DIR = path.join('docs', 'readme')
const EXTERNAL_TARGET = /^(?:[a-z][a-z0-9+.-]*:|#|\/\/)/i
-const HTML_ATTRIBUTE = /\b(?:src|srcset|href)\s*=\s*(?:"([^"]*)"|'([^']*)')/g
+const HTML_ATTRIBUTE = /\b(src|srcset|href)\s*=\s*(?:"([^"]*)"|'([^']*)')/g
const MARKDOWN_LINK = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g
function readmeFiles(root) {
@@ -32,10 +32,14 @@ function trackedFiles(root, candidates) {
}
function* localTargets(markdown) {
- for (const match of markdown.matchAll(HTML_ATTRIBUTE)) {
- // srcset lists candidates; each begins with its URL.
- for (const candidate of (match[1] ?? match[2]).split(',')) {
- const target = candidate.trim().split(/\s+/)[0]
+ 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 === 'srcset'
+ ? value.split(',').map((candidate) => candidate.trim().split(/\s+/)[0])
+ : [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 bbdbceb0529..54e5f5956b6 100644
--- a/config/scripts/check-readme-local-links.test.mjs
+++ b/config/scripts/check-readme-local-links.test.mjs
@@ -44,7 +44,9 @@ const validReadmes = {
"
",
'
',
'[Contributing](.github/CONTRIBUTING.md) [Docs](https://example.com/docs) [Top](#top)',
- ''
+ '',
+ // src/href hold one URL; only srcset is a comma-separated list.
+ '
Docs'
].join('\n'),
'docs/readme/README.ja.md': [
'
',
@@ -56,6 +58,7 @@ const validReadmes = {
'resources/onboarding/feature-wall/tile-01.poster.jpg': 'jpg',
'docs/site/public/docs/tab-split.gif': 'gif',
'docs/assets/hero image.jpg': 'jpg',
+ 'docs/assets/hero image,wide.jpg': 'jpg',
'.github/CONTRIBUTING.md': 'contributing',
LICENSE: 'mit'
}
@@ -77,6 +80,38 @@ describe('README local link check', () => {
expect(findBrokenReadmeLinks(makeFixture(validReadmes))).toEqual([])
})
+ it('rejects a missing full URL even when its split fragments are tracked', () => {
+ const root = makeFixture({
+ ...validReadmes,
+ 'README.md': '
',
+ 'docs/assets/missing': 'fragment',
+ 'wide.jpg': 'fragment'
+ })
+
+ expect(findBrokenReadmeLinks(root)).toEqual([
+ {
+ readme: 'README.md',
+ target: 'docs/assets/missing image,wide.jpg',
+ resolved: 'docs/assets/missing image,wide.jpg'
+ }
+ ])
+ })
+
+ it('continues checking each srcset candidate without its descriptor', () => {
+ const root = makeFixture({
+ ...validReadmes,
+ 'README.md': ''
+ })
+
+ expect(findBrokenReadmeLinks(root)).toEqual([
+ {
+ readme: 'README.md',
+ target: 'docs/assets/missing.png',
+ resolved: 'docs/assets/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'))