Files
sjy01-image-proc/vendor/github.com/go-latex/latex/mtex/parser.go
2024-10-24 15:46:01 +08:00

574 lines
14 KiB
Go

// Copyright ©2020 The go-latex Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mtex
import (
"fmt"
"math"
"strconv"
"strings"
"github.com/go-latex/latex"
"github.com/go-latex/latex/ast"
"github.com/go-latex/latex/font"
"github.com/go-latex/latex/internal/tex2unicode"
"github.com/go-latex/latex/mtex/symbols"
"github.com/go-latex/latex/tex"
)
// Parse parses a LaTeX math expression and returns the TeX-like box model
// and an error if any.
func Parse(expr string, fontSize, DPI float64, backend font.Backend) (tex.Node, error) {
p := newParser(backend)
return p.parse(expr, fontSize, DPI)
}
type parser struct {
be font.Backend
expr string
macros map[string]handler
}
func newParser(be font.Backend) *parser {
p := &parser{
be: be,
macros: make(map[string]handler),
}
p.init()
return p
}
func (p *parser) parse(x string, size, dpi float64) (tex.Node, error) {
p.expr = x
node, err := latex.ParseExpr(x)
if err != nil {
return nil, fmt.Errorf("could not parse latex expression %q: %w", x, err)
}
state := tex.NewState(p.be, font.Font{
Name: "default",
Size: size,
Type: "rm",
}, dpi)
v := visitor{p: p, state: state}
ast.Walk(&v, node)
nodes := tex.HListOf(v.nodes, true)
return nodes, nil
}
type visitor struct {
p *parser
nodes []tex.Node
state tex.State
math bool
}
func (v *visitor) Visit(n ast.Node) ast.Visitor {
switch n := n.(type) {
case ast.List:
case *ast.Symbol:
switch {
case v.math:
h := v.p.handler(n.Text)
if h == nil {
panic("no handler for symbol [" + n.Text + "]")
}
v.nodes = append(v.nodes, h.Handle(v.p, n, v.state, v.math))
default:
v.nodes = append(v.nodes, tex.NewChar(string(n.Text), v.state, v.math))
}
case *ast.Word:
var nodes []tex.Node
for _, x := range n.Text {
nodes = append(nodes, tex.NewChar(string(x), v.state, v.math))
}
v.nodes = append(v.nodes, tex.HListOf(nodes, true))
case *ast.Literal:
h := handlerFunc(handleSymbol)
for _, c := range n.Text {
n := &ast.Literal{Text: string(c)}
v.nodes = append(v.nodes, h.Handle(v.p, n, v.state, v.math))
}
case *ast.MathExpr:
oldm := v.math
oldt := v.state.Font.Type
v.math = true
v.state.Font.Type = rcparams("mathtext.default").(string)
for _, x := range n.List {
v.Visit(x)
}
v.math = oldm
v.state.Font.Type = oldt
return nil
case *ast.Macro:
if n.Name == nil {
panic("macro with nil identifier")
}
macro := n.Name.Name
h := v.p.handler(macro)
if h == nil {
panic(fmt.Errorf("unknown macro %q", macro))
}
v.nodes = append(v.nodes, h.Handle(v.p, n, v.state, v.math))
return nil
case nil:
return v
default:
panic(fmt.Errorf("unknown ast node %T", n))
}
return v
}
func (p *parser) handleNode(node ast.Node, state tex.State, math bool) tex.Node {
v := visitor{p: p, state: state, math: math}
ast.Walk(&v, node)
return tex.HListOf(v.nodes, true)
}
func (p *parser) handler(name string) handler {
if _, ok := spaceWidth[name]; ok {
return handlerFunc(handleSpace)
}
if symbols.IsSpaced(name) || symbols.PunctuationSymbols.Has(name) {
return handlerFunc(handleSymbol)
}
if name == `\hspace` {
return handlerFunc(handleCustomSpace)
}
if symbols.FunctionNames.Has(name[1:]) { // drop leading `\`
return handlerFunc(handleFunction)
}
switch name {
case `\frac`:
return handlerFunc(handleFrac)
case `\dfrac`:
return handlerFunc(handleDFrac)
case `\tfrac`:
return handlerFunc(handleTFrac)
case `\binom`:
return handlerFunc(handleBinom)
// case `\genfrac`:
// return handlerFunc(handleGenFrac)
case `\sqrt`:
return handlerFunc(handleSqrt)
case `\overline`:
return handlerFunc(handleOverline)
}
_, ok := p.macros[name]
if ok {
return handlerFunc(handleSymbol)
}
return nil
}
func (p *parser) init() {
for _, k := range tex2unicode.Symbols() {
p.macros[`\`+k] = builtinMacro("")
}
for k, v := range builtinMacros {
p.macros[k] = v
}
}
func handleSymbol(p *parser, node ast.Node, state tex.State, math bool) tex.Node {
pos := int(node.Pos())
sym := ""
switch node := node.(type) {
case *ast.Macro:
sym = node.Name.Name
case *ast.Symbol:
sym = node.Text
case *ast.Word:
sym = node.Text
case *ast.Literal:
sym = node.Text
default:
panic("invalid ast Node")
}
ch := tex.NewChar(sym, state, math)
switch {
case symbols.IsSpaced(sym):
i := strings.LastIndexFunc(p.expr[:pos], func(r rune) bool {
return r != ' '
})
prev := ""
if i >= 0 {
prev = string(p.expr[i])
}
switch {
case symbols.BinaryOperators.Has(sym) && (len(strings.Split(p.expr[:pos], " ")) == 0 ||
prev == "{" ||
symbols.LeftDelim.Has(prev)):
// binary operators at start of string should not be spaced
return ch
default:
return tex.HListOf([]tex.Node{
p.makeSpace(state, 0.2),
ch,
p.makeSpace(state, 0.2),
}, true)
}
case symbols.PunctuationSymbols.Has(sym):
switch sym {
case ".":
pos := strings.Index(p.expr[pos:], sym)
if (pos > 0 && isdigit(p.expr[pos-1])) &&
(pos < len(p.expr)-1 && isdigit(p.expr[pos+1])) {
// do not space dots as decimal separators.
return ch
}
return tex.HListOf([]tex.Node{
ch,
p.makeSpace(state, 0.2),
}, true)
}
panic("not implemented")
}
return ch
}
var spaceWidth = map[string]float64{
`\,`: 0.16667, // 3/18 em = 3 mu
`\thinspace`: 0.16667, // 3/18 em = 3 mu
`\/`: 0.16667, // 3/18 em = 3 mu
`\>`: 0.22222, // 4/18 em = 4 mu
`\:`: 0.22222, // 4/18 em = 4 mu
`\;`: 0.27778, // 5/18 em = 5 mu
`\ `: 0.33333, // 6/18 em = 6 mu
`~`: 0.33333, // 6/18 em = 6 mu, nonbreakable
`\enspace`: 0.5, // 9/18 em = 9 mu
`\quad`: 1, // 1 em = 18 mu
`\qquad`: 2, // 2 em = 36 mu
`\!`: -0.16667, // -3/18 em = -3 mu
}
func handleSpace(p *parser, node ast.Node, state tex.State, math bool) tex.Node {
var (
width float64
ok bool
)
switch node := node.(type) {
case *ast.Symbol:
width, ok = spaceWidth[node.Text]
case *ast.Macro:
width, ok = spaceWidth[node.Name.Name]
default:
panic(fmt.Errorf("invalid ast node %#v (%T)", node, node))
}
if !ok {
panic(fmt.Errorf("could not find a width for %#v (%T)", node, node))
}
return p.makeSpace(state, width)
}
func handleCustomSpace(p *parser, node ast.Node, state tex.State, math bool) tex.Node {
macro := node.(*ast.Macro)
arg := macro.Args[0].(*ast.Arg).List[0].(*ast.Literal).Text
val, err := strconv.ParseFloat(arg, 64)
if err != nil {
panic(fmt.Errorf("could not parse customspace: %+v", err))
}
return p.makeSpace(state, val)
}
func handleFunction(p *parser, node ast.Node, state tex.State, math bool) tex.Node {
macro := node.(*ast.Macro)
state.Font.Type = "rm"
fun := macro.Name.Name[1:] // drop leading `\`
nodes := make([]tex.Node, 0, len(fun))
for _, c := range fun {
nodes = append(nodes, tex.NewChar(string(c), state, math))
}
return tex.HListOf(nodes, true)
}
func handleFrac(p *parser, node ast.Node, state tex.State, math bool) tex.Node {
var (
macro = node.(*ast.Macro)
thickness = state.Backend().UnderlineThickness(state.Font, state.DPI)
numNode = ast.List(macro.Args[0].(*ast.Arg).List)
denNode = ast.List(macro.Args[1].(*ast.Arg).List)
)
num := p.handleNode(numNode, state, math)
den := p.handleNode(denNode, state, math)
// FIXME(sbinet): this should be infered from the context.
// ie: textStyle when in $ $ environment.
// displayStyle when in \[\] environment.
sty := textStyle
return p.genfrac("", "", thickness, sty, num, den, state)
}
func handleDFrac(p *parser, node ast.Node, state tex.State, math bool) tex.Node {
var (
macro = node.(*ast.Macro)
thickness = state.Backend().UnderlineThickness(state.Font, state.DPI)
numNode = ast.List(macro.Args[0].(*ast.Arg).List)
denNode = ast.List(macro.Args[1].(*ast.Arg).List)
)
num := p.handleNode(numNode, state, math)
den := p.handleNode(denNode, state, math)
return p.genfrac("", "", thickness, displayStyle, num, den, state)
}
func handleTFrac(p *parser, node ast.Node, state tex.State, math bool) tex.Node {
var (
macro = node.(*ast.Macro)
thickness = state.Backend().UnderlineThickness(state.Font, state.DPI)
numNode = ast.List(macro.Args[0].(*ast.Arg).List)
denNode = ast.List(macro.Args[1].(*ast.Arg).List)
)
num := p.handleNode(numNode, state, math)
den := p.handleNode(denNode, state, math)
return p.genfrac("", "", thickness, textStyle, num, den, state)
}
func handleBinom(p *parser, node ast.Node, state tex.State, math bool) tex.Node {
var (
macro = node.(*ast.Macro)
numNode = ast.List(macro.Args[0].(*ast.Arg).List)
denNode = ast.List(macro.Args[1].(*ast.Arg).List)
)
num := p.handleNode(numNode, state, math)
den := p.handleNode(denNode, state, math)
return p.genfrac("(", ")", 0, textStyle, num, den, state)
}
func (p *parser) genfrac(ldelim, rdelim string, rule float64, style mathStyleKind, num, den tex.Node, state tex.State) tex.Node {
thickness := state.Backend().UnderlineThickness(state.Font, state.DPI)
if style != displayStyle {
num.Shrink()
den.Shrink()
}
cnum := tex.HCentered([]tex.Node{num})
cden := tex.HCentered([]tex.Node{den})
width := math.Max(num.Width(), den.Width())
const additional = false // i.e.: exactly
cnum.HPack(width, additional)
cden.HPack(width, additional)
vlist := tex.VListOf([]tex.Node{
cnum, // numerator
tex.VBox(0, thickness*2), // space
tex.HRule(state, rule), // rule
tex.VBox(0, thickness*2), // space
cden, // denominator
})
// shift so the fraction line sits in the middle of the '=' sign
fnt := state.Font
fnt.Type = rcparams("mathtext.default").(string)
metrics := state.Backend().Metrics("=", fnt, state.DPI, true)
shift := cden.Height() - ((metrics.YMax+metrics.YMin)/2 - 3*thickness)
vlist.SetShift(shift)
box := tex.HListOf([]tex.Node{vlist, tex.HBox(2 * thickness)}, true)
if ldelim != "" || rdelim != "" {
if ldelim == "" {
ldelim = "."
}
if rdelim == "" {
rdelim = "."
}
return p.autoSizedDelimiter(ldelim, []tex.Node{box}, rdelim, state)
}
return box
}
func handleSqrt(p *parser, node ast.Node, state tex.State, math bool) tex.Node {
var (
macro = node.(*ast.Macro)
root tex.Node
body *tex.HList
)
switch len(macro.Args) {
case 2:
root = p.handleNode(
ast.List(macro.Args[0].(*ast.OptArg).List),
state, math,
)
body = p.handleNode(
ast.List(macro.Args[1].(*ast.Arg).List),
state, math,
).(*tex.HList)
case 1:
// ok
body = p.handleNode(
ast.List(macro.Args[0].(*ast.Arg).List),
state, math,
).(*tex.HList)
default:
panic("invalid sqrt")
}
thickness := state.Backend().UnderlineThickness(state.Font, state.DPI)
// determine the height of the body, add a little extra to it so
// it doesn't seem too cramped.
height := body.Height() - body.Shift() + 5*thickness
depth := body.Depth() + body.Shift()
check := tex.AutoHeightChar(`\__sqrt__`, height, depth, state, 0)
height = check.Height() - check.Shift()
depth = check.Depth() + check.Shift()
// put a little extra space to the left and right of the body
padded := tex.HListOf([]tex.Node{
tex.HBox(2 * thickness),
body,
tex.HBox(2 * thickness),
}, true)
rhs := tex.VListOf([]tex.Node{
tex.HRule(state, -1),
tex.NewGlue("fill"),
padded,
})
// stretch the glue between the HRule and the body
const additional = false
rhs.VPack(height+(state.Font.Size*state.DPI)/(100*12), additional, depth)
// add the root and shift it upward so it is above the tick.
switch root {
case nil:
root = tex.HBox(check.Width() * 0.5)
default:
root.Shrink()
root.Shrink()
}
vl := tex.VListOf([]tex.Node{
tex.HListOf([]tex.Node{
root,
}, true),
})
vl.SetShift(-height * 0.6)
hl := tex.HListOf([]tex.Node{
vl, // root
// negative kerning to put root over tick
tex.NewKern(-check.Width() * 0.5),
check,
rhs,
}, true)
return hl
}
func handleOverline(p *parser, node ast.Node, state tex.State, math bool) tex.Node {
macro := node.(*ast.Macro)
body := p.handleNode(
ast.List(macro.Args[0].(*ast.Arg).List),
state, math,
).(*tex.HList)
thickness := state.Backend().UnderlineThickness(state.Font, state.DPI)
height := body.Height() - body.Shift() + 3*thickness
depth := body.Depth() + body.Shift()
// place overline above body
rhs := tex.VListOf([]tex.Node{
tex.HRule(state, -1),
tex.NewGlue("fill"),
tex.HListOf([]tex.Node{body}, true),
})
// stretch the glue between the HRule and the body
const additional = false
rhs.VPack(height+(state.Font.Size*state.DPI)/(100*12), additional, depth)
hl := tex.HListOf([]tex.Node{rhs}, true)
return hl
}
func (p *parser) makeSpace(state tex.State, percentage float64) *tex.Kern {
const math = true
fnt := state.Font
fnt.Name = "it"
fnt.Type = rcparams("mathtext.default").(string)
width := p.be.Metrics("m", fnt, state.DPI, math).Advance
return tex.NewKern(width * percentage)
}
func (p *parser) autoSizedDelimiter(left string, middle []tex.Node, right string, state tex.State) tex.Node {
var (
height float64
depth float64
factor float64 = 1
)
if len(middle) > 0 {
for _, node := range middle {
height = math.Max(height, node.Height())
depth = math.Max(depth, node.Depth())
}
factor = 0
}
var parts []tex.Node
if left != "." {
// \left. isn't supposed to produce any symbol
ahc := tex.AutoHeightChar(left, height, depth, state, factor)
parts = append(parts, ahc)
}
parts = append(parts, middle...)
if right != "." {
// \right. isn't supposed to produce any symbol
ahc := tex.AutoHeightChar(right, height, depth, state, factor)
parts = append(parts, ahc)
}
return tex.HListOf(parts, true)
}
type mathStyleKind int
const (
displayStyle mathStyleKind = iota
textStyle
//scriptStyle // FIXME
//scriptScriptStyle // FIXME
)
func rcparams(k string) interface{} {
switch k {
case "mathtext.default":
return "it"
default:
panic("unknown rc.params key [" + k + "]")
}
}
func isdigit(v byte) bool {
switch v {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
return true
}
return false
}