// Copyright (c) 2018-2022 MathJax Consortium // SPDX-License-Identifier: Apache-2.0 // Go translation of cases/CasesConfiguration.ts and EmpheqUtil.left.
package tex import ( ) // NumCases pushes the same CasesBegin object twice below its EqnArray. // The first matching End removes one entry; left decoration runs on Last // (the oldest node), and the second End removes the remaining entry. type casesBeginItem struct { end bool entries int closing *environmentEndItem } // NumCases is an EqnArray owner. Its left program is retained verbatim until // the array closes, so declarations made in either rows or that program can // resolve the appended helper through the ordinary shared command maps. func ( *parser) ( string) ( []*mml.Node, bool, error) { if != "numcases" && != "subnumcases" { return nil, false, nil } , , := .readArgument("begin", false) if != nil { return nil, true, } // EqnArray first pushes CasesBegin, reducing pending row items, before // checking equation nesting or parsing any physical body input. if .commandCasesBegin != nil { if := .commandCasesBegin(); != nil { return nil, true, } } if := .checkEquationEnvironment(); != nil { return nil, true, } := .environmentOwner .casesBegin = &casesBeginItem{end: true, entries: 2} := &ordinaryArrayItem{begin: } := .amsTags() .start(, true, true) := false defer func() { if ! && !.popped { .end() } }() := node("mtable") := 0 := func( *mml.Node) error { if len(.Children) > { = len(.Children) } , := .getTag() if != nil { return } if != nil { = node("mlabeledtr", append([]*mml.Node{}, .Children...)...) } .AppendChild() .clearTag() return nil } := newEquationTableState() .initialSpacing = ".2em" := &equationRowState{table: } = .parseArrayBody("", arrayBodyOwner{ environment: , ordinary: , rules: .rules, configure: func( *parser) { .arrayCell.equation, .arrayCell.numCases = , true }, prepareEntry: func( *parser, *cellItem) ([]*mml.Node, error) { if == nil || !.numCasesText { return nil, nil } return .prepareNumCasesText() }, hasEntries: func() bool { return len(.entries) != 0 }, endEntry: func( []*mml.Node, *arrayCellState) error { .endEntry() return nil }, endRow: .endRow, addSpacing: .addSpacing, }) if != nil { return nil, true, } if .popped { // Stack.Pop().toMml() delivers only pending nodes to CasesBegin. // Completed equation rows and EndTable/tag cleanup are bypassed. , := .continueCasesBegin(, []*mml.Node{row(.nodes, true)}) if != nil { return nil, true, } if .closed && .casesBegin.closing == nil { // Both aliases were directly popped. Their final MML goes to // the outer recipient without running the Cases End handler. return , true, nil } if .closed { // The first matching End discarded the final alias with no // MML replacement. Select Last from the exposed recipient, // then replay the synthetic second End on that same row. .commandCasesLeft = &environmentEndItem{name: .casesBegin.closing.name} return nil, true, nil } if len() == 0 { return nil, true, fmt.Errorf("Cases left block requires a pending node") } // NodeStack.Last is nodes[0], even after more input appends nodes. := [0] := .copyNode() if := .empheqAddLeft(, , +"\\empheqlbrace\\,", "numcases-left"); != nil { return nil, true, } if := .closeEnvironment(, &environmentEndItem{name: .casesBegin.closing.name}); != nil { return nil, true, } return , true, nil } resetTableAttributes(, "displaystyle", false, "rowspacing", ".2em", "columnalign", repeatAMSEqnArrayDefinition("left left", ), "columnspacing", "1em", "side", "right", "minlabelspacing", "0.8em", ) .applySpacing() // EqnArray EndTable restores the tag stack before Empheq parses left. .end() = true if .end == nil { return nil, true, .missing() } if := .closeEnvironment(, .end); != nil { return nil, true, } // Cases receives the finalized ArrayItem MML before Empheq decorates it. = finishArrayRules() := .copyNode() if := .empheqAddLeft(, , +"\\empheqlbrace\\,", "numcases-left"); != nil { return nil, true, } if := .closeEnvironment(, &environmentEndItem{name: .casesBegin.closing.name}); != nil { return nil, true, } return []*mml.Node{}, true, nil } // Continue the same physical input in CasesBegin's lexical environment. // Its shared first End must run before the deferred left child is parsed. func ( *parser) ( *environmentFrame, []*mml.Node) ([]*mml.Node, error) { , := .environmentRow, .ordinaryArray , , , := .arrayCell, .matrixClose, .cdArrayEntry, .braketOwner .environmentRow, .ordinaryArray = , nil .arrayCell, .matrixClose, .cdArrayEntry, .braketOwner = nil, false, false, nil defer func() { .environmentRow, .ordinaryArray = , .arrayCell, .matrixClose, .cdArrayEntry, .braketOwner = , , , }() , , := .parseRowContinuation(0, false, false, nil, "", ) if != nil { return nil, } if !.closed && (.casesBegin.end || .casesBegin.closing == nil) { return nil, .missing() } return , nil } // Cases.Entry's scanner differs from BaseMethods.Entry: only leading JS // whitespace is trimmed; \label ends the scan; a complete \text remains raw // input to internalMath; commands inside braces do not end the scan. func numCasesTextEnd( string, int) (int, error) { := 0 for := ; < len(); { switch [] { case '{': ++ ++ case '}': if == 0 { return , nil } -- ++ case '&': if == 0 { return , texError("ExtraCasesAlignTab", "Extra alignment tab in text for numcase environment") } ++ case '\\': if != 0 { ++ continue } if +1 == len() { return , fmt.Errorf("NumCases text scanner has no control-sequence token") } := + 1 for < len() && isASCIILetter(rune([])) { ++ } if == +1 { , := utf8.DecodeRuneInString([:]) if == '\n' || == '\r' || == '\u2028' || == '\u2029' { return , fmt.Errorf("NumCases text scanner has no control-sequence token") } += } := [+1 : ] if == "\\" || == "cr" || == "end" || == "label" { return , nil } // The source advances by cs.length from the backslash position, // then examines the last character on its next iteration. In // particular, a nonletter brace is still a brace to this scanner. if isASCIILetter(rune([+1])) { = - 1 } else { ++ } default: , := utf8.DecodeRuneInString([:]) += } } return len(), nil } func ( *parser) ( string) ([]*mml.Node, error) { // The actual end remains visible in this same-input program. The text // scanner stops before it; ordinary command dispatch owns the token. , := numCasesTextEnd(.source, .pos) if != nil { return nil, } if > len(.source) { = len(.source) } := strings.TrimLeftFunc(.source[.pos:], internalTextSpace) .pos = return .internalMath(, "", true) } // Finish the deferred Cases handler on the actual exposed row recipient. // An absent left property concatenates as JavaScript undefined, rather than // reusing the removed CasesBegin's left program. Copy precedes mutation. func ( *parser) ( *mml.Node, string) error { if == nil { return fmt.Errorf("Cases left block requires a pending node") } := .copyNode() return .empheqAddLeft(, , +"\\empheqlbrace\\,", "numcases-left") }