feat: render Markdown, mathematics and Mermaid diagrams server-side
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package diagram renders Mermaid diagrams to SVG with an engine of its
|
||||
// own, built on the standard library alone. The flowchart grammar,
|
||||
// including the historical "graph" spelling, and the sequenceDiagram
|
||||
// grammar are covered; every other diagram type is refused with an error
|
||||
// naming it.
|
||||
//
|
||||
// The output is deterministic: the same source always renders
|
||||
// byte-identical SVG, because every layout decision falls back to the
|
||||
// order of appearance and never to map iteration.
|
||||
package diagram
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Render renders the Mermaid diagram source to SVG. Statements are lines;
|
||||
// blank lines and lines opening with %% are comments. A diagram type the
|
||||
// engine does not carry is refused with an error naming the type, and a
|
||||
// statement the grammar does not know is refused with an error naming the
|
||||
// line.
|
||||
func Render(source []byte) ([]byte, error) {
|
||||
lines := statementLines(source)
|
||||
if len(lines) == 0 {
|
||||
return nil, fmt.Errorf("diagram: empty source")
|
||||
}
|
||||
header := strings.Fields(lines[0])
|
||||
switch strings.ToLower(header[0]) {
|
||||
case "flowchart", "graph":
|
||||
dir := "TB"
|
||||
if len(header) > 1 {
|
||||
dir = strings.ToUpper(header[1])
|
||||
if !flowDirections[dir] {
|
||||
return nil, fmt.Errorf("diagram: unknown flow direction %q", header[1])
|
||||
}
|
||||
}
|
||||
return renderFlowchart(lines[1:], dir)
|
||||
case "sequencediagram":
|
||||
auto := len(header) > 1 && strings.ToLower(header[1]) == "autonumber"
|
||||
return renderSequence(lines[1:], auto)
|
||||
default:
|
||||
return nil, fmt.Errorf("diagram: unsupported diagram type %q", header[0])
|
||||
}
|
||||
}
|
||||
|
||||
var flowDirections = map[string]bool{
|
||||
"TB": true, "TD": true, "BT": true, "LR": true, "RL": true,
|
||||
}
|
||||
|
||||
// statementLines splits the source into statements: one statement per
|
||||
// line, comments and blank lines dropped.
|
||||
func statementLines(source []byte) []string {
|
||||
var lines []string
|
||||
for raw := range strings.SplitSeq(string(source), "\n") {
|
||||
line := strings.TrimSpace(raw)
|
||||
if line == "" || strings.HasPrefix(line, "%%") {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
Reference in New Issue
Block a user