222 lines
5.1 KiB
Go
222 lines
5.1 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package mathml
|
|
|
|
import "strings"
|
|
|
|
// macro is a user-defined command: the number of arguments its body takes
|
|
// and the body itself as tokens.
|
|
type macro struct {
|
|
args int
|
|
body []token
|
|
}
|
|
|
|
// The depth limits one call site from nesting forever; the budget bounds
|
|
// the whole parse, so a self-splicing macro can never outrun the parser.
|
|
const (
|
|
macroExpansionDepth = 64
|
|
macroExpansionBudget = 10000
|
|
)
|
|
|
|
// expandMacros replaces the macro call at the cursor with its expanded
|
|
// body, ready for the parser to read. A call beyond the limits degrades
|
|
// to its own source.
|
|
func (p *parser) expandMacros() {
|
|
for range macroExpansionDepth {
|
|
t := p.toks[p.pos]
|
|
if t.kind != tokCommand {
|
|
return
|
|
}
|
|
if p.expansions >= macroExpansionBudget {
|
|
p.degradeAt(p.pos, t)
|
|
return
|
|
}
|
|
m, ok := p.macros[t.text[1:]]
|
|
if !ok {
|
|
return
|
|
}
|
|
j := p.pos + 1
|
|
args := make([][]token, 0, m.args)
|
|
for range m.args {
|
|
arg, next, ok := p.argTokens(j)
|
|
if !ok {
|
|
break
|
|
}
|
|
args = append(args, arg)
|
|
j = next
|
|
}
|
|
if len(args) != m.args {
|
|
p.degradeAt(p.pos, t)
|
|
return
|
|
}
|
|
var body []token
|
|
for i := 0; i < len(m.body); i++ {
|
|
bt := m.body[i]
|
|
if bt.kind == tokChar && bt.text == "#" && i+1 < len(m.body) &&
|
|
m.body[i+1].kind == tokChar && len(m.body[i+1].text) == 1 &&
|
|
isDigitByte(m.body[i+1].text[0]) {
|
|
k := int(m.body[i+1].text[0] - '0')
|
|
if k >= 1 && k <= len(args) {
|
|
body = append(body, args[k-1]...)
|
|
}
|
|
i++
|
|
continue
|
|
}
|
|
body = append(body, bt)
|
|
}
|
|
// The spliced tokens carry the call site as their position, so a
|
|
// construct that fails inside a macro degrades at the call.
|
|
for i := range body {
|
|
body[i].start = t.start
|
|
body[i].end = t.end
|
|
}
|
|
spliced := make([]token, 0, len(p.toks)-(j-p.pos)+len(body))
|
|
spliced = append(spliced, p.toks[:p.pos]...)
|
|
spliced = append(spliced, body...)
|
|
spliced = append(spliced, p.toks[j:]...)
|
|
p.toks = spliced
|
|
p.expansions++
|
|
}
|
|
t := p.toks[p.pos]
|
|
p.degradeAt(p.pos, t)
|
|
}
|
|
|
|
// degradeAt replaces one token with a degraded token.
|
|
func (p *parser) degradeAt(i int, t token) {
|
|
p.toks[i] = token{kind: tokDegraded, text: t.text, start: t.start, end: t.end}
|
|
}
|
|
|
|
// argTokens reads one macro argument from position j: a braced group with
|
|
// its braces, or a single token.
|
|
func (p *parser) argTokens(j int) ([]token, int, bool) {
|
|
if j >= len(p.toks) || p.toks[j].kind == tokEOF {
|
|
return nil, j, false
|
|
}
|
|
if p.toks[j].kind != tokLBrace {
|
|
return []token{p.toks[j]}, j + 1, true
|
|
}
|
|
depth := 0
|
|
for k := j; k < len(p.toks); k++ {
|
|
switch p.toks[k].kind {
|
|
case tokLBrace:
|
|
depth++
|
|
case tokRBrace:
|
|
depth--
|
|
if depth == 0 {
|
|
group := make([]token, k+1-j)
|
|
copy(group, p.toks[j:k+1])
|
|
return group, k + 1, true
|
|
}
|
|
case tokEOF:
|
|
return nil, j, false
|
|
}
|
|
}
|
|
return nil, j, false
|
|
}
|
|
|
|
// macroDefinition registers a \newcommand or \def style definition and
|
|
// produces no output. The cursor sits just after the definition command.
|
|
func (p *parser) macroDefinition(kind string) *node {
|
|
source := `\` + kind
|
|
if kind == "DeclareMathOperator" || kind == "DeclareMathOperator*" {
|
|
name := p.defName()
|
|
if name == "" {
|
|
return errorNode(source)
|
|
}
|
|
body, ok := p.rawBraced()
|
|
if !ok {
|
|
return errorNode(source)
|
|
}
|
|
wrap := `\operatorname{` + body + `}`
|
|
if strings.HasSuffix(kind, "*") {
|
|
wrap = `\operatorname*{` + body + `}`
|
|
}
|
|
p.macros[name] = macro{body: tokenise([]byte(wrap))}
|
|
return nil
|
|
}
|
|
if kind == "def" || kind == "gdef" {
|
|
return p.tecDefinition(source)
|
|
}
|
|
name := p.defName()
|
|
if name == "" {
|
|
return errorNode(source)
|
|
}
|
|
args := 0
|
|
if count, ok := p.bracketArg(); ok && count != "" {
|
|
n := 0
|
|
for i := 0; i < len(count); i++ {
|
|
if !isDigitByte(count[i]) {
|
|
return errorNode(source)
|
|
}
|
|
n = n*10 + int(count[i]-'0')
|
|
}
|
|
if n > 9 {
|
|
return errorNode(source)
|
|
}
|
|
args = n
|
|
}
|
|
body, ok := p.rawBraced()
|
|
if !ok {
|
|
return errorNode(source)
|
|
}
|
|
p.macros[name] = macro{args: args, body: tokenise([]byte(body))}
|
|
return nil
|
|
}
|
|
|
|
// tecDefinition registers a \def, whose parameter text names undelimited
|
|
// arguments with #1 up to #9.
|
|
func (p *parser) tecDefinition(source string) *node {
|
|
name := p.defName()
|
|
if name == "" {
|
|
return errorNode(source)
|
|
}
|
|
args := 0
|
|
for {
|
|
t := p.cur()
|
|
if t.kind == tokChar && t.text == "#" {
|
|
p.pos++
|
|
d := p.cur()
|
|
if d.kind != tokChar || len(d.text) != 1 || !isDigitByte(d.text[0]) {
|
|
return errorNode(source)
|
|
}
|
|
if int(d.text[0]-'0') != args+1 {
|
|
return errorNode(source)
|
|
}
|
|
args++
|
|
p.pos++
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
body, ok := p.rawBraced()
|
|
if !ok {
|
|
return errorNode(source)
|
|
}
|
|
p.macros[name] = macro{args: args, body: tokenise([]byte(body))}
|
|
return nil
|
|
}
|
|
|
|
// defName reads the name a definition declares: a braced command or a
|
|
// bare command.
|
|
func (p *parser) defName() string {
|
|
if p.at(tokLBrace) {
|
|
p.pos++
|
|
if p.at(tokCommand) {
|
|
name := p.cur().text[1:]
|
|
p.pos++
|
|
if p.at(tokRBrace) {
|
|
p.pos++
|
|
return name
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
if p.at(tokCommand) {
|
|
name := p.cur().text[1:]
|
|
p.pos++
|
|
return name
|
|
}
|
|
return ""
|
|
}
|