aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Majewsky <majewsky@gmx.net>2026-09-03 14:09:04 +0200
committerStefan Majewsky <majewsky@gmx.net>2026-09-03 14:09:04 +0200
commit91a03428957545c2601340b06de7baf7d6889f62 (patch)
tree74e84ff19685cea0fe6c3b12f591e4c1df93f78a
parent688a652304275d2e4fe4a4be4c0c3d8d640f1138 (diff)
downloadgofu-91a03428957545c2601340b06de7baf7d6889f62.tar.gz
mdedit: add optional mermaid support
-rw-r--r--README.md8
-rw-r--r--internal/mdedit/main.go66
-rw-r--r--internal/mdedit/res/app.js11
-rw-r--r--internal/mdedit/res/index.html1
4 files changed, 83 insertions, 3 deletions
diff --git a/README.md b/README.md
index 6007860..8a6bd79 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,14 @@ to the server (and thus to the source file) after one second of no typing.
It is recommended to not listen on anything besides `localhost`; there is no
authentication or anything.
+If the `--with-mermaid` option is given, the in-browser editor will use
+[mermaid.js](https://mermaid.ai/open-source/intro/getting-started.html)
+to render graphs from all code snippets with the `mermaid` language,
+similar to how Mermaid integration is handled e.g. on GitHub.
+
+**Privacy notice:** Activating this flag will cause mdedit to load mermaid.js
+from <https://cdn.jsdelivr.net> on startup.
+
### `prettyprompt`
This renders my shell prompt. Among other things, it identifies the current
diff --git a/internal/mdedit/main.go b/internal/mdedit/main.go
index 67cfdde..cbf86b1 100644
--- a/internal/mdedit/main.go
+++ b/internal/mdedit/main.go
@@ -6,12 +6,17 @@ package mdedit
import (
"bytes"
"context"
+ "crypto/sha256"
_ "embed"
+ "encoding/hex"
+ "fmt"
"io"
+ "io/ioutil"
"log"
"net/http"
"os"
"os/signal"
+ "regexp"
"sync"
"sync/atomic"
"syscall"
@@ -22,10 +27,26 @@ import (
"github.com/yuin/goldmark/renderer/html"
)
+var (
+ withMermaid bool
+)
+
// Exec executes the mdedit applet and returns an exit code (0 for success, >0 for error).
-func Exec(args []string) int {
+func Exec(fullArgs []string) int {
+ // consume optional arguments
+ var args []string
+ for _, arg := range fullArgs {
+ switch arg {
+ case "--with-mermaid":
+ withMermaid = true
+ default:
+ args = append(args, arg)
+ }
+ }
+
+ // consume positional arguments
if len(args) != 2 {
- os.Stderr.Write([]byte(" Usage: mdedit <file.md> <listenaddr>\nExample: mdedit todolist.md localhost:8080\n"))
+ os.Stderr.Write([]byte(" Usage: mdedit [--with-mermaid] <file.md> <listenaddr>\nExample: mdedit todolist.md localhost:8080\n Note: If --with-mermaid is given, the UI may connect to https://cdn.jsdelivr.net to fetch mermaid.js\n"))
return 1
}
markdownPath, listenAddress := args[0], args[1]
@@ -36,6 +57,14 @@ func Exec(args []string) int {
m.HandleFunc("GET /{$}", l.handleGetFile("index.html", embeddedHTML))
m.HandleFunc("GET /res.css", l.handleGetFile("res.css", embeddedCSS))
m.HandleFunc("GET /res.js", l.handleGetFile("res.js", embeddedJS))
+ if withMermaid {
+ buf, err := loadMermaidJS()
+ if err != nil {
+ log.Println(err.Error())
+ return 1
+ }
+ m.HandleFunc("GET /mermaid.js", l.handleGetFile("mermaid.js", buf))
+ }
m.HandleFunc("GET /data.html", l.handleGetDataHTML)
m.HandleFunc("GET /data.md", l.handleGetDataMarkdown)
m.HandleFunc("PUT /data.md", l.handlePutDataMarkdown)
@@ -73,6 +102,29 @@ func Exec(args []string) int {
return int(exitCode.Load())
}
+func loadMermaidJS() ([]byte, error) {
+ // load dependencies
+ const (
+ mermaidURL = "https://cdn.jsdelivr.net/npm/mermaid@11.17.2/dist/mermaid.min.js"
+ mermaidSHA256 = "581ed7d74bd9048d0e3a91363927d72ef22942d7722546b27f7cc29e35390eb8"
+ )
+ resp, err := http.Get(mermaidURL)
+ if err != nil {
+ return nil, fmt.Errorf("could not fetch %s: %w", mermaidURL, err)
+ }
+ defer resp.Body.Close()
+ buf, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("could not read response from %s: %w", mermaidURL, err)
+ }
+ hash := sha256.Sum256(buf)
+ digest := hex.EncodeToString(hash[:])
+ if digest != mermaidSHA256 {
+ return nil, fmt.Errorf("expected %s to match %s, but got %s", mermaidURL, mermaidSHA256, digest)
+ }
+ return buf, nil
+}
+
type logic struct {
MarkdownPath string
}
@@ -84,11 +136,21 @@ var (
embeddedCSS []byte
//go:embed res/app.js
embeddedJS []byte
+
+ withMermaidBlockRx = regexp.MustCompile(`<with-mermaid>(.*)</with-mermaid>`)
)
// Handles `GET /` and `GET /res/...`.
func (l logic) handleGetFile(name string, contents []byte) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
+ if name == "index.html" {
+ if withMermaid {
+ contents = withMermaidBlockRx.ReplaceAll(contents, []byte("$1"))
+ } else {
+ contents = withMermaidBlockRx.ReplaceAll(contents, nil)
+ }
+ }
+
http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(contents))
}
}
diff --git a/internal/mdedit/res/app.js b/internal/mdedit/res/app.js
index c934697..f022bae 100644
--- a/internal/mdedit/res/app.js
+++ b/internal/mdedit/res/app.js
@@ -29,9 +29,13 @@
const receiveHTML = body => {
$("div#preview").innerHTML = body;
$("span#status").innerText = "Saved";
+
+ if (window.mermaid) {
+ mermaid.run({ querySelector: "div#preview code.language-mermaid" });
+ }
};
- // whenever the <textedit> changes, wait 3 seconds and then save all changes
+ // whenever the <textedit> changes, wait 1 second and then save all changes
let timeoutID = null;
const onTextChange = event => {
if (timeoutID !== null) {
@@ -52,6 +56,11 @@
handleResponse(fetch("/data.md", opts), receiveHTML);
};
+ // initialize Mermaid on startup
+ if (window.mermaid) {
+ window.mermaid.initialize({ startOnLoad: false });
+ }
+
// load Markdown and rendered HTML on startup
handleResponse(fetch("/data.md"), body => {
const editor = $("textarea#editor");
diff --git a/internal/mdedit/res/index.html b/internal/mdedit/res/index.html
index 20aea13..601d41d 100644
--- a/internal/mdedit/res/index.html
+++ b/internal/mdedit/res/index.html
@@ -14,5 +14,6 @@
<textarea id="editor"></textarea>
<div id="preview"></div>
</body>
+<with-mermaid><script src="/mermaid.js"></script></with-mermaid>
<script src="/res.js"></script>
</html>