Files
scriptorium/internal/diagram/diagram.go
T

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
}