Files

98 lines
2.8 KiB
Go

// 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("&", "&amp;", "<", "&lt;", ">", "&gt;", `"`, "&quot;")
func escapeXML(b *strings.Builder, s string) {
xmlEscaper.WriteString(b, s)
}