From 0b604956ecc7c5c1a5d798650c6600db2b9ca1ef Mon Sep 17 00:00:00 2001 From: Blue Daydreaming <26271654+blueDaydreaming725@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:07:46 +0800 Subject: [PATCH] fix(scripts): preserve complete HTML src/href URLs in README link checks (#34817) Only srcset is a comma-separated candidate list; src and href hold one URL. Splitting them invented missing targets for paths containing commas or spaces and could pass a missing target whose fragments were tracked. Fixes #34815 --- config/scripts/check-readme-local-links.mjs | 14 ++++--- .../scripts/check-readme-local-links.test.mjs | 37 ++++++++++++++++++- 2 files changed, 45 insertions(+), 6 deletions(-) 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)', - '![hero](docs/assets/hero%20image.jpg "Hero")' + '![hero](docs/assets/hero%20image.jpg "Hero")', + // 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'))