on master help sign in
Markdown: rewrite build pipeline
commitff144bd1d116f0523585c64af29e5ab68b912889 authorMatilde Park <matilde.park@urbit.org> dateWed 1 Jun 2022, 15:53 (4 years ago) parent74ca72c
1 file changed+244−86
components/Markdown.js+244−86
components/Markdown.js+244−86
@@ -1,94 +1,252 @@
1−import remark from "remark";
2−import gfm from "remark-gfm";
3−import slug from "remark-slug";
4−import heading from "remark-heading-id";
5−import remarkprism from "remark-prism";
6−import normalize from "mdurl/encode";
7−import merge from "deepmerge";
8−import github from "hast-util-sanitize/lib/github";
9−import remarkParse from "remark-parse";
10−import remarkRehype, { defaultHandlers, all } from "remark-rehype";
11−import rehypeStringify from "rehype-stringify";
12−import rehypeRaw from "rehype-raw";
1+import React from "react";
2+import Markdoc, { Ast, Node, Tag } from "@markdoc/markdoc";
3+import { heading } from "./schema/Heading.markdoc";
4+import { footnoteRef } from "./schema/footnoteRef.markdoc";
5+import { footnoteItem } from "./schema/footnoteItem.markdoc";
6+import Tabs from "../components/Tabs";
7+import Tab from "../components/Tab";
8+import Highlight, { defaultProps } from "prism-react-renderer";
9+import Prism from "prism-react-renderer/prism";
10+(typeof global !== "undefined" ? global : window).Prism = Prism;
11+require("prismjs/components/prism-hoon");
1312
1413// img is wrapped in figure so that images can be extra wide in the blog posts
15−function img(h, node) {
16− var props = { src: normalize(node.url), alt: node.alt };
17− if (node.title !== null && node.title !== undefined) {
18− props.title = node.title;
19− }
20− return {
21− type: "element",
22− tagName: "figure",
23− properties: {},
24− children: [
25− {
26− type: "element",
27− tagName: "img",
28− properties: props,
29− },
30− ],
31− };
32−}
14+// function img(h, node) {
15+// var props = { src: normalize(node.url), alt: node.alt };
16+// if (node.title !== null && node.title !== undefined) {
17+// props.title = node.title;
18+// }
19+// return {
20+// type: "element",
21+// tagName: "figure",
22+// properties: {},
23+// children: [
24+// {
25+// type: "element",
26+// tagName: "img",
27+// properties: props,
28+// },
29+// ],
30+// };
31+// }
3332
34−function a(h, node) {
35− const url = node.url;
36−
37− if (
38− url.includes("https://urbit.org") ||
39− url.startsWith("/") ||
40− url.startsWith("#")
41− ) {
42− return {
43− type: "element",
44− tagName: "a",
45− properties: {
46− href: url,
47− },
48− children: all(h, node),
49− };
50− } else {
51− return {
52− type: "element",
53− tagName: "a",
54− properties: {
55− target: "_blank",
56− rel: "noopener",
57− href: url,
58− },
59− children: all(h, node),
60− };
61− }
33+// function a(h, node) {
34+// const url = node.url;
35+
36+// if (
37+// url.includes("https://urbit.org") ||
38+// url.startsWith("/") ||
39+// url.startsWith("#")
40+// ) {
41+// return {
42+// type: "element",
43+// tagName: "a",
44+// properties: {
45+// href: url,
46+// },
47+// children: all(h, node),
48+// };
49+// } else {
50+// return {
51+// type: "element",
52+// tagName: "a",
53+// properties: {
54+// target: "_blank",
55+// rel: "noopener",
56+// href: url,
57+// },
58+// children: all(h, node),
59+// };
60+// }
61+// }
62+
63+// const handlers = defaultHandlers;
64+// handlers.link = a;
65+// handlers.image = img;
66+
67+export function Fence({ children, language }) {
68+ return (
69+ <Highlight
70+ {...defaultProps}
71+ key={language}
72+ language={language}
73+ code={children}
74+ theme={undefined}
75+ >
76+ {({ className, style, tokens, getLineProps, getTokenProps }) => (
77+ <pre className={className} style={style}>
78+ {tokens.slice(0, -1).map((line, i) => (
79+ <div {...getLineProps({ line, key: i })}>
80+ {line.map((token, key) => (
81+ <span {...getTokenProps({ token, key })} />
82+ ))}
83+ </div>
84+ ))}
85+ </pre>
86+ )}
87+ </Highlight>
88+ );
6289}
6390
64−const handlers = defaultHandlers;
65−handlers.link = a;
66−handlers.image = img;
67−
68−const options = {
69− allowDangerousHtml: true,
70− handlers: handlers,
71− sanitize: merge(github, {
72− // remove user-content from github.json to remark-slug work as expected
73− clobberPrefix: "",
74− attributes: { "*": ["className"] },
75− }),
91+const fence = {
92+ render: "Fence",
93+ attributes: {
94+ language: {
95+ type: String,
96+ description:
97+ "The programming language of the code block. Place it after the backticks.",
98+ },
99+ },
76100};
77101
78−// Converts markdown strings into markdown HTML/React components
79−export default async function Markdown({ post }, disablePlugins) {
80− const result = await remark()
81− .use(remarkParse)
82− .use(remarkprism, {
83− plugins: !disablePlugins ? ["show-invisibles"] : [],
84− })
85− .use(gfm)
86− .use(slug)
87− .use(heading)
88− .use(remarkRehype, options)
89− .use(rehypeRaw)
90− .use(rehypeStringify)
91− .process(post.content);
92−
93− return result.toString();
102+const tab = {
103+ render: "Tab",
104+ attributes: {
105+ label: {
106+ type: String,
107+ },
108+ },
109+};
110+
111+const tabs = {
112+ render: "Tabs",
113+ attributes: {},
114+ children: [],
115+ transform(node, config) {
116+ const labels = node
117+ .transformChildren(config)
118+ .filter((child) => child && child.name === "Tab")
119+ .map((tab) => (typeof tab === "object" ? tab.attributes.label : null));
120+
121+ return new Tag(this.render, { labels }, tabs);
122+ },
123+};
124+
125+export default function Markdown({ post }) {
126+ const ast = Markdoc.parse(post.content);
127+ const finalAst = footnoteParse(ast);
128+ const content = Markdoc.transform(finalAst, {
129+ nodes: {
130+ fence,
131+ heading,
132+ footnoteItem,
133+ footnoteRef,
134+ },
135+ // tags: {
136+ // tabs,
137+ // tab,
138+ // },
139+ });
140+ return Markdoc.renderers.react(content, React, {
141+ components: {
142+ Fence,
143+ Tabs,
144+ Tab,
145+ },
146+ });
94147}
148+
149+const footnoteParse = (partialAst) => {
150+ const endNotePattern = /\[\^(\d+)\]:\s/m;
151+ const inlineFnPattern = /\[\^(\d+)\](?!:)/gm;
152+
153+ function* getFootnoteItemNodes(nodes) {
154+ let results = [];
155+ let itemsProcessed = 0;
156+ for (const n of nodes) {
157+ itemsProcessed += 1;
158+ if (n.type !== "softbreak") results.push(n);
159+ if (n.type === "softbreak" || itemsProcessed === nodes.length) {
160+ yield results;
161+ results = [];
162+ }
163+ }
164+ }
165+
166+ function findFootnoteContainerNode(ast) {
167+ const generator = ast.walk();
168+ let container;
169+ let match = false;
170+ for (const node of generator) {
171+ if (
172+ node.attributes.content &&
173+ endNotePattern.test(node.attributes.content)
174+ ) {
175+ match = true;
176+ generator.return();
177+ }
178+ if (node.type === "inline") container = node;
179+ }
180+ return match ? container : undefined;
181+ }
182+
183+ function processFootnotes(ast) {
184+ // Get a refrence to the node containing endNotes; if not present, early return
185+ const fnContainerNode = findFootnoteContainerNode(ast);
186+ if (!fnContainerNode) return;
187+ // We have footnotes, so create a new list node which will contain the list of endNotes
188+ const fnList = new Ast.Node("list", {
189+ ordered: true,
190+ class: "footnotes",
191+ });
192+ // Get the children nodes for each footnote item
193+ const fnItems = getFootnoteItemNodes(fnContainerNode.children);
194+ for (const fn of fnItems) {
195+ const token = endNotePattern.exec(fn[0].attributes.content);
196+ if (token) {
197+ // Remove the markdown footnote syntax (e.g. [^1]) from the string
198+ fn[0].attributes.content = fn[0].attributes.content.replace(
199+ token[0],
200+ ""
201+ );
202+ // Create a new footnote item and append to the fnList
203+ const id = token[1];
204+ const fnItem = new Ast.Node(
205+ "footnoteItem",
206+ {
207+ id: `fn${id}`,
208+ href: `#fnref${id}`,
209+ },
210+ fn
211+ );
212+ fnList.push(fnItem);
213+ }
214+ }
215+ ast.children.pop(); // remove the last paragraph in the doc being replaced by the fnList
216+ ast.push(fnList);
217+ }
218+
219+ function processFootnoteRefs(ast) {
220+ let parent = ast;
221+ for (const node of ast.walk()) {
222+ if (node.attributes.content) {
223+ // Check if there's a footnote ref token
224+ const token = inlineFnPattern.exec(node.attributes.content);
225+ if (token) {
226+ // Break the string where the foonote ref is, assign first part of string to the current node content
227+ const [prevText, nextText] = node.attributes.content.split(token[0]);
228+ node.attributes.content = prevText;
229+
230+ // Create a footnote node and insert it in the tree
231+ const id = token[1];
232+ const fn = new Ast.Node("footnoteRef", {
233+ id: `fnref${id}`,
234+ href: `#fn${id}`,
235+ label: `${id}`,
236+ });
237+ parent.push(fn);
238+
239+ // Create a text node for the text which follows after the footnote and insert it in the tree
240+ const next = new Ast.Node("text", { content: nextText });
241+ parent.push(next);
242+ }
243+ }
244+ // If the node is of inline type, update parent
245+ if (node.type == "inline") parent = node;
246+ }
247+ }
248+
249+ processFootnoteRefs(partialAst);
250+ processFootnotes(partialAst);
251+ return partialAst;
252+};