Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
18 changes: 17 additions & 1 deletion contrib/mermaid_ssr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
"""mdbook preprocessor: render ```mermaid blocks to inline SVG with mmdc.

Expects the `mmdc` from nix/mermaid.nix, which comes wrapped with the
headless chromium it drives.
headless chromium it drives. If MERMAID_SVG_DIR is set, it renders nothing
itself: it takes each diagram from MERMAID_SVG_DIR/<sha256 of its
source>/<theme>.svg, as nix/mermaid.nix renders them, with PLACEHOLDER
standing for the svg id.
"""
import hashlib
import json
import os
import re
Expand All @@ -17,9 +21,21 @@
r"^(?P<fence>`{3,})mermaid[^\n]*\n(?P<body>.*?)^(?P=fence)[ \t]*$",
re.S | re.M,
)
PLACEHOLDER = "MERMAID_SSR_ID"


def render(source, theme, svg_id):
rendered = os.environ.get("MERMAID_SVG_DIR")
if rendered:
digest = hashlib.sha256(source.encode()).hexdigest()
try:
with open(os.path.join(rendered, digest, theme + ".svg")) as f:
return f.read().replace(PLACEHOLDER, svg_id)
except FileNotFoundError:
sys.stderr.write(
"mermaid_ssr: %s has no diagram %s; nix/mermaid.nix did not "
"find this block:\n%s" % (rendered, digest, source))
sys.exit(1)
with tempfile.TemporaryDirectory() as tmp:
mmd = os.path.join(tmp, "in.mmd")
svg = os.path.join(tmp, "out.svg")
Expand Down
27 changes: 27 additions & 0 deletions contrib/test_mermaid_ssr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import hashlib

import mermaid_ssr
import pytest

SOURCE = "flowchart LR\n a --> b\n"


def prerender(tmp_path, source, theme, svg):
diagram = tmp_path / hashlib.sha256(source.encode()).hexdigest()
diagram.mkdir(exist_ok=True)
(diagram / (theme + ".svg")).write_text(svg)


def test_prerendered_diagram_gets_its_own_id(tmp_path, monkeypatch):
prerender(tmp_path, SOURCE, "dark",
'<svg id="MERMAID_SSR_ID"><style>#MERMAID_SSR_ID{}</style></svg>')
monkeypatch.setenv("MERMAID_SVG_DIR", str(tmp_path))
assert mermaid_ssr.render(SOURCE, "dark", "mermaid-3-dark") == (
'<svg id="mermaid-3-dark"><style>#mermaid-3-dark{}</style></svg>')


def test_diagram_missing_from_the_prerendered_set_fails(tmp_path, monkeypatch):
prerender(tmp_path, SOURCE, "default", "<svg/>")
monkeypatch.setenv("MERMAID_SVG_DIR", str(tmp_path))
with pytest.raises(SystemExit):
mermaid_ssr.render(SOURCE + " b --> c\n", "default", "mermaid-1-light")
5 changes: 5 additions & 0 deletions cspell/tooling.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ devshell
DNSSEC
dontInstall
dylanowen
fileset
foldl
fontconfig
Graphviz
KaTeX
Expand All @@ -14,8 +16,11 @@ mmdc
NixOS
nixpkgs
pkgs
prerender
prerendered
pytest
PyYAML
stdenv
svgs
toplevel
unfetched
82 changes: 76 additions & 6 deletions nix/mermaid.nix
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{ ... }:
{
# The only chromium nixpkgs has for darwin sits inside playwright's prebuilt
# browsers, under a directory whose name changes with every bump. Resolve it
# once and bake it in, so no caller has to export a path.
perSystem =
{ pkgs, ... }:
{
_module.args.mmdc = pkgs.symlinkJoin {
{ pkgs, lib, ... }:
let
# The only chromium nixpkgs has for darwin sits inside playwright's
# prebuilt browsers, under a directory whose name changes with every
# bump. Resolve it once and bake it in, so no caller has to export a
# path.
mmdc = pkgs.symlinkJoin {
name = "mmdc-with-browser";
paths = [ pkgs.mermaid-cli ];
nativeBuildInputs = [ pkgs.makeWrapper ];
Expand All @@ -25,5 +26,74 @@
--add-flags "--puppeteerConfigFile $out/puppeteer.json"
'';
};

# mermaid measures text with getBBox and rejects a zero-sized box, so
# the build sandbox has to carry a font of its own.
fontconfig = pkgs.makeFontsConf { fontDirectories = [ pkgs.dejavu_fonts ]; };

# The bodies of the ```mermaid blocks in a markdown text, as FENCE in
# contrib/mermaid_ssr.py finds them.
blocks =
text:
let
step =
state: line:
if state.fence == null then
let
open = builtins.match "(`{3,})mermaid.*" line;
in
if open == null then state else state // { fence = builtins.head open; }
else if builtins.match "${state.fence}[ \t]*" line != null then
{
fence = null;
lines = [ ];
found = state.found ++ [ (lib.concatMapStrings (l: l + "\n") state.lines) ];
}
else
state // { lines = state.lines ++ [ line ]; };
in
(builtins.foldl' step {
fence = null;
lines = [ ];
found = [ ];
} (lib.splitString "\n" text)).found;

# One derivation per diagram, whose only input is the diagram's text,
# so edits elsewhere in the book leave it cached. mermaid_ssr.py finds
# it by the sha256 of that text, and replaces the placeholder svg ids.
render =
body:
let
hash = builtins.hashString "sha256" body;
in
{
name = hash;
path =
pkgs.runCommand "mermaid-${builtins.substring 0 12 hash}"
{ nativeBuildInputs = [ mmdc ]; }
''
# mmdc writes a chromium profile under $HOME.
export HOME=$(mktemp -d)
export FONTCONFIG_FILE=${fontconfig}
mkdir $out
for theme in default dark; do
mmdc -i ${builtins.toFile "diagram.mmd" body} -o $out/$theme.svg \
-b transparent -t $theme -I MERMAID_SSR_ID
done
'';
};
in
{
_module.args = {
inherit mmdc fontconfig;

# The diagrams of these markdown files, rendered, as mermaid_ssr.py
# expects to find them in MERMAID_SVG_DIR.
mermaidSvgs =
files:
pkgs.linkFarm "mermaid-svgs" (
map render (lib.unique (lib.concatMap (f: blocks (builtins.readFile f)) files))
);
};
};
}
38 changes: 29 additions & 9 deletions nix/site.nix
Original file line number Diff line number Diff line change
@@ -1,26 +1,46 @@
{ ... }:
{
perSystem =
{ pkgs, mmdc, ... }:
{
pkgs,
lib,
fontconfig,
mermaidSvgs,
...
}:
let
# Only what the book is built from, so that changes to the nix files,
# the tests or the spelling dictionaries do not rebuild it.
src = lib.fileset.toSource {
root = ../.;
fileset = lib.fileset.unions [
../README.md
../book.toml
../docs
../theme
(lib.fileset.fileFilter (f: f.hasExt "py" && !lib.hasPrefix "test_" f.name) ../contrib)
];
};

site = pkgs.stdenvNoCC.mkDerivation {
name = "fungi-docs";
src = ../.;
inherit src;

nativeBuildInputs = [ mmdc ] ++ (with pkgs; [
nativeBuildInputs = with pkgs; [
mdbook
mdbook-graphviz
mdbook-katex
graphviz
(python3.withPackages (ps: [ ps.pyyaml ]))
]);
];

MERMAID_SVG_DIR = mermaidSvgs (
[ ../README.md ]
++ lib.filter (f: lib.hasSuffix ".md" (toString f)) (lib.filesystem.listFilesRecursive ../docs)
);

buildPhase = ''
# mmdc writes a chromium profile under $HOME.
export HOME=$(mktemp -d)
# mermaid measures text with getBBox and rejects a zero-sized box, so
# the build sandbox has to carry a font of its own.
export FONTCONFIG_FILE=${pkgs.makeFontsConf { fontDirectories = [ pkgs.dejavu_fonts ]; }}
export FONTCONFIG_FILE=${fontconfig}
python3 contrib/book.py .
mdbook build -d $out
'';
Expand Down
Loading