mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
fix(agents): parse prose and rendered HTML anchors
This commit is contained in:
1 parent
1206f0d255
commit
bdf7017a97
7 files changed
+96
-35
No files matched your search
@@ -38,10 +38,11 @@ aliases and dotted code symbols are excluded. Bare dotted names with conventiona
|
||||
file suffixes (such as .md, .json or .ts) are treated as filenames. Use a `./`
|
||||
prefix or Markdown link for other ambiguous filenames that resemble code symbols.
|
||||
Multi-part dotfiles are path candidates too. Link paths and fragments are decoded
|
||||
separately so encoded filename delimiters stay in the filename. Fenced examples
|
||||
separately so encoded filename delimiters stay in the filename. Fenced and indented examples
|
||||
do not count as root guidance imports or satisfy the required Claude import.
|
||||
Heading anchors are derived from parsed text, not HTML sanitization. The Nx test
|
||||
hash includes `marked` so dependency changes invalidate parser coverage.
|
||||
Heading anchors decode HTML character references in text. Explicit HTML anchors
|
||||
use `parse5`, excluding comments, scripts, styles and template contents.
|
||||
The Nx test hash includes `marked` and `parse5` so dependency changes invalidate parser coverage.
|
||||
It cannot prove semantic equivalence; review changed contracts as well.
|
||||
|
||||
## Protected Markdown edits
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
"node-gyp": "12.4.0",
|
||||
"nx": "23.2.1",
|
||||
"nx-electron": "22.0.0",
|
||||
"parse5": "8.0.1",
|
||||
"prettier": "^3.9.6",
|
||||
"sharp": "0.35.4",
|
||||
"tailwindcss": "^3.4.19",
|
||||
|
||||
Generated
+3
@@ -455,6 +455,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)
|
||||
parse5:
|
||||
specifier: 8.0.1
|
||||
version: 8.0.1
|
||||
prettier:
|
||||
specifier: ^3.9.6
|
||||
version: 3.9.6
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Marked, Tokenizer } from 'marked';
|
||||
import { parseFragment } from 'parse5';
|
||||
|
||||
// Lex only: no Markdown is rendered and no HTML is sanitized or re-emitted.
|
||||
const markdownLexer = new Marked({
|
||||
@@ -34,14 +35,50 @@ function inlineText(tokens) {
|
||||
.map((token) => {
|
||||
if (token.type === 'html') return '';
|
||||
if (token.tokens) return inlineText(token.tokens);
|
||||
return token.text ?? '';
|
||||
return token.type === 'text'
|
||||
? decodeEntities(token.text ?? '')
|
||||
: (token.text ?? '');
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
function decodeEntities(text) {
|
||||
return text.replace(
|
||||
/&(?:#(?:x[0-9a-f]+|[0-9]+)|[a-z][a-z0-9]+);/giu,
|
||||
(entity) => parseFragment(entity).childNodes[0]?.value ?? entity
|
||||
);
|
||||
}
|
||||
|
||||
function htmlAnchors(html) {
|
||||
const found = [];
|
||||
function visit(node) {
|
||||
if (['script', 'style', 'template'].includes(node.tagName)) return;
|
||||
for (const attribute of node.attrs ?? []) {
|
||||
if (
|
||||
attribute.name === 'id' ||
|
||||
(node.tagName === 'a' && attribute.name === 'name')
|
||||
)
|
||||
found.push(attribute.value);
|
||||
}
|
||||
for (const child of node.childNodes ?? []) visit(child);
|
||||
}
|
||||
visit(parseFragment(html));
|
||||
return found;
|
||||
}
|
||||
|
||||
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 ?? '';
|
||||
}
|
||||
return markdownLexer.lexer(markdown).map(prose).join('\n');
|
||||
}
|
||||
|
||||
export function guidanceAnchors(markdown) {
|
||||
const found = new Set();
|
||||
const explicit = new Set();
|
||||
const html = [];
|
||||
markdownLexer.walkTokens(markdownLexer.lexer(markdown), (token) => {
|
||||
if (token.type === 'heading') {
|
||||
const slug = inlineText(token.tokens)
|
||||
@@ -53,14 +90,9 @@ export function guidanceAnchors(markdown) {
|
||||
while (found.has(unique)) unique = `${slug}-${++suffix}`;
|
||||
found.add(unique);
|
||||
}
|
||||
if (token.type === 'html') {
|
||||
for (const match of token.raw.matchAll(
|
||||
/<(?:a|[a-z][\w-]*)\b[^>]*\b(?:id|name)=["']([^"']+)["']/giu
|
||||
))
|
||||
explicit.add(match[1]);
|
||||
}
|
||||
if (token.type === 'html') html.push(token.raw);
|
||||
});
|
||||
return new Set([...found, ...explicit]);
|
||||
return new Set([...found, ...htmlAnchors(html.join('\n'))]);
|
||||
}
|
||||
|
||||
function isLiteralRepositoryPath(token) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"{projectRoot}/*.mjs",
|
||||
"{projectRoot}/project.json",
|
||||
{
|
||||
"externalDependencies": ["marked"]
|
||||
"externalDependencies": ["marked", "parse5"]
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
guidanceAnchors as anchors,
|
||||
guidanceProse,
|
||||
guidanceReferences as references,
|
||||
} from './agent-guidance-markdown.mjs';
|
||||
|
||||
@@ -21,27 +22,6 @@ function within(root, path) {
|
||||
);
|
||||
}
|
||||
|
||||
// Deliberately scoped to authored guidance, not a general Markdown crawler.
|
||||
function withoutFences(markdown) {
|
||||
let fence;
|
||||
return markdown
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => {
|
||||
const match = /^\s{0,3}(`{3,}|~{3,})/u.exec(line);
|
||||
if (match) {
|
||||
if (!fence) fence = match[1];
|
||||
else if (
|
||||
match[1][0] === fence[0] &&
|
||||
match[1].length >= fence.length
|
||||
)
|
||||
fence = undefined;
|
||||
return '';
|
||||
}
|
||||
return fence ? '' : line;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
async function validateReference(
|
||||
rootDir,
|
||||
source,
|
||||
@@ -109,7 +89,7 @@ export async function validateAgentGuidance({ rootDir }) {
|
||||
diagnostics.push(
|
||||
`${source}: at most ${maxBytes} UTF-8 bytes allowed (received ${bytes})`
|
||||
);
|
||||
const unfenced = withoutFences(markdown);
|
||||
const unfenced = guidanceProse(markdown);
|
||||
const imports = [...unfenced.matchAll(/^\s*@([^\s]+)\s*$/gmu)].map(
|
||||
(match) => match[1]
|
||||
);
|
||||
|
||||
@@ -454,3 +454,47 @@ for (const [encoded, filename] of [
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
for (const source of ['AGENTS.md', 'CLAUDE.md']) {
|
||||
test(`${source}: indented decorators are not imports`, async (t) => {
|
||||
assert.deepEqual(
|
||||
await diagnostics(t, {
|
||||
[source]:
|
||||
(source === 'CLAUDE.md' ? '@AGENTS.md\n\n' : '') +
|
||||
' @Injectable()\n @docs/missing.md\n',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
});
|
||||
}
|
||||
for (const body of [
|
||||
'<!-- <a id="old"></a> -->',
|
||||
`<script>const s = '<a id="old"></a>';</script>`,
|
||||
'<template><a id="old"></a></template>',
|
||||
]) {
|
||||
test(`non-rendered HTML does not define anchors: ${body}`, async (t) => {
|
||||
assert.match(
|
||||
(
|
||||
await diagnostics(t, {
|
||||
'AGENTS.md': '[Old](docs/example.md#old)',
|
||||
'docs/example.md': body,
|
||||
})
|
||||
).join('\n'),
|
||||
/missing anchor/
|
||||
);
|
||||
});
|
||||
}
|
||||
for (const [heading, anchor] of [
|
||||
['A & B', 'a--b'],
|
||||
['Café A B', 'café-a-b'],
|
||||
]) {
|
||||
test(`heading entities produce rendered anchors: ${heading}`, async (t) => {
|
||||
assert.deepEqual(
|
||||
await diagnostics(t, {
|
||||
'AGENTS.md': `[Heading](docs/example.md#${anchor})`,
|
||||
'docs/example.md': '# ' + heading,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user