Skip to content
This repository was archived by the owner on Feb 15, 2022. It is now read-only.

Commit 58d78a4

Browse files
author
kanishka-work
authored
Generate markdown TOC (#296)
* derive table of contents from markdown, making use of unified.js * embellish the generated html for markdown to include header id's * remove next-mdx-remote and mdxjs, now that remark and rehype are being used directly
1 parent 1e49ae0 commit 58d78a4

File tree

15 files changed

+347
-1631
lines changed

15 files changed

+347
-1631
lines changed

bindings/Unified.res

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
module MarkdownTableOfContents = {
2+
type rec toc = {
3+
label: string,
4+
id: string,
5+
children: list<toc>,
6+
}
7+
8+
type t = list<toc>
9+
}
10+
11+
type processor
12+
13+
@module("unified") external unified: unit => processor = "default"
14+
15+
type node = {\"type": string, depth: option<int>}
16+
17+
type data = {id: string}
18+
19+
type headingnode = {
20+
depth: int,
21+
data: data,
22+
}
23+
24+
external asHeadingNode: node => headingnode = "%identity"
25+
26+
type rootnode = {children: array<node>}
27+
28+
type vfile = {mutable toc: MarkdownTableOfContents.t, contents: string}
29+
30+
type transformer = (rootnode, vfile) => unit
31+
32+
type attacher = unit => transformer
33+
34+
@send external use: (processor, attacher) => processor = "use"
35+
36+
@send external process: (processor, string) => Js.Promise.t<vfile> = "process"
37+
38+
@module("remark-slug") external remarkSlug: attacher = "default"
39+
40+
@module("remark-parse") external remarkParse: attacher = "default"
41+
42+
@module("remark-rehype") external remark2rehype: attacher = "default"
43+
44+
@module("rehype-stringify") external rehypeStringify: attacher = "default"
45+
46+
module MdastUtilToString = {
47+
@module("mdast-util-to-string") external toString: headingnode => string = "default"
48+
}

bindings/Unified.resi

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
module MarkdownTableOfContents: {
2+
type rec toc = {
3+
label: string,
4+
id: string,
5+
children: list<toc>,
6+
}
7+
8+
type t = list<toc>
9+
}
10+
11+
type processor
12+
13+
let unified: unit => processor
14+
15+
type node = {\"type": string, depth: option<int>}
16+
17+
type data = {id: string}
18+
19+
type headingnode = {
20+
depth: int,
21+
data: data,
22+
}
23+
24+
let asHeadingNode: node => headingnode
25+
26+
type rootnode = {children: array<node>}
27+
28+
type vfile = {mutable toc: MarkdownTableOfContents.t, contents: string}
29+
30+
type transformer = (rootnode, vfile) => unit
31+
32+
type attacher = unit => transformer
33+
34+
let use: (processor, attacher) => processor
35+
36+
let process: (processor, string) => Js.Promise.t<vfile>
37+
38+
let remarkSlug: attacher
39+
40+
let remarkParse: attacher
41+
42+
let remark2rehype: attacher
43+
44+
let rehypeStringify: attacher
45+
46+
module MdastUtilToString: {
47+
let toString: headingnode => string
48+
}

common/MdastToc.res

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// TODO: define scanleft (https://github.com/reazen/relude/blob/e733128d0df8022448398a44c80cba6f28443b94/src/list/Relude_List_Base.re#L487)
2+
// and use it below
3+
4+
// TODO: factor out the core algorithm from other concerns, and
5+
// write unit tests for the core algorithm.
6+
7+
let transformer = (rootnode: Unified.rootnode, file: Unified.vfile) => {
8+
let rec collect = (nodes, inProgress) => {
9+
switch nodes {
10+
| list{} =>
11+
switch inProgress {
12+
| None => list{}
13+
| Some(_, entry) => list{entry}
14+
}
15+
| list{h: Unified.headingnode, ...tail} =>
16+
let d = h.depth
17+
if d >= 2 || d <= 3 {
18+
let entry = {
19+
Unified.MarkdownTableOfContents.label: Unified.MdastUtilToString.toString(h),
20+
id: h.data.id,
21+
children: list{},
22+
}
23+
switch inProgress {
24+
| None => collect(tail, Some(d, entry))
25+
| Some(lastRootDepth, inProgress) if d <= lastRootDepth => list{
26+
inProgress,
27+
...collect(tail, Some(d, entry)),
28+
}
29+
| Some(lastRootDepth, inProgress) =>
30+
let inProgress = {
31+
...inProgress,
32+
children: Belt.List.concat(inProgress.children, list{entry}),
33+
}
34+
collect(tail, Some(lastRootDepth, inProgress))
35+
}
36+
} else {
37+
// TODO: Should we guard against unusual jumps in depth here?
38+
// No, instead validate the list of headings
39+
// in an earlier pass and produce new type with
40+
// stronger guarantees about heading sequences.
41+
collect(tail, inProgress)
42+
}
43+
}
44+
}
45+
let headings = collect(
46+
Array.to_list(
47+
Belt.Array.keepMap(rootnode.children, ch =>
48+
switch ch {
49+
| {\"type": "heading", depth: Some(_)} => Some(Unified.asHeadingNode(ch))
50+
| _ => None
51+
}
52+
),
53+
),
54+
None,
55+
)
56+
57+
file.toc = headings
58+
}
59+
60+
let plugin = () => {
61+
transformer
62+
}

common/MdastToc.resi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let plugin: Unified.attacher

common/Mdx.res

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

common/Mdx.resi

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

common/NextMdxRemote.res

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

common/NextMdxRemote.resi

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

components/MarkdownPage.res

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,17 @@ let s = React.string
22

33
module MarkdownPageBody = {
44
@react.component
5-
let make = (~margins, ~children) =>
6-
<div className={margins ++ ` prose prose-yellow prose-lg text-gray-500 mx-auto`}>
7-
children
8-
</div>
5+
let make = (~margins, ~renderedMarkdown) =>
6+
<div
7+
className={margins ++ ` prose prose-yellow prose-lg text-gray-500 mx-auto`}
8+
dangerouslySetInnerHTML={{"__html": renderedMarkdown}}
9+
/>
910
}
1011

1112
module TableOfContents = {
12-
// TODO: define general heading tree type and recursively traverse when rendering
13-
type subHeading = {
14-
subName: string,
15-
subHeadingId: string,
16-
}
17-
18-
type heading = {
19-
name: string,
20-
headingId: string,
21-
subHeadings: array<subHeading>,
22-
}
23-
2413
type t = {
2514
contents: string,
26-
headings: array<heading>,
15+
toc: Unified.MarkdownTableOfContents.t,
2716
}
2817

2918
@react.component
@@ -32,29 +21,30 @@ module TableOfContents = {
3221
className="hidden lg:sticky lg:self-start lg:top-2 lg:flex lg:flex-col lg:col-span-2 border-r border-gray-200 pt-5 pb-4 overflow-y-auto">
3322
<div className="px-4"> <span className="text-lg"> {s(content.contents)} </span> </div>
3423
<div className="mt-5 ">
24+
// TODO: implement a completely general recursive traversal a toc forest
3525
<nav className="px-2 space-y-1" ariaLabel="Sidebar">
36-
{content.headings
37-
|> Js.Array.mapi((hdg, idx) =>
26+
{content.toc
27+
->Belt.List.mapWithIndex((idx, hdg) =>
3828
<div key={Js.Int.toString(idx)} className="space-y-1">
3929
// Expanded: "text-gray-400 rotate-90", Collapsed: "text-gray-300"
4030
<a
41-
href={"#" ++ hdg.headingId}
31+
href={"#" ++ hdg.id}
4232
className="block text-gray-600 hover:text-gray-900 pr-2 py-2 text-sm font-medium">
43-
{s(hdg.name)}
33+
{s(hdg.label)}
4434
</a>
45-
{hdg.subHeadings
46-
|> Js.Array.mapi((sub, idx) =>
35+
{hdg.children
36+
->Belt.List.mapWithIndex((idx, sub) =>
4737
<a
48-
href={"#" ++ sub.subHeadingId}
38+
href={"#" ++ sub.id}
4939
className="block pl-6 pr-2 py-2 text-sm font-medium text-gray-600 hover:text-gray-900"
5040
key={Js.Int.toString(idx)}>
51-
{s(sub.subName)}
41+
{s(sub.label)}
5242
</a>
5343
)
54-
|> React.array}
44+
->Belt.List.toArray |> React.array}
5545
</div>
5646
)
57-
|> React.array}
47+
->Belt.List.toArray |> React.array}
5848
</nav>
5949
</div>
6050
</div>

0 commit comments

Comments
 (0)