feat: render Markdown, mathematics and Mermaid diagrams server-side
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package mathml converts TeX mathematics to MathML Core with an engine of
|
||||
// its own, built on the standard library alone. The grammar covers the
|
||||
// standard command surface that maps to MathML: the complete symbol tables,
|
||||
// fractions, scripts, operators with movable limits, stretchy delimiters,
|
||||
// the amsmath environments and bounded macros.
|
||||
//
|
||||
// A construct outside the mappable surface is neither dropped nor
|
||||
// mistranslated: it stays in the output as its verbatim source inside an
|
||||
// merror element, so the author sees exactly what was not understood.
|
||||
package mathml
|
||||
|
||||
import "strings"
|
||||
|
||||
// Render converts the TeX source to a MathML Core math element. The block
|
||||
// form sets display="block". The same source always produces
|
||||
// byte-identical output.
|
||||
func Render(source []byte, display bool) []byte {
|
||||
p := newParser(source, display)
|
||||
body := p.parseAll()
|
||||
var b strings.Builder
|
||||
b.WriteString(`<math xmlns="http://www.w3.org/1998/Math/MathML"`)
|
||||
if display {
|
||||
b.WriteString(` display="block"`)
|
||||
}
|
||||
b.WriteString(">")
|
||||
writeNodes(&b, body)
|
||||
b.WriteString("</math>")
|
||||
return []byte(b.String())
|
||||
}
|
||||
|
||||
// node is one element of the MathML tree. Attributes keep the order the
|
||||
// parser gave them, which keeps the output deterministic.
|
||||
type node struct {
|
||||
kind string
|
||||
text string
|
||||
attrs []attribute
|
||||
children []*node
|
||||
}
|
||||
|
||||
type attribute struct {
|
||||
key string
|
||||
value string
|
||||
}
|
||||
|
||||
func el(kind string, children ...*node) *node {
|
||||
return &node{kind: kind, children: children}
|
||||
}
|
||||
|
||||
// elA builds an element that carries attributes.
|
||||
func elA(kind string, attrs []attribute, children ...*node) *node {
|
||||
return &node{kind: kind, attrs: attrs, children: children}
|
||||
}
|
||||
|
||||
func text(kind, text string, attrs ...attribute) *node {
|
||||
return &node{kind: kind, text: text, attrs: attrs}
|
||||
}
|
||||
|
||||
// errorNode degrades a span of source to its verbatim text in a marked
|
||||
// element.
|
||||
func errorNode(source string) *node {
|
||||
return &node{kind: "merror", children: []*node{{kind: "mtext", text: source}}}
|
||||
}
|
||||
|
||||
// writeNodes renders nodes without indentation; the output is one line,
|
||||
// which keeps it a single inline unit in the surrounding HTML.
|
||||
func writeNodes(b *strings.Builder, nodes []*node) {
|
||||
for _, n := range nodes {
|
||||
writeNode(b, n)
|
||||
}
|
||||
}
|
||||
|
||||
func writeNode(b *strings.Builder, n *node) {
|
||||
b.WriteString("<")
|
||||
b.WriteString(n.kind)
|
||||
for _, a := range n.attrs {
|
||||
b.WriteString(" ")
|
||||
b.WriteString(a.key)
|
||||
b.WriteString(`="`)
|
||||
escapeXML(b, a.value)
|
||||
b.WriteString(`"`)
|
||||
}
|
||||
b.WriteString(">")
|
||||
escapeXML(b, n.text)
|
||||
writeNodes(b, n.children)
|
||||
b.WriteString("</")
|
||||
b.WriteString(n.kind)
|
||||
b.WriteString(">")
|
||||
}
|
||||
|
||||
var xmlEscaper = strings.NewReplacer("&", "&", "<", "<", ">", ">", `"`, """)
|
||||
|
||||
func escapeXML(b *strings.Builder, s string) {
|
||||
xmlEscaper.WriteString(b, s)
|
||||
}
|
||||
Reference in New Issue
Block a user