Skip to content

Commit 2ba8af0

Browse files
authored
fix: keep underscores in identifier heading anchors (#400)
## Summary `npm run validate` fails on `main` for a correct link: the slug library mis-slugged any identifier heading with two or more underscores (`flexible_http_request` became `flexiblehttprequest`, `canister_inspect_message` likewise). - `renderedText` parses the heading with `mdast-util-from-markdown` and `mdast-util-to-string`, replacing the regexes that approximated inline markdown - It parses the whole line, `#` markers included, so `1. Create a target canister` keeps its number - Both packages are declared in `devDependencies`; the lockfile root is hand-edited rather than regenerated Verified against a built site: every heading id in `dist/` matches the library across all 200 pages, both directions. A footnote reference in a heading stays out of reach, noted in the code.
1 parent 7155199 commit 2ba8af0

3 files changed

Lines changed: 33 additions & 15 deletions

File tree

‎package-lock.json‎

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
"@resvg/resvg-js": "^2.6.2",
3030
"github-slugger": "^2.0.0",
3131
"glob": "^13.0.6",
32-
"gray-matter": "^4.0.3"
32+
"gray-matter": "^4.0.3",
33+
"mdast-util-from-markdown": "^2.0.3",
34+
"mdast-util-to-string": "^4.0.0"
3335
}
3436
}

‎scripts/lib/anchors.mjs‎

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,39 @@
11
// Heading ids as the site generates them.
22
//
3-
// Starlight slugs the *rendered* heading text with github-slugger, so inline
4-
// markdown is stripped first and an explicit `{#id}` wins. Using the same
5-
// library rather than an approximation is deliberate: compared over the 2995
6-
// headings in docs/, a hand-rolled slug disagreed on 21 of them, all headings
7-
// containing an arrow, an ampersand, or `/*`.
3+
// Starlight slugs the *rendered* heading text with github-slugger, so the
4+
// heading is parsed to its text first and an explicit `{#id}` wins. Both steps
5+
// use the libraries the site itself uses rather than an approximation of them:
6+
// compared against the ids in a built site, every heading on all 200 pages
7+
// agrees, in both directions.
88
//
99
// Shared by scripts/validate.js (every page, every PR) and
1010
// scripts/sync-static-site.mjs (the synced tree, before it is written).
1111

1212
import fs from 'fs';
1313
import GithubSlugger from 'github-slugger';
14+
import { fromMarkdown } from 'mdast-util-from-markdown';
15+
import { toString } from 'mdast-util-to-string';
1416
import { HEADING_ID } from '../../plugins/remark-heading-id.mjs';
1517

16-
function renderedText(heading) {
17-
return heading
18-
.replace(/`([^`]*)`/g, '$1')
19-
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
20-
.replace(/[*_]{1,3}([^*_]+)[*_]{1,3}/g, '$1')
21-
.replace(/<[^>]+>/g, '')
22-
.trim();
18+
// The rendered text of a heading, which is what the site slugs: inline markdown
19+
// resolved away, so `code`, **strong**, [links](x) and raw HTML contribute their
20+
// text and nothing else. Parsed rather than pattern-matched, because inline
21+
// markdown does not reduce to a set of regexes. Two cases that defeated one:
22+
// `flexible_http_request` in a code span (an inner `_http_` read as emphasis,
23+
// so both underscores vanished), and `_foo_bar_`, where CommonMark pairs the
24+
// outer underscores and keeps the intraword one.
25+
//
26+
// Takes the whole line, `#` markers included. The text alone is not the same
27+
// document: "1. Create a canister" parses as an ordered list and renders as
28+
// "Create a canister", losing the number the site slugs into the id.
29+
//
30+
// One construct is out of reach: a footnote reference in a heading renders as
31+
// the note's *number*, which is assigned while the document is rendered, so no
32+
// parse of the heading alone can produce it. `## API[^note]` is `footnote-api1`
33+
// on the site. Neither CommonMark nor GFM parsing yields that, so a link to
34+
// such a heading would be reported as broken. No heading in docs/ does this.
35+
function renderedText(line) {
36+
return toString(fromMarkdown(line), { includeHtml: false }).trim();
2337
}
2438

2539
export function anchorsOfText(text) {
@@ -36,7 +50,7 @@ export function anchorsOfText(text) {
3650
if (!m) continue;
3751
// `{#id}` and `{$id}` both set an explicit id; the plugin accepts both.
3852
const explicit = HEADING_ID.exec(m[1]);
39-
anchors.add(explicit ? explicit[1] : slugger.slug(renderedText(m[1])));
53+
anchors.add(explicit ? explicit[1] : slugger.slug(renderedText(line)));
4054
}
4155
return anchors;
4256
}

0 commit comments

Comments
 (0)