diff --git a/appview/pages/markup/markdown.go b/appview/pages/markup/markdown.go --- a/appview/pages/markup/markdown.go +++ b/appview/pages/markup/markdown.go @@ -22,6 +22,7 @@ "github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" callout "gitlab.com/staticnoise/goldmark-callout" + "go.abhg.dev/goldmark/mermaid" htmlparse "golang.org/x/net/html" "tangled.org/core/api/tangled" @@ -56,6 +57,10 @@ func NewMarkdown(hostname string) goldmark.Markdown { md := goldmark.New( goldmark.WithExtensions( extension.GFM, + &mermaid.Extender{ + RenderMode: mermaid.RenderModeClient, + NoScript: true, + }, highlighting.NewHighlighting( highlighting.WithFormatOptions( chromahtml.Standalone(false), diff --git a/appview/pages/markup/markdown_test.go b/appview/pages/markup/markdown_test.go --- a/appview/pages/markup/markdown_test.go +++ b/appview/pages/markup/markdown_test.go @@ -2,8 +2,54 @@ package markup import ( "bytes" + "strings" "testing" ) + +func TestMermaidExtension(t *testing.T) { + tests := []struct { + name string + markdown string + contains string + notContains string + }{ + { + name: "mermaid block produces pre.mermaid", + markdown: "```mermaid\ngraph TD\n A-->B\n```", + contains: `
`,
+ notContains: `B\n```",
+ contains: "graph TD",
+ },
+ {
+ name: "non-mermaid code block is not affected",
+ markdown: "```go\nfunc main() {}\n```",
+ contains: ``,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ md := NewMarkdown("tangled.org")
+
+ var buf bytes.Buffer
+ if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
+ t.Fatalf("failed to convert markdown: %v", err)
+ }
+
+ result := buf.String()
+ if !strings.Contains(result, tt.contains) {
+ t.Errorf("expected output to contain:\n%s\ngot:\n%s", tt.contains, result)
+ }
+ if tt.notContains != "" && strings.Contains(result, tt.notContains) {
+ t.Errorf("expected output NOT to contain:\n%s\ngot:\n%s", tt.notContains, result)
+ }
+ })
+ }
+}
func TestAtExtension_Rendering(t *testing.T) {
tests := []struct {
diff --git a/appview/pages/markup/sanitizer.go b/appview/pages/markup/sanitizer.go
--- a/appview/pages/markup/sanitizer.go
+++ b/appview/pages/markup/sanitizer.go
@@ -72,7 +72,7 @@ policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
// for code blocks
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`chroma`)).OnElements("pre")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`chroma|mermaid`)).OnElements("pre")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`anchor|footnote-ref|footnote-backref`)).OnElements("a")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`heading`)).OnElements("h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8")
policy.AllowAttrs("class").Matching(regexp.MustCompile(strings.Join(slices.Collect(maps.Values(chroma.StandardTypes)), "|"))).OnElements("span")
diff --git a/appview/pages/templates/layouts/base.html b/appview/pages/templates/layouts/base.html
--- a/appview/pages/templates/layouts/base.html
+++ b/appview/pages/templates/layouts/base.html
@@ -39,6 +39,23 @@
+
+
{{ block "title" . }}{{ end }}
{{ block "extrameta" . }}{{ end }}
diff --git a/flake.lock b/flake.lock
--- a/flake.lock
+++ b/flake.lock
@@ -148,6 +148,18 @@ "type": "tarball",
"url": "https://github.com/lucide-icons/lucide/releases/download/0.536.0/lucide-icons-0.536.0.zip"
}
},
+ "mermaid-src": {
+ "flake": false,
+ "locked": {
+ "narHash": "sha256-/YOdECG2V5c3kJ1QfGvhziTT6K/Dx/4mOk2mr3Fs/do=",
+ "type": "file",
+ "url": "https://cdn.jsdelivr.net/npm/mermaid@11.12.3/dist/mermaid.min.js"
+ },
+ "original": {
+ "type": "file",
+ "url": "https://cdn.jsdelivr.net/npm/mermaid@11.12.3/dist/mermaid.min.js"
+ }
+ },
"nixpkgs": {
"locked": {
"lastModified": 1766070988,
@@ -175,6 +187,7 @@ "ibm-plex-mono-src": "ibm-plex-mono-src",
"indigo": "indigo",
"inter-fonts-src": "inter-fonts-src",
"lucide-src": "lucide-src",
+ "mermaid-src": "mermaid-src",
"nixpkgs": "nixpkgs",
"sqlite-lib-src": "sqlite-lib-src"
}
diff --git a/flake.nix b/flake.nix
--- a/flake.nix
+++ b/flake.nix
@@ -37,6 +37,10 @@ actor-typeahead-src = {
url = "git+https://tangled.org/@jakelazaroff.com/actor-typeahead";
flake = false;
};
+ mermaid-src = {
+ url = "https://cdn.jsdelivr.net/npm/mermaid@11.12.3/dist/mermaid.min.js";
+ flake = false;
+ };
ibm-plex-mono-src = {
url = "https://github.com/IBM/plex/releases/download/%40ibm%2Fplex-mono%401.1.0/ibm-plex-mono.zip";
flake = false;
@@ -59,6 +63,7 @@ inter-fonts-src,
sqlite-lib-src,
ibm-plex-mono-src,
actor-typeahead-src,
+ mermaid-src,
...
}: let
supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"];
@@ -85,7 +90,7 @@ };
lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;};
goat = self.callPackage ./nix/pkgs/goat.nix {inherit indigo;};
appview-static-files = self.callPackage ./nix/pkgs/appview-static-files.nix {
- inherit htmx-src htmx-ws-src lucide-src inter-fonts-src ibm-plex-mono-src actor-typeahead-src;
+ inherit htmx-src htmx-ws-src lucide-src inter-fonts-src ibm-plex-mono-src actor-typeahead-src mermaid-src;
};
appview = self.callPackage ./nix/pkgs/appview.nix {};
docs = self.callPackage ./nix/pkgs/docs.nix {
diff --git a/go.mod b/go.mod
--- a/go.mod
+++ b/go.mod
@@ -48,6 +48,7 @@ github.com/yuin/goldmark v1.7.13
github.com/yuin/goldmark-emoji v1.0.6
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab
+ go.abhg.dev/goldmark/mermaid v0.6.0
golang.org/x/crypto v0.40.0
golang.org/x/image v0.31.0
golang.org/x/net v0.42.0
diff --git a/go.sum b/go.sum
--- a/go.sum
+++ b/go.sum
@@ -105,6 +105,12 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
+github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU=
+github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
+github.com/chromedp/chromedp v0.14.0 h1:/xE5m6wEBwivhalHwlCOyYfBcAJNwg4nLw96QiCfYr0=
+github.com/chromedp/chromedp v0.14.0/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo=
+github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
+github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -175,6 +181,8 @@ github.com/go-git/go-git-fixtures/v5 v5.0.0-20241203230421-0753e18f8f03 h1:LumE+tQdnYW24a9RoO08w64LHTzkNkdUqBD/0QPtlEY=
github.com/go-git/go-git-fixtures/v5 v5.0.0-20241203230421-0753e18f8f03/go.mod h1:hMKrMnUE4W0SJ7bFyM00dyz/HoknZoptGWzrj6M+dEM=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
+github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs=
+github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -189,6 +197,12 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
+github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
+github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
+github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
+github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
+github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -516,6 +530,8 @@ gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA=
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
+go.abhg.dev/goldmark/mermaid v0.6.0 h1:VvkYFWuOjD6cmSBVJpLAtzpVCGM1h0B7/DQ9IzERwzY=
+go.abhg.dev/goldmark/mermaid v0.6.0/go.mod h1:uMc+PcnIH2NVL7zjH10Q1wr7hL3+4n4jUMifhyBYB9I=
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
diff --git a/input.css b/input.css
--- a/input.css
+++ b/input.css
@@ -215,6 +215,11 @@ .prose input[type="checkbox"] {
@apply disabled:accent-blue-500 checked:accent-blue-500 disabled:checked:accent-blue-500;
}
+ /* Mermaid diagrams */
+ .prose pre.mermaid {
+ @apply flex justify-center my-4 overflow-x-auto bg-transparent border-0;
+ }
+
/* Base callout */
details[data-callout] {
@apply border-l-4 pl-3 py-2 text-gray-800 dark:text-gray-200 my-4;
diff --git a/nix/gomod2nix.toml b/nix/gomod2nix.toml
--- a/nix/gomod2nix.toml
+++ b/nix/gomod2nix.toml
@@ -545,6 +545,9 @@ hash = "sha256-X8INg01LTg13iOuwPI3uOhPN7r01sPZtmtwJ2sudjCA="
[mod."gitlab.com/yawning/tuplehash"]
version = "v0.0.0-20230713102510-df83abbf9a02"
hash = "sha256-pehQduoaJRLchebhgvMYacVvbuNIBA++XkiqCuqdato="
+ [mod."go.abhg.dev/goldmark/mermaid"]
+ version = "v0.6.0"
+ hash = "sha256-JmjaCfzJU/M/R0TnXSzNwBaHmoLLooiXwQJeVRbZ3AQ="
[mod."go.etcd.io/bbolt"]
version = "v1.4.0"
hash = "sha256-nR/YGQjwz6ue99IFbgw/01Pl8PhoOjpKiwVy5sJxlps="
diff --git a/nix/pkgs/appview-static-files.nix b/nix/pkgs/appview-static-files.nix
--- a/nix/pkgs/appview-static-files.nix
+++ b/nix/pkgs/appview-static-files.nix
@@ -6,6 +6,7 @@ lucide-src,
inter-fonts-src,
ibm-plex-mono-src,
actor-typeahead-src,
+ mermaid-src,
sqlite-lib,
tailwindcss,
dolly,
@@ -21,6 +22,7 @@ } ''
mkdir -p $out/{fonts,icons,logos} && cd $out
cp -f ${htmx-src} htmx.min.js
cp -f ${htmx-ws-src} htmx-ext-ws.min.js
+ cp -f ${mermaid-src} mermaid.min.js
cp -rf ${lucide-src}/*.svg icons/
cp -f ${inter-fonts-src}/web/InterVariable*.woff2 fonts/
cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 fonts/