import type { ErrorListener , Style , ParserErrorCode , MacroDictionary , ParseMode , Registers , RegisterValue , Dimension , Glue , } from '../public/core' ; import { isArray } from '../common/types' ; import { Token , tokenize , tokensToString } from './tokenizer' ; import { Atom , BBoxParameter } from './atom-class' ; import { Mode } from './modes-utils' ; import { Argument , FunctionDefinition , NormalizedMacroDictionary , getEnvironmentDefinition , getInfo , getMacros , } from '../core-definitions/definitions-utils' ; import type { ColumnFormat } from '../core-atoms/array' ; import { GroupAtom } from '../core-atoms/group' ; import { SpacingAtom } from '../core-atoms/spacing' ; import { LeftRightAtom } from '../core-atoms/leftright' ; import { SubsupAtom } from '../core-atoms/subsup' ; import { PlaceholderAtom } from '../core-atoms/placeholder' ; import { ErrorAtom } from '../core-atoms/error' ; import { MacroAtom } from '../core-atoms/macro' ; import { TextAtom } from '../core-atoms/text' ; import { defaultBackgroundColorMap , defaultColorMap } from './color' ; import type { MathstyleName } from './mathstyle' ; export type ArgumentType = ParseMode | ( 'bbox' | 'colspec' | 'delim' | 'dimen' | 'number' | 'rest' | 'glue' | 'string' | 'balanced-string' | 'auto' ) ; function isLiteral ( token ) { return ! /^<({|}|\$|\$\$|space)>$/ . test ( token ) ; } export type ParsingContext = { mathlist : Atom [ ] ; style : Style ; parseMode : ParseMode ; mathstyle : MathstyleName ; registers : Registers | null ; tabular : boolean ; } ; /**
 * Transform a list of tokens into a list of atoms (a mathlist in TeX's parlance)
 *
 */ export class Parser { tokens : Token [ ] ; index = 0 ; macros ? : NormalizedMacroDictionary ; smartFence = false ; args : null | ( ( arg : string ) => string ) ; colorMap ? : ( name : string ) => string | undefined ; backgroundColorMap ? : ( name : string ) => string | undefined ; endCount = 0 ; onError : ErrorListener < ParserErrorCode > ; _parsingContexts : ParsingContext [ ] ; param tokens constructor ( tokens , options ) { this . tokens = tokens ; this . args = options . args ?? null ; this . macros = options . macros ; this . colorMap = options . colorMap ; this . backgroundColorMap = options . backgroundColorMap ?? this . colorMap ; this . smartFence = options . smartFence ?? false ; this . onError = options . onError ? ( err ) : void => options . onError ! ( { before : tokensToString ( this . tokens . slice ( this . index , this . index + 10 ) ) , after : tokensToString ( this . tokens . slice ( Math . max ( 0 , this . index - 10 ) , this . index ) ) , ... err , } ) : ( ) => { } ; this . _parsingContexts = [ { parseMode : options . parseMode ?? 'math' , mathstyle : options . mathstyle ?? 'displaystyle' , registers : options . registers , tabular : false , style : options . style ?? { } , mathlist : [ ] , } , ] ; } get currentContext ( ) : ParsingContext { return this . _parsingContexts [ this . _parsingContexts . length - 1 ] ; } beginContext ( options ? ) { this . _parsingContexts . push ( { parseMode : options ?. mode ?? this . currentContext . parseMode , mathstyle : options ?. mathstyle ?? this . currentContext . mathstyle , registers : null , style : { ... this . currentContext . style } , tabular : options ?. tabular ?? false , mathlist : [ ] , } ) ; } endContext ( ) { this . _parsingContexts . pop ( ) ; } get mathlist ( ) : Atom [ ] { return this . _parsingContexts [ this . _parsingContexts . length - 1 ] . mathlist ; } set mathlist ( value ) { this . _parsingContexts [ this . _parsingContexts . length - 1 ] . mathlist = value ; } get parseMode ( ) : ParseMode { return this . _parsingContexts [ this . _parsingContexts . length - 1 ] . parseMode ; } set parseMode ( value ) { this . _parsingContexts [ this . _parsingContexts . length - 1 ] . parseMode = value ; } get tabularMode ( ) : boolean { return this . _parsingContexts [ this . _parsingContexts . length - 1 ] . tabular ; } get style ( ) : Style { let i = this . _parsingContexts . length - 1 ; let result = this . _parsingContexts [ i ] . style ; while ( result === undefined && i > 0 ) { i -= 1 ; result = this . _parsingContexts [ i ] . style ; } return result ?? { } ; } set style ( value ) { this . _parsingContexts [ this . _parsingContexts . length - 1 ] . style = value ; } getRegister ( name ) { console . assert ( name [ 0 ] !== '\\' ) ; if ( name . startsWith ( 'global ' ) ) { return this . _parsingContexts [ 0 ] . registers ?. [ name . slice ( 7 ) ] ?? 0 ; } let i = this . _parsingContexts . length - 1 ; let registers = this . _parsingContexts [ i ] . registers ; while ( i >= 0 && registers && registers [ name ] === undefined ) { i -= 1 ; registers = this . _parsingContexts [ i ] ?. registers ; } return registers ?. [ name ] ?? 0 ; } setRegister ( name , value ) { if ( name . startsWith ( 'global ' ) ) { name = name . slice ( 7 ) ; let i = this . _parsingContexts . length - 1 ; while ( i > 0 ) { if ( this . _parsingContexts [ i ] . registers ) { delete this . _parsingContexts [ i ] . registers ! [ name ] ; } i -= 1 ; } this . _parsingContexts [ 0 ] . registers ! [ name ] = value ; return ; } if ( ! this . _parsingContexts [ this . _parsingContexts . length - 1 ] . registers ) { this . _parsingContexts [ this . _parsingContexts . length - 1 ] . registers = { } ; } this . _parsingContexts [ this . _parsingContexts . length - 1 ] . registers ! [ name ] = value ; } /**
     * True if we've reached the end of the token stream
     */ end ( ) { this . endCount ++ ; return this . index >= this . tokens . length || this . endCount > 1000 ; } get ( ) { this . endCount = 0 ; return this . index < this . tokens . length ? this . tokens [ this . index ++ ] : '' ; } peek ( ) { return this . tokens [ this . index ] ; } return match ( input ) { if ( this . tokens [ this . index ] === input ) { this . index ++ ; return true ; } return false ; } /**
     * Return the last atom that can have a subscript/superscript attached to it.
     * If there isn't one, insert a `SubsupAtom` and return it.
     */ lastSubsupAtom ( ) { let atom ; if ( this . mathlist . length > 0 ) { atom = this . mathlist [ this . mathlist . length - 1 ] ; if ( atom . type === 'msubsup' ) return atom ; if ( atom . subsupPlacement !== undefined ) return atom ; } atom = new SubsupAtom ( { style : this . style } ) ; this . mathlist . push ( atom ) ; return atom ; } return hasPattern ( pattern ) { return pattern . test ( this . tokens [ this . index ] ) ; } hasInfixCommand ( ) { const { index } = this ; if ( index < this . tokens . length && this . tokens [ index ] . startsWith ( '\\' ) ) { const info = getInfo ( this . tokens [ index ] , this . parseMode , this . macros ) ; if ( ! info ) return false ; if ( info . ifMode && ! info . ifMode . includes ( this . parseMode ) ) { return false ; } return info . infix ?? false ; } return false ; } matchColumnSeparator ( ) { const { index } = this ; if ( this . tabularMode && this . tokens [ index ] === '&' ) { this . index ++ ; return true ; } return false ; } matchRowSeparator ( ) { const { index } = this ; if ( this . tabularMode && ( this . tokens [ index ] === '\\\\' || this . tokens [ index ] === '\\cr' ) ) { this . index ++ ; return true ; } return false ; } /**
     * Return the appropriate value for a placeholder, either a default
     * one, or if a value was provided for #? via args, that value.
     */ placeholder ( ) { const placeHolderArg = this . args ?. ( '?' ) ; if ( ! placeHolderArg ) { return [ new PlaceholderAtom ( { mode : this . parseMode , style : this . style , } ) , ] ; } return parseLatex ( placeHolderArg , { parseMode : this . parseMode , onError : this . onError , registers : this . currentContext . registers , mathstyle : 'textstyle' , colorMap : this . colorMap , backgroundColorMap : this . backgroundColorMap , } ) ; } matchWhitespace ( ) { let found = false ; while ( this . match ( '<space>' ) ) { found = true ; } return found ; } skipUntilToken ( input ) { let token = this . tokens [ this . index ] ; while ( token && token !== input ) { token = this . tokens [ ++ this . index ] ; } if ( token === input ) { this . index ++ ; } } skipFiller ( ) { let done = false ; do { const skippedSpace = this . matchWhitespace ( ) ; const skippedRelax = this . match ( '\\relax' ) ; done = ! skippedSpace && ! skippedRelax ; } while ( ! done ) ; } return matchKeyword ( keyword ) { const savedIndex = this . index ; let done = this . end ( ) ; let value = '' ; while ( ! done ) { const token = this . get ( ) ; if ( isLiteral ( token ) ) { value += token ; done = this . end ( ) || value . length >= keyword . length ; } else { done = true ; } } const hasKeyword = keyword . toUpperCase ( ) === value . toUpperCase ( ) ; if ( ! hasKeyword ) { this . index = savedIndex ; } return hasKeyword ; } /**
     * Return a sequence of characters as a string.
     * i.e. 'abcd' returns 'abcd'.
     * Terminates on the first non-literal token encountered
     * e.g. '<{>', '<}>' etc...
     * Will also terminate on character literal ']'
     */ scanString ( ) { let result = '' ; while ( ! this . end ( ) ) { if ( this . match ( '<space>' ) ) { result += ' ' ; } else { const token = this . peek ( ) ; if ( token === ']' ) break ; if ( isLiteral ( token ) ) { result += this . get ( ) ; } else if ( token . startsWith ( '\\' ) ) { this . onError ( { code : 'unbalanced-braces' } ) ; result += this . get ( ) ; } else { break ; } } } return result ; } /**
     * Return a sequence of characters as a string.
     * Terminates on a balanced closing bracket
     * This is used by the `\ce` command
     */ scanBalancedString ( ) { let result = '' ; let done = this . end ( ) ; let level = 1 ; while ( ! done ) { if ( this . match ( '<space>' ) ) { result += ' ' ; } else { const token = this . get ( ) ; if ( token === '<{>' ) { result += '{' ; level += 1 ; } else if ( token === '<}>' ) { level -= 1 ; if ( level > 0 ) { result += '}' ; } else { this . index -= 1 ; } } else if ( token === '<$>' ) { result += '$' ; } else if ( token === '<$$>' ) { result += '$$' ; } else { result += token ; } } done = level === 0 || this . end ( ) ; } return result ; } /**
     * Return the literal tokens, as a string, until a matching closing "}"
     */ matchLiteralArg ( ) { let result = '' ; if ( this . match ( '<{>' ) ) { let depth = 1 ; while ( depth > 0 && ! this . end ( ) ) { const token = this . get ( ) ! ; if ( token === '<}>' ) { depth -= 1 ; if ( depth > 0 ) { result += '}' ; } } else if ( token === '<{>' ) { depth += 1 ; result += '{' ; } else { result += { '<space>' : ' ' , '<$$>' : '$$' , '<$>' : '$' , } [ token ] ?? token ; } } } return result ; } /**
     * Return as a number a group of characters representing a
     * numerical quantity.
     *
     * From TeX:8695 (scan_int):
     * > An integer number can be preceded by any number of spaces and `+' or
     * > `-' signs. Then comes either a decimal constant (i.e., radix 10), an
     * > octal constant (i.e., radix 8, preceded by '), a hexadecimal constant
     * > (radix 16, preceded by "), an alphabetic constant (preceded by `), or
     * > an internal variable.
     */ scanNumber ( isInteger = true ) { let negative = false ; let token = this . peek ( ) ; while ( token === '<space>' || token === '+' || token === '-' ) { this . get ( ) ; if ( token === '-' ) negative = ! negative ; token = this . peek ( ) ; } isInteger = Boolean ( isInteger ) ; let radix = 10 ; let digits = /\d/ ; if ( this . match ( "'" ) ) { radix = 8 ; digits = /[0-7]/ ; isInteger = true ; } else if ( this . match ( '"' ) || this . match ( 'x' ) ) { radix = 16 ; digits = /[\dA-F]/ ; isInteger = true ; } else if ( this . match ( '`' ) ) { token = this . get ( ) ; if ( token ) { if ( token . startsWith ( '\\' ) && token . length === 2 ) { return ( negative ? - 1 : 1 ) * ( token . codePointAt ( 1 ) ?? 0 ) ; } return ( negative ? - 1 : 1 ) * ( token . codePointAt ( 0 ) ?? 0 ) ; } return null ; } let value = '' ; while ( this . hasPattern ( digits ) ) { value += this . get ( ) ; } if ( ! isInteger && ( this . match ( '.' ) || this . match ( ',' ) ) ) { value += '.' ; while ( this . hasPattern ( digits ) ) { value += this . get ( ) ; } } const result = isInteger ? Number . parseInt ( value , radix ) : Number . parseFloat ( value ) ; if ( Number . isNaN ( result ) ) return null ; return negative ? - result : result ; } /**
     * Return a dimension
     *
     * See TeX:8831
     */ scanDimen ( ) { let value = this . scanNumber ( false ) ; if ( value === null ) { if ( this . peek ( ) ?. startsWith ( '\\' ) ) { value = 1 ; } else { return null ; } } this . matchWhitespace ( ) ; let result ; if ( this . matchKeyword ( 'pt' ) ) { result = { dimension : value , unit : 'pt' } ; } else if ( this . matchKeyword ( 'mm' ) ) { result = { dimension : value , unit : 'mm' } ; } else if ( this . matchKeyword ( 'cm' ) ) { result = { dimension : value , unit : 'cm' } ; } else if ( this . matchKeyword ( 'ex' ) ) { result = { dimension : value , unit : 'ex' } ; } else if ( this . matchKeyword ( 'px' ) ) { result = { dimension : value , unit : 'px' } ; } else if ( this . matchKeyword ( 'em' ) ) { result = { dimension : value , unit : 'em' } ; } else if ( this . matchKeyword ( 'bp' ) ) { result = { dimension : value , unit : 'bp' } ; } else if ( this . matchKeyword ( 'dd' ) ) { result = { dimension : value , unit : 'dd' } ; } else if ( this . matchKeyword ( 'pc' ) ) { result = { dimension : value , unit : 'pc' } ; } else if ( this . matchKeyword ( 'in' ) ) { result = { dimension : value , unit : 'in' } ; } else if ( this . matchKeyword ( 'mu' ) ) { result = { dimension : value , unit : 'mu' } ; } else { if ( this . peek ( ) ?. startsWith ( '\\' ) ) { result = convertToDimension ( this . getRegister ( this . get ( ) . slice ( 1 ) ) , this . currentContext . registers ) ?? { dimension : 0 } ; result . dimension *= value ; } else { if ( ! this . match ( '\\relax' ) ) { this . onError ( { code : 'missing-unit' } ) ; } result = { dimension : value , unit : 'pt' } ; } } return result ; } scanGlue ( ) { const dimen = this . scanDimen ( ) ; if ( dimen === null ) return null ; const result = { glue : dimen } ; this . matchWhitespace ( ) ; if ( this . match ( '\\relax' ) ) return result ; if ( this . matchKeyword ( 'plus' ) ) { this . scanDimen ( ) ; } this . matchWhitespace ( ) ; if ( this . match ( '\\relax' ) ) return result ; this . matchWhitespace ( ) ; if ( this . matchKeyword ( 'minus' ) ) { this . scanDimen ( ) ; } return result ; } scanColspec ( ) { this . matchWhitespace ( ) ; const result = [ ] ; while ( ! this . end ( ) && ! ( this . peek ( ) === '<}>' || this . peek ( ) === ']' ) ) { const literal = this . get ( ) ; if ( literal === 'c' || literal === 'r' || literal === 'l' ) { result . push ( { align : literal } ) ; } else if ( literal === '|' ) { result . push ( { separator : 'solid' } ) ; } else if ( literal === ':' ) { result . push ( { separator : 'dashed' } ) ; } else if ( literal === '@' ) { if ( this . match ( '<{>' ) ) { this . beginContext ( { mode : 'math' } ) ; result . push ( { gap : this . parse ( ( token ) => token === '<}>' ) , } ) ; this . endContext ( ) ; } if ( ! this . match ( '<}>' ) ) { this . onError ( { code : 'unbalanced-braces' } ) ; } } } return result ; } return parseModeSet ( ) { let final = '' ; if ( this . match ( '\\(' ) ) final = '\\)' ; if ( ! final && this . match ( '\\[' ) ) final = '\\]' ; if ( ! final ) return null ; this . beginContext ( { mode : 'math' , mathstyle : final === '\\)' ? 'textstyle' : 'displaystyle' , } ) ; const result = new GroupAtom ( this . parse ( ( token ) => token === final ) , { mathstyleName : final === '\\)' ? 'textstyle' : 'displaystyle' , latexOpen : final === '\\]' ? '\\[' : '\\(' , latexClose : final , boxType : 'mord' , } ) ; if ( ! this . match ( final ) ) { this . onError ( { code : 'unbalanced-mode-shift' } ) ; } this . endContext ( ) ; if ( result . hasEmptyBranch ( 'body' ) ) return null ; return result ; } /**
     * Parse a `$...$` or `$$...$$` sequence
     */ parseModeShift ( ) { let final = '' ; if ( this . match ( '<$>' ) ) final = '<$>' ; if ( ! final && this . match ( '<$$>' ) ) final = '<$$>' ; if ( ! final ) return null ; this . beginContext ( { mode : 'math' , mathstyle : '<$>' ? 'textstyle' : 'displaystyle' , } ) ; const result = new GroupAtom ( this . parse ( ( token ) => token === final ) , { mathstyleName : final === '<$>' ? 'textstyle' : 'displaystyle' , latexOpen : final === '<$>' ? '$ ' : '$$ ' , latexClose : final === '<$>' ? ' $' : ' $$' , } ) ; if ( ! this . match ( final ) ) { this . onError ( { code : 'unbalanced-mode-shift' } ) ; } this . endContext ( ) ; if ( result . hasEmptyBranch ( 'body' ) ) return null ; return result ; } /**
     * Parse a \begin{env}...\end{end} sequence
     */ parseEnvironment ( ) { if ( ! this . match ( '\\begin' ) ) return null ; const envName = this . parseArgument ( 'string' ) ; if ( ! envName ) return null ; const def = getEnvironmentDefinition ( envName ) ; if ( ! def ) { this . onError ( { code : 'unknown-environment' , arg : envName , } ) ; return null ; } const args = [ ] ; if ( def . params ) { for ( const parameter of def . params ) { if ( parameter . isOptional ) { args . push ( this . parseOptionalArgument ( parameter . type ) ) ; } else { const arg = this . parseArgument ( parameter . type ) ; if ( ! arg ) { this . onError ( { code : 'missing-argument' , arg : envName , } ) ; } args . push ( arg ) ; } } } this . beginContext ( { tabular : def . tabular } ) ; const array = [ ] ; const rowGaps = [ ] ; let row = [ ] ; let done = false ; do { if ( this . end ( ) ) { this . onError ( { code : 'unbalanced-environment' , arg : envName } ) ; done = true ; } if ( ! done && this . match ( '\\end' ) ) { if ( this . parseArgument ( 'string' ) !== envName ) { this . onError ( { code : 'unbalanced-environment' , arg : envName , } ) ; } done = true ; } if ( ! done ) { if ( this . matchColumnSeparator ( ) ) { row . push ( this . mathlist ) ; this . mathlist = [ ] ; } else if ( this . matchRowSeparator ( ) ) { row . push ( this . mathlist ) ; this . mathlist = [ ] ; let gap = null ; this . matchWhitespace ( ) ; if ( this . match ( '[' ) ) { gap = this . scanDimen ( ) ; this . matchWhitespace ( ) ; this . match ( ']' ) ; } rowGaps . push ( gap ?? { dimension : 0 } ) ; array . push ( row ) ; row = [ ] ; } else { this . mathlist = this . mathlist . concat ( this . parse ( ( token ) => token === '<}>' || token === '&' || token === '\\end' || token === '\\cr' || token === '\\\\' ) ) ; } } } while ( ! done ) ; row . push ( this . mathlist ) ; if ( row . length > 0 ) array . push ( row ) ; this . endContext ( ) ; return def . createAtom ( envName , array , rowGaps , args ) ; } param done parse ( done ? ) { this . beginContext ( ) ; if ( ! done ) { done = ( token ) : boolean => token === '<}>' ; } let infix = '' ; let infixInfo = null ; let infixArgs = [ ] ; let prefix = null ; const saveAtoms = this . mathlist ; this . mathlist = [ ] ; while ( ! this . end ( ) && ! done ( this . peek ( ) ) ) { if ( this . hasInfixCommand ( ) && ! infix ) { infix = this . get ( ) ; infixInfo = getInfo ( infix , 'math' , this . macros ) as FunctionDefinition ; if ( infixInfo ) { infixArgs = this . parseArguments ( infixInfo ) [ 1 ] as Atom [ ] [ ] ; } prefix = this . mathlist ; this . mathlist = [ ] ; } else { this . parseToken ( ) ; } } let result ; if ( infix ) { console . assert ( Boolean ( infixInfo ) ) ; infixArgs . unshift ( this . mathlist ) ; this . mathlist = saveAtoms ; if ( prefix ) infixArgs . unshift ( prefix ) ; result = [ infixInfo ! . createAtom ! ( infix , infixArgs , this . style , { colorMap : this . colorMap , backgroundColorMap : this . backgroundColorMap , } ) , ] ; } else { result = this . mathlist ; this . mathlist = saveAtoms ; } this . endContext ( ) ; return result ; } /**
     * Parse a group enclosed in a pair of braces: `{...}`.
     *
     * Return either a group Atom or null if not a group.
     *
     * Return a group Atom with an empty body if an empty
     * group (i.e. `{}`).
     */ parseGroup ( ) { if ( ! this . match ( '<{>' ) ) return null ; const result = new GroupAtom ( this . parse ( ( token ) => token === '<}>' ) , { mode : this . parseMode , latexOpen : '{' , latexClose : '}' , } ) ; if ( ! this . match ( '<}>' ) ) { this . onError ( { code : 'unbalanced-braces' } ) ; } return result ; } scanSmartFence ( ) { this . matchWhitespace ( ) ; if ( ! this . match ( '(' ) ) return null ; this . beginContext ( ) ; let nestLevel = 1 ; while ( ! this . end ( ) && nestLevel !== 0 ) { if ( this . match ( '(' ) ) nestLevel += 1 ; if ( this . match ( ')' ) ) nestLevel -= 1 ; if ( nestLevel !== 0 ) this . parseToken ( ) ; } if ( nestLevel === 0 ) this . match ( ')' ) ; const result = new LeftRightAtom ( this . mathlist , { inner : false , leftDelim : '(' , rightDelim : nestLevel === 0 ? ')' : '?' , } ) ; this . endContext ( ) ; return result ; } return scanDelim ( ) { this . matchWhitespace ( ) ; const token = this . get ( ) ; if ( ! token ) { this . onError ( { code : 'unexpected-end-of-string' } ) ; return null ; } let delim = '.' ; if ( token . startsWith ( '\\' ) || isLiteral ( token ) ) { delim = token ; } const info = getInfo ( delim , 'math' , this . macros ) ; if ( ! info ) { this . onError ( { code : 'unknown-command' , arg : delim } ) ; return null ; } if ( info . ifMode && ! info . ifMode . includes ( this . parseMode ) ) { this . onError ( { code : 'unexpected-delimiter' , arg : delim } ) ; return null ; } if ( info . type === 'mopen' || info . type === 'mclose' ) { return delim ; } if ( /^(\.|\?|\||<|>|\\vert|\\Vert|\\\||\\surd|\\uparrow|\\downarrow|\\Uparrow|\\Downarrow|\\updownarrow|\\Updownarrow|\\mid|\\mvert|\\mVert)$/ . test ( delim ) ) { return delim ; } this . onError ( { code : 'unexpected-delimiter' , arg : delim } ) ; return null ; } /**
     * Parse a `/left.../right` sequence.
     *
     * Note: the `/middle` command can occur multiple times inside a
     * `/left.../right` sequence, and is handled separately.
     *
     * Return either an atom of type `'leftright'` or null
     */ parseLeftRight ( ) { if ( this . match ( '\\right' ) || this . match ( '\\mright' ) ) { this . onError ( { code : 'unbalanced-braces' } ) ; return null ; } let close = '\\right' ; if ( ! this . match ( '\\left' ) ) { if ( ! this . match ( '\\mleft' ) ) return null ; close = '\\mright' ; } const leftDelim = this . scanDelim ( ) ; if ( ! leftDelim ) return null ; this . beginContext ( ) ; while ( ! this . end ( ) && ! this . match ( close ) ) { this . parseToken ( ) ; } const body = this . mathlist ; this . endContext ( ) ; const rightDelim = this . scanDelim ( ) ?? '.' ; return new LeftRightAtom ( body , { leftDelim , rightDelim , inner : close === '\\right' , style : this . style , } ) ; } /**
     * Parse a subscript/superscript: `^` and `_`.
     *
     * Modify the last atom accordingly, or create a new 'msubsup' carrier.
     *
     */ parseSupSub ( ) { if ( this . parseMode !== 'math' ) return false ; let token = this . peek ( ) ; if ( token !== '^' && token !== '_' && token !== "'" ) return false ; while ( token === '^' || token === '_' || token === "'" ) { if ( this . match ( "'" ) ) { if ( this . match ( "'" ) ) { this . lastSubsupAtom ( ) . addChild ( new Atom ( 'mord' , { command : '\\doubleprime' , mode : 'math' , value : '\u2032\u2032' , } ) , 'superscript' ) ; } else { this . lastSubsupAtom ( ) . addChild ( new Atom ( 'mord' , { command : '\\prime' , mode : 'math' , value : '\u2032' , } ) , 'superscript' ) ; } } else if ( this . match ( '^' ) || this . match ( '_' ) ) { const arg = this . parseArgument ( 'math' ) ; if ( arg ) { this . lastSubsupAtom ( ) . addChildren ( arg , token === '_' ? 'subscript' : 'superscript' ) ; } else { this . lastSubsupAtom ( ) . createBranch ( token === '_' ? 'subscript' : 'superscript' ) ; } } token = this . peek ( ) ; } return true ; } /**
     * Parse a `\limits` or `\nolimits` command.
     *
     * This will change the placement of limits to be either above or below
     * (if `\limits`) or in the superscript/subscript position (if `\nolimits`).
     *
     * This overrides the calculation made for the placement, which is usually
     * dependent on the displaystyle (`textstyle` prefers `\nolimits`, while
     * `displaystyle` prefers `\limits`).
     */ parseLimits ( ) { const isLimits = this . match ( '\\limits' ) ; const isNoLimits = ! isLimits && this . match ( '\\nolimits' ) ; const isDisplayLimits = ! isNoLimits && ! isLimits && this . match ( '\\displaylimits' ) ; if ( ! isLimits && ! isNoLimits && ! isDisplayLimits ) return false ; const opAtom = this . mathlist . length > 0 ? this . mathlist [ this . mathlist . length - 1 ] : null ; if ( opAtom === null || opAtom . type !== 'mop' ) return false ; if ( isLimits ) { opAtom . subsupPlacement = 'over-under' ; opAtom . explicitSubsupPlacement = true ; return true ; } if ( isNoLimits ) { opAtom . subsupPlacement = 'adjacent' ; opAtom . explicitSubsupPlacement = true ; return true ; } if ( isDisplayLimits ) { opAtom . subsupPlacement = 'auto' ; opAtom . explicitSubsupPlacement = true ; return true ; } return false ; } parseArguments ( info ) { if ( ! info || ! info . params ) return [ undefined , [ ] ] ; let explicitGroup = undefined ; const args = [ ] ; let i = info . infix ? 2 : 0 ; while ( i < info . params . length ) { const parameter = info . params [ i ] ; if ( parameter . type === 'rest' ) { args . push ( this . parse ( ( token ) => token === '<}>' || token === '&' || token === '\\end' || token === '\\cr' || token === '\\\\' ) ) ; } else if ( parameter . isOptional ) { args . push ( this . parseOptionalArgument ( parameter . type ) ) ; } else if ( parameter . type . endsWith ( '*' ) ) { explicitGroup = parameter . type . slice ( 0 , - 1 ) as ParseMode ; } else { const arg = this . parseArgument ( parameter . type ) ; if ( arg !== null ) { args . push ( arg ) ; } else { this . onError ( { code : 'missing-argument' } ) ; switch ( parameter . type ) { case 'number' : args . push ( 0 ) ; break ; case 'dimen' : args . push ( { dimension : 0 , unit : 'pt' } ) ; break ; case 'glue' : args . push ( { glue : { dimension : 0 , unit : 'pt' } } ) ; break ; case 'string' : case 'balanced-string' : args . push ( '' ) ; break ; case 'delim' : args . push ( '.' ) ; break ; case 'colspec' : args . push ( 'llllllllll' ) ; break ; case 'auto' : default : args . push ( this . placeholder ( ) ) ; break ; } } } i += 1 ; } return [ explicitGroup , args ] ; } /**
     * Parse a math field (in the TeX parlance), an argument to a function.
     *
     * An argument can either be a single atom or  a sequence of atoms enclosed
     * in braces.
     *
     */ parseArgument ( argType ) ; parseArgument ( argType ) ; parseArgument ( argType ) ; parseArgument ( argType ) ; parseArgument ( argType ) ; parseArgument ( argType ) ; parseArgument ( argType ) ; parseArgument ( argType ) ; parseArgument ( argType ) ; parseArgument ( argType ) ; parseArgument ( argType ) { this . skipFiller ( ) ; argType = argType === 'auto' ? this . parseMode : argType ; let result = null ; if ( ! this . match ( '<{>' ) ) { if ( argType === 'delim' ) { return this . scanDelim ( ) ?? '.' ; } if ( argType === 'text' || argType === 'math' ) { this . beginContext ( ) ; const atom = this . parseSimpleToken ( ) ; this . endContext ( ) ; return atom ; } } if ( argType === 'text' || argType === 'math' ) { this . beginContext ( { mode : argType } ) ; const initialIndex = this . index ; let depth = 1 ; do { const token = this . get ( ) ; if ( token === '<}>' ) depth -= 1 ; if ( token === '<{>' ) depth += 1 ; } while ( depth > 0 && ! this . end ( ) ) ; result = Mode . parseTokens ( argType , this . tokens . slice ( initialIndex , this . index - 1 ) , this . onError , { args : this . args ?? ( ( ) => '' ) , macros : this . macros ?? { } , smartFence : this . smartFence , style : this . style , parse : ( mode , tokens , options ) : [ Atom [ ] , Token [ ] ] => { const parser = new Parser ( tokens , { args : options . args , parseMode : mode , smartFence : this . smartFence , macros : options . macros , registers : this . currentContext . registers , mathstyle : this . currentContext . mathstyle , colorMap : this . colorMap , backgroundColorMap : this . backgroundColorMap , style : options . style , onError : this . onError , } ) ; result = parser . parse ( ) ; return [ result , tokens . slice ( parser . index ) ] ; } , } ) ; if ( ! result ) { this . index = initialIndex ; do { this . mathlist = this . mathlist . concat ( this . parse ( ) ) ; } while ( ! this . match ( '<}>' ) && ! this . end ( ) ) ; } } else { this . beginContext ( ) ; if ( argType === 'string' ) { result = this . scanString ( ) ; } else if ( argType === 'balanced-string' ) { result = this . scanBalancedString ( ) ; } else if ( argType === 'number' ) { result = this . scanNumber ( ) ; } else if ( argType === 'colspec' ) { result = this . scanColspec ( ) ; } else if ( argType === 'dimen' ) { result = this . scanDimen ( ) ; } else if ( argType === 'glue' ) { result = this . scanGlue ( ) ; } else if ( argType === 'delim' ) { result = this . scanDelim ( ) ?? '.' ; } if ( result === null ) { this . endContext ( ) ; return null ; } this . skipUntilToken ( '<}>' ) ; } const atoms = this . mathlist ; this . endContext ( ) ; return result ?? atoms ; } parseOptionalArgument ( argType ) { argType = argType === 'auto' ? this . parseMode : argType ; this . matchWhitespace ( ) ; if ( ! this . match ( '[' ) ) return null ; let result = null ; while ( ! this . end ( ) && ! this . match ( ']' ) ) { if ( argType === 'string' ) { result = this . scanString ( ) ; } else if ( argType === 'number' ) { result = this . scanNumber ( ) ; } else if ( argType === 'dimen' ) { result = this . scanDimen ( ) ; } else if ( argType === 'glue' ) { result = this . scanGlue ( ) ; } else if ( argType === 'colspec' ) { result = this . scanColspec ( ) ; } else if ( argType === 'bbox' ) { const list = this . scanString ( ) . toLowerCase ( ) . trim ( ) . split ( /,(?![^(]*\)(?:(?:[^(]*\)){2})*[^"]*$)/ ) ; const bboxParameter = { } ; for ( const element of list ) { const color = this . backgroundColorMap ?. ( element ) ; if ( color ) { bboxParameter . backgroundcolor = color ; } else { const m = element . match ( /^\s*([\d.]+)\s*([a-z]{2})/ ) ; if ( m ) { bboxParameter . padding = m [ 0 ] ; } else { const m = element . match ( /^\s*border\s*:\s*(.*)/ ) ; if ( m ) { bboxParameter . border = m [ 1 ] ; } } } } result = bboxParameter ; } else if ( argType === 'math' ) { this . beginContext ( { mode : 'math' } ) ; result = this . mathlist . concat ( this . parse ( ( token ) => token === ']' ) ) ; this . endContext ( ) ; } } return result ; } parseCommand ( command ) { let result = null ; if ( command === '\\placeholder' ) { const placeholder = new PlaceholderAtom ( { mode : this . parseMode , placeholderId : this . parseOptionalArgument ( 'string' ) as string , default : this . parseOptionalArgument ( 'math' ) as Atom [ ] , value : this . parseArgument ( 'string' ) ?? undefined , style : this . style , } ) ; return [ placeholder ] ; } if ( command === '\\char' ) { const initialIndex = this . index ; let codepoint = Math . floor ( this . scanNumber ( true ) ?? Number . NaN ) ; if ( ! Number . isFinite ( codepoint ) || codepoint < 0 || codepoint > 0x10ffff ) { codepoint = 0x2753 ; } const verbatimLatex = '\\char' + tokensToString ( this . tokens . slice ( initialIndex , this . index ) ) ; result = new Atom ( this . parseMode === 'math' ? 'mord' : 'text' , { command : '\\char' , mode : this . parseMode , value : String . fromCodePoint ( codepoint ) , serialize : ( ) => verbatimLatex , } ) ; result . verbatimLatex = verbatimLatex ; return [ result ] ; } if ( command === '\\hskip' || command === '\\kern' ) { const width = this . scanGlue ( ) ; if ( ! width ) return null ; return [ new SpacingAtom ( command , this . style , width ) ] ; } result = this . scanMacro ( command ) ; if ( result ) return [ result ] ; const info = getInfo ( command , this . parseMode , { } ) ; if ( ! info ) { this . onError ( { code : 'unknown-command' , arg : command , } ) ; return [ new ErrorAtom ( command ) ] ; } if ( info . ifMode && ! info . ifMode . includes ( this . parseMode ) ) { return [ ] ; } const savedMode = this . parseMode ; if ( info . applyMode ) { this . parseMode = info . applyMode ; } const initialIndex = this . index ; const [ deferredArg , args ] = this . parseArguments ( info ) ; this . parseMode = savedMode ; if ( ! args ) return null ; if ( info . applyMode && ! info . applyStyle && ! info . createAtom ) { return args [ 0 ] as Atom [ ] ; } if ( info . infix ) { this . onError ( { code : 'too-many-infix-commands' , arg : command , } ) ; return null ; } if ( typeof info . createAtom === 'function' ) { result = info . createAtom ( command , args , this . style , { colorMap : this . colorMap , backgroundColorMap : this . backgroundColorMap , } ) ; if ( deferredArg ) { result ! . body = this . parseArgument ( deferredArg ) ?? undefined ; } } else if ( typeof info . applyStyle === 'function' ) { const style = info . applyStyle ( command , args , { colorMap : this . colorMap , backgroundColorMap : this . backgroundColorMap , } ) ; const savedMode = this . parseMode ; if ( info . applyMode ) { this . parseMode = info . applyMode ; } if ( deferredArg ) { const saveStyle = this . style ; this . style = { ... this . style , ... style } ; const atoms = this . parseArgument ( deferredArg ) ; this . style = saveStyle ; this . parseMode = savedMode ; return atoms ; } this . style = { ... this . style , ... style } ; this . parseMode = savedMode ; } else { const style = { ... this . style } ; if ( info . variant ) { style . variant = info . variant ; } result = new Atom ( info . type ?? 'mop' , { command , style , value : info . codepoint ? String . fromCodePoint ( info . codepoint ) : command , mode : info . applyMode ?? this . parseMode , } ) ; } if ( result instanceof Atom && ! result . verbatimLatex && ! /^\\(llap|rlap|class|cssId|htmlData)$/ . test ( command ) ) { result . verbatimLatex = ( result . command ?? '' ) + tokensToString ( this . tokens . slice ( initialIndex , this . index ) ) ; if ( result . isFunction && this . smartFence ) { const smartFence = this . scanSmartFence ( ) ; if ( smartFence ) return [ result , smartFence ] ; } } if ( ! result ) return null ; return [ result ] ; } parseLiteral ( literal ) { const result = Mode . createAtom ( this . parseMode , literal , { ... this . style , } ) ; if ( ! result ) return null ; if ( result . isFunction && this . smartFence ) { const smartFence = this . scanSmartFence ( ) ; if ( smartFence ) return [ result , smartFence ] ; } return [ result ] ; } parseSimpleToken ( ) { const token = this . get ( ) ; if ( ! token ) return null ; if ( token === '<space>' ) { if ( this . parseMode === 'text' ) { return [ new TextAtom ( ' ' , ' ' , this . style ) ] ; } return null ; } if ( token . startsWith ( '\\' ) ) return this . parseCommand ( token ) ; if ( isLiteral ( token ) ) return this . parseLiteral ( token ) ; if ( token === '<}>' ) { this . onError ( { latex : '' , code : 'unbalanced-braces' } ) ; } else { this . onError ( { latex : '' , code : 'unexpected-token' , arg : token , } ) ; } return null ; } /**
     * Attempt to scan the macro name and return an atom list if successful.
     * Otherwise, it wasn't a macro.
     */ scanMacro ( macro ) { const macroName = macro . slice ( 1 ) ; if ( ! this . macros || ! this . macros [ macroName ] ) return null ; const initialIndex = this . index ; const def = this . macros [ macroName ] . def ; const argCount = this . macros [ macroName ] . args ! ; const args = { } ; for ( let i = 1 ; i <= argCount ; i ++ ) { args [ i ] = this . matchLiteralArg ( ) ; } args [ '?' ] = this . args ?. ( '?' ) ; return new MacroAtom ( macro , { expand : this . macros [ macroName ] . expand , captureSelection : this . macros [ macroName ] . captureSelection , args : tokensToString ( this . tokens . slice ( initialIndex , this . index ) ) , body : parseLatex ( def , { parseMode : this . parseMode , args : ( arg ) : string => args [ arg ] ! , macros : this . macros , registers : this . currentContext . registers ?? null , mathstyle : this . currentContext . mathstyle , colorMap : this . colorMap , backgroundColorMap : this . backgroundColorMap , onError : this . onError , } ) , } ) ; } /**
     * Make an atom for the current token or token group and
     * add it to the parser's mathlist
     */ parseToken ( ) { let result = this . parseEnvironment ( ) ?? this . parseModeShift ( ) ?? this . parseModeSet ( ) ?? this . parseGroup ( ) ?? this . parseLeftRight ( ) ; if ( result === null ) { if ( this . parseSupSub ( ) ) return true ; if ( this . parseLimits ( ) ) return true ; result = this . parseSimpleToken ( ) ; } if ( isArray < Atom > ( result ) ) { this . mathlist = this . mathlist . concat ( result ) ; } else if ( result ) { this . mathlist . push ( result ) ; } return result !== null ; } } param args param smartFence export function parseLatex ( s , options ? ) { const parser = new Parser ( tokenize ( s , options ?. args ?? null ) , { args : options ?. args ?? null , macros : getMacros ( options ?. macros ) , registers : options ?. registers ?? null , mathstyle : options ?. mathstyle ?? 'displaystyle' , colorMap : options ?. colorMap ?? defaultColorMap , parseMode : options ?. parseMode ?? 'math' , smartFence : options ?. smartFence , backgroundColorMap : options ?. backgroundColorMap ?? options ?. colorMap ?? defaultBackgroundColorMap , onError : ( err ) => { if ( typeof options ?. onError === 'function' ) { options . onError ( { ... err , latex : s } ) ; } else { } } , } ) ; let atoms = [ ] ; while ( ! parser . end ( ) ) { const more = parser . parse ( ) ; if ( ! more ) break ; atoms = atoms . concat ( more ) ; } return atoms ; } export function convertToGlue ( value , registers ) { if ( typeof value === 'object' && 'glue' in value ) return value ; if ( typeof value === 'object' && 'dimension' in value ) { return { glue : value } ; } if ( typeof value === 'number' ) return { glue : { dimension : value } } ; const parser = new Parser ( tokenize ( value ) , { registers } ) ; return parser . scanGlue ( ) ; } /**  Return a dimension. */ export function convertToDimension ( value , registers ) { if ( typeof value === 'number' ) return { dimension : value , unit : 'pt' } ; if ( typeof value === 'object' && 'glue' in value ) { value = value . glue ; } if ( typeof value === 'object' && 'dimension' in value ) { return value ; } const parser = new Parser ( tokenize ( value ) , { registers } ) ; return parser . scanDimen ( ) ; }