import type { Style , ParseMode , FontSize , Glue } from '../public/core' ; import { isArray } from '../common/types' ; import { Context , PrivateStyle } from './context' ; import { PT_PER_EM , X_HEIGHT } from './font-metrics' ; import { BoxType , isBoxType , Box } from './box' ; import { makeLimitsStack , VBox } from './v-box' ; import { joinLatex } from './tokenizer' ; import { getModeRuns , getPropertyRuns , Mode } from './modes-utils' ; import { unicodeCharToLatex } from '../core-definitions/definitions-utils' ; import MathfieldElement from '../public/mathfield-element' ; import { MathfieldBox } from './mathfield-box' ; export const ATOM_REGISTRY = { } ; export type BranchName = 'above' | 'body' | 'below' | 'superscript' | 'subscript' ; export type Branch = BranchName | [ row : number , col : number ] ; /**
 * The order of these branches specify the keyboard navigation order
 */ const NAMED_BRANCHES = [ 'above' , 'body' , 'below' , 'superscript' , 'subscript' , ] ; /**
 * A _branch_ is a set of children of an atom.
 *
 * There are two kind of branches:
 * - **colRow** branches are adressed with a column and row number and are
 * used with ArrayAtom
 * - **named branches** used with other kind of atoms. There is a fixed set of
 * possible named branches.
 */ export function isNamedBranch ( branch ) { return typeof branch === 'string' && NAMED_BRANCHES . includes ( branch ) ; } export function isColRowBranch ( branch ? ) { return branch !== undefined && Array . isArray ( branch ) && branch . length === 2 ; } export type Branches = { [ branch in BranchName ] ? : Atom [ ] ; } ; export type ToLatexOptions = { expandMacro ? : boolean ; skipModeCommand ? : boolean ; defaultMode : 'math' | 'text' | 'inline-math' ; } ; export type AtomType = 'accent' | 'array' | 'box' | 'chem' | 'composition' | 'delim' | 'enclose' | 'error' | 'first' | 'genfrac' | 'group' | 'latex' | 'leftright' | 'line' | 'macro' | 'mbin' | 'mclose' | 'minner' | 'mop' | 'mopen' | 'mord' | 'mpunct' | 'mrel' | 'msubsup' | 'overlap' | 'overunder' | 'placeholder' | 'phantom' | 'root' | 'rule' | 'sizeddelim' | 'space' | 'spacing' | 'surd' | 'text' ; export type BBoxParameter = { backgroundcolor ? : string ; padding ? : string ; border ? : string ; } ; /**
 * An atom is an object encapsulating an elementary mathematical unit,
 * independent of its graphical representation.
 *
 * It keeps track of the content, while the dimensions, position and style
 * are tracked by Box objects which are created by the `createBox()` function.
 */ export class Atom { parent : Atom | undefined ; treeBranch ? : Branch ; value : string ; id ? : string ; type : AtomType ; command ? : string ; verbatimLatex ? : string ; style : PrivateStyle ; mode : ParseMode ; isFunction : boolean ; isExtensibleSymbol : boolean ; private _isDirty : boolean ; private _changeCounter : number ; private _children : Atom [ ] | undefined ; serializeOverride ? : ( atom : Atom , options : ToLatexOptions ) => string ; private _branches : Branches ; subsupPlacement ? : 'auto' | 'over-under' | 'adjacent' ; explicitSubsupPlacement ? : boolean ; skipBoundary ? : boolean ; captureSelection ? : boolean ; displayContainsHighlight : boolean ; changeMode ? : boolean ; kern ? : Glue ; interAtomType : BoxType ; interAtomKern : Glue ; isSelected : boolean ; containsCaret : boolean ; caret : ParseMode | '' ; constructor ( type , options ? ) { this . command = options ?. command ; this . type = type ; if ( typeof options ?. value === 'string' ) { this . value = options . value ; } this . _isDirty = false ; this . _changeCounter = 0 ; this . mode = options ?. mode ?? 'math' ; this . isFunction = options ?. isFunction ?? false ; this . subsupPlacement = options ?. limits ; this . style = options ?. style ?? { } ; this . serializeOverride = options ?. serialize ; this . displayContainsHighlight = options ?. displayContainsHighlight ?? false ; } get changeCounter ( ) : number { return this . _changeCounter ; } get isDirty ( ) : boolean { return this . _isDirty ; } set isDirty ( dirty ) { this . _isDirty = dirty ; if ( dirty ) { this . _changeCounter ++ ; this . verbatimLatex = undefined ; this . _children = undefined ; let { parent } = this ; while ( parent ) { parent . _isDirty = true ; parent . _changeCounter ++ ; parent . verbatimLatex = undefined ; parent . _children = undefined ; parent = parent . parent ; } } } param parentContext param options . newList static createBox ( parentContext , atoms , options ? ) { if ( ! atoms ) return null ; const runs = getStyleRuns ( atoms ) ; if ( runs . length === 1 ) { const run = runs [ 0 ] ; if ( run [ 0 ] . style ) { return renderStyleRun ( parentContext , run , { ... options , style : { color : run [ 0 ] . style . color , backgroundColor : run [ 0 ] . style . backgroundColor , fontSize : run [ 0 ] . style . fontSize , } , } ) ; } return renderStyleRun ( parentContext , run , options ) ; } const boxes = [ ] ; let newList = options ?. newList ; for ( const run of runs ) { const context = new Context ( parentContext , { color : run [ 0 ] . style ?. color , backgroundColor : run [ 0 ] . style ?. backgroundColor , fontSize : run [ 0 ] . style ?. fontSize , isSelected : run [ 0 ] . isSelected , } ) ; const box = renderStyleRun ( context , run , { newList } ) ; if ( box ) { newList = false ; boxes . push ( box ) ; } } if ( boxes . length === 0 ) return null ; if ( boxes . length === 1 && ! options ?. classes && ! options ?. type ) { return boxes [ 0 ] . wrap ( parentContext ) ; } return new Box ( boxes , { classes : options ?. classes , type : options ?. type , newList : options ?. newList , } ) . wrap ( parentContext ) ; } /**
     * Given an atom or an array of atoms, return a LaTeX string representation
     */ static serialize ( value , options ) { let result = '' ; if ( isArray < Atom > ( value ) ) { result = atomsToLatex ( value , options ) ; } else if ( typeof value === 'number' || typeof value === 'boolean' ) { result = value . toString ( ) ; } else if ( typeof value === 'string' ) { result = value . replace ( /\s/g , '~' ) ; } else if ( value !== undefined ) { if ( ! options . expandMacro && typeof value . verbatimLatex === 'string' ) { return value . verbatimLatex ; } if ( value . serializeOverride ) { return value . serializeOverride ( value , options ) ; } result = value . serialize ( options ) ; } return result ; } /**
     * The common ancestor between two atoms
     */ static commonAncestor ( a , b ) { if ( a === b ) return a . parent ; if ( a . parent === b . parent ) return a . parent ; const parents = new WeakSet < Atom > ( ) ; let { parent } = a ; while ( parent ) { parents . add ( parent ) ; parent = parent . parent ; } parent = b . parent ; while ( parent ) { if ( parents . has ( parent ) ) return parent ; parent = parent . parent ; } console . assert ( Boolean ( parent ) ) ; return undefined ; } /**
     * Default Latex emmiter.
     * Avoid calling directly, instead call `Atom.serialize(atom)`
     * to correctly call per-definition emitters and use the cached verbatim
     * latex when applicable.
     */ serialize ( options ) { if ( this . body && this . command ) { return joinLatex ( [ this . command , '{' , this . bodyToLatex ( options ) , '}' , this . supsubToLatex ( options ) , ] ) ; } if ( this . body ) { return joinLatex ( [ this . bodyToLatex ( options ) , this . supsubToLatex ( options ) , ] ) ; } if ( this . value && this . value !== '\u200B' ) { return this . command ?? unicodeCharToLatex ( this . mode , this . value ) ; } return '' ; } bodyToLatex ( options ) { return atomsToLatex ( this . body , options ) ; } aboveToLatex ( options ) { return atomsToLatex ( this . above , options ) ; } belowToLatex ( options ) { return atomsToLatex ( this . below , options ) ; } supsubToLatex ( options ) { let result = '' ; if ( this . branch ( 'subscript' ) !== undefined ) { const sub = atomsToLatex ( this . subscript , options ) ; if ( sub . length === 0 ) { result += '_' ; } else if ( sub . length === 1 ) { result += '_' + sub ; } else { result += `_{${ sub }}` ; } } if ( this . branch ( 'superscript' ) !== undefined ) { const sup = atomsToLatex ( this . superscript , options ) ; if ( sup . length === 0 ) { result += '^' ; } else if ( sup . length === 1 ) { if ( sup === '\u2032' ) { result += '^\\prime ' ; } else if ( sup === '\u2033' ) { result += '^\\doubleprime ' ; } else { result += '^' + sup ; } } else { result += `^{${ sup }}` ; } } return result ; } get treeDepth ( ) : number { let result = 1 ; let atom = this . parent ; while ( atom ) { atom = atom . parent ; result += 1 ; } return result ; } get inCaptureSelection ( ) : boolean { let result = false ; let atom = this ; while ( atom ) { if ( atom . captureSelection ) { result = true ; break ; } atom = atom . parent ; } return result ; } /**
     * Return the atoms in the branch, if it exists, otherwise null
     */ branch ( name ) { if ( ! isNamedBranch ( name ) ) return undefined ; if ( ! this . _branches ) return undefined ; return this . _branches [ name ] ; } /**
     * Return all the branches that exist.
     * Some of them may be empty.
     */ get branches ( ) : Branch [ ] { if ( ! this . _branches ) return [ ] ; const result = [ ] ; for ( const branch of NAMED_BRANCHES ) { if ( this . _branches [ branch ] ) { result . push ( branch ) ; } } return result ; } /**
     * Return the atoms in the branch, if it exists, otherwise create it
     */ createBranch ( name ) { console . assert ( isNamedBranch ( name ) ) ; if ( ! isNamedBranch ( name ) ) return [ ] ; if ( ! this . _branches ) { this . _branches = { [ name ] : [ this . makeFirstAtom ( name ) ] , } ; } else if ( ! this . _branches [ name ] ) { this . _branches [ name ] = [ this . makeFirstAtom ( name ) ] ; } this . isDirty = true ; return this . _branches [ name ] ! ; } get row ( ) : number { if ( ! isColRowBranch ( this . treeBranch ) ) return - 1 ; return this . treeBranch [ 0 ] ; } get col ( ) : number { if ( ! isColRowBranch ( this . treeBranch ) ) return - 1 ; return this . treeBranch [ 1 ] ; } get body ( ) : Atom [ ] | undefined { return this . _branches ?. body ; } set body ( atoms ) { this . setChildren ( atoms , 'body' ) ; } get superscript ( ) : Atom [ ] | undefined { return this . _branches ?. superscript ; } set superscript ( atoms ) { this . setChildren ( atoms , 'superscript' ) ; } get subscript ( ) : Atom [ ] | undefined { return this . _branches ?. subscript ; } set subscript ( atoms ) { this . setChildren ( atoms , 'subscript' ) ; } get above ( ) : Atom [ ] | undefined { return this . _branches ?. above ; } set above ( atoms ) { this . setChildren ( atoms , 'above' ) ; } get below ( ) : Atom [ ] | undefined { return this . _branches ?. below ; } set below ( atoms ) { this . setChildren ( atoms , 'below' ) ; } get computedStyle ( ) : PrivateStyle { const style = { ... this . style } ; const hadVerbatimColor = this . style . verbatimColor !== undefined ; const hadVerbatimBackgroundColor = this . style . verbatimBackgroundColor !== undefined ; if ( style ) { delete style . variant ; delete style . variantStyle ; } if ( ! this . parent ) return style ?? { } ; const result = { ... this . parent . computedStyle , ... style } ; if ( ! hadVerbatimBackgroundColor ) delete result . verbatimBackgroundColor ; if ( ! hadVerbatimColor ) delete result . verbatimColor ; return result ; } applyStyle ( style ) { this . isDirty = true ; this . style = { ... this . style , ... style } ; if ( this . style . fontFamily === 'none' ) { delete this . style . fontFamily ; } if ( this . style . fontShape === 'auto' ) { delete this . style . fontShape ; } if ( this . style . fontSeries === 'auto' ) { delete this . style . fontSeries ; } if ( this . style . color === 'none' ) { delete this . style . color ; delete this . style . verbatimColor ; } if ( this . style . backgroundColor === 'none' ) { delete this . style . backgroundColor ; delete this . style . verbatimBackgroundColor ; } if ( this . style . fontSize === 'auto' ) { delete this . style . fontSize ; } } getInitialBaseElement ( ) { let result = undefined ; if ( ! this . hasEmptyBranch ( 'body' ) ) { console . assert ( this . body ?. [ 0 ] . type === 'first' ) ; result = this . body ! [ 1 ] . getInitialBaseElement ( ) ; } return result ?? this ; } getFinalBaseElement ( ) { if ( ! this . hasEmptyBranch ( 'body' ) ) { return this . body ! [ this . body ! . length - 1 ] . getFinalBaseElement ( ) ; } return this ; } isCharacterBox ( ) { const base = this . getInitialBaseElement ( ) ; return /mord/ . test ( base . type ) ; } hasEmptyBranch ( branch ) { const atoms = this . branch ( branch ) ; if ( ! atoms ) return true ; console . assert ( atoms . length > 0 ) ; console . assert ( atoms [ 0 ] . type === 'first' ) ; return atoms . length === 1 ; } setChildren ( children , branch ) { if ( ! children ) return ; console . assert ( isNamedBranch ( branch ) ) ; if ( ! isNamedBranch ( branch ) ) return ; console . assert ( children [ 0 ] ?. type !== 'first' ) ; if ( this . _branches ) { this . _branches [ branch ] = [ this . makeFirstAtom ( branch ) , ... children ] ; } else { this . _branches = { [ branch ] : [ this . makeFirstAtom ( branch ) , ... children ] , } ; } this . isDirty = true ; for ( const child of children ) { child . parent = this ; child . treeBranch = branch ; } } makeFirstAtom ( branch ) { const result = new Atom ( 'first' , { mode : this . mode } ) ; result . parent = this ; result . treeBranch = branch ; return result ; } addChild ( child , branch ) { console . assert ( child . type !== 'first' ) ; this . createBranch ( branch ) . push ( child ) ; this . isDirty = true ; child . parent = this ; child . treeBranch = branch ; } addChildBefore ( child , before ) { console . assert ( before . treeBranch !== undefined ) ; const branch = this . createBranch ( before . treeBranch ! ) ; branch . splice ( branch . indexOf ( before ) , 0 , child ) ; this . isDirty = true ; child . parent = this ; child . treeBranch = before . treeBranch ; } addChildAfter ( child , after ) { console . assert ( after . treeBranch !== undefined ) ; const branch = this . createBranch ( after . treeBranch ! ) ; branch . splice ( branch . indexOf ( after ) + 1 , 0 , child ) ; this . isDirty = true ; child . parent = this ; child . treeBranch = after . treeBranch ; } addChildren ( children , branch ) { for ( const child of children ) this . addChild ( child , branch ) ; } /**
     * Return the last atom that was added
     */ addChildrenAfter ( children , after ) { console . assert ( children . length === 0 || children [ 0 ] . type !== 'first' ) ; console . assert ( after . treeBranch !== undefined ) ; const branch = this . createBranch ( after . treeBranch ! ) ; branch . splice ( branch . indexOf ( after ) + 1 , 0 , ... children ) ; this . isDirty = true ; for ( const child of children ) { child . parent = this ; child . treeBranch = after . treeBranch ; } return children [ children . length - 1 ] ; } removeBranch ( name ) { const children = this . branch ( name ) ; if ( isNamedBranch ( name ) ) { this . _branches [ name ] = undefined ; } if ( ! children ) return [ ] ; for ( const child of children ) { child . parent = undefined ; child . treeBranch = undefined ; } console . assert ( children [ 0 ] . type === 'first' ) ; children . shift ( ) ; this . isDirty = true ; return children ; } removeChild ( child ) { console . assert ( child . parent === this ) ; if ( child . type === 'first' ) return ; const branch = this . branch ( child . treeBranch ! ) ! ; const index = branch . indexOf ( child ) ; console . assert ( index >= 0 ) ; branch . splice ( index , 1 ) ; this . isDirty = true ; child . parent = undefined ; child . treeBranch = undefined ; } get siblings ( ) : Atom [ ] { if ( this . type === 'root' ) return [ ] ; return this . parent ! . branch ( this . treeBranch ! ) ! ; } get firstSibling ( ) : Atom { return this . siblings [ 0 ] ; } get lastSibling ( ) : Atom { const { siblings } = this ; return siblings [ siblings . length - 1 ] ; } get isFirstSibling ( ) : boolean { return this === this . firstSibling ; } get isLastSibling ( ) : boolean { return this === this . lastSibling ; } get hasNoSiblings ( ) : boolean { return this . siblings . length === 1 ; } get leftSibling ( ) : Atom { console . assert ( this . parent !== undefined ) ; const siblings = this . parent ! . branch ( this . treeBranch ! ) ! ; return siblings [ siblings . indexOf ( this ) - 1 ] ; } get rightSibling ( ) : Atom { console . assert ( this . parent !== undefined ) ; const siblings = this . parent ! . branch ( this . treeBranch ! ) ! ; return siblings [ siblings . indexOf ( this ) + 1 ] ; } get hasChildren ( ) : boolean { return this . _branches && this . children . length > 0 ; } get firstChild ( ) : Atom { console . assert ( this . hasChildren ) ; return this . children [ 0 ] ; } get lastChild ( ) : Atom { console . assert ( this . hasChildren ) ; const { children } = this ; return children [ children . length - 1 ] ; } /**
     * All the children of this atom.
     *
     * The order of the atoms is the order in which they
     * are navigated using the keyboard.
     */ get children ( ) : Atom [ ] { if ( this . _children ) return this . _children ; if ( ! this . _branches ) return [ ] ; const result = [ ] ; for ( const branchName of NAMED_BRANCHES ) { if ( this . _branches [ branchName ] ) { for ( const x of this . _branches [ branchName ] ! ) { result . push ( ... x . children ) ; result . push ( x ) ; } } } this . _children = result ; return result ; } /**
     * Render this atom as an array of boxes.
     *
     * The parent context (color, size...) will be applied
     * to the result.
     *
     */ render ( parentContext , options ? ) { if ( this . type === 'first' && ! parentContext . atomIdsSettings ) return null ; const context = new Context ( parentContext , this . style ) ; let result = this . createBox ( context , { classes : this . type === 'root' ? ' ML__base' : '' , newList : options ?. newList === true || this . type === 'first' , } ) ; if ( ! result ) return null ; if ( ! this . subsupPlacement && ( this . superscript || this . subscript ) ) { result = this . attachSupsub ( context , { base : result } ) ; } return result . wrap ( context ) ; } attachSupsub ( parentContext , options ) { const base = options . base ; const superscript = this . superscript ; const subscript = this . subscript ; if ( ! superscript && ! subscript ) { return base ; } let supBox = null ; let subBox = null ; const isCharacterBox = options . isCharacterBox ?? this . isCharacterBox ( ) ; let supShift = 0 ; if ( superscript ) { const context = new Context ( parentContext , undefined , 'superscript' ) ; supBox = Atom . createBox ( context , superscript , { newList : true } ) ; if ( ! isCharacterBox ) { supShift = base . height - parentContext . metrics . supDrop * context . scalingFactor ; } } let subShift = 0 ; if ( subscript ) { const context = new Context ( parentContext , undefined , 'subscript' ) ; subBox = Atom . createBox ( context , subscript , { newList : true } ) ; if ( ! isCharacterBox ) { subShift = base . depth + parentContext . metrics . subDrop * context . scalingFactor ; } } let minSupShift ; if ( parentContext . isDisplayStyle ) { minSupShift = parentContext . metrics . sup1 ; } else if ( parentContext . isCramped ) { minSupShift = parentContext . metrics . sup3 ; } else { minSupShift = parentContext . metrics . sup2 ; } const scriptspace = 0.5 / PT_PER_EM / parentContext . scalingFactor ; let supsub = null ; if ( subBox && supBox ) { supShift = Math . max ( supShift , minSupShift , supBox . depth + 0.25 * parentContext . metrics . xHeight ) ; subShift = Math . max ( subShift , parentContext . metrics . sub2 ) ; const ruleWidth = parentContext . metrics . defaultRuleThickness ; if ( supShift - supBox . depth - ( subBox . height - subShift ) < 4 * ruleWidth ) { subShift = 4 * ruleWidth - ( supShift - supBox . depth ) + subBox . height ; const psi = 0.8 * parentContext . metrics . xHeight - ( supShift - supBox . depth ) ; if ( psi > 0 ) { supShift += psi ; subShift -= psi ; } } const slant = this . isExtensibleSymbol && base . italic ? - base . italic : 0 ; supsub = new VBox ( { individualShift : [ { box : subBox , shift : subShift , marginLeft : slant } , { box : supBox , shift : - supShift } , ] , } ) . wrap ( parentContext ) ; } else if ( subBox && ! supBox ) { subShift = Math . max ( subShift , parentContext . metrics . sub1 , subBox . height - 0.8 * X_HEIGHT ) ; supsub = new VBox ( { shift : subShift , children : [ { box : subBox , marginRight : scriptspace , marginLeft : this . isCharacterBox ( ) ? - ( base . italic ?? 0 ) : 0 , } , ] , } ) ; } else if ( ! subBox && supBox ) { supShift = Math . max ( supShift , minSupShift , supBox . depth + 0.25 * X_HEIGHT ) ; supsub = new VBox ( { shift : - supShift , children : [ { box : supBox , marginRight : scriptspace } ] , } ) ; supsub . wrap ( parentContext ) ; } const supsubContainer = new Box ( supsub , { classes : 'msubsup' } ) ; if ( this . caret ) { supsubContainer . caret = this . caret ; } return new Box ( [ base , supsubContainer ] , { type : options . type } ) ; } attachLimits ( parentContext , options ) { const above = this . superscript ? Atom . createBox ( new Context ( parentContext , this . style , 'superscript' ) , this . superscript , { newList : true } ) : null ; const below = this . subscript ? Atom . createBox ( new Context ( parentContext , this . style , 'subscript' ) , this . subscript , { newList : true } ) : null ; if ( ! above && ! below ) return options . base . wrap ( parentContext ) ; return makeLimitsStack ( parentContext , { ... options , above , below , type : options ?. type ?? 'mop' , } ) ; } /**
     * Add an ID attribute to both the box and this atom so that the atom
     * can be retrieved from the box later on, e.g. when the box is clicked on.
     */ bind ( context , box ) { if ( ! box ) return null ; if ( context . isPhantom ) return box ; if ( ! box || this . value === '\u200B' ) return box ; if ( ! this . id ) this . id = context . makeID ( ) ; box . atomID = this . id ! ; return box ; } /**
     * Create a box with the specified body.
     */ createMathfieldBox ( context , options ) { const type = 'mathfield' ; const classes = options ?. classes ?? '' ; const result = new MathfieldBox ( options . placeholderId , options . element , { type , mode : this . mode , maxFontSize : context . scalingFactor , style : { variant : 'normal' , ... this . style , letterShapeStyle : context . letterShapeStyle , fontSize : Math . max ( 1 , context . size + context . mathstyle . sizeDelta ) as FontSize , } , classes , newList : options ?. newList , } ) ; if ( context . isTight ) result . isTight = true ; if ( this . mode !== 'math' ) result . italic = 0 ; result . right = result . italic ; this . bind ( context , result ) ; return result ; } /**
     * Create a box with the specified body.
     */ createBox ( context , options ? ) { const value = this . value ?? this . body ; const type = isBoxType ( this . type ) ? this . type : undefined ; let classes = options ?. classes ?? '' ; if ( this . mode === 'text' ) classes += ' ML__text' ; const result = typeof value === 'string' || value === undefined ? new Box ( ( value as string | undefined ) ?? null , { type , mode : this . mode , maxFontSize : context . scalingFactor , style : { variant : 'normal' , ... this . style , letterShapeStyle : context . letterShapeStyle , fontSize : Math . max ( 1 , context . size + context . mathstyle . sizeDelta ) as FontSize , } , classes , newList : options ?. newList , } ) : Atom . createBox ( context , value , { type , mode : this . mode , style : this . style , classes , newList : options ?. newList , } ) ?? new Box ( null ) ; if ( context . isTight ) result . isTight = true ; if ( this . mode !== 'math' ) result . italic = 0 ; result . right = result . italic ; this . bind ( context , result ) ; if ( this . caret ) { if ( ! this . superscript && ! this . subscript ) { result . caret = this . caret ; } } return result ; } } param atoms param options . expandMacro result function atomsToLatex ( atoms , options ) { if ( ! atoms || atoms . length === 0 ) return '' ; if ( atoms [ 0 ] . type === 'first' ) { if ( atoms . length === 1 ) return '' ; atoms = atoms . slice ( 1 ) ; } if ( atoms . length === 0 ) return '' ; return joinLatex ( getPropertyRuns ( atoms , 'cssClass' ) . map ( ( x ) => joinLatex ( getPropertyRuns ( x , 'color' ) . map ( ( x ) => joinLatex ( getModeRuns ( x ) . map ( ( x ) => Mode . serialize ( x , options ) ) ) ) ) ) ) ; } function getStyleRuns ( atoms ) { let style = undefined ; let selected ; const runs = [ ] ; let run = [ ] ; for ( const atom of atoms ) { const atomStyle = atom . computedStyle ; if ( ! style && ! atom . style ) { run . push ( atom ) ; } else if ( style && selected === atom . isSelected && atomStyle . color === style . color && atomStyle . backgroundColor === style . backgroundColor && atomStyle . fontSize === style . fontSize ) { run . push ( atom ) ; } else { if ( run . length > 0 ) runs . push ( run ) ; run = [ atom ] ; style = atom . computedStyle ; selected = atom . isSelected ; } } if ( run . length > 0 ) runs . push ( run ) ; return runs ; } /**
 * Render a list of atoms with the same style (color, backgroundColor, size)
 */ function renderStyleRun ( parentContext , atoms , options ? ) { function isDigit ( atom ) { return ( atom . type === 'mord' && Boolean ( atom . value ) && /^[\d,.]$/ . test ( atom . value ) ) ; } function isText ( atom ) { return atom . mode === 'text' ; } if ( ! atoms || atoms . length === 0 ) return null ; const context = new Context ( parentContext , options ?. style ) ; const displaySelection = ! context . atomIdsSettings || ! context . atomIdsSettings . groupNumbers ; let boxes = [ ] ; if ( atoms . length === 1 ) { const box = atoms [ 0 ] . render ( context , { newList : options ?. newList ?? false , } ) ; if ( box && displaySelection && atoms [ 0 ] . isSelected ) { box . selected ( true ) ; } if ( box ) boxes = [ box ] ; } else { let selection = [ ] ; let digitOrTextStringID = '' ; let lastWasDigit = true ; let isNewList = options ?. newList ?? false ; for ( const atom of atoms ) { if ( context . atomIdsSettings ?. groupNumbers && digitOrTextStringID && ( ( lastWasDigit && isDigit ( atom ) ) || ( ! lastWasDigit && isText ( atom ) ) ) ) { context . atomIdsSettings . overrideID = digitOrTextStringID ; } const box = atom . render ( context , { newList : isNewList } ) ; if ( context . atomIdsSettings ) { context . atomIdsSettings . overrideID = undefined ; } if ( box ) { isNewList = false ; if ( context . atomIdsSettings ?. groupNumbers ) { if ( isDigit ( atom ) || isText ( atom ) ) { if ( ! digitOrTextStringID || lastWasDigit !== isDigit ( atom ) ) { lastWasDigit = isDigit ( atom ) ; digitOrTextStringID = atom . id ?? '' ; } } if ( ( ! ( isDigit ( atom ) || isText ( atom ) ) || ! atom . hasEmptyBranch ( 'superscript' ) || ! atom . hasEmptyBranch ( 'subscript' ) ) && digitOrTextStringID ) { digitOrTextStringID = '' ; } } if ( displaySelection && atom . isSelected ) { selection . push ( box ) ; for ( const box of selection ) box . selected ( true ) ; } else { if ( selection . length > 0 ) { boxes = [ ... boxes , ... selection ] ; selection = [ ] ; } boxes . push ( box ) ; } } } if ( selection . length > 0 ) { boxes = [ ... boxes , ... selection ] ; selection = [ ] ; } } if ( ! boxes || boxes . length === 0 ) return null ; let result = boxes [ 0 ] ; if ( options || boxes . length > 1 ) { result = new Box ( boxes , { isTight : context . isTight , ... options , } ) ; result . selected ( boxes [ 0 ] . isSelected ) ; } return result . wrap ( context ) . wrap ( parentContext ) ; }