@@ -1,94 +1,252 @@
1−import remark from "remark";
1+import React from "react";
2−import gfm from "remark-gfm";
2+import Markdoc, { Ast, Node, Tag } from "@markdoc/markdoc";
3−import slug from "remark-slug";
4−import heading from "remark-heading-id";
3+import { heading } from "./schema/Heading.markdoc";
5−import remarkprism from "remark-prism";
4+import { footnoteRef } from "./schema/footnoteRef.markdoc";
6−import normalize from "mdurl/encode";
5+import { footnoteItem } from "./schema/footnoteItem.markdoc";
7−import merge from "deepmerge";
6+import Tabs from "../components/Tabs";
8−import github from "hast-util-sanitize/lib/github";
7+import Tab from "../components/Tab";
9−import remarkParse from "remark-parse";
8+import Highlight, { defaultProps } from "prism-react-renderer";
10−import remarkRehype, { defaultHandlers, all } from "remark-rehype";
9+import Prism from "prism-react-renderer/prism";
11−import rehypeStringify from "rehype-stringify";
10+(typeof global !== "undefined" ? global : window).Prism = Prism;
12−import rehypeRaw from "rehype-raw";
11+require("prismjs/components/prism-hoon");
14// img is wrapped in figure so that images can be extra wide in the blog posts
13// img is wrapped in figure so that images can be extra wide in the blog posts
15−function img(h, node) {
14+// function img(h, node) {
16− var props = { src: normalize(node.url), alt: node.alt };
15+// var props = { src: normalize(node.url), alt: node.alt };
17− if (node.title !== null && node.title !== undefined) {
16+// if (node.title !== null && node.title !== undefined) {
18− props.title = node.title;
17+// props.title = node.title;
20− return {
19+// return {
21− type: "element",
20+// type: "element",
22− tagName: "figure",
21+// tagName: "figure",
23− properties: {},
22+// properties: {},
24− children: [
23+// children: [
26− type: "element",
25+// type: "element",
27− tagName: "img",
26+// tagName: "img",
28− properties: props,
27+// properties: props,
34−function a(h, node) {
33+// function a(h, node) {
35− const url = node.url;
34+// const url = node.url;
38− url.includes("https://urbit.org") ||
37+// url.includes("https://urbit.org") ||
39− url.startsWith("/") ||
38+// url.startsWith("/") ||
40− url.startsWith("#")
39+// url.startsWith("#")
42− return {
41+// return {
43− type: "element",
42+// type: "element",
44− tagName: "a",
43+// tagName: "a",
45− properties: {
44+// properties: {
46− href: url,
45+// href: url,
48− children: all(h, node),
47+// children: all(h, node),
50− } else {
49+// } else {
51− return {
50+// return {
52− type: "element",
51+// type: "element",
53− tagName: "a",
52+// tagName: "a",
54− properties: {
53+// properties: {
55− target: "_blank",
54+// target: "_blank",
56− rel: "noopener",
55+// rel: "noopener",
57− href: url,
56+// href: url,
59− children: all(h, node),
58+// children: all(h, node),
63+// const handlers = defaultHandlers;
65+// handlers.image = img;
67+export function Fence({ children, language }) {
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 })} />
64−const handlers = defaultHandlers;
91+const fence = {
65−handlers.link = a;
92+ render: "Fence",
69− allowDangerousHtml: true,
71− sanitize: merge(github, {
72− // remove user-content from github.json to remark-slug work as expected
74− attributes: { "*": ["className"] },
93+ attributes: {
97+ "The programming language of the code block. Place it after the backticks.",
78−// Converts markdown strings into markdown HTML/React components
102+const tab = {
115+ transform(node, config) {
117+ .transformChildren(config)
118+ .filter((child) => child && child.name === "Tab")
119+ .map((tab) => (typeof tab === "object" ? tab.attributes.label : null));
121+ return new Tag(this.render, { labels }, tabs);
79−export default async function Markdown({ post }, disablePlugins) {
125+export default function Markdown({ post }) {
80− const result = await remark()
126+ const ast = Markdoc.parse(post.content);
81− .use(remarkParse)
127+ const finalAst = footnoteParse(ast);
82− .use(remarkprism, {
128+ const content = Markdoc.transform(finalAst, {
83− plugins: !disablePlugins ? ["show-invisibles"] : [],
129+ nodes: {
87− .use(heading)
131+ heading,
88− .use(remarkRehype, options)
132+ footnoteItem,
89− .use(rehypeRaw)
133+ footnoteRef,
90− .use(rehypeStringify)
134+ },
91− .process(post.content);
135+ // tags: {
93− return result.toString();
137+ // tab,
140+ return Markdoc.renderers.react(content, React, {
149+const footnoteParse = (partialAst) => {
150+ const endNotePattern = /\[\^(\d+)\]:\s/m;
151+ const inlineFnPattern = /\[\^(\d+)\](?!:)/gm;
153+ function* getFootnoteItemNodes(nodes) {
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) {
166+ function findFootnoteContainerNode(ast) {
167+ const generator = ast.walk();
170+ for (const node of generator) {
172+ node.attributes.content &&
173+ endNotePattern.test(node.attributes.content)
178+ if (node.type === "inline") container = node;
180+ return match ? container : undefined;
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", {
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);
197+ // Remove the markdown footnote syntax (e.g. [^1]) from the string
198+ fn[0].attributes.content = fn[0].attributes.content.replace(
202+ // Create a new footnote item and append to the fnList
203+ const id = token[1];
204+ const fnItem = new Ast.Node(
208+ href: `#fnref${id}`,
212+ fnList.push(fnItem);
215+ ast.children.pop(); // remove the last paragraph in the doc being replaced by the fnList
219+ function processFootnoteRefs(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);
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;
230+ // Create a footnote node and insert it in the tree
231+ const id = token[1];
232+ const fn = new Ast.Node("footnoteRef", {
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 });
244+ // If the node is of inline type, update parent
245+ if (node.type == "inline") parent = node;
249+ processFootnoteRefs(partialAst);
250+ processFootnotes(partialAst);