98 lines
2.8 KiB
Go
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("&", "&", "<", "<", ">", ">", `"`, """)
|
||
|
|
|
||
|
|
func escapeXML(b *strings.Builder, s string) {
|
||
|
|
xmlEscaper.WriteString(b, s)
|
||
|
|
}
|