253 lines
6.2 KiB
Go
253 lines
6.2 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
|
||
|
|
package mathml
|
||
|
|
|
||
|
|
import "strings"
|
||
|
|
|
||
|
|
// envSpec describes a table environment: its delimiters, its column
|
||
|
|
// alignment, whether it renders in script size, whether it takes a column
|
||
|
|
// specification and whether it takes a number argument first.
|
||
|
|
type envSpec struct {
|
||
|
|
fences [2]string
|
||
|
|
align string
|
||
|
|
scripted bool
|
||
|
|
colSpec bool
|
||
|
|
number bool
|
||
|
|
}
|
||
|
|
|
||
|
|
var environments = map[string]envSpec{
|
||
|
|
"matrix": {}, "pmatrix": {fences: [2]string{"(", ")"}},
|
||
|
|
"bmatrix": {fences: [2]string{"[", "]"}},
|
||
|
|
"Bmatrix": {fences: [2]string{"{", "}"}},
|
||
|
|
"vmatrix": {fences: [2]string{"|", "|"}},
|
||
|
|
"Vmatrix": {fences: [2]string{"‖", "‖"}},
|
||
|
|
"matrix*": {},
|
||
|
|
"pmatrix*": {fences: [2]string{"(", ")"}},
|
||
|
|
"bmatrix*": {fences: [2]string{"[", "]"}},
|
||
|
|
"Bmatrix*": {fences: [2]string{"{", "}"}},
|
||
|
|
"vmatrix*": {fences: [2]string{"|", "|"}},
|
||
|
|
"Vmatrix*": {fences: [2]string{"‖", "‖"}},
|
||
|
|
"smallmatrix": {scripted: true},
|
||
|
|
"cases": {fences: [2]string{"{", ""}, align: "left"},
|
||
|
|
"rcases": {fences: [2]string{"", "}"}, align: "left"},
|
||
|
|
"dcases": {fences: [2]string{"{", ""}, align: "left"},
|
||
|
|
"drcases": {fences: [2]string{"", "}"}, align: "left"},
|
||
|
|
"aligned": {align: "right left"},
|
||
|
|
"align": {align: "right left"},
|
||
|
|
"align*": {align: "right left"},
|
||
|
|
"alignedat": {align: "right left", number: true},
|
||
|
|
"alignat": {align: "right left", number: true},
|
||
|
|
"alignat*": {align: "right left", number: true},
|
||
|
|
"split": {align: "right left"},
|
||
|
|
"gather": {}, "gather*": {},
|
||
|
|
"equation": {}, "equation*": {},
|
||
|
|
"array": {colSpec: true},
|
||
|
|
"darray": {colSpec: true},
|
||
|
|
"subarray": {colSpec: true, scripted: true},
|
||
|
|
}
|
||
|
|
|
||
|
|
// environment parses a whole \begin{name}...\end{name} construct. The
|
||
|
|
// cursor sits just after the \begin token.
|
||
|
|
func (p *parser) environment() *node {
|
||
|
|
begin := p.toks[p.pos-1]
|
||
|
|
name, ok := p.envName()
|
||
|
|
if !ok {
|
||
|
|
return errorNode(begin.text)
|
||
|
|
}
|
||
|
|
spec, supported := environments[name]
|
||
|
|
if !supported {
|
||
|
|
return p.degradeEnvironment(begin)
|
||
|
|
}
|
||
|
|
|
||
|
|
if spec.colSpec {
|
||
|
|
raw, ok := p.rawBraced()
|
||
|
|
if !ok {
|
||
|
|
return errorNode(begin.text)
|
||
|
|
}
|
||
|
|
_, supported = parseColSpec(raw)
|
||
|
|
if !supported {
|
||
|
|
return p.degradeEnvironment(begin)
|
||
|
|
}
|
||
|
|
} else if spec.number {
|
||
|
|
if _, ok := p.rawBraced(); !ok {
|
||
|
|
return errorNode(begin.text)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
rows := p.tableRows()
|
||
|
|
if !p.atCommand("end") {
|
||
|
|
p.pos = len(p.toks) - 1
|
||
|
|
return errorNode(string(p.src[begin.start:]))
|
||
|
|
}
|
||
|
|
endStart := p.toks[p.pos].start
|
||
|
|
p.pos++
|
||
|
|
endName, ok := p.envName()
|
||
|
|
if !ok || endName != name {
|
||
|
|
p.pos = len(p.toks) - 1
|
||
|
|
return errorNode(string(p.src[begin.start:]))
|
||
|
|
}
|
||
|
|
_ = endStart
|
||
|
|
table := buildTable(rows, spec)
|
||
|
|
if spec.scripted {
|
||
|
|
inner := elA("mstyle", []attribute{{"scriptlevel", "1"}})
|
||
|
|
inner.children = []*node{table}
|
||
|
|
table = inner
|
||
|
|
}
|
||
|
|
if spec.fences == [2]string{"", ""} {
|
||
|
|
return table
|
||
|
|
}
|
||
|
|
row := el("mrow")
|
||
|
|
row.children = []*node{p.fenceOf(spec.fences[0]), table, p.fenceOf(spec.fences[1])}
|
||
|
|
return row
|
||
|
|
}
|
||
|
|
|
||
|
|
// envName reads the environment name in braces.
|
||
|
|
func (p *parser) envName() (string, bool) {
|
||
|
|
raw, ok := p.rawBraced()
|
||
|
|
if !ok || raw == "" {
|
||
|
|
return "", false
|
||
|
|
}
|
||
|
|
return raw, true
|
||
|
|
}
|
||
|
|
|
||
|
|
// degradeEnvironment consumes a whole unsupported environment, up to and
|
||
|
|
// including its matching \end, and degrades it as its verbatim source.
|
||
|
|
func (p *parser) degradeEnvironment(begin token) *node {
|
||
|
|
depth := 1
|
||
|
|
end := len(p.src)
|
||
|
|
i := p.pos
|
||
|
|
for ; i < len(p.toks); i++ {
|
||
|
|
t := p.toks[i]
|
||
|
|
if t.kind != tokCommand {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
switch t.text {
|
||
|
|
case `\begin`:
|
||
|
|
depth++
|
||
|
|
case `\end`:
|
||
|
|
depth--
|
||
|
|
if depth == 0 {
|
||
|
|
end = t.end
|
||
|
|
// include the name argument of \end
|
||
|
|
if i+2 < len(p.toks) && p.toks[i+1].kind == tokLBrace && p.toks[i+2].kind == tokRBrace {
|
||
|
|
end = p.toks[i+2].end
|
||
|
|
i += 2
|
||
|
|
}
|
||
|
|
i++
|
||
|
|
p.pos = i
|
||
|
|
return errorNode(string(p.src[begin.start:end]))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
p.pos = len(p.toks) - 1
|
||
|
|
return errorNode(string(p.src[begin.start:]))
|
||
|
|
}
|
||
|
|
|
||
|
|
// tableRows parses the rows of a table, each row a slice of cells, until
|
||
|
|
// the \end or the end of input.
|
||
|
|
func (p *parser) tableRows() [][]*node {
|
||
|
|
var rows [][]*node
|
||
|
|
for {
|
||
|
|
var cells []*node
|
||
|
|
for {
|
||
|
|
nodes := p.sequence(true)
|
||
|
|
cell := el("mtd")
|
||
|
|
cell.children = nodes
|
||
|
|
cells = append(cells, cell)
|
||
|
|
if p.at(tokAmpersand) {
|
||
|
|
p.pos++
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
break
|
||
|
|
}
|
||
|
|
rows = append(rows, cells)
|
||
|
|
if isRowEnd(p.cur()) {
|
||
|
|
p.pos++
|
||
|
|
p.rowSpacing()
|
||
|
|
if p.at(tokEOF) || p.atCommand("end") {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
break
|
||
|
|
}
|
||
|
|
return rows
|
||
|
|
}
|
||
|
|
|
||
|
|
// rowSpacing skips the optional bracket after a row separator.
|
||
|
|
func (p *parser) rowSpacing() {
|
||
|
|
if p.at(tokChar) && p.cur().text == "[" {
|
||
|
|
for {
|
||
|
|
t := p.cur()
|
||
|
|
p.pos++
|
||
|
|
if t.kind == tokEOF || t.kind == tokChar && t.text == "]" {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// parseColSpec reads an array column specification: alignment letters and
|
||
|
|
// vertical rules, nothing else.
|
||
|
|
func parseColSpec(raw string) (int, bool) {
|
||
|
|
count := 0
|
||
|
|
for _, c := range raw {
|
||
|
|
switch c {
|
||
|
|
case 'l', 'c', 'r':
|
||
|
|
count++
|
||
|
|
case '|', ' ', '\t':
|
||
|
|
default:
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if count == 0 {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
return count, true
|
||
|
|
}
|
||
|
|
|
||
|
|
// buildTable assembles the mtable with its alignment attributes. The
|
||
|
|
// "right left" alignment alternates over the widest row.
|
||
|
|
func buildTable(rows [][]*node, spec envSpec) *node {
|
||
|
|
table := el("mtable")
|
||
|
|
cols := 0
|
||
|
|
for _, row := range rows {
|
||
|
|
if len(row) > cols {
|
||
|
|
cols = len(row)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
switch {
|
||
|
|
case spec.align == "left" && cols > 0:
|
||
|
|
table.attrs = append(table.attrs, attribute{"columnalign", "left"})
|
||
|
|
case spec.align == "right left" && cols > 0:
|
||
|
|
var b strings.Builder
|
||
|
|
for i := range cols {
|
||
|
|
if i > 0 {
|
||
|
|
b.WriteString(" ")
|
||
|
|
}
|
||
|
|
if i%2 == 0 {
|
||
|
|
b.WriteString("right")
|
||
|
|
} else {
|
||
|
|
b.WriteString("left")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
table.attrs = append(table.attrs, attribute{"columnalign", b.String()})
|
||
|
|
}
|
||
|
|
for _, row := range rows {
|
||
|
|
tr := el("mtr")
|
||
|
|
tr.children = row
|
||
|
|
table.children = append(table.children, tr)
|
||
|
|
}
|
||
|
|
return table
|
||
|
|
}
|
||
|
|
|
||
|
|
// substack renders the rows of a \substack argument in script size.
|
||
|
|
func (p *parser) substack(raw string) *node {
|
||
|
|
q := newParser([]byte(raw), false)
|
||
|
|
rows := q.tableRows()
|
||
|
|
table := buildTable(rows, envSpec{})
|
||
|
|
inner := elA("mstyle", []attribute{{"scriptlevel", "1"}})
|
||
|
|
inner.children = []*node{table}
|
||
|
|
return inner
|
||
|
|
}
|