Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@
},
"dependencies": {
"@observablehq/runtime": "^5.9.4",
"@viz-js/viz": "^3.2.3",
"acorn": "^8.11.2",
"acorn-walk": "^8.3.0",
"fast-array-diff": "^1.1.0",
"fast-deep-equal": "^3.1.3",
"gray-matter": "^4.0.3",
"highlight.js": "^11.8.0",
"jsdom": "^22.1.0",
"katex": "^0.16.9",
"linkedom": "^0.15.6",
"markdown-it": "^13.0.2",
Expand Down
60 changes: 59 additions & 1 deletion src/markdown.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {readFile} from "node:fs/promises";
import {join} from "node:path";
import {instance} from "@viz-js/viz";
import {type Patch, type PatchItem, getPatch} from "fast-array-diff";
import equal from "fast-deep-equal";
import matter from "gray-matter";
import hljs from "highlight.js";
import jsdom from "jsdom";
import katex from "katex";
import {parseHTML} from "linkedom";
import MarkdownIt from "markdown-it";
Expand Down Expand Up @@ -61,6 +63,9 @@ interface ParseContext {

const TEXT_NODE = 3; // Node.TEXT_NODE

// For Dot SSR
const viz = await instance();

// Returns true if the given document contains exactly one top-level element,
// ignoring any surrounding whitespace text nodes.
function isSingleElement(document: Document): boolean {
Expand Down Expand Up @@ -88,7 +93,7 @@ function getLiveSource(content, language, option): {source?: string; html?: stri
: language === "tex"
? maybeStaticTeX(content, {displayMode: true})
: language === "dot"
? {source: transpileTag(content, "dot", false)}
? maybeStaticDot(content)
: language === "mermaid"
? {source: transpileTag(content, "await mermaid", false)}
: {};
Expand All @@ -112,6 +117,59 @@ function maybeStaticTeX(content, {displayMode = false} = {}) {
}
}

function maybeStaticDot(content) {
// We try SSR first. katex.renderToString errors when the expression contains
// some ${interpolation}, so this guarantees that interpolations will be
// handled in the browser. By way of consequence, TeX errors stemming from
// static text (e.g., ParseError on tex`\left{x}`) are handled in the browser,
// and don't stop the build process.
try {
// TODO: unique insertion of the TeX stylesheet?
return {html: dot(content)};
} catch {
return {source: transpileTag(content, "dot", true)};
}
}

// SSR, see client.js for the client counterpart
function dot(string) {
const {JSDOM} = jsdom;
const {DOMParser} = global;
global.DOMParser = new JSDOM().window.DOMParser;
const svg = viz.renderSVGElement(string, {
graphAttributes: {
bgcolor: "none",
color: "#00000101",
fontcolor: "#00000101",
fontname: "var(--sans-serif)",
fontsize: "12"
},
nodeAttributes: {
color: "#00000101",
fontcolor: "#00000101",
fontname: "var(--sans-serif)",
fontsize: "12"
},
edgeAttributes: {
color: "#00000101"
}
});
// @ts-expect-error stupid
for (const e of svg.querySelectorAll("[stroke='#000001'][stroke-opacity='0.003922']")) {
e.setAttribute("stroke", "currentColor");
e.removeAttribute("stroke-opacity");
}
// @ts-expect-error stupid
for (const e of svg.querySelectorAll("[fill='#000001'][fill-opacity='0.003922']")) {
e.setAttribute("fill", "currentColor");
e.removeAttribute("fill-opacity");
}
// @ts-expect-error stupid
svg.style = "max-width: 100%; height: auto;";
global.DOMParser = DOMParser;
return svg.outerHTML;
}

function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: string): RenderRule {
return (tokens, idx, options, context: ParseContext, self) => {
const token = tokens[idx];
Expand Down
Loading