on abf1b49 help sign in
urbit.org/components/Markdown.js
285 lines 7.3 kB history raw
1import React from "react";
2import Link from "next/link";
3import Markdoc, { Ast, Tag } from "@markdoc/markdoc";
4import { heading } from "./schema/Heading.markdoc";
5import { footnoteRef } from "./schema/footnoteRef.markdoc";
6import { footnoteItem } from "./schema/footnoteItem.markdoc";
7import { link } from "./schema/link.markdoc";
8import { image } from "./schema/image.markdoc";
9import { sup } from "./schema/superscript.markdoc";
10import Tabs from "./content/Tabs";
11import Tab from "./content/Tab";
12import Button from "./content/Button";
13import Callout from "./content/Callout";
14import Highlight, { defaultProps } from "prism-react-renderer";
15import parse from "html-react-parser";
16import Prism from "prism-react-renderer/prism";
17(typeof global !== "undefined" ? global : window).Prism = Prism;
18require("prismjs/components/prism-hoon");
20export function Fence({ children, language }) {
21 return (
22 <Highlight
23 {...defaultProps}
24 key={language}
25 language={language}
26 code={children}
27 theme={undefined}
28 >
29 {({ className, style, tokens, getLineProps, getTokenProps }) => (
30 <pre className={className} style={style}>
31 {tokens.slice(0, -1).map((line, i) => (
32 <div {...getLineProps({ line, key: i })}>
33 {line.map((token, key) => (
34 <span {...getTokenProps({ token, key })} />
35 ))}
36 </div>
37 ))}
38 </pre>
39 )}
40 </Highlight>
41 );
44const fence = {
45 render: "Fence",
46 attributes: {
47 language: {
48 type: String,
49 description:
50 "The programming language of the code block. Place it after the backticks.",
51 },
52 },
53};
55const tab = {
56 render: "Tab",
57 attributes: {
58 label: {
59 type: String,
60 },
61 },
62};
64const tabs = {
65 render: "Tabs",
66 attributes: {},
67 transform(node, config) {
68 const labels = node
69 .transformChildren(config)
70 .filter((child) => child && child.name === "Tab")
71 .map((tab) => (typeof tab === "object" ? tab.attributes.label : null));
73 return new Tag(this.render, { labels }, node.transformChildren(config));
74 },
75};
77const button = {
78 render: "Button",
79 attributes: {
80 label: {
81 type: String,
82 },
83 link: {
84 type: String,
85 },
86 color: {
87 type: String,
88 },
89 },
90};
92const callout = {
93 render: "Callout",
94 attributes: {
95 title: {
96 type: String,
97 },
98 },
99};
101const customFence = {
102 render: "CustomFence",
103 attributes: {},
104};
106const html = {
107 render: "RenderHtml",
108 attributes: {
109 content: {
110 type: String,
111 },
112 },
113 transform(node) {
114 return node.attributes.content
115 ? new Tag("RenderHtml", { content: node.attributes.content }, [
116 node.inline,
117 ])
118 : null;
119 },
120};
122const RenderHtml = ({ content }) => {
123 return parse(content);
124};
126const superscript = ({ children }) => <sup>{children}</sup>;
128const CustomFence = ({ children }) => <pre>{children}</pre>;
130const NextLink = ({ href, target, children }) => {
131 return (
132 <Link href={href} passHref>
133 <a target={target}>{children}</a>
134 </Link>
135 );
136};
138export function MarkdownParse({ post }) {
139 const tokeniser = new Markdoc.Tokenizer({ html: true, linkify: true });
140 const tokens = tokeniser.tokenize(post.content);
141 const ast = Markdoc.parse(tokens);
142 const finalAst = footnoteParse(ast);
143 return Markdoc.transform(finalAst, {
144 nodes: {
145 fence,
146 heading,
147 image,
148 link,
149 footnoteItem,
150 footnoteRef,
151 html,
152 sup,
153 },
154 tags: {
155 tabs,
156 tab,
157 button,
158 callout,
159 customFence,
160 RenderHtml,
161 superscript,
162 NextLink,
163 },
164 });
167export default function Markdown({ content }) {
168 return Markdoc.renderers.react(content, React, {
169 components: {
170 Fence,
171 Tabs,
172 Tab,
173 Button,
174 Callout,
175 CustomFence,
176 RenderHtml,
177 NextLink,
178 },
179 });
182const footnoteParse = (partialAst) => {
183 const endNotePattern = /\[\^(\d+)\]:\s/m;
184 const inlineFnPattern = /\[\^(\d+)\](?!:)/gm;
186 function* getFootnoteItemNodes(nodes) {
187 let results = [];
188 let itemsProcessed = 0;
189 for (const n of nodes) {
190 itemsProcessed += 1;
191 if (n.type !== "softbreak") results.push(n);
192 if (n.type === "softbreak" || itemsProcessed === nodes.length) {
193 yield results;
194 results = [];
195 }
196 }
197 }
199 function findFootnoteContainerNode(ast) {
200 const generator = ast.walk();
201 let container;
202 let match = false;
203 for (const node of generator) {
204 if (
205 node.attributes.content &&
206 endNotePattern.test(node.attributes.content)
207 ) {
208 match = true;
209 generator.return();
210 }
211 if (node.type === "inline") container = node;
212 }
213 return match ? container : undefined;
214 }
216 function processFootnotes(ast) {
217 // Get a refrence to the node containing endNotes; if not present, early return
218 const fnContainerNode = findFootnoteContainerNode(ast);
219 if (!fnContainerNode) return;
220 // We have footnotes, so create a new list node which will contain the list of endNotes
221 const fnList = new Ast.Node("list", {
222 ordered: true,
223 class: "footnotes",
224 });
225 // Get the children nodes for each footnote item
226 const fnItems = getFootnoteItemNodes(fnContainerNode.children);
227 for (const fn of fnItems) {
228 const token = endNotePattern.exec(fn[0].attributes.content);
229 if (token) {
230 // Remove the markdown footnote syntax (e.g. [^1]) from the string
231 fn[0].attributes.content = fn[0].attributes.content.replace(
232 token[0],
233 ""
234 );
235 // Create a new footnote item and append to the fnList
236 const id = token[1];
237 const fnItem = new Ast.Node(
238 "footnoteItem",
239 {
240 id: `fn${id}`,
241 href: `#fnref${id}`,
242 },
243 fn
244 );
245 fnList.push(fnItem);
246 }
247 }
248 ast.children.pop(); // remove the last paragraph in the doc being replaced by the fnList
249 ast.push(fnList);
250 }
252 function processFootnoteRefs(ast) {
253 let parent = ast;
254 for (const node of ast.walk()) {
255 if (node.attributes.content) {
256 // Check if there's a footnote ref token
257 const token = inlineFnPattern.exec(node.attributes.content);
258 if (token) {
259 // Break the string where the foonote ref is, assign first part of string to the current node content
260 const [prevText, nextText] = node.attributes.content.split(token[0]);
261 node.attributes.content = prevText;
263 // Create a footnote node and insert it in the tree
264 const id = token[1];
265 const fn = new Ast.Node("footnoteRef", {
266 id: `fnref${id}`,
267 href: `#fn${id}`,
268 label: `${id}`,
269 });
270 parent.push(fn);
272 // Create a text node for the text which follows after the footnote and insert it in the tree
273 const next = new Ast.Node("text", { content: nextText });
274 parent.push(next);
275 }
276 }
277 // If the node is of inline type, update parent
278 if (node.type == "inline") parent = node;
279 }
280 }
282 processFootnoteRefs(partialAst);
283 processFootnotes(partialAst);
284 return partialAst;
285};