feat: render Markdown, mathematics and Mermaid diagrams server-side
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package diagram
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// point is a coordinate in the layout. Everything is an integer, so the
|
||||
// same layout always writes the same bytes.
|
||||
type point struct {
|
||||
x, y int
|
||||
}
|
||||
|
||||
// svgBuilder writes SVG elements in the order they are given.
|
||||
type svgBuilder struct {
|
||||
b strings.Builder
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
func newSVGBuilder(width, height int) *svgBuilder {
|
||||
s := &svgBuilder{width: width, height: height}
|
||||
fmt.Fprintf(&s.b, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" font-family="sans-serif">`, width, height)
|
||||
return s
|
||||
}
|
||||
|
||||
// rect draws a rectangle, rounded when rx is positive.
|
||||
func (s *svgBuilder) rect(x, y, w, h, rx int, attrs string) {
|
||||
if w < 0 || h < 0 {
|
||||
return
|
||||
}
|
||||
if rx > 0 {
|
||||
fmt.Fprintf(&s.b, `<rect x="%d" y="%d" width="%d" height="%d" rx="%d"%s/>`, x, y, w, h, rx, attrs)
|
||||
} else {
|
||||
fmt.Fprintf(&s.b, `<rect x="%d" y="%d" width="%d" height="%d"%s/>`, x, y, w, h, attrs)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *svgBuilder) line(x1, y1, x2, y2 int, attrs string) {
|
||||
fmt.Fprintf(&s.b, `<line x1="%d" y1="%d" x2="%d" y2="%d"%s/>`, x1, y1, x2, y2, attrs)
|
||||
}
|
||||
|
||||
func (s *svgBuilder) path(d, attrs string) {
|
||||
s.b.WriteString(`<path d="`)
|
||||
s.b.WriteString(d)
|
||||
s.b.WriteString(`"`)
|
||||
s.b.WriteString(attrs)
|
||||
s.b.WriteString(`/>`)
|
||||
}
|
||||
|
||||
// text writes one line of text. The anchor is middle, start or end.
|
||||
func (s *svgBuilder) text(p point, str, anchor, attrs string) {
|
||||
s.b.WriteString(`<text x="`)
|
||||
fmt.Fprintf(&s.b, `%d`, p.x)
|
||||
s.b.WriteString(`" y="`)
|
||||
fmt.Fprintf(&s.b, `%d`, p.y)
|
||||
if anchor != "" {
|
||||
s.b.WriteString(`" text-anchor="`)
|
||||
s.b.WriteString(anchor)
|
||||
}
|
||||
s.b.WriteString(`"`)
|
||||
s.b.WriteString(attrs)
|
||||
s.b.WriteString(`>`)
|
||||
s.b.WriteString(escapeXML(str))
|
||||
s.b.WriteString(`</text>`)
|
||||
}
|
||||
|
||||
func (s *svgBuilder) polygon(pts []point, attrs string) {
|
||||
if len(pts) < 3 {
|
||||
return
|
||||
}
|
||||
s.b.WriteString(`<polygon points="`)
|
||||
for i, p := range pts {
|
||||
if i > 0 {
|
||||
s.b.WriteString(" ")
|
||||
}
|
||||
fmt.Fprintf(&s.b, `%d,%d`, p.x, p.y)
|
||||
}
|
||||
s.b.WriteString(`"`)
|
||||
s.b.WriteString(attrs)
|
||||
s.b.WriteString(`/>`)
|
||||
}
|
||||
|
||||
func (s *svgBuilder) finish() []byte {
|
||||
s.b.WriteString("</svg>")
|
||||
return []byte(s.b.String())
|
||||
}
|
||||
|
||||
var xmlEscaper = strings.NewReplacer("&", "&", "<", "<", ">", ">", `"`, """)
|
||||
|
||||
func escapeXML(s string) string {
|
||||
return xmlEscaper.Replace(s)
|
||||
}
|
||||
|
||||
// textWidth estimates the width a string needs at the given font size.
|
||||
// Characters outside the basic plane are wide: the estimate stays on the
|
||||
// generous side so labels keep their padding.
|
||||
func textWidth(s string, size int) int {
|
||||
w := 0
|
||||
for _, r := range s {
|
||||
if r > 0x2E7F {
|
||||
w += size
|
||||
} else {
|
||||
w += (size*6 + 4) / 10
|
||||
}
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// labelLines splits a label into its lines.
|
||||
func labelLines(s string) []string {
|
||||
if s == "" {
|
||||
return []string{""}
|
||||
}
|
||||
normalised := strings.ReplaceAll(s, "<br/>", "<br>")
|
||||
normalised = strings.ReplaceAll(normalised, "<br />", "<br>")
|
||||
lines := strings.Split(normalised, "<br>")
|
||||
if len(lines) == 0 {
|
||||
return []string{""}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// styleString joins style pairs into a style attribute, declaration order
|
||||
// preserved.
|
||||
func styleString(pairs []stylePair) string {
|
||||
if len(pairs) == 0 {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(` style="`)
|
||||
for i, p := range pairs {
|
||||
if i > 0 {
|
||||
b.WriteString(";")
|
||||
}
|
||||
b.WriteString(p.key)
|
||||
b.WriteString(":")
|
||||
b.WriteString(p.value)
|
||||
}
|
||||
b.WriteString(`"`)
|
||||
return b.String()
|
||||
}
|
||||
Reference in New Issue
Block a user