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)',
''
@@ -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', () => {