mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(agents): validate visible HTML and image source sets
This commit is contained in:
1 parent
e028712e61
commit
d6a35cfa31
6 files changed
+119
-9
No files matched your search
@@ -53,7 +53,11 @@ as Markdown links, including decoded attributes and fragment validation.
|
||||
Image references check file existence without interpreting image fragments as
|
||||
Markdown headings; document links keep anchor checks even when sharing a target.
|
||||
Inline guidance imports are rejected after punctuation as well as whitespace.
|
||||
The Nx test hash includes `marked`, `parse5` and `github-slugger` so dependency changes invalidate parser coverage.
|
||||
The import scan includes visible HTML text and excludes code and non-rendered containers.
|
||||
Image source sets use `parse-srcset` to check each candidate URL. Root-relative
|
||||
literals never suppress source-relative definition checks.
|
||||
The Nx test hash includes `marked`, `parse5`, `github-slugger` and `parse-srcset`
|
||||
so dependency changes invalidate parser coverage.
|
||||
It cannot prove semantic equivalence; review changed contracts as well.
|
||||
|
||||
## Protected Markdown edits
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"node-gyp": "12.4.0",
|
||||
"nx": "23.2.1",
|
||||
"nx-electron": "22.0.0",
|
||||
"parse-srcset": "1.0.2",
|
||||
"parse5": "8.0.1",
|
||||
"prettier": "^3.9.6",
|
||||
"sharp": "0.35.4",
|
||||
|
||||
Generated
+8
@@ -458,6 +458,9 @@ importers:
|
||||
nx-electron:
|
||||
specifier: 22.0.0
|
||||
version: 22.0.0(patch_hash=4d5ac9c5b10268dcc40998d7a2b166d004105ae7875fa182130d6810871a1e84)(@nx/devkit@23.2.1(nx@23.2.1(@swc-node/register@1.12.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@swc/core@1.16.1(@swc/helpers@0.5.23))(@swc/types@0.1.28)(typescript@6.0.3))(@swc/core@1.16.1(@swc/helpers@0.5.23))))(@nx/workspace@23.2.1(@swc-node/register@1.12.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@swc/core@1.16.1(@swc/helpers@0.5.23))(@swc/types@0.1.28)(typescript@6.0.3))(@swc/core@1.16.1(@swc/helpers@0.5.23)))(@swc/core@1.16.1(@swc/helpers@0.5.23))(electron-builder-squirrel-windows@26.15.7)(electron@43.3.0)(esbuild@0.28.2)(rxjs@7.8.2)(typescript@6.0.3)
|
||||
parse-srcset:
|
||||
specifier: 1.0.2
|
||||
version: 1.0.2
|
||||
parse5:
|
||||
specifier: 8.0.1
|
||||
version: 8.0.1
|
||||
@@ -8030,6 +8033,9 @@ packages:
|
||||
resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
parse-srcset@1.0.2:
|
||||
resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==}
|
||||
|
||||
parse5-html-rewriting-stream@8.0.1:
|
||||
resolution: {integrity: sha512-NaRku2aMpUN1Sh1Gyk1KWUh2A7EJx2c6qYzvwsPtqhoHoaURshdrceYK3LunVCm3WHhm6FS7Vcczbvdh3/UIVw==}
|
||||
|
||||
@@ -18621,6 +18627,8 @@ snapshots:
|
||||
parse-node-version@1.0.1:
|
||||
optional: true
|
||||
|
||||
parse-srcset@1.0.2: {}
|
||||
|
||||
parse5-html-rewriting-stream@8.0.1:
|
||||
dependencies:
|
||||
entities: 8.0.0
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import parseSrcset from 'parse-srcset';
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { Marked, Tokenizer } from 'marked';
|
||||
import { parseFragment } from 'parse5';
|
||||
@@ -69,6 +70,12 @@ function htmlNavigation(html) {
|
||||
target: attribute.value,
|
||||
image: node.tagName === 'img',
|
||||
});
|
||||
if (
|
||||
['img', 'source'].includes(node.tagName) &&
|
||||
attribute.name === 'srcset'
|
||||
)
|
||||
for (const candidate of parseSrcset(attribute.value))
|
||||
references.push({ target: candidate.url, image: true });
|
||||
}
|
||||
for (const child of node.childNodes ?? []) visit(child);
|
||||
}
|
||||
@@ -77,13 +84,32 @@ function htmlNavigation(html) {
|
||||
}
|
||||
|
||||
export function guidanceProse(markdown) {
|
||||
function prose(token) {
|
||||
if (['code', 'codespan', 'html'].includes(token.type)) return '';
|
||||
if (token.items) return token.items.map(prose).join('\n');
|
||||
if (token.tokens) return token.tokens.map(prose).join('');
|
||||
return token.text ?? '';
|
||||
function text(node) {
|
||||
if (
|
||||
['script', 'style', 'template', 'pre', 'code'].includes(
|
||||
node.tagName
|
||||
)
|
||||
)
|
||||
return ' ';
|
||||
if (node.nodeName === '#text') return node.value;
|
||||
const content = (node.childNodes ?? []).map(text).join('');
|
||||
return [
|
||||
'p',
|
||||
'li',
|
||||
'blockquote',
|
||||
'div',
|
||||
'br',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
].includes(node.tagName)
|
||||
? content + '\n'
|
||||
: content;
|
||||
}
|
||||
return markdownLexer.lexer(markdown).map(prose).join('\n');
|
||||
return text(parseFragment(new Marked().parse(markdown)));
|
||||
}
|
||||
|
||||
export function guidanceStandaloneImports(markdown) {
|
||||
@@ -164,7 +190,7 @@ export function guidanceReferences(markdown, includeLiterals) {
|
||||
const usedTargets = new Set();
|
||||
function add(target, literal = false, image = false) {
|
||||
const key = `${literal}:${image}:${target}`;
|
||||
usedTargets.add(target);
|
||||
if (!literal) usedTargets.add(target);
|
||||
if (!destinations.has(key)) {
|
||||
destinations.add(key);
|
||||
result.push({ target, literal, image });
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
"{projectRoot}/*.mjs",
|
||||
"{projectRoot}/project.json",
|
||||
{
|
||||
"externalDependencies": ["marked", "parse5", "github-slugger"]
|
||||
"externalDependencies": [
|
||||
"marked",
|
||||
"parse5",
|
||||
"github-slugger",
|
||||
"parse-srcset"
|
||||
]
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
|
||||
@@ -638,3 +638,69 @@ test('sharing an image reference does not suppress document anchor checks', asyn
|
||||
/missing anchor/
|
||||
);
|
||||
});
|
||||
|
||||
test('root literals cannot suppress source-relative definitions', async (t) => {
|
||||
assert.match(
|
||||
(
|
||||
await diagnostics(t, {
|
||||
[map]: '`README.md`\n\n[unused]: README.md',
|
||||
'README.md': '# Root',
|
||||
})
|
||||
).join('\n'),
|
||||
/does not exist: README.md/
|
||||
);
|
||||
});
|
||||
test('visible HTML text cannot hide inline imports', async (t) => {
|
||||
assert.match(
|
||||
(
|
||||
await diagnostics(t, {
|
||||
'CLAUDE.md': '@AGENTS.md\n\n<p>Read @docs/example.md</p>',
|
||||
})
|
||||
).join('\n'),
|
||||
/imports are not allowed/
|
||||
);
|
||||
});
|
||||
for (const html of [
|
||||
'<picture><source srcset="docs/dark.png"><img src="docs/light.png"></picture>',
|
||||
'<img src="docs/light.png" srcset="docs/light.png 1x, docs/dark.png 2x">',
|
||||
]) {
|
||||
test(`validates every srcset asset: ${html}`, async (t) => {
|
||||
assert.match(
|
||||
(
|
||||
await diagnostics(t, {
|
||||
'AGENTS.md': html,
|
||||
'docs/light.png': '',
|
||||
})
|
||||
).join('\n'),
|
||||
/does not exist: docs\/dark.png/
|
||||
);
|
||||
assert.deepEqual(
|
||||
await diagnostics(t, {
|
||||
'AGENTS.md': html,
|
||||
'docs/light.png': '',
|
||||
'docs/dark.png': '',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test('srcset data URLs do not become local filenames', async (t) => {
|
||||
assert.deepEqual(
|
||||
await diagnostics(t, {
|
||||
'AGENTS.md':
|
||||
'<img srcset="data:image/png;base64,AAAA 1x, docs/image.png 2x">',
|
||||
'docs/image.png': '',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
});
|
||||
test('non-rendered HTML text and code do not introduce imports', async (t) => {
|
||||
assert.deepEqual(
|
||||
await diagnostics(t, {
|
||||
'CLAUDE.md':
|
||||
'@AGENTS.md\n\n<!-- @docs/missing.md -->\n<script>@docs/missing.md</script>\n<style>@docs/missing.md</style>\n<template>@docs/missing.md</template>\n<code>@docs/missing.md</code>',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user