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
1212import fs from 'fs' ;
1313import GithubSlugger from 'github-slugger' ;
14+ import { fromMarkdown } from 'mdast-util-from-markdown' ;
15+ import { toString } from 'mdast-util-to-string' ;
1416import { 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
2539export 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