package tex
import (
"fmt"
"strings"
"github.com/d2lang/mathjax-go/internal/mml"
)
var empheqMO = map [string ]string {
"empheqlbrace" : "{" , "empheqrbrace" : "}" ,
"empheqlbrack" : "[" , "empheqrbrack" : "]" ,
"empheqlangle" : "⟨" , "empheqrangle" : "⟩" ,
"empheqlparen" : "(" , "empheqrparen" : ")" ,
"empheqlvert" : "|" , "empheqrvert" : "|" ,
"empheqlVert" : "‖" , "empheqrVert" : "‖" ,
"empheqlfloor" : "⌊" , "empheqrfloor" : "⌋" ,
"empheqlceil" : "⌈" , "empheqrceil" : "⌉" ,
"empheqbiglbrace" : "{" , "empheqbigrbrace" : "}" ,
"empheqbiglbrack" : "[" , "empheqbigrbrack" : "]" ,
"empheqbiglangle" : "⟨" , "empheqbigrangle" : "⟩" ,
"empheqbiglparen" : "(" , "empheqbigrparen" : ")" ,
"empheqbiglvert" : "|" , "empheqbigrvert" : "|" ,
"empheqbiglVert" : "‖" , "empheqbigrVert" : "‖" ,
"empheqbiglfloor" : "⌊" , "empheqbigrfloor" : "⌋" ,
"empheqbiglceil" : "⌈" , "empheqbigrceil" : "⌉" ,
}
var empheqDelimiters = map [string ]bool {
"empheql" : true , "empheqr" : true ,
"empheqbigl" : true , "empheqbigr" : true ,
}
func (p *parser ) empheqCommand (name string ) (nodes []*mml .Node , handled bool , err error ) {
if character , ok := empheqMO [name ]; ok {
return []*mml .Node {p .token ("mo" , character )}, true , nil
}
if !empheqDelimiters [name ] {
return nil , false , nil
}
delimiter , err := p .readDelimiter (name , false )
if err != nil {
if texErr , ok := err .(*Error ); ok && texErr .ID == "MissingOrUnrecognizedDelim" {
err = texError ("MissingOrUnrecognizedDelim" , "Missing or unrecognized delimiter for \\%s" , name )
}
return nil , true , err
}
mo := p .token ("mo" , delimiter )
mo .Attributes .Set ("stretchy" , true )
mo .Attributes .Set ("symmetric" , true )
return []*mml .Node {mo }, true , nil
}
func (p *parser ) empheqEnvironment (name string ) (nodes []*mml .Node , handled bool , err error ) {
if name != "empheq" {
return nil , false , nil
}
options , _ , err := p .readBrackets (nil )
if err != nil {
return nil , true , err
}
innerArgument , _ , err := p .readArgument ("begin{" +name +"}" , false )
if err != nil {
return nil , true , err
}
parts := strings .Split (innerArgument , "=" )
innerName := parts [0 ]
innerArgumentN := ""
if len (parts ) > 1 {
innerArgumentN = parts [1 ]
}
if !empheqAllowedEnvironment (innerName ) {
return nil , true , texError ("UnknownEnv" , "Unknown environment %q" , innerName )
}
settings := map [string ]any {}
if options != "" {
settings , err = empheqSplitOptions (options )
if err != nil {
return nil , true , err
}
}
body , err := p .captureEnvironment (name )
if err != nil {
return nil , true , err
}
parseName := innerName
if strings .TrimSuffix (innerName , "*" ) == "flalign" {
parseName = "align"
if strings .HasSuffix (innerName , "*" ) {
parseName += "*"
}
}
innerSource := "\\begin{" + parseName + "}"
if innerArgumentN != "" {
innerSource += "{" + innerArgumentN + "}"
}
innerSource += body + "\\end{" + parseName + "}"
result , err := p .parseString (innerSource )
if err != nil {
return nil , true , err
}
left , hasLeft := settings ["left" ]
right , hasRight := settings ["right" ]
if (hasLeft && empheqTruthy (left )) || (hasRight && empheqTruthy (right )) {
original := p .copyNode (result )
if hasLeft && empheqTruthy (left ) {
if err := p .empheqAddLeft (result , original , empheqOptionTeX (left )); err != nil {
return nil , true , err
}
}
if hasRight && empheqTruthy (right ) {
if err := p .empheqAddRight (result , original , empheqOptionTeX (right )); err != nil {
return nil , true , err
}
}
}
return []*mml .Node {result }, true , nil
}
func empheqAllowedEnvironment(name string ) bool {
switch strings .TrimSuffix (name , "*" ) {
case "equation" , "align" , "gather" , "flalign" , "alignat" , "multline" :
return true
}
return false
}
func empheqTruthy(value any ) bool {
switch value := value .(type ) {
case bool :
return value
case string :
return value != ""
}
return value != nil
}
func empheqOptionTeX(value any ) string {
if value , ok := value .(string ); ok {
return value
}
return ""
}
func empheqSplitOptions(text string ) (map [string ]any , error ) {
options , err := parseUtilKeyvalOptions (text , func (key string ) bool {
return key == "left" || key == "right"
}, true )
if err != nil {
return nil , err
}
result := make (map [string ]any , options .Len ())
options .Range (func (key string , value any ) bool {
result [key ] = value
return true
})
return result , nil
}
func empheqReadOptionValue(text , endings string ) (value string , ending byte , rest string , err error ) {
braces := 0
start := 0
startCount := true
stopCount := false
var b strings .Builder
for index := 0 ; index < len (text ); index ++ {
character := text [index ]
switch character {
case ' ' :
case '{' :
if startCount {
start ++
} else {
stopCount = false
if start > braces {
start = braces
}
}
braces ++
case '}' :
if braces > 0 {
braces --
}
if startCount || stopCount {
start --
stopCount = true
}
startCount = false
default :
if braces == 0 && strings .IndexByte (endings , character ) >= 0 {
cleaned := empheqRemoveOptionBraces (b .String (), start )
if stopCount {
cleaned = "true"
}
return cleaned , character , text [index +1 :], nil
}
startCount = false
stopCount = false
}
b .WriteByte (character )
}
if braces != 0 {
return "" , 0 , "" , texError ("ExtraOpenMissingClose" , "Extra open brace or missing close brace" )
}
cleaned := empheqRemoveOptionBraces (b .String (), start )
if stopCount {
cleaned = "true"
}
return cleaned , 0 , "" , nil
}
func empheqRemoveOptionBraces(text string , count int ) string {
for count > 0 {
text = strings .TrimFunc (text , internalTextSpace )
if len (text ) >= 2 {
text = text [1 : len (text )-1 ]
}
count --
}
return strings .TrimFunc (text , internalTextSpace )
}
func empheqColumnCount(table *mml .Node ) int {
count := 0
for _ , row := range table .Children {
columns := len (row .Children )
if row .Kind == "mlabeledtr" {
columns --
}
if columns > count {
count = columns
}
}
return count
}
func (p *parser ) empheqCellBlock (tex string , table *mml .Node , environment ...string ) (*mml .Node , error ) {
block := node ("mpadded" )
block .Attributes .Set ("height" , 0 )
block .Attributes .Set ("depth" , 0 )
block .Attributes .Set ("voffset" , "-1height" )
contents , err := p .parseChild (tex )
if err != nil {
return nil , err
}
if len (environment ) != 0 && environment [0 ] != "" {
tags := p .amsTags ()
if tags .current .labelID != "" {
tags .current .environment = environment [0 ]
if _ , err := tags .getTag (p ); err != nil {
return nil , err
}
}
}
children := []*mml .Node {contents }
if contents .Kind == "mrow" && contents .Flags .Inferred {
children = contents .Children
}
for _ , child := range children {
block .Children [0 ].AppendChild (child )
}
tableSize := node ("mpadded" , table )
tableSize .Attributes .Set ("width" , 0 )
block .Children [0 ].AppendChild (node ("mphantom" , tableSize ))
refreshDynamicFlags (block .Children [0 ])
refreshDynamicFlags (block )
return block , nil
}
func empheqTopRowTable(original *mml .Node ) *mml .Node {
return empheqCopiedTopRowTable (original .Clone ())
}
func empheqCopiedTopRowTable(table *mml .Node ) *mml .Node {
if table .Flags .Arity < 0 && len (table .Children ) == 1 && table .Children [0 ].Flags .Inferred {
table .Children [0 ].SetChildren (nil )
refreshDynamicFlags (table .Children [0 ])
refreshDynamicFlags (table )
} else if len (table .Children ) > 1 {
table .SetChildren (table .Children [:1 ])
}
table .Attributes .Set ("align" , "baseline 1" )
padded := node ("mpadded" , table )
padded .Attributes .Set ("width" , 0 )
return node ("mphantom" , padded )
}
func (p *parser ) empheqRowspanCell (cell *mml .Node , tex string , table *mml .Node , environment ...string ) error {
block , err := p .empheqCellBlock (tex , p .copyNode (table ), environment ...)
if err != nil {
return err
}
content := node ("mpadded" , block , p .empheqTopRowTable (table ))
content .Attributes .Set ("height" , 0 )
content .Attributes .Set ("depth" , 0 )
content .Attributes .Set ("voffset" , "height" )
cell .Children [0 ].AppendChild (content )
refreshDynamicFlags (cell .Children [0 ])
refreshDynamicFlags (cell )
return nil
}
func (p *parser ) empheqAddLeft (table , original *mml .Node , left string , environment ...string ) error {
columnAlign , _ := table .Attributes .Get ("columnalign" )
columnSpacing , _ := table .Attributes .Get ("columnspacing" )
table .Attributes .Set ("columnalign" , "right " +sourceValueString (columnAlign ))
table .Attributes .Set ("columnspacing" , "0em " +sourceValueString (columnSpacing ))
var topCell *mml .Node
for rowIndex := len (table .Children ) - 1 ; rowIndex >= 0 ; rowIndex -- {
row := table .Children [rowIndex ]
cell := node ("mtd" )
children := append ([]*mml .Node {cell }, row .Children ...)
if row .Kind == "mlabeledtr" && len (children ) > 1 {
children [0 ], children [1 ] = children [1 ], children [0 ]
}
row .SetChildren (children )
topCell = cell
}
if topCell == nil {
return fmt .Errorf ("Empheq left block requires a table row" )
}
return p .empheqRowspanCell (topCell , left , original , environment ...)
}
func (p *parser ) empheqAddRight (table , original *mml .Node , right string ) error {
if len (table .Children ) == 0 {
table .AppendChild (node ("mtr" ))
}
columns := empheqColumnCount (table )
first := table .Children [0 ]
for len (first .Children ) < columns {
first .AppendChild (node ("mtd" ))
}
cell := first .AppendChild (node ("mtd" ))
if err := p .empheqRowspanCell (cell , right , original ); err != nil {
return err
}
columnAlign , _ := table .Attributes .Get ("columnalign" )
aligns := strings .Split (sourceValueString (columnAlign ), " " )
if columns < len (aligns ) {
aligns = aligns [:columns ]
}
table .Attributes .Set ("columnalign" , strings .Join (aligns , " " )+" left" )
columnSpacing , _ := table .Attributes .Get ("columnspacing" )
spacings := strings .Split (sourceValueString (columnSpacing ), " " )
limit := columns - 1
if limit < 0 {
limit = 0
}
if limit < len (spacings ) {
spacings = spacings [:limit ]
}
table .Attributes .Set ("columnspacing" , strings .Join (spacings , " " )+" 0em" )
return nil
}
The pages are generated with Golds v0.8.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .