Skip to content

Commit f2413fd

Browse files
committed
graphviz: optionally pre-generate the SVGs
Since the WebAssembly version of Graphviz is not exactly light (1.4MB at the time of writing), and since it is a waste to let everybody re-render the SVG client-side over and over again even though the diagram's definition hasn't changed, let's add a script that performs that rendering "on the server side" (more precisely: during deployment). This script takes no arguments and post-processes the output of Hugo in `public/`. For performance reasons, it requires the list of files that contain Graphviz diagrams. With this here site, it might not matter much because there are only a handful images here. However, I want to reuse the same method on git-scm.com where there _are_ thousands of files that do not contain any Graphviz diagrams, and therefore it is a necessary optimization to process only the files that _do_ contain Graphviz diagrams. To get that list, a new layout and page are added that Hugo processes, identifyng said list of files and writing the result to `public/diagram-list.html`. Since this script runs via Node.JS and therefore lacks the convenient built-in HTML parser of browser-based JavaScript engines, a prerequisite is to run `npm install` so that the `node-html-parser` package is available. A note about the slightly inelegant way the `graphviz` class is added to the generated SVGs: Sadly, `node-html-parser` is not smart enough to handle `element.classList.add(...)` followed by `element.toString()` correctly, and as a result the output would _not_ have the added class. But there's always crude string processing to save the day. Signed-off-by: Johannes Schindelin <[email protected]>
1 parent d9f9c5b commit f2413fd

File tree

4 files changed

+56
-1
lines changed

4 files changed

+56
-1
lines changed

content/diagram-list.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
layout: diagram-list
3+
---

layouts/diagram-list.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<pre>
2+
{{ range .Site.Pages -}}
3+
{{- if in .RawContent "{{< graphviz" -}}
4+
public{{ .Path }}.html{{ "\n" -}}
5+
{{- end -}}
6+
{{- end -}}
7+
</pre>

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"license": "MIT",
66
"devDependencies": {
77
"@playwright/test": "^1.47.2",
8-
"@types/node": "^22.5.4"
8+
"@types/node": "^22.5.4",
9+
"node-html-parser": "^7.0.1"
910
}
1011
}

script/graphviz-ssr.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env node
2+
3+
import { readFileSync, writeFileSync } from "node:fs"
4+
import { parse } from "node-html-parser"
5+
import Viz from "../static/js/viz-global.js"
6+
7+
/*
8+
* This script post-processes the site as generated via Hugo, replacing the
9+
* `<pre class="graphviz">` elements with inline `<svg>` ones, pre-rendered
10+
* via `viz-js`.
11+
*/
12+
;(async () => {
13+
for (const path of readFileSync("public/diagram-list.html", "utf-8")
14+
.split("\n")
15+
.filter((x) => x.startsWith("public/"))) {
16+
const contents = readFileSync(path, "utf-8")
17+
const html = parse(contents)
18+
const vizImport = html.querySelector('script[src$="viz-global.js"]')
19+
if (!vizImport) {
20+
console.error(`No 'viz-global.js' import found in ${path}; skipping`)
21+
continue
22+
}
23+
vizImport.nextElementSibling.remove() // remove the inline script
24+
vizImport.remove() // remove the import
25+
26+
for (const pre of html.querySelectorAll("pre.graphviz")) {
27+
const engine = pre.getAttribute("engine") || "dot"
28+
const svg = (await Viz.instance()).renderString(pre.textContent, {
29+
format: "svg",
30+
graphAttributes: {
31+
bgcolor: "transparent",
32+
},
33+
engine,
34+
})
35+
const dataURL = `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`
36+
pre.replaceWith(`<img src="${dataURL}"`)
37+
}
38+
console.log(`Rewriting ${path}`)
39+
writeFileSync(`${path}`, html.toString())
40+
}
41+
})().catch((e) => {
42+
console.error(e)
43+
process.exitCode = 1
44+
})

0 commit comments

Comments
 (0)