Skip to content

Commit c59dd4a

Browse files
committed
Use custom toc and heading-links remark plugins
1 parent 563526d commit c59dd4a

11 files changed

+827
-522
lines changed

.eslintrc renamed to .eslintrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"generator-star-spacing": ["error", { "before": false, "after": true }],
2323
"indent": ["error", 2, { "ignoreComments": true, "SwitchCase": 1 }],
2424
"linebreak-style": "error",
25+
"no-console": ["error", { "allow": ["error"] }],
2526
"no-trailing-spaces": "error",
2627
"no-unused-vars": ["error", { "argsIgnorePattern": "_.*" }],
2728
"prefer-const": ["error", { "destructuring": "all" }],

README.md

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,37 @@ features they make available to you.
4343
- [remark-gfm](https://github.com/remarkjs/remark-gfm) -- Adds support for
4444
Github Flavored Markdown specific markdown features such as autolink literals,
4545
footnotes, strikethrough, tables, and tasklists.
46-
- [remark-number-headings](/json-schema-org/json-schema-spec/blob/main/remark-number-headings.js)
47-
-- Adds hierarchical section numbers to headings.
48-
- [remark-toc](https://github.com/remarkjs/remark-toc) -- Adds a table of
49-
contents in a section with a header called "Table of Contents".
46+
- [remark-heading-id](https://github.com/imcuttle/remark-heading-id) -- Adds
47+
support for `{#my-anchor}` syntax to add an `id` to an element so it can be
48+
referenced using URI fragment syntax.
49+
- [remark-headings](/json-schema-org/json-schema-spec/blob/main/remark-headings.js)
50+
-- A collection of enhancements for headings.
51+
- Adds hierarchical section numbers to headings.
52+
- Use the `[Appendix]` prefix on headings that should be numbered as an
53+
appendix.
54+
- Adds id anchors to headers that don't have one
55+
- Example: `#section-2-13`
56+
- Example: `#appendix-a`
57+
- Makes the heading a link utilizing its anchor
58+
- [remark-reference-links](/json-schema-org/json-schema-spec/blob/main/remark-reference-link.js)
59+
-- Adds new syntax for referencing a section of the spec using the section
60+
number as the link text.
61+
- Example:
62+
```markdown
63+
## Foo {#foo}
64+
65+
## Bar
66+
This is covered in {{foo}} // --> Renders to "This is covered in [Section 2.3](#foo)"
67+
- Link text will use "Section" or "Appendix" as needed
68+
```
69+
- [remark-table-of-contents](/json-schema-org/json-schema-spec/blob/main/remark-table-of-contents.js)
70+
-- Adds a table of contents in a section with a header called "Table of
71+
Contents".
5072
- [remark-torchlight](https://github.com/torchlight-api/remark-torchlight) --
5173
Syntax highlighting and more using https://torchlight.dev. Features include
5274
line numbers and line highlighting.
53-
- [rehype-slug](https://github.com/rehypejs/rehype-slug) -- Adds `id` anchors to
54-
header so they can be linked to with URI fragment syntax.
55-
- [rehype-autolink-headings](https://github.com/rehypejs/rehype-autolink-headings)
56-
-- Makes headings clickable.
5775
- [remark-flexible-containers](https://github.com/ipikuka/remark-flexible-containers)
58-
-- Add a callout box using the following syntax. Supported container types are
76+
- Add a callout box using the following syntax. Supported container types are
5977
`warning`, `note`, and `experimental`.
6078

6179
```

build/build.js

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,17 @@ import dotenv from "dotenv";
22
import { readFileSync, writeFileSync } from "node:fs";
33
import { reporter } from "vfile-reporter";
44
import { remark } from "remark";
5-
import rehypeAutolinkHeadings from "rehype-autolink-headings";
6-
import rehypeSlug from "rehype-slug";
7-
import rehypeStringify from "rehype-stringify";
85
import remarkFlexibleContainers from "remark-flexible-containers";
96
import remarkGfm from "remark-gfm";
107
import remarkHeadingId from "remark-heading-id";
11-
import remarkHeadings from "@vcarl/remark-headings";
12-
import remarkNumberHeadings from "./remark-number-headings.js";
8+
import remarkHeadings from "./remark-headings.js";
139
import remarkPresetLintMarkdownStyleGuide from "remark-preset-lint-markdown-style-guide";
1410
import remarkRehype from "remark-rehype";
15-
import remarkSectionLinks from "./remark-section-links.js";
16-
import remarkToc from "remark-toc";
11+
import remarkReferenceLinks from "./remark-reference-links.js";
12+
import remarkTableOfContents from "./remark-table-of-contents.js";
13+
import remarkTorchLight from "remark-torchlight";
1714
import remarkValidateLinks from "remark-validate-links";
18-
import torchLight from "remark-torchlight";
15+
import rehypeStringify from "rehype-stringify";
1916

2017

2118
dotenv.config();
@@ -25,26 +22,20 @@ dotenv.config();
2522
const html = await remark()
2623
.use(remarkPresetLintMarkdownStyleGuide)
2724
.use(remarkGfm)
28-
.use(torchLight)
29-
.use(remarkFlexibleContainers)
3025
.use(remarkHeadingId)
31-
.use(remarkNumberHeadings, {
26+
.use(remarkHeadings, {
3227
startDepth: 2,
33-
skip: ["Abstract", "Note to Readers", "Table of Contents", "Authors' Addresses", "\\[.*\\]", "draft-.*"],
34-
appendixToken: "[Appendix]",
35-
appendixPrefix: "Appendix"
28+
skip: ["Abstract", "Note to Readers", "Table of Contents", "Authors' Addresses", "\\[.*\\]", "draft-.*"]
3629
})
37-
.use(remarkHeadings)
38-
.use(remarkSectionLinks)
39-
.use(remarkToc, {
40-
tight: true,
41-
heading: "Table of Contents",
42-
skip: "\\[.*\\]|draft-.*"
30+
.use(remarkReferenceLinks)
31+
.use(remarkFlexibleContainers)
32+
.use(remarkTorchLight)
33+
.use(remarkTableOfContents, {
34+
startDepth: 2,
35+
skip: ["Abstract", "Note to Readers", "\\[.*\\]", "Authors' Addresses", "draft-.*"]
4336
})
4437
.use(remarkValidateLinks)
4538
.use(remarkRehype)
46-
.use(rehypeSlug)
47-
.use(rehypeAutolinkHeadings, { behavior: "wrap" })
4839
.use(rehypeStringify)
4940
.process(md);
5041

@@ -122,7 +113,7 @@ dotenv.config();
122113
</style>
123114
</head>
124115
<body>
125-
${String(html)}
116+
${html.toString()}
126117
</body>
127118
</html>`);
128119

build/remark-headings.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { visit } from "unist-util-visit";
2+
import { link, text } from "mdast-builder";
3+
import { findAndReplace } from "mdast-util-find-and-replace";
4+
import { toString as nodeToString } from "mdast-util-to-string";
5+
6+
7+
const defaultOptions = {
8+
startDepth: 1,
9+
skip: [],
10+
appendixToken: "[Appendix]",
11+
appendixPrefix: "Appendix"
12+
};
13+
14+
const remarkNumberHeadings = (options) => (tree, file) => {
15+
options = { ...defaultOptions, ...options };
16+
options.skip = new RegExp(`^(${options.skip.join("|")})$`, "u");
17+
18+
// Auto-number headings
19+
let sectionNumbers = [];
20+
21+
visit(tree, "heading", (headingNode) => {
22+
if (headingNode.depth < options.startDepth) {
23+
return;
24+
}
25+
26+
const headingText = nodeToString(headingNode);
27+
if (options.skip.test(headingText)) {
28+
return;
29+
}
30+
31+
if (!("data" in headingNode)) {
32+
headingNode.data = {};
33+
}
34+
35+
if (!("hProperties" in headingNode.data)) {
36+
headingNode.data.hProperties = {};
37+
}
38+
39+
if (headingText.startsWith(options.appendixToken)) {
40+
findAndReplace(headingNode, [options.appendixToken]);
41+
42+
const currentIndex = typeof sectionNumbers[headingNode.depth] === "string"
43+
? sectionNumbers[headingNode.depth]
44+
: "@";
45+
sectionNumbers[headingNode.depth] = String.fromCharCode(currentIndex.charCodeAt(0) + 1);
46+
sectionNumbers = sectionNumbers.slice(0, headingNode.depth + 1);
47+
48+
const sectionNumber = sectionNumbers.slice(options.startDepth, headingNode.depth + 1).join(".");
49+
headingNode.data.section = `${options.appendixPrefix} ${sectionNumber}`;
50+
51+
headingNode.children.splice(0, 0, text(`${headingNode.data.section}. `));
52+
} else {
53+
sectionNumbers[headingNode.depth] = (sectionNumbers[headingNode.depth] ?? 0) + 1;
54+
sectionNumbers = sectionNumbers.slice(0, headingNode.depth + 1);
55+
56+
const sectionNumber = sectionNumbers.slice(options.startDepth, headingNode.depth + 1).join(".");
57+
const prefix = typeof sectionNumbers[options.startDepth] === "string"
58+
? options.appendixPrefix
59+
: "Section";
60+
headingNode.data.section = `${prefix} ${sectionNumber}`;
61+
62+
headingNode.children.splice(0, 0, text(`${sectionNumber}. `));
63+
}
64+
65+
if (!("id" in headingNode.data)) {
66+
const sectionSlug = headingNode.data?.id
67+
?? headingNode.data.section.replaceAll(/[ .]/g, "-").toLowerCase();
68+
headingNode.data.hProperties.id = sectionSlug;
69+
headingNode.data.id = sectionSlug;
70+
}
71+
});
72+
73+
// Build headings data used by ./remark-reference-links.js
74+
if (!("data" in file)) {
75+
file.data = {};
76+
}
77+
78+
file.data.headings = {};
79+
80+
visit(tree, "heading", (headingNode) => {
81+
if (headingNode.data?.id) {
82+
if (headingNode.data.id in file.data.headings) {
83+
file.message(`Found duplicate heading id "${headingNode.data.id}"`);
84+
}
85+
file.data.headings[headingNode.data.id] = headingNode;
86+
}
87+
});
88+
89+
// Make heading a link
90+
visit(tree, "heading", (headingNode) => {
91+
if (headingNode.data?.id) {
92+
headingNode.children = [link(`#${headingNode.data.id}`, "", headingNode.children)];
93+
}
94+
});
95+
};
96+
97+
export default remarkNumberHeadings;

build/remark-number-headings.js

Lines changed: 0 additions & 47 deletions
This file was deleted.

build/remark-reference-links.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { text, link } from "mdast-builder";
2+
import { toString as nodeToString } from "mdast-util-to-string";
3+
import { findAndReplace } from "mdast-util-find-and-replace";
4+
5+
6+
const referenceLink = /\{\{(?<id>.*?)\}\}/ug;
7+
8+
const remarkReferenceLinks = () => (tree, file) => {
9+
findAndReplace(tree, [referenceLink, (value, id) => {
10+
// file.data.headings comes from ./remark-headings.js
11+
if (!(id in file.data.headings)) {
12+
throw Error(`ReferenceLinkError: No header found with id "${id}"`);
13+
}
14+
15+
const headerText = nodeToString(file.data.headings[id]);
16+
const linkText = text(file.data.headings[id].data.section);
17+
return link(`#${id}`, headerText, [linkText]);
18+
}]);
19+
};
20+
21+
export default remarkReferenceLinks;

build/remark-section-links.js

Lines changed: 0 additions & 43 deletions
This file was deleted.

0 commit comments

Comments
 (0)