fix(agents): use GitHub-compatible heading slugs

This commit is contained in:
4gray committed 2026-09-20 20:08:22 +02:00
1 parent 244b1e43ce
commit 98e6fd3983
6 files changed
+32 -11

No files matched your search

+3 -2
View File
@@ -40,11 +40,12 @@ 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 and indented examples
do not count as root guidance imports or satisfy the required Claude import.
Heading anchors decode HTML character references in text. Explicit HTML anchors
Heading anchors decode HTML character references in text and use `github-slugger`
for GitHub-compatible character filtering and duplicate suffixes. Explicit HTML anchors
use `parse5`, excluding comments, scripts, styles and template contents.
Rendered HTML anchor hrefs and image sources use the same local-reference checks
as Markdown links, including decoded attributes and fragment validation.
The Nx test hash includes `marked` and `parse5` so dependency changes invalidate parser coverage.
The Nx test hash includes `marked`, `parse5` and `github-slugger` so dependency changes invalidate parser coverage.
It cannot prove semantic equivalence; review changed contracts as well.
## Protected Markdown edits
+1
View File
@@ -215,6 +215,7 @@
"eslint-plugin-import": "2.32.0",
"eslint-plugin-playwright": "^1.6.2",
"express": "5.2.1",
"github-slugger": "2.0.0",
"globals": "15.9.0",
"html-escaper": "3.0.3",
"istanbul-lib-coverage": "3.2.2",
+3
View File
@@ -404,6 +404,9 @@ importers:
express:
specifier: 5.2.1
version: 5.2.1
github-slugger:
specifier: 2.0.0
version: 2.0.0
globals:
specifier: 15.9.0
version: 15.9.0
+3 -8
View File
@@ -1,3 +1,4 @@
import GithubSlugger from 'github-slugger';
import { Marked, Tokenizer } from 'marked';
import { parseFragment } from 'parse5';
@@ -83,18 +84,12 @@ export function guidanceProse(markdown) {
}
export function guidanceAnchors(markdown) {
const slugger = new GithubSlugger();
const found = new Set();
const html = [];
markdownLexer.walkTokens(markdownLexer.lexer(markdown), (token) => {
if (token.type === 'heading') {
const slug = inlineText(token.tokens)
.toLowerCase()
.replace(/[^\p{L}\p{M}\p{N}_\-\s]/gu, '')
.replace(/\s/gu, '-');
let unique = slug;
let suffix = 0;
while (found.has(unique)) unique = `${slug}-${++suffix}`;
found.add(unique);
found.add(slugger.slug(inlineText(token.tokens)));
}
if (token.type === 'html') html.push(token.raw);
});
+1 -1
View File
@@ -12,7 +12,7 @@
"{projectRoot}/*.mjs",
"{projectRoot}/project.json",
{
"externalDependencies": ["marked", "parse5"]
"externalDependencies": ["marked", "parse5", "github-slugger"]
}
],
"options": {
@@ -537,3 +537,24 @@ test('non-rendered HTML navigation is ignored', async (t) => {
[]
);
});
test('GitHub heading slugs remove non-ASCII whitespace', async (t) => {
for (const heading of ['A B', 'A\u00a0B', 'A\u2003B']) {
assert.deepEqual(
await diagnostics(t, {
'AGENTS.md': '[Heading](docs/example.md#ab)',
'docs/example.md': '# ' + heading,
}),
[]
);
assert.match(
(
await diagnostics(t, {
'AGENTS.md': '[Heading](docs/example.md#a-b)',
'docs/example.md': '# ' + heading,
})
).join('\n'),
/missing anchor/
);
}
});