Repository navigation
Expand file tree
/
Copy pathmarkdown.mjs
More file actions
101 lines (91 loc) · 3.4 KB
/
Copy pathmarkdown.mjs
File metadata and controls
101 lines (91 loc) · 3.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* markdown.mjs — render the vendored specification documents to HTML.
*
* The spec is authored as Markdown in the specification repository and is the
* normative text; the site renders that same file rather than a retelling of
* it, so the two cannot disagree. Headings get stable anchors and a table of
* contents is collected on the way through.
*/
import { Marked } from "marked";
import { codeBlock } from "./highlight.mjs";
import { escapeHtml } from "./layout.mjs";
/** GitHub-compatible heading slugs, so links from the repo keep working. */
export function slug(text) {
return text
.toLowerCase()
.replace(/<[^>]+>/g, "")
.replace(/[`*_~]/g, "")
.replace(/[^\w\s§.-]/g, "")
.trim()
.replace(/\s+/g, "-")
.replace(/\.+$/, "");
}
/**
* Render Markdown, returning the HTML and a flat table of contents.
*
* @param {string} src
* @param {object} [opts]
* @param {(href: string) => string} [opts.rewriteLink] map repo-relative links onto site paths
* @param {number} [opts.tocMin] shallowest heading level to include in the TOC
* @param {number} [opts.tocMax] deepest heading level to include in the TOC
*/
export function render(src, { rewriteLink = (h) => h, tocMin = 2, tocMax = 3 } = {}) {
const toc = [];
const seen = new Map();
const marked = new Marked({ gfm: true });
marked.use({
renderer: {
heading({ tokens, depth }) {
const text = this.parser.parseInline(tokens);
const plain = text.replace(/<[^>]+>/g, "");
let id = slug(plain);
if (seen.has(id)) {
const n = seen.get(id) + 1;
seen.set(id, n);
id = `${id}-${n}`;
} else {
seen.set(id, 0);
}
if (depth >= tocMin && depth <= tocMax) toc.push({ id, depth, text: plain });
return `<h${depth} id="${id}">${text}<a class="anchor" href="#${id}" aria-label="Permalink to this section">#</a></h${depth}>\n`;
},
code({ text, lang }) {
return codeBlock(text, (lang || "json").trim().split(/\s+/)[0]);
},
link({ href, title, tokens }) {
const text = this.parser.parseInline(tokens);
const target = rewriteLink(href);
const external = /^https?:/.test(target) && !target.startsWith("https://openpredicate.tech");
const attrs = [
`href="${escapeHtml(target)}"`,
title ? `title="${escapeHtml(title)}"` : "",
external ? 'rel="noopener"' : "",
]
.filter(Boolean)
.join(" ");
return `<a ${attrs}>${text}</a>`;
},
table({ header, rows }) {
const head = header.map((c) => `<th>${this.parser.parseInline(c.tokens)}</th>`).join("");
const body = rows
.map((row) => `<tr>${row.map((c) => `<td>${this.parser.parseInline(c.tokens)}</td>`).join("")}</tr>`)
.join("\n");
return `<div class="table-scroll"><table><thead><tr>${head}</tr></thead><tbody>\n${body}\n</tbody></table></div>\n`;
},
},
});
return { html: marked.parse(src), toc };
}
/** Render a table of contents as a nested-looking, flat list. */
export function tocHtml(toc, { title = "On this page" } = {}) {
if (!toc.length) return "";
const items = toc
.map((h) => `<li class="lvl-${h.depth}"><a href="#${h.id}">${escapeHtml(h.text)}</a></li>`)
.join("\n");
return `<nav class="toc" aria-label="${escapeHtml(title)}">
<h2>${escapeHtml(title)}</h2>
<ul>
${items}
</ul>
</nav>`;
}