HASH e646511a19c0
DATE 2026-07-17
SUBJECT web: diagram fences, hand-drawn ascii and generated mermaid
FILES 10 CHANGED
HASH e646511a19c0
DATE 2026-07-17
SUBJECT web: diagram fences, hand-drawn ascii
and generated mermaid
FILES 10 CHANGED
┌─ flake.lock ───────────────────────────────────────────────────────────────┐
│ diff --git a/flake.lock b/flake.lock │
│ index 3272809..4ac09af 100644 │
│ --- a/flake.lock │
│ +++ b/flake.lock │
│ @@ -1,5 +1,41 @@ │
│ { │
│ "nodes": { │
│ + "alex": { │
│ + "inputs": { │
│ + "flake-utils": "flake-utils", │
│ + "nixpkgs": "nixpkgs", │
│ + "nixpkgs-python": "nixpkgs-python" │
│ + }, │
│ + "locked": { │
│ + "lastModified": 1727894075, │
│ + "narHash": "sha256-2yGczQTBlMkImM95Q7fmrUX16CJ5y1OTe/KlyXwIo8g=", │
│ + "owner": "AlexanderGrooff", │
│ + "repo": "nix-flakes", │
│ + "rev": "248ec1e226de7e3cd86092312910158c1fea3402", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "AlexanderGrooff", │
│ + "repo": "nix-flakes", │
│ + "type": "github" │
│ + } │
│ + }, │
│ + "flake-compat": { │
│ + "flake": false, │
│ + "locked": { │
│ + "lastModified": 1696426674, │
│ + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", │
│ + "owner": "edolstra", │
│ + "repo": "flake-compat", │
│ + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "edolstra", │
│ + "repo": "flake-compat", │
│ + "type": "github" │
│ + } │
│ + }, │
│ "flake-parts": { │
│ "inputs": { │
│ "nixpkgs-lib": "nixpkgs-lib" │
│ @@ -18,18 +54,72 @@ │
│ "type": "github" │
│ } │
│ }, │
│ + "flake-utils": { │
│ + "inputs": { │
│ + "systems": "systems" │
│ + }, │
│ + "locked": { │
│ + "lastModified": 1726560853, │
│ + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", │
│ + "owner": "numtide", │
│ + "repo": "flake-utils", │
│ + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "numtide", │
│ + "repo": "flake-utils", │
│ + "type": "github" │
│ + } │
│ + }, │
│ + "flake-utils_2": { │
│ + "inputs": { │
│ + "systems": "systems_2" │
│ + }, │
│ + "locked": { │
│ + "lastModified": 1726560853, │
│ + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", │
│ + "owner": "numtide", │
│ + "repo": "flake-utils", │
│ + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "id": "flake-utils", │
│ + "type": "indirect" │
│ + } │
│ + }, │
│ + "mermaid-ascii": { │
│ + "inputs": { │
│ + "alex": "alex", │
│ + "flake-utils": "flake-utils_2", │
│ + "nixpkgs": "nixpkgs_3" │
│ + }, │
│ + "locked": { │
│ + "lastModified": 1783875381, │
│ + "narHash": "sha256-n75YJ7Wwat9/ty+4qeEhufcaJKz8SSeFRZ3piK/TSGU=", │
│ + "owner": "AlexanderGrooff", │
│ + "repo": "mermaid-ascii", │
│ + "rev": "a4f23212201cbd62b5a8707b7502b281bb18543f", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "AlexanderGrooff", │
│ + "repo": "mermaid-ascii", │
│ + "type": "github" │
│ + } │
│ + }, │
│ "nixpkgs": { │
│ "locked": { │
│ - "lastModified": 1776877367, │
│ - "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=", │
│ + "lastModified": 1727893769, │
│ + "narHash": "sha256-RQIsGOy+7VzPJ4eHS2UENKGgGnW6WXQJqm+8d7BsRKc=", │
│ "owner": "NixOS", │
│ "repo": "nixpkgs", │
│ - "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57", │
│ + "rev": "bc63f402fdf4ad861b18746174d4e1fd37fcc4b0", │
│ "type": "github" │
│ }, │
│ "original": { │
│ "owner": "NixOS", │
│ - "ref": "nixos-unstable", │
│ "repo": "nixpkgs", │
│ "type": "github" │
│ } │
│ @@ -49,11 +139,77 @@ │
│ "type": "github" │
│ } │
│ }, │
│ + "nixpkgs-python": { │
│ + "inputs": { │
│ + "flake-compat": "flake-compat", │
│ + "nixpkgs": "nixpkgs_2" │
│ + }, │
│ + "locked": { │
│ + "lastModified": 1722978926, │
│ + "narHash": "sha256-sqOOEaKJJSUFBzag/cGeeXV491TrrVFY3DFBs1w20V8=", │
│ + "owner": "cachix", │
│ + "repo": "nixpkgs-python", │
│ + "rev": "7c550bca7e6cf95898e32eb2173efe7ebb447460", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "cachix", │
│ + "repo": "nixpkgs-python", │
│ + "type": "github" │
│ + } │
│ + }, │
│ + "nixpkgs_2": { │
│ + "locked": { │
│ + "lastModified": 1719253556, │
│ + "narHash": "sha256-A/76RFUVxZ/7Y8+OMVL1Lc8LRhBxZ8ZE2bpMnvZ1VpY=", │
│ + "owner": "NixOS", │
│ + "repo": "nixpkgs", │
│ + "rev": "fc07dc3bdf2956ddd64f24612ea7fc894933eb2e", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "NixOS", │
│ + "ref": "nixos-24.05", │
│ + "repo": "nixpkgs", │
│ + "type": "github" │
│ + } │
│ + }, │
│ + "nixpkgs_3": { │
│ + "locked": { │
│ + "lastModified": 1727811607, │
│ + "narHash": "sha256-2ByOBflaIUJKeF9q6efVcYHljZXGZ7MnCWtseRvmpm8=", │
│ + "owner": "NixOS", │
│ + "repo": "nixpkgs", │
│ + "rev": "1839883cd0068572aed75fb9442b508bbd9ef09c", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "id": "nixpkgs", │
│ + "type": "indirect" │
│ + } │
│ + }, │
│ + "nixpkgs_4": { │
│ + "locked": { │
│ + "lastModified": 1776877367, │
│ + "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=", │
│ + "owner": "NixOS", │
│ + "repo": "nixpkgs", │
│ + "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "NixOS", │
│ + "ref": "nixos-unstable", │
│ + "repo": "nixpkgs", │
│ + "type": "github" │
│ + } │
│ + }, │
│ "root": { │
│ "inputs": { │
│ "flake-parts": "flake-parts", │
│ - "nixpkgs": "nixpkgs", │
│ - "systems": "systems" │
│ + "mermaid-ascii": "mermaid-ascii", │
│ + "nixpkgs": "nixpkgs_4", │
│ + "systems": "systems_3" │
│ } │
│ }, │
│ "systems": { │
│ @@ -70,6 +226,36 @@ │
│ "repo": "default", │
│ "type": "github" │
│ } │
│ + }, │
│ + "systems_2": { │
│ + "locked": { │
│ + "lastModified": 1681028828, │
│ + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", │
│ + "owner": "nix-systems", │
│ + "repo": "default", │
│ + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "nix-systems", │
│ + "repo": "default", │
│ + "type": "github" │
│ + } │
│ + }, │
│ + "systems_3": { │
│ + "locked": { │
│ + "lastModified": 1681028828, │
│ + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", │
│ + "owner": "nix-systems", │
│ + "repo": "default", │
│ + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "nix-systems", │
│ + "repo": "default", │
│ + "type": "github" │
│ + } │
│ } │
│ }, │
│ "root": "root", │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ flake.lock ─────────────────────────┐
│ diff --git a/flake.lock b/flake.lock │
│ index 3272809..4ac09af 100644 │
│ --- a/flake.lock │
│ +++ b/flake.lock │
│ @@ -1,5 +1,41 @@ │
│ { │
│ "nodes": { │
│ + "alex": { │
│ + "inputs": { │
│ + "flake-utils": "flake-utils", │
│ + "nixpkgs": "nixpkgs", │
│ + "nixpkgs-python": "nixpkgs-python" │
│ + }, │
│ + "locked": { │
│ + "lastModified": 1727894075, │
│ + "narHash": "sha256-2yGczQTBlMkImM95 │
│ Q7fmrUX16CJ5y1OTe/KlyXwIo8g=", │
│ + "owner": "AlexanderGrooff", │
│ + "repo": "nix-flakes", │
│ + "rev": "248ec1e226de7e3cd8609231291 │
│ 0158c1fea3402", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "AlexanderGrooff", │
│ + "repo": "nix-flakes", │
│ + "type": "github" │
│ + } │
│ + }, │
│ + "flake-compat": { │
│ + "flake": false, │
│ + "locked": { │
│ + "lastModified": 1696426674, │
│ + "narHash": "sha256-kvjfFW7WAETZlt09 │
│ AgDn1MrtKzP7t90Vf7vypd3OL1U=", │
│ + "owner": "edolstra", │
│ + "repo": "flake-compat", │
│ + "rev": "0f9255e01c2351cc7d116c072cb │
│ 317785dd33b33", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "edolstra", │
│ + "repo": "flake-compat", │
│ + "type": "github" │
│ + } │
│ + }, │
│ "flake-parts": { │
│ "inputs": { │
│ "nixpkgs-lib": "nixpkgs-lib" │
│ @@ -18,18 +54,72 @@ │
│ "type": "github" │
│ } │
│ }, │
│ + "flake-utils": { │
│ + "inputs": { │
│ + "systems": "systems" │
│ + }, │
│ + "locked": { │
│ + "lastModified": 1726560853, │
│ + "narHash": "sha256-X6rJYSESBVr3hBoH │
│ 0WbKE5KvhPU5bloyZ2L4K60/fPQ=", │
│ + "owner": "numtide", │
│ + "repo": "flake-utils", │
│ + "rev": "c1dfcf08411b08f6b8615f7d897 │
│ 1a2bfa81d5e8a", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "numtide", │
│ + "repo": "flake-utils", │
│ + "type": "github" │
│ + } │
│ + }, │
│ + "flake-utils_2": { │
│ + "inputs": { │
│ + "systems": "systems_2" │
│ + }, │
│ + "locked": { │
│ + "lastModified": 1726560853, │
│ + "narHash": "sha256-X6rJYSESBVr3hBoH │
│ 0WbKE5KvhPU5bloyZ2L4K60/fPQ=", │
│ + "owner": "numtide", │
│ + "repo": "flake-utils", │
│ + "rev": "c1dfcf08411b08f6b8615f7d897 │
│ 1a2bfa81d5e8a", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "id": "flake-utils", │
│ + "type": "indirect" │
│ + } │
│ + }, │
│ + "mermaid-ascii": { │
│ + "inputs": { │
│ + "alex": "alex", │
│ + "flake-utils": "flake-utils_2", │
│ + "nixpkgs": "nixpkgs_3" │
│ + }, │
│ + "locked": { │
│ + "lastModified": 1783875381, │
│ + "narHash": "sha256-n75YJ7Wwat9/ty+4 │
│ qeEhufcaJKz8SSeFRZ3piK/TSGU=", │
│ + "owner": "AlexanderGrooff", │
│ + "repo": "mermaid-ascii", │
│ + "rev": "a4f23212201cbd62b5a8707b750 │
│ 2b281bb18543f", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "AlexanderGrooff", │
│ + "repo": "mermaid-ascii", │
│ + "type": "github" │
│ + } │
│ + }, │
│ "nixpkgs": { │
│ "locked": { │
│ - "lastModified": 1776877367, │
│ - "narHash": "sha256-EHq1/OX139R1RvBz │
│ OJ0aMRT3xnWyqtHBRUBuO1gFzjI=", │
│ + "lastModified": 1727893769, │
│ + "narHash": "sha256-RQIsGOy+7VzPJ4eH │
│ S2UENKGgGnW6WXQJqm+8d7BsRKc=", │
│ "owner": "NixOS", │
│ "repo": "nixpkgs", │
│ - "rev": "0726a0ecb6d4e08f6adced58726 │
│ b95db924cef57", │
│ + "rev": "bc63f402fdf4ad861b18746174d │
│ 4e1fd37fcc4b0", │
│ "type": "github" │
│ }, │
│ "original": { │
│ "owner": "NixOS", │
│ - "ref": "nixos-unstable", │
│ "repo": "nixpkgs", │
│ "type": "github" │
│ } │
│ @@ -49,11 +139,77 @@ │
│ "type": "github" │
│ } │
│ }, │
│ + "nixpkgs-python": { │
│ + "inputs": { │
│ + "flake-compat": "flake-compat", │
│ + "nixpkgs": "nixpkgs_2" │
│ + }, │
│ + "locked": { │
│ + "lastModified": 1722978926, │
│ + "narHash": "sha256-sqOOEaKJJSUFBzag │
│ /cGeeXV491TrrVFY3DFBs1w20V8=", │
│ + "owner": "cachix", │
│ + "repo": "nixpkgs-python", │
│ + "rev": "7c550bca7e6cf95898e32eb2173 │
│ efe7ebb447460", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "cachix", │
│ + "repo": "nixpkgs-python", │
│ + "type": "github" │
│ + } │
│ + }, │
│ + "nixpkgs_2": { │
│ + "locked": { │
│ + "lastModified": 1719253556, │
│ + "narHash": "sha256-A/76RFUVxZ/7Y8+O │
│ MVL1Lc8LRhBxZ8ZE2bpMnvZ1VpY=", │
│ + "owner": "NixOS", │
│ + "repo": "nixpkgs", │
│ + "rev": "fc07dc3bdf2956ddd64f24612ea │
│ 7fc894933eb2e", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "NixOS", │
│ + "ref": "nixos-24.05", │
│ + "repo": "nixpkgs", │
│ + "type": "github" │
│ + } │
│ + }, │
│ + "nixpkgs_3": { │
│ + "locked": { │
│ + "lastModified": 1727811607, │
│ + "narHash": "sha256-2ByOBflaIUJKeF9q │
│ 6efVcYHljZXGZ7MnCWtseRvmpm8=", │
│ + "owner": "NixOS", │
│ + "repo": "nixpkgs", │
│ + "rev": "1839883cd0068572aed75fb9442 │
│ b508bbd9ef09c", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "id": "nixpkgs", │
│ + "type": "indirect" │
│ + } │
│ + }, │
│ + "nixpkgs_4": { │
│ + "locked": { │
│ + "lastModified": 1776877367, │
│ + "narHash": "sha256-EHq1/OX139R1RvBz │
│ OJ0aMRT3xnWyqtHBRUBuO1gFzjI=", │
│ + "owner": "NixOS", │
│ + "repo": "nixpkgs", │
│ + "rev": "0726a0ecb6d4e08f6adced58726 │
│ b95db924cef57", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "NixOS", │
│ + "ref": "nixos-unstable", │
│ + "repo": "nixpkgs", │
│ + "type": "github" │
│ + } │
│ + }, │
│ "root": { │
│ "inputs": { │
│ "flake-parts": "flake-parts", │
│ - "nixpkgs": "nixpkgs", │
│ - "systems": "systems" │
│ + "mermaid-ascii": "mermaid-ascii", │
│ + "nixpkgs": "nixpkgs_4", │
│ + "systems": "systems_3" │
│ } │
│ }, │
│ "systems": { │
│ @@ -70,6 +226,36 @@ │
│ "repo": "default", │
│ "type": "github" │
│ } │
│ + }, │
│ + "systems_2": { │
│ + "locked": { │
│ + "lastModified": 1681028828, │
│ + "narHash": "sha256-Vy1rq5AaRuLzOxct │
│ 8nz4T6wlgyUR7zLU309k9mBC768=", │
│ + "owner": "nix-systems", │
│ + "repo": "default", │
│ + "rev": "da67096a3b9bf56a91d16901293 │
│ e51ba5b49a27e", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "nix-systems", │
│ + "repo": "default", │
│ + "type": "github" │
│ + } │
│ + }, │
│ + "systems_3": { │
│ + "locked": { │
│ + "lastModified": 1681028828, │
│ + "narHash": "sha256-Vy1rq5AaRuLzOxct │
│ 8nz4T6wlgyUR7zLU309k9mBC768=", │
│ + "owner": "nix-systems", │
│ + "repo": "default", │
│ + "rev": "da67096a3b9bf56a91d16901293 │
│ e51ba5b49a27e", │
│ + "type": "github" │
│ + }, │
│ + "original": { │
│ + "owner": "nix-systems", │
│ + "repo": "default", │
│ + "type": "github" │
│ + } │
│ } │
│ }, │
│ "root": "root", │
└──────────────────────────────────────────────┘
┌─ flake.nix ────────────────────────────────────────────────────────────────┐
│ diff --git a/flake.nix b/flake.nix │
│ index 380d8b3..3ed3ade 100644 │
│ --- a/flake.nix │
│ +++ b/flake.nix │
│ @@ -5,6 +5,7 @@ │
│ flake-parts.url = "github:hercules-ci/flake-parts"; │
│ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; │
│ systems.url = "github:nix-systems/default"; │
│ + mermaid-ascii.url = "github:AlexanderGrooff/mermaid-ascii"; │
│ }; │
│ │
│ outputs = inputs @ {flake-parts, ...}: │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ flake.nix ──────────────────────────┐
│ diff --git a/flake.nix b/flake.nix │
│ index 380d8b3..3ed3ade 100644 │
│ --- a/flake.nix │
│ +++ b/flake.nix │
│ @@ -5,6 +5,7 @@ │
│ flake-parts.url = "github:hercules-ci/f │
│ lake-parts"; │
│ nixpkgs.url = "github:NixOS/nixpkgs/nix │
│ os-unstable"; │
│ systems.url = "github:nix-systems/defau │
│ lt"; │
│ + mermaid-ascii.url = "github:AlexanderGr │
│ ooff/mermaid-ascii"; │
│ }; │
│ │
│ outputs = inputs @ {flake-parts, ...}: │
└──────────────────────────────────────────────┘
┌─ nix/shell.nix ────────────────────────────────────────────────────────────┐
│ diff --git a/nix/shell.nix b/nix/shell.nix │
│ index 8e20846..b5f1f52 100644 │
│ --- a/nix/shell.nix │
│ +++ b/nix/shell.nix │
│ @@ -1,5 +1,5 @@ │
│ {inputs, ...}: { │
│ - perSystem = {system, ...}: let │
│ + perSystem = {system, inputs', ...}: let │
│ pkgs = import inputs.nixpkgs { │
│ inherit system; │
│ config.allowUnfree = true; │
│ @@ -9,9 +9,11 @@ │
│ │
│ devShells.default = pkgs.mkShellNoCC { │
│ name = "project devshell"; │
│ - packages = builtins.attrValues { │
│ - inherit (pkgs) nodejs obsidian; │
│ - }; │
│ + packages = │
│ + builtins.attrValues { │
│ + inherit (pkgs) nodejs obsidian; │
│ + } │
│ + ++ [inputs'.mermaid-ascii.packages.default]; │
│ }; │
│ }; │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ nix/shell.nix ──────────────────────┐
│ diff --git a/nix/shell.nix b/nix/shell.nix │
│ index 8e20846..b5f1f52 100644 │
│ --- a/nix/shell.nix │
│ +++ b/nix/shell.nix │
│ @@ -1,5 +1,5 @@ │
│ {inputs, ...}: { │
│ - perSystem = {system, ...}: let │
│ + perSystem = {system, inputs', ...}: let │
│ pkgs = import inputs.nixpkgs { │
│ inherit system; │
│ config.allowUnfree = true; │
│ @@ -9,9 +9,11 @@ │
│ │
│ devShells.default = pkgs.mkShellNoCC { │
│ name = "project devshell"; │
│ - packages = builtins.attrValues { │
│ - inherit (pkgs) nodejs obsidian; │
│ - }; │
│ + packages = │
│ + builtins.attrValues { │
│ + inherit (pkgs) nodejs obsidian; │
│ + } │
│ + ++ [inputs'.mermaid-ascii.packages. │
│ default]; │
│ }; │
│ }; │
│ } │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/components/Prose/Prose.scss ────────────────────────────┐
│ diff --git a/ooknet-design/src/components/Prose/Prose.scss b/ooknet-design/src/com │
│ ponents/Prose/Prose.scss │
│ index 5553e92..f2efa34 100644 │
│ --- a/ooknet-design/src/components/Prose/Prose.scss │
│ +++ b/ooknet-design/src/components/Prose/Prose.scss │
│ @@ -204,5 +204,10 @@ │
│ color: var(--ink); │
│ } │
│ .fig-caption { color: var(--ink-soft); } │
│ + │
│ + pre.ascii.diagram-art { │
│ + font-family: var(--mono-frame); │
│ + margin: 0.5em 0; │
│ + } │
│ } │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...c/components/Prose/Prose.scss ───┐
│ diff --git a/ooknet-design/src/components/Pr │
│ ose/Prose.scss b/ooknet-design/src/component │
│ s/Prose/Prose.scss │
│ index 5553e92..f2efa34 100644 │
│ --- a/ooknet-design/src/components/Prose/Pro │
│ se.scss │
│ +++ b/ooknet-design/src/components/Prose/Pro │
│ se.scss │
│ @@ -204,5 +204,10 @@ │
│ color: var(--ink); │
│ } │
│ .fig-caption { color: var(--ink-soft); │
│ } │
│ + │
│ + pre.ascii.diagram-art { │
│ + font-family: var(--mono-frame); │
│ + margin: 0.5em 0; │
│ + } │
│ } │
│ } │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/content/notes/n0-style-sheet.md ────────────────────────┐
│ diff --git a/ooknet-design/src/content/notes/n0-style-sheet.md b/ooknet-design/src │
│ /content/notes/n0-style-sheet.md │
│ index e7f2cb0..1264e20 100644 │
│ --- a/ooknet-design/src/content/notes/n0-style-sheet.md │
│ +++ b/ooknet-design/src/content/notes/n0-style-sheet.md │
│ @@ -54,6 +54,49 @@ A wider table that must shrink and wrap on the narrow frame: │
│ │
│  │
│ │
│ +# Diagrams │
│ + │
│ +A `diagram` fence renders hand-drawn ASCII art as a numbered figure. │
│ +The first line may set a caption with `::`. │
│ + │
│ +```diagram │
│ +:: THE SAME BUILD FUNCTION AT BOTH WIDTHS │
│ + ┌──────────────┐ │
│ + │ build(width) │ │
│ + └──────┬───────┘ │
│ + ┌────────────┴────────────┐ │
│ + ┌──────┴───────┐ ┌────────┴───────┐ │
│ + │ FRAME_W = 86 │ │ MOBILE_W = 48 │ │
│ + │ .frame-wide │ │ .frame-narrow │ │
│ + └──────────────┘ └────────────────┘ │
│ +``` │
│ + │
│ +A `mermaid` fence renders at build time - and previews live in │
│ +Obsidian and GitHub, since it's plain mermaid. Linear chains lay out │
│ +through the house renderer; chains that fit the frame run │
│ +horizontally, longer ones stack on a vertical spine. │
│ + │
│ +```mermaid │
│ +%% :: NOTE PIPELINE, GENERATED FROM MERMAID SOURCE │
│ +graph LR │
│ +md[markdown] -->|transforms| rh[rehype] │
│ +rh -->|emits| fr[frames] │
│ +fr -->|validates| vg[checkGrid] │
│ +``` │
│ + │
│ +Branching graphs delegate to mermaid-ascii. Keep edge labels to │
│ +chains - labels on branching edges can collide (an upstream │
│ +mermaid-ascii issue): │
│ + │
│ +```mermaid │
│ +%% :: MARKDOWN FANS OUT AND REJOINS │
│ +graph TD │
│ +md[markdown] --> t[tables] │
│ +md --> d[diagrams] │
│ +t --> pg[page] │
│ +d --> pg │
│ +``` │
│ + │
│ # Code │
│ │
│ ```ts │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...ntent/notes/n0-style-sheet.md ───┐
│ diff --git a/ooknet-design/src/content/notes │
│ /n0-style-sheet.md b/ooknet-design/src/conte │
│ nt/notes/n0-style-sheet.md │
│ index e7f2cb0..1264e20 100644 │
│ --- a/ooknet-design/src/content/notes/n0-sty │
│ le-sheet.md │
│ +++ b/ooknet-design/src/content/notes/n0-sty │
│ le-sheet.md │
│ @@ -54,6 +54,49 @@ A wider table that must s │
│ hrink and wrap on the narrow frame: │
│ │
│  │
│ │
│ +# Diagrams │
│ + │
│ +A `diagram` fence renders hand-drawn ASCII │
│ art as a numbered figure. │
│ +The first line may set a caption with `::`. │
│ + │
│ +```diagram │
│ +:: THE SAME BUILD FUNCTION AT BOTH WIDTHS │
│ + ┌──────────────┐ │
│ + │ build(width) │ │
│ + └──────┬───────┘ │
│ + ┌────────────┴────────────┐ │
│ + ┌──────┴───────┐ ┌────────┴────── │
│ ─┐ │
│ + │ FRAME_W = 86 │ │ MOBILE_W = 48 │
│ │ │
│ + │ .frame-wide │ │ .frame-narrow │
│ │ │
│ + └──────────────┘ └─────────────── │
│ ─┘ │
│ +``` │
│ + │
│ +A `mermaid` fence renders at build time - a │
│ nd previews live in │
│ +Obsidian and GitHub, since it's plain merma │
│ id. Linear chains lay out │
│ +through the house renderer; chains that fit │
│ the frame run │
│ +horizontally, longer ones stack on a vertic │
│ al spine. │
│ + │
│ +```mermaid │
│ +%% :: NOTE PIPELINE, GENERATED FROM MERMAID │
│ SOURCE │
│ +graph LR │
│ +md[markdown] -->|transforms| rh[rehype] │
│ +rh -->|emits| fr[frames] │
│ +fr -->|validates| vg[checkGrid] │
│ +``` │
│ + │
│ +Branching graphs delegate to mermaid-ascii. │
│ Keep edge labels to │
│ +chains - labels on branching edges can coll │
│ ide (an upstream │
│ +mermaid-ascii issue): │
│ + │
│ +```mermaid │
│ +%% :: MARKDOWN FANS OUT AND REJOINS │
│ +graph TD │
│ +md[markdown] --> t[tables] │
│ +md --> d[diagrams] │
│ +t --> pg[page] │
│ +d --> pg │
│ +``` │
│ + │
│ # Code │
│ │
│ ```ts │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/flow.test.ts ───────────────────────────────────────┐
│ diff --git a/ooknet-design/src/lib/flow.test.ts b/ooknet-design/src/lib/flow.test. │
│ ts │
│ new file mode 100644 │
│ index 0000000..081e628 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/flow.test.ts │
│ @@ -0,0 +1,67 @@ │
│ +import { describe, expect, it } from "vitest"; │
│ +import { renderChain, type FlowGraph } from "./flow"; │
│ +import { checkGrid, len } from "./ascii"; │
│ + │
│ +const graph = (edges: [string, string, string?][], labels: [string, string][] = [ │
│ ]): FlowGraph => ({ │
│ + labels: new Map(labels), │
│ + edges: edges.map(([from, to, label]) => ({ from, to, label: label ?? "" })), │
│ +}); │
│ + │
│ +describe("renderChain", () => { │
│ + it("lays a fitting chain out horizontally with labels over the wires", () => { │
│ + expect(renderChain(graph([["a", "b", "go"]]), 40)).toBe( │
│ + [ │
│ + " go", │
│ + "┌───┐ ┌───┐", │
│ + "│ a │───>│ b │", │
│ + "└───┘ └───┘", │
│ + ].join("\n"), │
│ + ); │
│ + }); │
│ + │
│ + it("omits the label row when no edge has a label", () => { │
│ + expect(renderChain(graph([["a", "b"]]), 40)!.split("\n").length).toBe(3); │
│ + }); │
│ + │
│ + it("falls back to a straight vertical spine when too wide", () => { │
│ + expect(renderChain(graph([["alpha", "b", "go"]]), 10)).toBe( │
│ + [ │
│ + "┌───────┐", │
│ + "│ alpha │", │
│ + "└───┬───┘", │
│ + " │ go", │
│ + " ↓", │
│ + " ┌─┴─┐", │
│ + " │ b │", │
│ + " └───┘", │
│ + ].join("\n"), │
│ + ); │
│ + }); │
│ + │
│ + it("renders vertically when horizontal is not preferred", () => { │
│ + const art = renderChain(graph([["a", "b"]]), 80, false)!; │
│ + expect(art.split("\n").length).toBeGreaterThan(4); │
│ + expect(art).toContain("↓"); │
│ + }); │
│ + │
│ + it("uses display labels and stays on-grid within width", () => { │
│ + const art = renderChain( │
│ + graph([["md", "rh", "transforms"], ["rh", "fr", "emits"]], [["md", "markdow │
│ n"], ["rh", "rehype"], ["fr", "frames"]]), │
│ + 86, │
│ + )!; │
│ + expect(art).toContain("markdown"); │
│ + expect(() => checkGrid(art, 86, "t")).not.toThrow(); │
│ + for (const line of art.split("\n")) expect(len(line)).toBeLessThanOrEqual(86) │
│ ; │
│ + }); │
│ + │
│ + it("realigns edges authored out of chain order", () => { │
│ + const art = renderChain(graph([["b", "c", "second"], ["a", "b", "first"]]), 8 │
│ 0)!; │
│ + const labelRow = art.split("\n")[0]; │
│ + expect(labelRow.indexOf("first")).toBeLessThan(labelRow.indexOf("second")); │
│ + }); │
│ + │
│ + it("returns null for branching graphs and cycles", () => { │
│ + expect(renderChain(graph([["a", "b"], ["a", "c"]]), 80)).toBeNull(); │
│ + expect(renderChain(graph([["a", "b"], ["b", "a"]]), 80)).toBeNull(); │
│ + }); │
│ +}); │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...t-design/src/lib/flow.test.ts ───┐
│ diff --git a/ooknet-design/src/lib/flow.test │
│ .ts b/ooknet-design/src/lib/flow.test.ts │
│ new file mode 100644 │
│ index 0000000..081e628 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/flow.test.ts │
│ @@ -0,0 +1,67 @@ │
│ +import { describe, expect, it } from "vites │
│ t"; │
│ +import { renderChain, type FlowGraph } from │
│ "./flow"; │
│ +import { checkGrid, len } from "./ascii"; │
│ + │
│ +const graph = (edges: [string, string, stri │
│ ng?][], labels: [string, string][] = []): Fl │
│ owGraph => ({ │
│ + labels: new Map(labels), │
│ + edges: edges.map(([from, to, label]) => ( │
│ { from, to, label: label ?? "" })), │
│ +}); │
│ + │
│ +describe("renderChain", () => { │
│ + it("lays a fitting chain out horizontally │
│ with labels over the wires", () => { │
│ + expect(renderChain(graph([["a", "b", "g │
│ o"]]), 40)).toBe( │
│ + [ │
│ + " go", │
│ + "┌───┐ ┌───┐", │
│ + "│ a │───>│ b │", │
│ + "└───┘ └───┘", │
│ + ].join("\n"), │
│ + ); │
│ + }); │
│ + │
│ + it("omits the label row when no edge has │
│ a label", () => { │
│ + expect(renderChain(graph([["a", "b"]]), │
│ 40)!.split("\n").length).toBe(3); │
│ + }); │
│ + │
│ + it("falls back to a straight vertical spi │
│ ne when too wide", () => { │
│ + expect(renderChain(graph([["alpha", "b" │
│ , "go"]]), 10)).toBe( │
│ + [ │
│ + "┌───────┐", │
│ + "│ alpha │", │
│ + "└───┬───┘", │
│ + " │ go", │
│ + " ↓", │
│ + " ┌─┴─┐", │
│ + " │ b │", │
│ + " └───┘", │
│ + ].join("\n"), │
│ + ); │
│ + }); │
│ + │
│ + it("renders vertically when horizontal is │
│ not preferred", () => { │
│ + const art = renderChain(graph([["a", "b │
│ "]]), 80, false)!; │
│ + expect(art.split("\n").length).toBeGrea │
│ terThan(4); │
│ + expect(art).toContain("↓"); │
│ + }); │
│ + │
│ + it("uses display labels and stays on-grid │
│ within width", () => { │
│ + const art = renderChain( │
│ + graph([["md", "rh", "transforms"], [" │
│ rh", "fr", "emits"]], [["md", "markdown"], [ │
│ "rh", "rehype"], ["fr", "frames"]]), │
│ + 86, │
│ + )!; │
│ + expect(art).toContain("markdown"); │
│ + expect(() => checkGrid(art, 86, "t")).n │
│ ot.toThrow(); │
│ + for (const line of art.split("\n")) exp │
│ ect(len(line)).toBeLessThanOrEqual(86); │
│ + }); │
│ + │
│ + it("realigns edges authored out of chain │
│ order", () => { │
│ + const art = renderChain(graph([["b", "c │
│ ", "second"], ["a", "b", "first"]]), 80)!; │
│ + const labelRow = art.split("\n")[0]; │
│ + expect(labelRow.indexOf("first")).toBeL │
│ essThan(labelRow.indexOf("second")); │
│ + }); │
│ + │
│ + it("returns null for branching graphs and │
│ cycles", () => { │
│ + expect(renderChain(graph([["a", "b"], [ │
│ "a", "c"]]), 80)).toBeNull(); │
│ + expect(renderChain(graph([["a", "b"], [ │
│ "b", "a"]]), 80)).toBeNull(); │
│ + }); │
│ +}); │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/flow.ts ────────────────────────────────────────────┐
│ diff --git a/ooknet-design/src/lib/flow.ts b/ooknet-design/src/lib/flow.ts │
│ new file mode 100644 │
│ index 0000000..dcce0bb │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/flow.ts │
│ @@ -0,0 +1,104 @@ │
│ +// Chain layout for generated diagrams: a linear pipeline renders as │
│ +// horizontal boxes with labels over the wires, or stacks on a straight │
│ +// vertical spine when it doesn't fit. Branching layout is delegated to │
│ +// mermaid-ascii (see mermaid.ts). │
│ +import { G, center, len } from "./ascii"; │
│ + │
│ +export interface FlowEdge { │
│ + from: string; │
│ + to: string; │
│ + label: string; │
│ +} │
│ + │
│ +export interface FlowGraph { │
│ + labels: Map<string, string>; │
│ + edges: FlowEdge[]; │
│ +} │
│ + │
│ +// Orders the nodes if the edges form a single path, else null. │
│ +function chainOf(g: FlowGraph): string[] | null { │
│ + const nodes = new Set<string>(g.edges.flatMap((e) => [e.from, e.to])); │
│ + const out = new Map<string, string>(); │
│ + const hasIn = new Set<string>(); │
│ + for (const e of g.edges) { │
│ + if (out.has(e.from) || hasIn.has(e.to)) return null; │
│ + out.set(e.from, e.to); │
│ + hasIn.add(e.to); │
│ + } │
│ + const heads = [...nodes].filter((n) => !hasIn.has(n)); │
│ + if (heads.length !== 1) return null; │
│ + const chain = [heads[0]]; │
│ + while (out.has(chain[chain.length - 1])) chain.push(out.get(chain[chain.length │
│ - 1])!); │
│ + return chain.length === nodes.size ? chain : null; │
│ +} │
│ + │
│ +function horizontal(chain: string[], g: FlowGraph, width: number): string | null │
│ { │
│ + const disp = chain.map((id) => g.labels.get(id) ?? id); │
│ + const bw = disp.map((d) => len(d) + 4); │
│ + const ew = g.edges.map((e) => Math.max(len(e.label) + 2, 4)); │
│ + const total = bw.reduce((a, b) => a + b, 0) + ew.reduce((a, b) => a + b, 0); │
│ + if (total > width) return null; │
│ + │
│ + let labelRow = ""; │
│ + let top = ""; │
│ + let mid = ""; │
│ + let bot = ""; │
│ + chain.forEach((_, i) => { │
│ + labelRow += " ".repeat(bw[i]); │
│ + top += G.TL + G.H.repeat(bw[i] - 2) + G.TR; │
│ + mid += G.V + center(disp[i], bw[i] - 2) + G.V; │
│ + bot += G.BL + G.H.repeat(bw[i] - 2) + G.BR; │
│ + if (i < g.edges.length) { │
│ + labelRow += center(g.edges[i].label, ew[i]); │
│ + top += " ".repeat(ew[i]); │
│ + mid += G.H.repeat(ew[i] - 1) + ">"; │
│ + bot += " ".repeat(ew[i]); │
│ + } │
│ + }); │
│ + │
│ + const rows = [top, mid, bot].map((r) => r.replace(/\s+$/, "")); │
│ + if (g.edges.some((e) => e.label)) rows.unshift(labelRow.replace(/\s+$/, "")); │
│ + return rows.join("\n"); │
│ +} │
│ + │
│ +function vertical(chain: string[], g: FlowGraph): string { │
│ + const disp = chain.map((id) => g.labels.get(id) ?? id); │
│ + const bw = disp.map((d) => len(d) + 4); │
│ + const spine = Math.floor(Math.max(...bw) / 2); │
│ + │
│ + const border = (l: string, r: string, w: number, left: number, junction: string │
│ | null) => { │
│ + if (junction === null) return " ".repeat(left) + l + G.H.repeat(w - 2) + r; │
│ + const before = spine - left - 1; │
│ + return " ".repeat(left) + l + G.H.repeat(before) + junction + G.H.repeat(w - │
│ 2 - before - 1) + r; │
│ + }; │
│ + │
│ + const rows: string[] = []; │
│ + chain.forEach((_, i) => { │
│ + const w = bw[i]; │
│ + const left = spine - Math.floor(w / 2); │
│ + rows.push(border(G.TL, G.TR, w, left, i > 0 ? G.BJ : null)); │
│ + rows.push(" ".repeat(left) + G.V + center(disp[i], w - 2) + G.V); │
│ + rows.push(border(G.BL, G.BR, w, left, i < chain.length - 1 ? G.TJ : null)); │
│ + if (i < g.edges.length) { │
│ + const label = g.edges[i].label; │
│ + rows.push(" ".repeat(spine) + G.V + (label ? ` ${label}` : "")); │
│ + rows.push(" ".repeat(spine) + "↓"); │
│ + } │
│ + }); │
│ + return rows.join("\n"); │
│ +} │
│ + │
│ +/** Renders g if its edges form one linear chain; null otherwise. */ │
│ +export function renderChain(g: FlowGraph, width: number, preferHorizontal = true) │
│ : string | null { │
│ + const chain = chainOf(g); │
│ + if (!chain) return null; │
│ + │
│ + // Edges may be authored in any order; realign them with the chain. │
│ + const byFrom = new Map(g.edges.map((e) => [e.from, e])); │
│ + const ordered: FlowGraph = { │
│ + labels: g.labels, │
│ + edges: chain.slice(0, -1).map((id) => byFrom.get(id)!), │
│ + }; │
│ + if (!preferHorizontal) return vertical(chain, ordered); │
│ + return horizontal(chain, ordered, width) ?? vertical(chain, ordered); │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/flow.ts ──────┐
│ diff --git a/ooknet-design/src/lib/flow.ts b │
│ /ooknet-design/src/lib/flow.ts │
│ new file mode 100644 │
│ index 0000000..dcce0bb │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/flow.ts │
│ @@ -0,0 +1,104 @@ │
│ +// Chain layout for generated diagrams: a l │
│ inear pipeline renders as │
│ +// horizontal boxes with labels over the wi │
│ res, or stacks on a straight │
│ +// vertical spine when it doesn't fit. Bran │
│ ching layout is delegated to │
│ +// mermaid-ascii (see mermaid.ts). │
│ +import { G, center, len } from "./ascii"; │
│ + │
│ +export interface FlowEdge { │
│ + from: string; │
│ + to: string; │
│ + label: string; │
│ +} │
│ + │
│ +export interface FlowGraph { │
│ + labels: Map<string, string>; │
│ + edges: FlowEdge[]; │
│ +} │
│ + │
│ +// Orders the nodes if the edges form a sin │
│ gle path, else null. │
│ +function chainOf(g: FlowGraph): string[] | │
│ null { │
│ + const nodes = new Set<string>(g.edges.fla │
│ tMap((e) => [e.from, e.to])); │
│ + const out = new Map<string, string>(); │
│ + const hasIn = new Set<string>(); │
│ + for (const e of g.edges) { │
│ + if (out.has(e.from) || hasIn.has(e.to)) │
│ return null; │
│ + out.set(e.from, e.to); │
│ + hasIn.add(e.to); │
│ + } │
│ + const heads = [...nodes].filter((n) => !h │
│ asIn.has(n)); │
│ + if (heads.length !== 1) return null; │
│ + const chain = [heads[0]]; │
│ + while (out.has(chain[chain.length - 1])) │
│ chain.push(out.get(chain[chain.length - 1])! │
│ ); │
│ + return chain.length === nodes.size ? chai │
│ n : null; │
│ +} │
│ + │
│ +function horizontal(chain: string[], g: Flo │
│ wGraph, width: number): string | null { │
│ + const disp = chain.map((id) => g.labels.g │
│ et(id) ?? id); │
│ + const bw = disp.map((d) => len(d) + 4); │
│ + const ew = g.edges.map((e) => Math.max(le │
│ n(e.label) + 2, 4)); │
│ + const total = bw.reduce((a, b) => a + b, │
│ 0) + ew.reduce((a, b) => a + b, 0); │
│ + if (total > width) return null; │
│ + │
│ + let labelRow = ""; │
│ + let top = ""; │
│ + let mid = ""; │
│ + let bot = ""; │
│ + chain.forEach((_, i) => { │
│ + labelRow += " ".repeat(bw[i]); │
│ + top += G.TL + G.H.repeat(bw[i] - 2) + G │
│ .TR; │
│ + mid += G.V + center(disp[i], bw[i] - 2) │
│ + G.V; │
│ + bot += G.BL + G.H.repeat(bw[i] - 2) + G │
│ .BR; │
│ + if (i < g.edges.length) { │
│ + labelRow += center(g.edges[i].label, │
│ ew[i]); │
│ + top += " ".repeat(ew[i]); │
│ + mid += G.H.repeat(ew[i] - 1) + ">"; │
│ + bot += " ".repeat(ew[i]); │
│ + } │
│ + }); │
│ + │
│ + const rows = [top, mid, bot].map((r) => r │
│ .replace(/\s+$/, "")); │
│ + if (g.edges.some((e) => e.label)) rows.un │
│ shift(labelRow.replace(/\s+$/, "")); │
│ + return rows.join("\n"); │
│ +} │
│ + │
│ +function vertical(chain: string[], g: FlowG │
│ raph): string { │
│ + const disp = chain.map((id) => g.labels.g │
│ et(id) ?? id); │
│ + const bw = disp.map((d) => len(d) + 4); │
│ + const spine = Math.floor(Math.max(...bw) │
│ / 2); │
│ + │
│ + const border = (l: string, r: string, w: │
│ number, left: number, junction: string | nul │
│ l) => { │
│ + if (junction === null) return " ".repea │
│ t(left) + l + G.H.repeat(w - 2) + r; │
│ + const before = spine - left - 1; │
│ + return " ".repeat(left) + l + G.H.repea │
│ t(before) + junction + G.H.repeat(w - 2 - be │
│ fore - 1) + r; │
│ + }; │
│ + │
│ + const rows: string[] = []; │
│ + chain.forEach((_, i) => { │
│ + const w = bw[i]; │
│ + const left = spine - Math.floor(w / 2); │
│ + rows.push(border(G.TL, G.TR, w, left, i │
│ > 0 ? G.BJ : null)); │
│ + rows.push(" ".repeat(left) + G.V + cent │
│ er(disp[i], w - 2) + G.V); │
│ + rows.push(border(G.BL, G.BR, w, left, i │
│ < chain.length - 1 ? G.TJ : null)); │
│ + if (i < g.edges.length) { │
│ + const label = g.edges[i].label; │
│ + rows.push(" ".repeat(spine) + G.V + ( │
│ label ? ` ${label}` : "")); │
│ + rows.push(" ".repeat(spine) + "↓"); │
│ + } │
│ + }); │
│ + return rows.join("\n"); │
│ +} │
│ + │
│ +/** Renders g if its edges form one linear │
│ chain; null otherwise. */ │
│ +export function renderChain(g: FlowGraph, w │
│ idth: number, preferHorizontal = true): stri │
│ ng | null { │
│ + const chain = chainOf(g); │
│ + if (!chain) return null; │
│ + │
│ + // Edges may be authored in any order; re │
│ align them with the chain. │
│ + const byFrom = new Map(g.edges.map((e) => │
│ [e.from, e])); │
│ + const ordered: FlowGraph = { │
│ + labels: g.labels, │
│ + edges: chain.slice(0, -1).map((id) => b │
│ yFrom.get(id)!), │
│ + }; │
│ + if (!preferHorizontal) return vertical(ch │
│ ain, ordered); │
│ + return horizontal(chain, ordered, width) │
│ ?? vertical(chain, ordered); │
│ +} │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/mermaid.test.ts ────────────────────────────────────┐
│ diff --git a/ooknet-design/src/lib/mermaid.test.ts b/ooknet-design/src/lib/mermaid │
│ .test.ts │
│ new file mode 100644 │
│ index 0000000..203f700 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/mermaid.test.ts │
│ @@ -0,0 +1,61 @@ │
│ +import { describe, expect, it } from "vitest"; │
│ +import { mermaidToAscii, normalizeArt, parseMermaid } from "./mermaid"; │
│ +import { checkGrid } from "./ascii"; │
│ + │
│ +describe("parseMermaid", () => { │
│ + it("parses the flowchart subset with labels and edge text", () => { │
│ + const p = parseMermaid("graph LR\nmd[markdown] -->|transforms| rh[rehype]\nrh │
│ --> fr\n")!; │
│ + expect(p.direction).toBe("LR"); │
│ + expect(p.graph.labels.get("md")).toBe("markdown"); │
│ + expect(p.graph.edges).toEqual([ │
│ + { from: "md", to: "rh", label: "transforms" }, │
│ + { from: "rh", to: "fr", label: "" }, │
│ + ]); │
│ + }); │
│ + │
│ + it("parses multiple arrows on one line", () => { │
│ + const p = parseMermaid("graph LR\na --> b --> c\n")!; │
│ + expect(p.graph.edges.map((e) => e.from + e.to)).toEqual(["ab", "bc"]); │
│ + }); │
│ + │
│ + it("ignores %% comments", () => { │
│ + expect(parseMermaid("%% note\ngraph TD\na --> b\n")!.direction).toBe("TD"); │
│ + }); │
│ + │
│ + it("returns null for syntax beyond the subset", () => { │
│ + expect(parseMermaid("sequenceDiagram\nAlice->>Bob: hi")).toBeNull(); │
│ + expect(parseMermaid("graph LR\nsubgraph one\na --> b\nend")).toBeNull(); │
│ + expect(parseMermaid("graph RL\na --> b")).toBeNull(); │
│ + }); │
│ +}); │
│ + │
│ +describe("normalizeArt", () => { │
│ + it("swaps off-grid arrowheads and strips trailing space", () => { │
│ + expect(normalizeArt("─? \n?\n?─")).toBe("─>\n↓\n<─"); │
│ + }); │
│ +}); │
│ + │
│ +describe("mermaidToAscii", () => { │
│ + it("renders LR chains through the house layout", () => { │
│ + const art = mermaidToAscii("graph LR\na -->|go| b", 40); │
│ + expect(art).toBe( │
│ + [ │
│ + " go", │
│ + "┌───┐ ┌───┐", │
│ + "│ a │───>│ b │", │
│ + "└───┘ └───┘", │
│ + ].join("\n"), │
│ + ); │
│ + }); │
│ + │
│ + it("renders TD chains vertically", () => { │
│ + const art = mermaidToAscii("graph TD\na -->|go| b", 80); │
│ + expect(art).toContain("↓"); │
│ + }); │
│ + │
│ + it("delegates branching graphs to mermaid-ascii and stays on-grid", () => { │
│ + const art = mermaidToAscii("graph TD\na --> b\na --> c", 86); │
│ + expect(art).toContain("┌"); │
│ + expect(() => checkGrid(art, 86, "t")).not.toThrow(); │
│ + }); │
│ +}); │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...esign/src/lib/mermaid.test.ts ───┐
│ diff --git a/ooknet-design/src/lib/mermaid.t │
│ est.ts b/ooknet-design/src/lib/mermaid.test. │
│ ts │
│ new file mode 100644 │
│ index 0000000..203f700 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/mermaid.test.ts │
│ @@ -0,0 +1,61 @@ │
│ +import { describe, expect, it } from "vites │
│ t"; │
│ +import { mermaidToAscii, normalizeArt, pars │
│ eMermaid } from "./mermaid"; │
│ +import { checkGrid } from "./ascii"; │
│ + │
│ +describe("parseMermaid", () => { │
│ + it("parses the flowchart subset with labe │
│ ls and edge text", () => { │
│ + const p = parseMermaid("graph LR\nmd[ma │
│ rkdown] -->|transforms| rh[rehype]\nrh --> f │
│ r\n")!; │
│ + expect(p.direction).toBe("LR"); │
│ + expect(p.graph.labels.get("md")).toBe(" │
│ markdown"); │
│ + expect(p.graph.edges).toEqual([ │
│ + { from: "md", to: "rh", label: "trans │
│ forms" }, │
│ + { from: "rh", to: "fr", label: "" }, │
│ + ]); │
│ + }); │
│ + │
│ + it("parses multiple arrows on one line", │
│ () => { │
│ + const p = parseMermaid("graph LR\na --> │
│ b --> c\n")!; │
│ + expect(p.graph.edges.map((e) => e.from │
│ + e.to)).toEqual(["ab", "bc"]); │
│ + }); │
│ + │
│ + it("ignores %% comments", () => { │
│ + expect(parseMermaid("%% note\ngraph TD\ │
│ na --> b\n")!.direction).toBe("TD"); │
│ + }); │
│ + │
│ + it("returns null for syntax beyond the su │
│ bset", () => { │
│ + expect(parseMermaid("sequenceDiagram\nA │
│ lice->>Bob: hi")).toBeNull(); │
│ + expect(parseMermaid("graph LR\nsubgraph │
│ one\na --> b\nend")).toBeNull(); │
│ + expect(parseMermaid("graph RL\na --> b" │
│ )).toBeNull(); │
│ + }); │
│ +}); │
│ + │
│ +describe("normalizeArt", () => { │
│ + it("swaps off-grid arrowheads and strips │
│ trailing space", () => { │
│ + expect(normalizeArt("─? \n?\n?─")).toB │
│ e("─>\n↓\n<─"); │
│ + }); │
│ +}); │
│ + │
│ +describe("mermaidToAscii", () => { │
│ + it("renders LR chains through the house l │
│ ayout", () => { │
│ + const art = mermaidToAscii("graph LR\na │
│ -->|go| b", 40); │
│ + expect(art).toBe( │
│ + [ │
│ + " go", │
│ + "┌───┐ ┌───┐", │
│ + "│ a │───>│ b │", │
│ + "└───┘ └───┘", │
│ + ].join("\n"), │
│ + ); │
│ + }); │
│ + │
│ + it("renders TD chains vertically", () => │
│ { │
│ + const art = mermaidToAscii("graph TD\na │
│ -->|go| b", 80); │
│ + expect(art).toContain("↓"); │
│ + }); │
│ + │
│ + it("delegates branching graphs to mermaid │
│ -ascii and stays on-grid", () => { │
│ + const art = mermaidToAscii("graph TD\na │
│ --> b\na --> c", 86); │
│ + expect(art).toContain("┌"); │
│ + expect(() => checkGrid(art, 86, "t")).n │
│ ot.toThrow(); │
│ + }); │
│ +}); │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/mermaid.ts ─────────────────────────────────────────┐
│ diff --git a/ooknet-design/src/lib/mermaid.ts b/ooknet-design/src/lib/mermaid.ts │
│ new file mode 100644 │
│ index 0000000..448ea4c │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/mermaid.ts │
│ @@ -0,0 +1,94 @@ │
│ +// ```mermaid fences: linear chains render through the house chain │
│ +// layout (flow.ts); anything beyond the parsed subset - branching, │
│ +// subgraphs, RL/BT direction - shells out to mermaid-ascii from the │
│ +// dev shell. Mermaid is the one authoring syntax because Obsidian and │
│ +// GitHub preview it natively. │
│ +import { execFileSync } from "node:child_process"; │
│ +import { renderChain, type FlowGraph } from "./flow"; │
│ + │
│ +const HEADER_RE = /^(?:graph|flowchart)\s+(LR|RL|TD|TB|BT)\s*$/; │
│ +const NODE_TOKEN = /^([A-Za-z0-9_.-]+)(?:[\[({]+([^\]})]+)[\])}]+)?/; │
│ +const ARROW_TOKEN = /^-->(?:\|([^|]*)\|)?/; │
│ + │
│ +export interface ParsedMermaid { │
│ + graph: FlowGraph; │
│ + direction: string; │
│ +} │
│ + │
│ +/** Parses the simple flowchart subset; null means "let the binary │
│ + * handle it" - it supports far more syntax than this. */ │
│ +export function parseMermaid(source: string): ParsedMermaid | null { │
│ + const lines = source │
│ + .split("\n") │
│ + .map((l) => l.trim()) │
│ + .filter((l) => l && !l.startsWith("%%")); │
│ + if (!lines.length) return null; │
│ + │
│ + const header = lines[0].match(HEADER_RE); │
│ + if (!header) return null; │
│ + const direction = header[1]; │
│ + if (direction === "RL" || direction === "BT") return null; │
│ + │
│ + const labels = new Map<string, string>(); │
│ + const edges: FlowGraph["edges"] = []; │
│ + for (const line of lines.slice(1)) { │
│ + let rest = line; │
│ + const first = rest.match(NODE_TOKEN); │
│ + if (!first) return null; │
│ + let prev = first[1]; │
│ + if (first[2]) labels.set(prev, first[2].trim()); │
│ + rest = rest.slice(first[0].length).trimStart(); │
│ + while (rest) { │
│ + const arrow = rest.match(ARROW_TOKEN); │
│ + if (!arrow) return null; │
│ + rest = rest.slice(arrow[0].length).trimStart(); │
│ + const node = rest.match(NODE_TOKEN); │
│ + if (!node || !node[1]) return null; │
│ + if (node[2]) labels.set(node[1], node[2].trim()); │
│ + edges.push({ from: prev, to: node[1], label: (arrow[1] ?? "").trim() }); │
│ + prev = node[1]; │
│ + rest = rest.slice(node[0].length).trimStart(); │
│ + } │
│ + } │
│ + return edges.length ? { graph: { labels, edges }, direction } : null; │
│ +} │
│ + │
│ +// Arrowhead glyphs the frame font doesn't cover, swapped for on-grid │
│ +// equivalents. │
│ +const ARROWS: Record<string, string> = { │
│ + "?": "↓", "?": "↑", "?": ">", "?": ">", "?": "<", "?": "<", │
│ +}; │
│ + │
│ +export function normalizeArt(art: string): string { │
│ + return art │
│ + .replace(/[??????]/gu, (c) => ARROWS[c]) │
│ + .split("\n") │
│ + .map((l) => l.replace(/\s+$/, "")) │
│ + .join("\n") │
│ + .replace(/^\n+|\n+$/g, ""); │
│ +} │
│ + │
│ +function runMermaidAscii(source: string): string { │
│ + try { │
│ + return execFileSync( │
│ + "mermaid-ascii", │
│ + ["-f", "-", "--borderPadding", "0", "--paddingX", "5", "--paddingY", "2"], │
│ + { input: source, encoding: "utf8" }, │
│ + ); │
│ + } catch (e) { │
│ + const err = e as NodeJS.ErrnoException & { stderr?: string }; │
│ + if (err.code === "ENOENT") { │
│ + throw new Error("[mermaid] mermaid-ascii not found - is it in the dev shell │
│ ?"); │
│ + } │
│ + throw new Error(`[mermaid] render failed: ${err.stderr || err.message}`); │
│ + } │
│ +} │
│ + │
│ +export function mermaidToAscii(source: string, width: number): string { │
│ + const parsed = parseMermaid(source); │
│ + if (parsed) { │
│ + const art = renderChain(parsed.graph, width, parsed.direction === "LR"); │
│ + if (art) return art; │
│ + } │
│ + return normalizeArt(runMermaidAscii(source)); │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/mermaid.ts ───┐
│ diff --git a/ooknet-design/src/lib/mermaid.t │
│ s b/ooknet-design/src/lib/mermaid.ts │
│ new file mode 100644 │
│ index 0000000..448ea4c │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/mermaid.ts │
│ @@ -0,0 +1,94 @@ │
│ +// ```mermaid fences: linear chains render │
│ through the house chain │
│ +// layout (flow.ts); anything beyond the pa │
│ rsed subset - branching, │
│ +// subgraphs, RL/BT direction - shells out │
│ to mermaid-ascii from the │
│ +// dev shell. Mermaid is the one authoring │
│ syntax because Obsidian and │
│ +// GitHub preview it natively. │
│ +import { execFileSync } from "node:child_pr │
│ ocess"; │
│ +import { renderChain, type FlowGraph } from │
│ "./flow"; │
│ + │
│ +const HEADER_RE = /^(?:graph|flowchart)\s+( │
│ LR|RL|TD|TB|BT)\s*$/; │
│ +const NODE_TOKEN = /^([A-Za-z0-9_.-]+)(?:[\ │
│ [({]+([^\]})]+)[\])}]+)?/; │
│ +const ARROW_TOKEN = /^-->(?:\|([^|]*)\|)?/; │
│ + │
│ +export interface ParsedMermaid { │
│ + graph: FlowGraph; │
│ + direction: string; │
│ +} │
│ + │
│ +/** Parses the simple flowchart subset; nul │
│ l means "let the binary │
│ + * handle it" - it supports far more synta │
│ x than this. */ │
│ +export function parseMermaid(source: string │
│ ): ParsedMermaid | null { │
│ + const lines = source │
│ + .split("\n") │
│ + .map((l) => l.trim()) │
│ + .filter((l) => l && !l.startsWith("%%") │
│ ); │
│ + if (!lines.length) return null; │
│ + │
│ + const header = lines[0].match(HEADER_RE); │
│ + if (!header) return null; │
│ + const direction = header[1]; │
│ + if (direction === "RL" || direction === " │
│ BT") return null; │
│ + │
│ + const labels = new Map<string, string>(); │
│ + const edges: FlowGraph["edges"] = []; │
│ + for (const line of lines.slice(1)) { │
│ + let rest = line; │
│ + const first = rest.match(NODE_TOKEN); │
│ + if (!first) return null; │
│ + let prev = first[1]; │
│ + if (first[2]) labels.set(prev, first[2] │
│ .trim()); │
│ + rest = rest.slice(first[0].length).trim │
│ Start(); │
│ + while (rest) { │
│ + const arrow = rest.match(ARROW_TOKEN) │
│ ; │
│ + if (!arrow) return null; │
│ + rest = rest.slice(arrow[0].length).tr │
│ imStart(); │
│ + const node = rest.match(NODE_TOKEN); │
│ + if (!node || !node[1]) return null; │
│ + if (node[2]) labels.set(node[1], node │
│ [2].trim()); │
│ + edges.push({ from: prev, to: node[1], │
│ label: (arrow[1] ?? "").trim() }); │
│ + prev = node[1]; │
│ + rest = rest.slice(node[0].length).tri │
│ mStart(); │
│ + } │
│ + } │
│ + return edges.length ? { graph: { labels, │
│ edges }, direction } : null; │
│ +} │
│ + │
│ +// Arrowhead glyphs the frame font doesn't │
│ cover, swapped for on-grid │
│ +// equivalents. │
│ +const ARROWS: Record<string, string> = { │
│ + "?": "↓", "?": "↑", "?": ">", "?": ">", " │
│ ?": "<", "?": "<", │
│ +}; │
│ + │
│ +export function normalizeArt(art: string): │
│ string { │
│ + return art │
│ + .replace(/[??????]/gu, (c) => ARROWS[c] │
│ ) │
│ + .split("\n") │
│ + .map((l) => l.replace(/\s+$/, "")) │
│ + .join("\n") │
│ + .replace(/^\n+|\n+$/g, ""); │
│ +} │
│ + │
│ +function runMermaidAscii(source: string): s │
│ tring { │
│ + try { │
│ + return execFileSync( │
│ + "mermaid-ascii", │
│ + ["-f", "-", "--borderPadding", "0", " │
│ --paddingX", "5", "--paddingY", "2"], │
│ + { input: source, encoding: "utf8" }, │
│ + ); │
│ + } catch (e) { │
│ + const err = e as NodeJS.ErrnoException │
│ & { stderr?: string }; │
│ + if (err.code === "ENOENT") { │
│ + throw new Error("[mermaid] mermaid-as │
│ cii not found - is it in the dev shell?"); │
│ + } │
│ + throw new Error(`[mermaid] render faile │
│ d: ${err.stderr || err.message}`); │
│ + } │
│ +} │
│ + │
│ +export function mermaidToAscii(source: stri │
│ ng, width: number): string { │
│ + const parsed = parseMermaid(source); │
│ + if (parsed) { │
│ + const art = renderChain(parsed.graph, w │
│ idth, parsed.direction === "LR"); │
│ + if (art) return art; │
│ + } │
│ + return normalizeArt(runMermaidAscii(sourc │
│ e)); │
│ +} │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/rehype-ascii.ts ────────────────────────────────────┐
│ diff --git a/ooknet-design/src/lib/rehype-ascii.ts b/ooknet-design/src/lib/rehype- │
│ ascii.ts │
│ index 2a07ace..c781d6d 100644 │
│ --- a/ooknet-design/src/lib/rehype-ascii.ts │
│ +++ b/ooknet-design/src/lib/rehype-ascii.ts │
│ @@ -6,6 +6,7 @@ │
│ import type { Root, Element, ElementContent, RootContent, Properties } from "hast │
│ "; │
│ import { G, checkGrid, len, pad, wrap, type PadSide } from "./ascii"; │
│ import { FRAME_W, MOBILE_FRAME_W } from "./config"; │
│ +import { mermaidToAscii } from "./mermaid"; │
│ │
│ const FN_RE = /\b([a-zA-Z_]\w*)(?=\s*\()/g; │
│ const CALLOUT_RE = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/; │
│ @@ -230,6 +231,32 @@ function captionSpan(text: string, frameW: number, variant: s │
│ tring): Element { │
│ return blockSpan("fig-caption", variant, [{ type: "text", value: wrapped }]); │
│ } │
│ │
│ +function fenceInfo(node: Element): { lang: string; text: string } | null { │
│ + const codeEl = node.children.find( │
│ + (c): c is Element => c.type === "element" && c.tagName === "code", │
│ + ); │
│ + if (!codeEl) return null; │
│ + const codeCls = (codeEl.properties as Properties | undefined)?.className; │
│ + const langClass = Array.isArray(codeCls) │
│ + ? codeCls.find((c): c is string => typeof c === "string" && c.startsWith("lan │
│ guage-")) │
│ + : undefined; │
│ + const first = codeEl.children[0]; │
│ + return { │
│ + lang: langClass ? langClass.replace("language-", "") : "", │
│ + text: first && first.type === "text" ? first.value : "", │
│ + }; │
│ +} │
│ + │
│ +// Fences may open with a ":: CAPTION" line; mermaid fences use │
│ +// "%% :: CAPTION" so the source stays valid for Obsidian's preview. │
│ +function parseFence(text: string): { body: string; captionText: string | null } { │
│ + const trimmed = text.replace(/\n+$/, ""); │
│ + const m = trimmed.match(/^(?:%%\s*)?::[ \t]*(.+)\n/); │
│ + return m │
│ + ? { body: trimmed.slice(m[0].length), captionText: m[1].trim() } │
│ + : { body: trimmed, captionText: null }; │
│ +} │
│ + │
│ export function rehypeAscii() { │
│ return (tree: Root) => { │
│ let fig = 0; │
│ @@ -240,19 +267,41 @@ export function rehypeAscii() { │
│ // visitor recurses into them and reprocesses indefinitely. │
│ if (hasClass(node, "code-block") || hasClass(node, "table-frame")) return │
│ ; │
│ │
│ - const codeEl = node.children.find( │
│ - (c): c is Element => c.type === "element" && c.tagName === "code", │
│ - ); │
│ - if (!codeEl) return; │
│ - │
│ - const codeCls = (codeEl.properties as Properties | undefined)?.className; │
│ - const langClass = Array.isArray(codeCls) │
│ - ? codeCls.find((c): c is string => typeof c === "string" && c.startsWit │
│ h("language-")) │
│ - : undefined; │
│ - const lang = langClass ? langClass.replace("language-", "") : ""; │
│ - │
│ - const first = codeEl.children[0]; │
│ - const text = first && first.type === "text" ? first.value : ""; │
│ + const info = fenceInfo(node); │
│ + if (!info) return; │
│ + const { lang, text } = info; │
│ + │
│ + // Diagram fences render as numbered figures (optional │
│ + // ":: CAPTION" first line), not code frames: ```diagram is │
│ + // hand-drawn ASCII art kept verbatim; ```mermaid renders │
│ + // chains through the house layout and branching graphs │
│ + // through mermaid-ascii. Art wider than the narrow frame │
│ + // scrolls. │
│ + if (lang === "diagram" || lang === "mermaid") { │
│ + fig++; │
│ + const { body, captionText } = parseFence(text); │
│ + const caption = captionText ? `FIG. ${fig} - ${captionText.toUpperCase( │
│ )}` : `FIG. ${fig}`; │
│ + const art = lang === "mermaid" ? mermaidToAscii(body, FRAME_W) : body; │
│ + checkGrid(art, FRAME_W, "diagram"); │
│ + │
│ + node.tagName = "figure"; │
│ + node.properties = { className: ["figure-frame", "diagram-frame"], role: │
│ "img", "aria-label": caption }; │
│ + node.children = [ │
│ + ruleSpan(FRAME_W, "frame-wide", "fig-rule"), │
│ + ruleSpan(MOBILE_FRAME_W, "frame-narrow", "fig-rule"), │
│ + { │
│ + type: "element", │
│ + tagName: "pre", │
│ + properties: { className: ["ascii", "diagram-art"], "aria-hidden": " │
│ true" }, │
│ + children: [{ type: "text", value: art }], │
│ + }, │
│ + captionSpan(caption, FRAME_W, "frame-wide"), │
│ + captionSpan(caption, MOBILE_FRAME_W, "frame-narrow"), │
│ + ruleSpan(FRAME_W, "frame-wide", "fig-rule"), │
│ + ruleSpan(MOBILE_FRAME_W, "frame-narrow", "fig-rule"), │
│ + ]; │
│ + return; │
│ + } │
│ │
│ node.tagName = "div"; │
│ node.properties = { className: ["code-frames"] }; │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...esign/src/lib/rehype-ascii.ts ───┐
│ diff --git a/ooknet-design/src/lib/rehype-as │
│ cii.ts b/ooknet-design/src/lib/rehype-ascii. │
│ ts │
│ index 2a07ace..c781d6d 100644 │
│ --- a/ooknet-design/src/lib/rehype-ascii.ts │
│ +++ b/ooknet-design/src/lib/rehype-ascii.ts │
│ @@ -6,6 +6,7 @@ │
│ import type { Root, Element, ElementContent │
│ , RootContent, Properties } from "hast"; │
│ import { G, checkGrid, len, pad, wrap, type │
│ PadSide } from "./ascii"; │
│ import { FRAME_W, MOBILE_FRAME_W } from "./ │
│ config"; │
│ +import { mermaidToAscii } from "./mermaid"; │
│ │
│ const FN_RE = /\b([a-zA-Z_]\w*)(?=\s*\()/g; │
│ const CALLOUT_RE = /^\[!(NOTE|TIP|IMPORTANT │
│ |WARNING|CAUTION)\]\s*/; │
│ @@ -230,6 +231,32 @@ function captionSpan(te │
│ xt: string, frameW: number, variant: string) │
│ : Element { │
│ return blockSpan("fig-caption", variant, │
│ [{ type: "text", value: wrapped }]); │
│ } │
│ │
│ +function fenceInfo(node: Element): { lang: │
│ string; text: string } | null { │
│ + const codeEl = node.children.find( │
│ + (c): c is Element => c.type === "elemen │
│ t" && c.tagName === "code", │
│ + ); │
│ + if (!codeEl) return null; │
│ + const codeCls = (codeEl.properties as Pro │
│ perties | undefined)?.className; │
│ + const langClass = Array.isArray(codeCls) │
│ + ? codeCls.find((c): c is string => type │
│ of c === "string" && c.startsWith("language- │
│ ")) │
│ + : undefined; │
│ + const first = codeEl.children[0]; │
│ + return { │
│ + lang: langClass ? langClass.replace("la │
│ nguage-", "") : "", │
│ + text: first && first.type === "text" ? │
│ first.value : "", │
│ + }; │
│ +} │
│ + │
│ +// Fences may open with a ":: CAPTION" line │
│ ; mermaid fences use │
│ +// "%% :: CAPTION" so the source stays vali │
│ d for Obsidian's preview. │
│ +function parseFence(text: string): { body: │
│ string; captionText: string | null } { │
│ + const trimmed = text.replace(/\n+$/, ""); │
│ + const m = trimmed.match(/^(?:%%\s*)?::[ \ │
│ t]*(.+)\n/); │
│ + return m │
│ + ? { body: trimmed.slice(m[0].length), c │
│ aptionText: m[1].trim() } │
│ + : { body: trimmed, captionText: null }; │
│ +} │
│ + │
│ export function rehypeAscii() { │
│ return (tree: Root) => { │
│ let fig = 0; │
│ @@ -240,19 +267,41 @@ export function rehype │
│ Ascii() { │
│ // visitor recurses into them and r │
│ eprocesses indefinitely. │
│ if (hasClass(node, "code-block") || │
│ hasClass(node, "table-frame")) return; │
│ │
│ - const codeEl = node.children.find( │
│ - (c): c is Element => c.type === " │
│ element" && c.tagName === "code", │
│ - ); │
│ - if (!codeEl) return; │
│ - │
│ - const codeCls = (codeEl.properties │
│ as Properties | undefined)?.className; │
│ - const langClass = Array.isArray(cod │
│ eCls) │
│ - ? codeCls.find((c): c is string = │
│ > typeof c === "string" && c.startsWith("lan │
│ guage-")) │
│ - : undefined; │
│ - const lang = langClass ? langClass. │
│ replace("language-", "") : ""; │
│ - │
│ - const first = codeEl.children[0]; │
│ - const text = first && first.type == │
│ = "text" ? first.value : ""; │
│ + const info = fenceInfo(node); │
│ + if (!info) return; │
│ + const { lang, text } = info; │
│ + │
│ + // Diagram fences render as numbere │
│ d figures (optional │
│ + // ":: CAPTION" first line), not co │
│ de frames: ```diagram is │
│ + // hand-drawn ASCII art kept verbat │
│ im; ```mermaid renders │
│ + // chains through the house layout │
│ and branching graphs │
│ + // through mermaid-ascii. Art wider │
│ than the narrow frame │
│ + // scrolls. │
│ + if (lang === "diagram" || lang === │
│ "mermaid") { │
│ + fig++; │
│ + const { body, captionText } = par │
│ seFence(text); │
│ + const caption = captionText ? `FI │
│ G. ${fig} - ${captionText.toUpperCase()}` : │
│ `FIG. ${fig}`; │
│ + const art = lang === "mermaid" ? │
│ mermaidToAscii(body, FRAME_W) : body; │
│ + checkGrid(art, FRAME_W, "diagram" │
│ ); │
│ + │
│ + node.tagName = "figure"; │
│ + node.properties = { className: [" │
│ figure-frame", "diagram-frame"], role: "img" │
│ , "aria-label": caption }; │
│ + node.children = [ │
│ + ruleSpan(FRAME_W, "frame-wide", │
│ "fig-rule"), │
│ + ruleSpan(MOBILE_FRAME_W, "frame │
│ -narrow", "fig-rule"), │
│ + { │
│ + type: "element", │
│ + tagName: "pre", │
│ + properties: { className: ["as │
│ cii", "diagram-art"], "aria-hidden": "true" │
│ }, │
│ + children: [{ type: "text", va │
│ lue: art }], │
│ + }, │
│ + captionSpan(caption, FRAME_W, " │
│ frame-wide"), │
│ + captionSpan(caption, MOBILE_FRA │
│ ME_W, "frame-narrow"), │
│ + ruleSpan(FRAME_W, "frame-wide", │
│ "fig-rule"), │
│ + ruleSpan(MOBILE_FRAME_W, "frame │
│ -narrow", "fig-rule"), │
│ + ]; │
│ + return; │
│ + } │
│ │
│ node.tagName = "div"; │
│ node.properties = { className: ["co │
│ de-frames"] }; │
└──────────────────────────────────────────────┘
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET