fix(agents): validate srcdoc references and empty srcset

This commit is contained in:
4gray committed 2026-09-21 02:56:53 +02:00
1 parent a7150d1e72
commit 3dbd522d70
4 files changed
+59 -3

No files matched your search

+2
View File
@@ -60,6 +60,8 @@ as Markdown links, including decoded attributes and fragment validation.
URL attributes remove ASCII tabs/newlines throughout and discard surrounding
ASCII control/space characters before resolution.
Iframe/embed sources and object data attributes are document references and retain Markdown-target anchor checks.
Inline iframe srcdoc documents are traversed too, with their own fragment anchors.
Explicit srcset attributes must contain at least one parsed candidate.
Image and media references require nonempty targets that resolve to files, not directories.
Image references check file existence without interpreting image fragments as
Markdown headings; document links keep anchor checks even when sharing a target.
+19 -2
View File
@@ -92,12 +92,29 @@ function htmlNavigation(html, inspect = () => {}) {
node.tagName
),
});
if (node.tagName === 'iframe' && attribute.name === 'srcdoc') {
const embedded = htmlNavigation(attribute.value);
for (const reference of embedded.references)
references.push(
reference.target.startsWith('#') &&
!reference.embeddedAnchors
? {
...reference,
embeddedAnchors: embedded.anchors,
}
: reference
);
}
if (
['img', 'source'].includes(node.tagName) &&
attribute.name === 'srcset'
)
for (const candidate of parseSrcset(attribute.value))
) {
const candidates = parseSrcset(attribute.value);
if (!candidates.length)
references.push({ target: '', image: true });
for (const candidate of candidates)
references.push({ target: candidate.url, image: true });
}
}
for (const child of node.childNodes ?? []) visit(child);
}
+5 -1
View File
@@ -36,7 +36,7 @@ function within(root, path) {
async function validateReference(
rootDir,
source,
{ target, literal, image, unresolvedReference }
{ target, literal, image, unresolvedReference, embeddedAnchors }
) {
if (unresolvedReference !== undefined)
return `${source}: unresolved Markdown reference "${unresolvedReference}"`;
@@ -53,6 +53,10 @@ async function validateReference(
} catch {
return `${source}: malformed local link: ${target}`;
}
if (embeddedAnchors && !path && !image)
return !anchor || embeddedAnchors.includes(anchor)
? undefined
: `${source}: missing anchor "${anchor}" in iframe srcdoc`;
const absolute = path
? resolve(literal ? rootDir : dirname(resolve(rootDir, source)), path)
: resolve(rootDir, source);
@@ -1399,3 +1399,36 @@ for (const url of [
);
});
}
for (const value of ['', ' ']) {
test(`empty srcset is rejected: ${JSON.stringify(value)}`, async (t) => {
assert.ok(
(await diagnostics(t, { 'AGENTS.md': `<img srcset="${value}">` }))
.length > 0
);
});
}
for (const content of [
"<a href='docs/missing.md'>Guide</a>",
"<img src='missing.png'>",
"<a href='#missing'>Guide</a>",
]) {
test(`srcdoc references are validated: ${content}`, async (t) => {
assert.ok(
(
await diagnostics(t, {
'AGENTS.md': `<iframe srcdoc="${content}"></iframe>`,
})
).length > 0
);
});
}
test('srcdoc anchors are scoped and templates inert', async (t) => {
assert.deepEqual(
await diagnostics(t, {
'AGENTS.md':
"<iframe srcdoc=\"<p id=local>Hi</p><a href='#local'>Go</a><template><img src='missing.png'></template>\"></iframe>",
}),
[]
);
});