66 lines
2.1 KiB
Go
66 lines
2.1 KiB
Go
// 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
|
||
|
|
}
|