diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..25e17e0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + + - package-ecosystem: nix + directory: / + schedule: + interval: monthly diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..54ec645 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,31 @@ +name: Pages +on: + push: + branches: [main] + pull_request: +permissions: + contents: read +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 + - uses: DeterminateSystems/magic-nix-cache-action@908b263ff629f4cc17666315b7fd3ec127c6244d # v14 + - run: nix build + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: result + deploy: + if: github.event_name == 'push' + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8dade17 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +book/ +result diff --git a/book.toml b/book.toml new file mode 100644 index 0000000..8eaa8ab --- /dev/null +++ b/book.toml @@ -0,0 +1,19 @@ +[book] +title = "Fungi Protocol Suite" +language = "en" +src = "src" + +[preprocessor.graphviz] +command = "mdbook-graphviz" +after = ["links"] + +[preprocessor.mermaid] +command = "python3 contrib/mermaid_ssr.py" +after = ["links", "graphviz"] + +[preprocessor.katex] +command = "mdbook-katex" +after = ["links"] + +[output.html] +additional-css = ["theme/diagrams.css"] diff --git a/contrib/mermaid_ssr.py b/contrib/mermaid_ssr.py new file mode 100644 index 0000000..82f2bbd --- /dev/null +++ b/contrib/mermaid_ssr.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""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. +""" +import json +import os +import re +import subprocess +import sys +import tempfile + +# Fence length varies: preprocessors that re-serialize the markdown (mdbook-graphviz) +# emit four backticks, so the closing fence has to match the opening one. +FENCE = re.compile( + r"^(?P`{3,})mermaid[^\n]*\n(?P.*?)^(?P=fence)[ \t]*$", + re.S | re.M, +) + + +def render(source, theme, svg_id): + with tempfile.TemporaryDirectory() as tmp: + mmd = os.path.join(tmp, "in.mmd") + svg = os.path.join(tmp, "out.svg") + with open(mmd, "w") as f: + f.write(source) + result = subprocess.run( + ["mmdc", "-i", mmd, "-o", svg, + "-b", "transparent", "-t", theme, "-I", svg_id], + capture_output=True, + text=True, + ) + if result.returncode != 0: + sys.stderr.write(result.stdout + result.stderr) + sys.exit(1) + with open(svg) as f: + return f.read() + + +def block(source, index): + """Both themes of one diagram; css picks which to show. + + The svg is emitted inside a div because `svg` is not a block-level tag + in commonmark, so bare svg markup would be parsed as inline html and + escaped. Each svg needs its own id: mermaid scopes the stylesheet it + embeds by id, so two diagrams sharing one would style each other. + """ + return "\n\n" + "\n".join( + '
\n%s\n
' + % (name, render(source, theme, "mermaid-%d-%s" % (index, name))) + for name, theme in (("light", "default"), ("dark", "dark")) + ) + "\n\n" + + +def walk(items, counter): + for item in items: + chapter = item.get("Chapter") + if not chapter: + continue + + def substitute(match): + counter[0] += 1 + return block(match.group("body"), counter[0]) + + chapter["content"] = FENCE.sub(substitute, chapter["content"]) + walk(chapter["sub_items"], counter) + + +def main(): + if len(sys.argv) > 1 and sys.argv[1] == "supports": + sys.exit(0) + _context, book = json.load(sys.stdin) + # mdbook 0.5 renamed the top level key from `sections` to `items`. + walk(book.get("items", book.get("sections")), [0]) + json.dump(book, sys.stdout) + + +if __name__ == "__main__": + main() diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..c061d0d --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1782949081, + "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1785133411, + "narHash": "sha256-Yjv0WEg39KRYS0rBdTbu6Fc/or/ihAKk13W9sQ6VWd0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "2f5a153c270b70cb0f8c11f46d96d6d3bc39f4e3", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-26.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1782614948, + "narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..75d9c81 --- /dev/null +++ b/flake.nix @@ -0,0 +1,23 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; + flake-parts.url = "github:hercules-ci/flake-parts"; + }; + + outputs = + inputs: + inputs.flake-parts.lib.mkFlake { inherit inputs; } { + systems = [ + "aarch64-darwin" + "aarch64-linux" + "x86_64-linux" + ]; + + imports = [ + ./nix/mermaid.nix + ./nix/preprocessors.nix + ./nix/site.nix + ./nix/devshell.nix + ]; + }; +} diff --git a/nix/devshell.nix b/nix/devshell.nix new file mode 100644 index 0000000..5204607 --- /dev/null +++ b/nix/devshell.nix @@ -0,0 +1,16 @@ +{ ... }: +{ + perSystem = + { pkgs, mmdc, ... }: + { + devShells.default = pkgs.mkShell { + packages = [ mmdc ] ++ (with pkgs; [ + mdbook + mdbook-graphviz + mdbook-katex + graphviz + python3 + ]); + }; + }; +} diff --git a/nix/mermaid.nix b/nix/mermaid.nix new file mode 100644 index 0000000..861eb2c --- /dev/null +++ b/nix/mermaid.nix @@ -0,0 +1,29 @@ +{ ... }: +{ + # 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 { + name = "mmdc-with-browser"; + paths = [ pkgs.mermaid-cli ]; + nativeBuildInputs = [ pkgs.makeWrapper ]; + postBuild = '' + browser=$(find -L ${pkgs.playwright-driver.browsers} \ + \( -name chrome-headless-shell -o -name headless_shell \) \ + -type f | head -1) + test -n "$browser" || { echo "no chromium in playwright browsers"; exit 1; } + + # --no-sandbox: chromium's own sandbox cannot nest inside the nix + # build sandbox. + echo "{\"executablePath\":\"$browser\",\"args\":[\"--no-sandbox\"]}" \ + > $out/puppeteer.json + + wrapProgram $out/bin/mmdc \ + --add-flags "--puppeteerConfigFile $out/puppeteer.json" + ''; + }; + }; +} diff --git a/nix/preprocessors.nix b/nix/preprocessors.nix new file mode 100644 index 0000000..c677674 --- /dev/null +++ b/nix/preprocessors.nix @@ -0,0 +1,45 @@ +{ inputs, ... }: +{ + # nixpkgs ships mdbook 0.5 alongside preprocessor releases that predate it + # and still speak the 0.4 protocol, so the packaged set cannot build a book. + # Take the upstream releases that added 0.5 support instead. Drop this file + # once nixpkgs catches up. + perSystem = + { system, ... }: + { + _module.args.pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ + (final: prev: { + mdbook-graphviz = prev.mdbook-graphviz.overrideAttrs (old: rec { + version = "0.3.1"; + src = final.fetchFromGitHub { + owner = "dylanowen"; + repo = "mdbook-graphviz"; + tag = "v${version}"; + hash = "sha256-uqNgP1rRgP6NecReqpinsg7u01gNDpIxX2qag8IyklY="; + }; + cargoDeps = final.rustPlatform.fetchCargoVendor { + inherit src; + hash = "sha256-OBCECv9ZN9xjkOestZbjCXNAA/hAl2u0AtfqxA+cV78="; + }; + }); + + mdbook-katex = prev.mdbook-katex.overrideAttrs (old: rec { + version = "0.10.0"; + src = final.fetchFromGitHub { + owner = "lzanini"; + repo = "mdbook-katex"; + tag = "v${version}"; + hash = "sha256-bS8SUzpTqQNYKeGPBf1QD4/AL0TWn3NE4M7A8WLEjUE="; + }; + cargoDeps = final.rustPlatform.fetchCargoVendor { + inherit src; + hash = "sha256-YqQ8Uai2mCG+1X/TmWJPszLYumOjF455Aa5WldgGXF0="; + }; + }); + }) + ]; + }; + }; +} diff --git a/nix/site.nix b/nix/site.nix new file mode 100644 index 0000000..f498b36 --- /dev/null +++ b/nix/site.nix @@ -0,0 +1,33 @@ +{ ... }: +{ + perSystem = + { pkgs, mmdc, ... }: + let + site = pkgs.stdenvNoCC.mkDerivation { + name = "fungi-docs"; + src = ../.; + + nativeBuildInputs = [ mmdc ] ++ (with pkgs; [ + mdbook + mdbook-graphviz + mdbook-katex + graphviz + python3 + ]); + + buildPhase = '' + # mmdc writes a chromium profile under $HOME. + export HOME=$(mktemp -d) + mdbook build -d $out + ''; + + dontInstall = true; + }; + in + { + packages = { + inherit site; + default = site; + }; + }; +} diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..dae3167 --- /dev/null +++ b/src/README.md @@ -0,0 +1 @@ +{{#include ../README.md}} diff --git a/src/SUMMARY.md b/src/SUMMARY.md new file mode 100644 index 0000000..43a28d0 --- /dev/null +++ b/src/SUMMARY.md @@ -0,0 +1,3 @@ +# Summary + +[Introduction](README.md) diff --git a/theme/diagrams.css b/theme/diagrams.css new file mode 100644 index 0000000..bb71486 --- /dev/null +++ b/theme/diagrams.css @@ -0,0 +1,69 @@ +/* Diagrams are rendered to svg at build time, so they cannot restyle + themselves the way client-side mermaid does. Mermaid diagrams are emitted + once per theme and swapped here; graphviz keeps whatever its .dot source + asks for, with only graphviz's own defaults lifted for the dark themes. */ +.mdbook-graphviz-output, +.mermaid { + overflow-x: auto; + margin: 1rem 0; +} + +/* Scale wide graphs down to the content column instead of clipping them; + the svg viewBox keeps them sharp at any size. */ +.mdbook-graphviz-output svg, +.mermaid svg { + display: block; + max-width: 100%; + height: auto; +} + +/* On a phone the column is narrow enough that scaling to fit would leave the + labels unreadable, so diagrams keep their own size and scroll instead. The + diagram container is allowed past the page padding first, so the ones that + only just overflow still fit without scrolling. */ +@media (max-width: 700px) { + .mdbook-graphviz-output, + .mermaid { + margin-left: calc(-1 * var(--page-padding, 15px)); + margin-right: calc(-1 * var(--page-padding, 15px)); + padding: 0 var(--page-padding, 15px); + } + + .mdbook-graphviz-output svg, + .mermaid svg { + max-width: none; + } +} + +.mdbook-graphviz-output svg > g > polygon:first-of-type { + fill: transparent; +} + +.mermaid-dark { + display: none; +} + +html:is(.coal, .navy, .ayu) .mermaid-dark { + display: block; +} + +html:is(.coal, .navy, .ayu) .mermaid-light { + display: none; +} + +/* A .dot is expected to pick colours that hold up on either page background, + so nothing here touches them. Only graphviz's own defaults are lifted: plain + black disappears against a dark page, and a diagram that says nothing about + colour should still be readable. */ +html:is(.coal, .navy, .ayu) .mdbook-graphviz-output [stroke="black"] { + stroke: #b7b7b7; +} + +/* Arrow heads, not node bodies: an unfilled shape carries fill="none". */ +html:is(.coal, .navy, .ayu) .mdbook-graphviz-output [fill="black"] { + fill: #b7b7b7; +} + +html:is(.coal, .navy, .ayu) .mdbook-graphviz-output text:not([fill]) { + fill: #b7b7b7; +}