Skip to content

Commit

Permalink
Use custom toc and heading-links remark plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
jdesrosiers committed Aug 1, 2023
1 parent 563526d commit 8116f74
Show file tree
Hide file tree
Showing 11 changed files with 828 additions and 522 deletions.
1 change: 1 addition & 0 deletions .eslintrc → .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"generator-star-spacing": ["error", { "before": false, "after": true }],
"indent": ["error", 2, { "ignoreComments": true, "SwitchCase": 1 }],
"linebreak-style": "error",
"no-console": ["error", { "allow": ["error"] }],
"no-trailing-spaces": "error",
"no-unused-vars": ["error", { "argsIgnorePattern": "_.*" }],
"prefer-const": ["error", { "destructuring": "all" }],
Expand Down
37 changes: 28 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,38 @@ features they make available to you.
- [remark-gfm](https://github.com/remarkjs/remark-gfm) -- Adds support for
Github Flavored Markdown specific markdown features such as autolink literals,
footnotes, strikethrough, tables, and tasklists.
- [remark-number-headings](/json-schema-org/json-schema-spec/blob/main/remark-number-headings.js)
-- Adds hierarchical section numbers to headings.
- [remark-toc](https://github.com/remarkjs/remark-toc) -- Adds a table of
contents in a section with a header called "Table of Contents".
- [remark-heading-id](https://github.com/imcuttle/remark-heading-id) -- Adds
support for `{#my-anchor}` syntax to add an `id` to an element so it can be
referenced using URI fragment syntax.
- [remark-headings](/json-schema-org/json-schema-spec/blob/main/remark-headings.js)
-- A collection of enhancements for headings.
- Adds hierarchical section numbers to headings.
- Use the `[Appendix]` prefix on headings that should be numbered as an
appendix.
- Adds id anchors to headers that don't have one
- Example: `#section-2-13`
- Example: `#appendix-a`
- Makes the heading a link utilizing its anchor
- [remark-reference-links](/json-schema-org/json-schema-spec/blob/main/remark-reference-link.js)
-- Adds new syntax for referencing a section of the spec using the section
number as the link text.
- Example:
```markdown
## Foo {#foo}

## Bar
This is covered in {{foo}} // --> Renders to "This is covered in [Section
2.3](#foo)"
- Link text will use "Section" or "Appendix" as needed
```
- [remark-table-of-contents](/json-schema-org/json-schema-spec/blob/main/remark-table-of-contents.js)
-- Adds a table of contents in a section with a header called "Table of
Contents".
- [remark-torchlight](https://github.com/torchlight-api/remark-torchlight) --
Syntax highlighting and more using https://torchlight.dev. Features include
line numbers and line highlighting.
- [rehype-slug](https://github.com/rehypejs/rehype-slug) -- Adds `id` anchors to
header so they can be linked to with URI fragment syntax.
- [rehype-autolink-headings](https://github.com/rehypejs/rehype-autolink-headings)
-- Makes headings clickable.
- [remark-flexible-containers](https://github.com/ipikuka/remark-flexible-containers)
-- Add a callout box using the following syntax. Supported container types are
- Add a callout box using the following syntax. Supported container types are
`warning`, `note`, and `experimental`.

```
Expand Down
37 changes: 14 additions & 23 deletions build/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,17 @@ import dotenv from "dotenv";
import { readFileSync, writeFileSync } from "node:fs";
import { reporter } from "vfile-reporter";
import { remark } from "remark";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypeSlug from "rehype-slug";
import rehypeStringify from "rehype-stringify";
import remarkFlexibleContainers from "remark-flexible-containers";
import remarkGfm from "remark-gfm";
import remarkHeadingId from "remark-heading-id";
import remarkHeadings from "@vcarl/remark-headings";
import remarkNumberHeadings from "./remark-number-headings.js";
import remarkHeadings from "./remark-headings.js";
import remarkPresetLintMarkdownStyleGuide from "remark-preset-lint-markdown-style-guide";
import remarkRehype from "remark-rehype";
import remarkSectionLinks from "./remark-section-links.js";
import remarkToc from "remark-toc";
import remarkReferenceLinks from "./remark-reference-links.js";
import remarkTableOfContents from "./remark-table-of-contents.js";
import remarkTorchLight from "remark-torchlight";
import remarkValidateLinks from "remark-validate-links";
import torchLight from "remark-torchlight";
import rehypeStringify from "rehype-stringify";


dotenv.config();
Expand All @@ -25,26 +22,20 @@ dotenv.config();
const html = await remark()
.use(remarkPresetLintMarkdownStyleGuide)
.use(remarkGfm)
.use(torchLight)
.use(remarkFlexibleContainers)
.use(remarkHeadingId)
.use(remarkNumberHeadings, {
.use(remarkHeadings, {
startDepth: 2,
skip: ["Abstract", "Note to Readers", "Table of Contents", "Authors' Addresses", "\\[.*\\]", "draft-.*"],
appendixToken: "[Appendix]",
appendixPrefix: "Appendix"
skip: ["Abstract", "Note to Readers", "Table of Contents", "Authors' Addresses", "\\[.*\\]", "draft-.*"]
})
.use(remarkHeadings)
.use(remarkSectionLinks)
.use(remarkToc, {
tight: true,
heading: "Table of Contents",
skip: "\\[.*\\]|draft-.*"
.use(remarkReferenceLinks)
.use(remarkFlexibleContainers)
.use(remarkTorchLight)
.use(remarkTableOfContents, {
startDepth: 2,
skip: ["Abstract", "Note to Readers", "\\[.*\\]", "Authors' Addresses", "draft-.*"]
})
.use(remarkValidateLinks)
.use(remarkRehype)
.use(rehypeSlug)
.use(rehypeAutolinkHeadings, { behavior: "wrap" })
.use(rehypeStringify)
.process(md);

Expand Down Expand Up @@ -122,7 +113,7 @@ dotenv.config();
</style>
</head>
<body>
${String(html)}
${html.toString()}
</body>
</html>`);

Expand Down
97 changes: 97 additions & 0 deletions build/remark-headings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { visit } from "unist-util-visit";
import { link, text } from "mdast-builder";
import { findAndReplace } from "mdast-util-find-and-replace";
import { toString as nodeToString } from "mdast-util-to-string";


const defaultOptions = {
startDepth: 1,
skip: [],
appendixToken: "[Appendix]",
appendixPrefix: "Appendix"
};

const remarkNumberHeadings = (options) => (tree, file) => {
options = { ...defaultOptions, ...options };
options.skip = new RegExp(`^(${options.skip.join("|")})$`, "u");

// Auto-number headings
let sectionNumbers = [];

visit(tree, "heading", (headingNode) => {
if (headingNode.depth < options.startDepth) {
return;
}

const headingText = nodeToString(headingNode);
if (options.skip.test(headingText)) {
return;
}

if (!("data" in headingNode)) {
headingNode.data = {};
}

if (!("hProperties" in headingNode.data)) {
headingNode.data.hProperties = {};
}

if (headingText.startsWith(options.appendixToken)) {
findAndReplace(headingNode, [options.appendixToken]);

const currentIndex = typeof sectionNumbers[headingNode.depth] === "string"
? sectionNumbers[headingNode.depth]
: "@";
sectionNumbers[headingNode.depth] = String.fromCharCode(currentIndex.charCodeAt(0) + 1);
sectionNumbers = sectionNumbers.slice(0, headingNode.depth + 1);

const sectionNumber = sectionNumbers.slice(options.startDepth, headingNode.depth + 1).join(".");
headingNode.data.section = `${options.appendixPrefix} ${sectionNumber}`;

headingNode.children.splice(0, 0, text(`${headingNode.data.section}. `));
} else {
sectionNumbers[headingNode.depth] = (sectionNumbers[headingNode.depth] ?? 0) + 1;
sectionNumbers = sectionNumbers.slice(0, headingNode.depth + 1);

const sectionNumber = sectionNumbers.slice(options.startDepth, headingNode.depth + 1).join(".");
const prefix = typeof sectionNumbers[options.startDepth] === "string"
? options.appendixPrefix
: "Section";
headingNode.data.section = `${prefix} ${sectionNumber}`;

headingNode.children.splice(0, 0, text(`${sectionNumber}. `));
}

if (!("id" in headingNode.data)) {
const sectionSlug = headingNode.data?.id
?? headingNode.data.section.replaceAll(/[ .]/g, "-").toLowerCase();
headingNode.data.hProperties.id = sectionSlug;
headingNode.data.id = sectionSlug;
}
});

// Build headings data used by ./remark-reference-links.js
if (!("data" in file)) {
file.data = {};
}

file.data.headings = {};

visit(tree, "heading", (headingNode) => {
if (headingNode.data?.id) {
if (headingNode.data.id in file.data.headings) {
file.message(`Found duplicate heading id "${headingNode.data.id}"`);
}
file.data.headings[headingNode.data.id] = headingNode;
}
});

// Make heading a link
visit(tree, "heading", (headingNode) => {
if (headingNode.data?.id) {
headingNode.children = [link(`#${headingNode.data.id}`, "", headingNode.children)];
}
});
};

export default remarkNumberHeadings;
47 changes: 0 additions & 47 deletions build/remark-number-headings.js

This file was deleted.

21 changes: 21 additions & 0 deletions build/remark-reference-links.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { text, link } from "mdast-builder";
import { toString as nodeToString } from "mdast-util-to-string";
import { findAndReplace } from "mdast-util-find-and-replace";


const referenceLink = /\{\{(?<id>.*?)\}\}/ug;

const remarkReferenceLinks = () => (tree, file) => {
findAndReplace(tree, [referenceLink, (value, id) => {
// file.data.headings comes from ./remark-headings.js
if (!(id in file.data.headings)) {
throw Error(`ReferenceLinkError: No header found with id "${id}"`);
}

const headerText = nodeToString(file.data.headings[id]);
const linkText = text(file.data.headings[id].data.section);
return link(`#${id}`, headerText, [linkText]);
}]);
};

export default remarkReferenceLinks;
43 changes: 0 additions & 43 deletions build/remark-section-links.js

This file was deleted.

Loading

0 comments on commit 8116f74

Please sign in to comment.