From 45a7af02b712ff6af373ee71f2876619e588c511 Mon Sep 17 00:00:00 2001 From: Neil Date: Sat, 12 Sep 2026 19:30:36 -0700 Subject: [PATCH] fix: accept single-quoted attributes in README local link check The parser only matched double-quoted src/srcset/href, so was skipped and the guard passed a README that GitHub renders with a broken image. Regression test fails without the parser change. --- config/scripts/check-readme-local-links.mjs | 4 ++-- .../scripts/check-readme-local-links.test.mjs | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/config/scripts/check-readme-local-links.mjs b/config/scripts/check-readme-local-links.mjs index 7ac6f609320..723d7e3ccce 100644 --- a/config/scripts/check-readme-local-links.mjs +++ b/config/scripts/check-readme-local-links.mjs @@ -10,7 +10,7 @@ import { pathToFileURL } from 'node:url' // git index rather than the working tree. 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) { @@ -38,7 +38,7 @@ function trackedFiles(root, candidates) { function* localTargets(markdown) { for (const match of markdown.matchAll(HTML_ATTRIBUTE)) { // Why: srcset is a candidate list ("a.gif 1x, b.gif 2x"); each entry starts with a URL. - for (const candidate of match[1].split(',')) { + for (const candidate of (match[1] ?? match[2]).split(',')) { const target = candidate.trim().split(/\s+/)[0] 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 80edccaa5d8..2f7cfc76c92 100644 --- a/config/scripts/check-readme-local-links.test.mjs +++ b/config/scripts/check-readme-local-links.test.mjs @@ -39,6 +39,7 @@ const validReadmes = { '', '', '日本語', + "", '', '[Contributing](.github/CONTRIBUTING.md) [Docs](https://example.com/docs) [Top](#top)', '![hero](docs/assets/hero%20image.jpg "Hero")' @@ -119,6 +120,23 @@ describe('README local link check', () => { ]) }) + // Why: a single-quoted attribute is valid HTML and GitHub renders it, so a parser + // that only reads double quotes would pass a README with a broken image. + it('reports a missing target in a single-quoted attribute', () => { + const files = { + ...validReadmes, + 'README.md': `${validReadmes['README.md']}\n` + } + + expect(findBrokenReadmeLinks(makeFixture(files))).toEqual([ + { + readme: 'README.md', + target: 'docs/assets/missing.gif', + resolved: 'docs/assets/missing.gif' + } + ]) + }) + // Why the ungated job: static_analysis is skipped for docs-only diffs, which is // exactly the kind of PR that deletes a docs-site GIF the README embeds. it('runs on every PR through the ungated guard job and in the lint script', () => {