diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2025-09-30 13:34:10 +0200 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2025-09-30 13:37:25 +0200 |
| commit | e75d9e371269a4e46bffa6b3dd1fc7d040e82750 (patch) | |
| tree | c1f2a9aa8022a2710af5e5a577578855584540df /internal/mdedit/main.go | |
| parent | 0bb27576a37de4bf76dab35c73fa8790fcfa706a (diff) | |
| download | gofu-f8e0b3d1a52bae7500f4871cfcc855230216c279.tar.gz | |
add mdeditv2025.1
Diffstat (limited to 'internal/mdedit/main.go')
| -rw-r--r-- | internal/mdedit/main.go | 149 |
1 files changed, 149 insertions, 0 deletions
diff --git a/internal/mdedit/main.go b/internal/mdedit/main.go new file mode 100644 index 0000000..67cfdde --- /dev/null +++ b/internal/mdedit/main.go @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: 2025 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: GPL-3.0-only + +package mdedit + +import ( + "bytes" + "context" + _ "embed" + "io" + "log" + "net/http" + "os" + "os/signal" + "sync" + "sync/atomic" + "syscall" + "time" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/renderer/html" +) + +// Exec executes the mdedit applet and returns an exit code (0 for success, >0 for error). +func Exec(args []string) int { + if len(args) != 2 { + os.Stderr.Write([]byte(" Usage: mdedit <file.md> <listenaddr>\nExample: mdedit todolist.md localhost:8080\n")) + return 1 + } + markdownPath, listenAddress := args[0], args[1] + + // prepare HTTP server + l := logic{markdownPath} + m := http.NewServeMux() + 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)) + m.HandleFunc("GET /data.html", l.handleGetDataHTML) + m.HandleFunc("GET /data.md", l.handleGetDataMarkdown) + m.HandleFunc("PUT /data.md", l.handlePutDataMarkdown) + s := &http.Server{Addr: listenAddress, Handler: m} + + exitCode := &atomic.Int32{} + exitCode.Store(0) + + // setup termination of HTTP server on SIGINT/SIGTERM + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + var wg sync.WaitGroup + wg.Go(func() { + for range ctx.Done() { + } + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + err := s.Shutdown(ctx) + if err != nil { + log.Println("shutdown error: " + err.Error()) + exitCode.Store(1) + } + cancel() + }) + + // run HTTP server + log.Printf("listening on %s...\n", listenAddress) + err := s.ListenAndServe() + if err != http.ErrServerClosed { + log.Println("listen error: " + err.Error()) + exitCode.Store(1) + } + stop() + wg.Wait() + + return int(exitCode.Load()) +} + +type logic struct { + MarkdownPath string +} + +var ( + //go:embed res/index.html + embeddedHTML []byte + //go:embed res/style.css + embeddedCSS []byte + //go:embed res/app.js + embeddedJS []byte +) + +// Handles `GET /` and `GET /res/...`. +func (l logic) handleGetFile(name string, contents []byte) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(contents)) + } +} + +// Handles `GET /data.html`. +func (l logic) handleGetDataHTML(w http.ResponseWriter, r *http.Request) { + buf, err := os.ReadFile(l.MarkdownPath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + l.respondByRenderingMarkdown(w, buf) +} + +// Handles `GET /data.md`. +func (l logic) handleGetDataMarkdown(w http.ResponseWriter, r *http.Request) { + buf, err := os.ReadFile(l.MarkdownPath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("X-Mdedit-Path", l.MarkdownPath) + http.ServeContent(w, r, "data.md", time.Time{}, bytes.NewReader(buf)) +} + +// Handles `PUT /data.md`. +func (l logic) handlePutDataMarkdown(w http.ResponseWriter, r *http.Request) { + const maxSizeBytes = 8 * 1024 * 1024 // 8 MiB ought to be enough for a single Markdown file + buf, err := io.ReadAll(io.LimitReader(r.Body, maxSizeBytes)) + if err != nil { + http.Error(w, "while reading request body: "+err.Error(), http.StatusInternalServerError) + return + } + err = os.WriteFile(l.MarkdownPath, buf, 0666) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + l.respondByRenderingMarkdown(w, buf) +} + +var md = goldmark.New( + goldmark.WithExtensions(extension.GFM), + goldmark.WithRendererOptions(html.WithUnsafe()), +) + +func (l logic) respondByRenderingMarkdown(w http.ResponseWriter, source []byte) { + var buf bytes.Buffer + err := md.Convert(source, &buf) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write(buf.Bytes()) +} |
