Skip to content

Commit 58ee7b4

Browse files
committed
build: generate proper anchor ids for headings in markdown
Currently we compute the anchor ids for headings in guides and overviews manually. Our logic here does not match with the ids generated by Github, so one needs to either fix up all references to the anchor for the docs, but that will break the links in most markdown editors. We fix this by using the integrated `slugify` method provided by the `marked` package. This one seems to match the usual id generation in markdown files (as in Github and most markdown editors). This fixes links in the cdk-testing harness overview as observed originally in #19366.
1 parent caad0b5 commit 58ee7b4

File tree

4 files changed

+50
-20
lines changed

4 files changed

+50
-20
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
"@types/gulp": "3.8.32",
9292
"@types/inquirer": "^0.0.43",
9393
"@types/jasmine": "^3.5.4",
94-
"@types/marked": "^0.4.2",
94+
"@types/marked": "^0.7.4",
9595
"@types/merge2": "^0.3.30",
9696
"@types/minimist": "^1.2.0",
9797
"@types/node": "^12.11.1",
@@ -132,7 +132,7 @@
132132
"karma-sauce-launcher": "^2.0.2",
133133
"karma-sourcemap-loader": "^0.3.7",
134134
"madge": "^3.4.4",
135-
"marked": "^0.6.2",
135+
"marked": "^1.0.0",
136136
"merge2": "^1.2.3",
137137
"minimatch": "^3.0.4",
138138
"minimist": "^1.2.0",

tools/markdown-to-html/docs-marked-renderer.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import {Renderer} from 'marked';
1+
import {Renderer, Slugger} from 'marked';
22
import {basename, extname} from 'path';
33

4-
/** Regular expression that matches whitespace. */
5-
const whitespaceRegex = /\W+/g;
6-
74
/** Regular expression that matches example comments. */
85
const exampleCommentRegex = /<!--\s*example\(([^)]+)\)\s*-->/g;
96

@@ -13,15 +10,23 @@ const exampleCommentRegex = /<!--\s*example\(([^)]+)\)\s*-->/g;
1310
*/
1411
export class DocsMarkdownRenderer extends Renderer {
1512

13+
/** Set of fragment links discovered in the currently rendered file. */
14+
private _referencedFragments = new Set<string>();
15+
16+
/**
17+
* Slugger provided by the `marked` package. Can be used to create unique
18+
* ids for headings.
19+
*/
20+
private _slugger = new Slugger();
21+
1622
/**
1723
* Transforms a markdown heading into the corresponding HTML output. In our case, we
1824
* want to create a header-link for each H3 and H4 heading. This allows users to jump to
1925
* specific parts of the docs.
2026
*/
21-
heading(label: string, level: number, _raw: string) {
27+
heading(label: string, level: number, raw: string) {
2228
if (level === 3 || level === 4) {
23-
const headingId = label.toLowerCase().replace(whitespaceRegex, '-');
24-
29+
const headingId = this._slugger.slug(raw);
2530
return `
2631
<h${level} id="${headingId}" class="docs-header-link">
2732
<span header-link="${headingId}"></span>
@@ -42,6 +47,11 @@ export class DocsMarkdownRenderer extends Renderer {
4247
return super.link(`guide/${basename(href, extname(href))}`, title, text);
4348
}
4449

50+
// Keep track of all fragments discovered in a file.
51+
if (href.startsWith('#')) {
52+
this._referencedFragments.add(href.substr(1));
53+
}
54+
4555
return super.link(href, title, text);
4656
}
4757

@@ -90,7 +100,26 @@ export class DocsMarkdownRenderer extends Renderer {
90100
* Method that will be called after a markdown file has been transformed to HTML. This method
91101
* can be used to finalize the content (e.g. by adding an additional wrapper HTML element)
92102
*/
93-
finalizeOutput(output: string): string {
103+
finalizeOutput(output: string, fileName: string): string {
104+
const failures: string[] = [];
105+
106+
// Collect any fragment links that do not resolve to existing fragments in the
107+
// rendered file. We want to error for broken fragment links.
108+
this._referencedFragments.forEach(id => {
109+
if (this._slugger.seen[id] === undefined) {
110+
failures.push(`Found link to "${id}". This heading does not exist.`);
111+
}
112+
});
113+
114+
if (failures.length) {
115+
console.error(`Could not process file: ${fileName}. Please fix the following errors:`);
116+
failures.forEach(message => console.error(` - ${message}`));
117+
process.exit(1);
118+
}
119+
120+
this._slugger.seen = {};
121+
this._referencedFragments.clear();
122+
94123
return `<div class="docs-markdown">${output}</div>`;
95124
}
96125
}

tools/markdown-to-html/transform-markdown.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ if (require.main === module) {
2727
// Bazel bin directory.
2828
inputFiles.forEach(inputPath => {
2929
const outputPath = join(bazelBinPath, inputPath.replace(markdownExtension, '.html'));
30-
const htmlOutput = markdownRenderer.finalizeOutput(marked(readFileSync(inputPath, 'utf8')));
30+
const htmlOutput = markdownRenderer.finalizeOutput(
31+
marked(readFileSync(inputPath, 'utf8')), inputPath);
3132

3233
writeFileSync(outputPath, htmlOutput);
3334
});

yarn.lock

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,10 +1324,10 @@
13241324
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef"
13251325
integrity sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==
13261326

1327-
"@types/marked@^0.4.2":
1328-
version "0.4.2"
1329-
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.4.2.tgz#64a89e53ea37f61cc0f3ee1732c555c2dbf6452f"
1330-
integrity sha512-cDB930/7MbzaGF6U3IwSQp6XBru8xWajF5PV2YZZeV8DyiliTuld11afVztGI9+yJZ29il5E+NpGA6ooV/Cjkg==
1327+
"@types/marked@^0.7.4":
1328+
version "0.7.4"
1329+
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.7.4.tgz#607685669bb1bbde2300bc58ba43486cbbee1f0a"
1330+
integrity sha512-fdg0NO4qpuHWtZk6dASgsrBggY+8N4dWthl1bAQG9ceKUNKFjqpHaDKCAhRUI6y8vavG7hLSJ4YBwJtZyZEXqw==
13311331

13321332
"@types/merge2@^0.3.30":
13331333
version "0.3.30"
@@ -7776,16 +7776,16 @@ marked@^0.3.2:
77767776
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790"
77777777
integrity sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==
77787778

7779-
marked@^0.6.2:
7780-
version "0.6.2"
7781-
resolved "https://registry.yarnpkg.com/marked/-/marked-0.6.2.tgz#c574be8b545a8b48641456ca1dbe0e37b6dccc1a"
7782-
integrity sha512-LqxwVH3P/rqKX4EKGz7+c2G9r98WeM/SW34ybhgNGhUQNKtf1GmmSkJ6cDGJ/t6tiyae49qRkpyTw2B9HOrgUA==
7783-
77847779
marked@^0.7.0:
77857780
version "0.7.0"
77867781
resolved "https://registry.yarnpkg.com/marked/-/marked-0.7.0.tgz#b64201f051d271b1edc10a04d1ae9b74bb8e5c0e"
77877782
integrity sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==
77887783

7784+
marked@^1.0.0:
7785+
version "1.0.0"
7786+
resolved "https://registry.yarnpkg.com/marked/-/marked-1.0.0.tgz#d35784245a04871e5988a491e28867362e941693"
7787+
integrity sha512-Wo+L1pWTVibfrSr+TTtMuiMfNzmZWiOPeO7rZsQUY5bgsxpHesBEcIWJloWVTFnrMXnf/TL30eTFSGJddmQAng==
7788+
77897789
matchdep@^2.0.0:
77907790
version "2.0.0"
77917791
resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e"

0 commit comments

Comments
 (0)