import { isArray } from '../common/types' ; import { Style , ParseMode } from '../public/core' ; import { getCharacterMetrics } from './font-metrics' ; import { svgBodyToMarkup , svgBodyHeight } from './svg-box' ; import { Mode } from './modes-utils' ; import { Context } from './context' ; const BOX_TYPE = [ '' , 'chem' , 'mord' , 'mbin' , 'mop' , 'mrel' , 'mopen' , 'mclose' , 'mpunct' , 'minner' , 'spacing' , 'first' , 'latex' , 'composition' , 'error' , 'placeholder' , 'supsub' , 'none' , 'mathfield' , ] as const ; export type BoxType = typeof BOX_TYPE [ number ] ; export function isBoxType ( type ) { return ( BOX_TYPE as unknown as string [ ] ) . includes ( type ) ; } /**
 * TeXBook, p. 170
 *
 * > In fact, TEX’s rules for spacing in formulas are fairly simple. A formula is
 * > converted to a math list as described at the end of Chapter 17, and the math
 * > list consists chiefly of “atoms” of eight basic types: Ord (ordinary),
 * > Op (large operator), Bin (binary operation), Rel (relation), Open (opening),
 * > Close (closing), Punct (punctuation), and Inner (a delimited subformula).
 * > Other kinds of atoms, which arise from commands like \overline or
 * > \mathaccent or \vcenter, etc., are all treated as type Ord; fractions are
 * > treated as type Inner.
 *
 * > The following table is used to determine the spacing between pair of adjacent
 * > atoms.
 *
 * In this table
 * - "3" = `\thinmuskip`
 * - "4" = `\medmuskip`
 * - "5" = `\thickmuskip`
 *
 */ const INTER_ATOM_SPACING = { mord : { mop : 3 , mbin : 4 , mrel : 5 , minner : 3 } , mop : { mord : 3 , mop : 3 , rel : 5 , minner : 3 } , mbin : { mord : 4 , mop : 4 , mopen : 4 , minner : 4 } , mrel : { mord : 5 , mop : 5 , mopen : 5 , minner : 5 } , mclose : { mop : 3 , mbin : 4 , mrel : 5 , minner : 3 } , mpunct : { mord : 3 , mop : 3 , mrel : 3 , mopen : 3 , mpunct : 3 , minner : 3 } , minner : { mord : 3 , mop : 3 , mbin : 4 , mrel : 5 , mopen : 3 , mpunct : 3 , minner : 3 } , } ; /**
 * This table is used when the mathstyle is 'tight' (scriptstyle or
 * scriptscriptstyle).
 */ const INTER_ATOM_TIGHT_SPACING = { mord : { mop : 3 } , mop : { mord : 3 , mop : 3 } , mclose : { mop : 3 } , minner : { mop : 3 } , } ; /**
 * Return a string made up of the concatenated arguments.
 * Each arguments can be either a string, which is unchanged,
 * or a number, which is converted to a string with at most 2 fractional digits.
 *
 */ function toString ( value ) ; function toString ( value , unit ? ) ; function toString ( arg1 ? , arg2 ? ) { if ( typeof arg1 === 'string' ) { return arg1 ; } if ( typeof arg1 === 'number' ) { console . assert ( Number . isFinite ( arg1 ) ) ; const numValue = Math . ceil ( 1e2 * arg1 ) / 1e2 ; if ( numValue === 0 ) return '0' ; return numValue . toString ( ) + ( arg2 ?? '' ) ; } return '' ; } export type BoxCSSProperties = 'background-color' | 'border' | 'border-bottom' | 'border-color' | 'border-left' | 'border-radius' | 'border-right' | 'border-right-width' | 'border-top' | 'border-top-width' | 'box-sizing' | 'color' | 'display' | 'font-family' | 'left' | 'font-size' | 'height' | 'line-height' | 'margin' | 'margin-top' | 'margin-left' | 'margin-right' | 'opacity' | 'padding' | 'position' | 'top' | 'vertical-align' | 'width' | 'z-index' ; export type BoxOptions = { classes ? : string ; properties ? : Partial < Record < BoxCSSProperties , string > > ; attributes ? : Record < string , string > ; type ? : BoxType ; isTight ? : boolean ; height ? : number ; depth ? : number ; maxFontSize ? : number ; newList ? : boolean ; mode ? : ParseMode ; style ? : Style ; fontFamily ? : string ; } ; param content param classes property property property property property property property property export class Box { type : BoxType ; children ? : Box [ ] ; newList : boolean ; value : string ; classes : string ; caret : ParseMode ; isSelected : boolean ; height : number ; depth : number ; skew : number ; italic : number ; maxFontSize : number ; isTight ? : boolean ; cssId ? : string ; htmlData ? : string ; htmlStyle ? : string ; svgBody ? : string ; svgOverlay ? : string ; svgStyle ? : string ; delim ? : string ; protected attributes ? : Record < string , string > ; protected cssProperties : Partial < Record < BoxCSSProperties , string > > ; constructor ( content , options ? ) { if ( typeof content === 'number' ) { this . value = String . fromCodePoint ( content ) ; } else if ( typeof content === 'string' ) { this . value = content ; } else if ( isArray < Box | null > ( content ) ) { this . children = content . filter ( ( x ) => x !== null ) as Box [ ] ; } else if ( content && content instanceof Box ) { this . children = [ content ] ; } this . type = options ?. type ?? '' ; this . isSelected = false ; this . isTight = options ?. isTight ?? false ; this . newList = options ?. newList ?? false ; if ( options ?. properties ) { for ( const prop of Object . keys ( options . properties ) ) { this . setStyle ( prop as BoxCSSProperties , options . properties [ prop ] ) ; } } if ( options ?. attributes ) this . attributes = options . attributes ; this . classes = options ?. classes ?? '' ; let fontName = options ?. fontFamily ?? 'Main-Regular' ; if ( options ?. style && this . value ) { fontName = Mode . applyStyle ( options . mode ?? 'math' , this , options . style ) ?? 'Main-Regular' ; } this . height = 0 ; this . depth = 0 ; this . skew = 0 ; this . italic = 0 ; this . maxFontSize = 0 ; if ( typeof content === 'number' ) { const metrics = getCharacterMetrics ( content , fontName ) ; this . height = metrics . height ; this . depth = metrics . depth ; this . skew = metrics . skew ; this . italic = metrics . italic ; this . right = metrics . italic ; } else if ( this . value ) { this . height = - Infinity ; this . depth = - Infinity ; this . skew = - Infinity ; this . italic = - Infinity ; for ( let i = 0 ; i < this . value . length ; i ++ ) { const metrics = getCharacterMetrics ( this . value . codePointAt ( i ) , fontName ?? 'Main-Regular' ) ; this . height = Math . max ( this . height , metrics . height ) ; this . depth = Math . max ( this . depth , metrics . depth ) ; this . skew = metrics . skew ; this . italic = metrics . italic ; } this . right = this . italic ; } else if ( this . type === 'latex' ) { this . height = 0.8 ; this . depth = 0.2 ; } else if ( this . children && this . children . length > 0 ) { if ( this . children . length === 1 ) { const child = this . children [ 0 ] ; this . height = child . height ; this . depth = child . depth ; this . maxFontSize = child . maxFontSize ; this . skew = child . skew ; this . italic = child . italic ; } else { let height = - Infinity ; let depth = - Infinity ; let maxFontSize = 0 ; for ( const child of this . children ) { if ( child . height > height ) height = child . height ; if ( child . depth > depth ) depth = child . depth ; maxFontSize = Math . max ( maxFontSize , child . maxFontSize ?? 0 ) ; } this . height = height ; this . depth = depth ; this . maxFontSize = maxFontSize ; } } if ( options ?. height !== undefined ) this . height = options . height ; if ( options ?. depth !== undefined ) this . depth = options . depth ; if ( options ?. maxFontSize !== undefined ) { this . maxFontSize = options . maxFontSize ; } } set atomID ( id ) { if ( id === undefined ) return ; if ( ! this . attributes ) this . attributes = { } ; this . attributes [ 'data-atom-id' ] = id ; } selected ( isSelected ) { this . isSelected = isSelected ; if ( this . children ) { for ( const child of this . children ) { child . selected ( isSelected ) ; } } } param prop param value setStyle ( prop , value ) ; setStyle ( prop , value , unit ? ) ; setStyle ( prop , value , unit ? ) { const v = toString ( value as number , unit ) ; if ( v . length > 0 ) { if ( ! this . cssProperties ) this . cssProperties = { } ; this . cssProperties [ prop ] = v ; } } setTop ( top ) { if ( Number . isFinite ( top ) && Math . abs ( top ) > 1e-2 ) { if ( ! this . cssProperties ) this . cssProperties = { } ; this . cssProperties . top = toString ( top , 'em' ) ; this . height -= top ; this . depth += top ; } } get left ( ) : number { if ( this . cssProperties ?. [ 'margin-left' ] ) { return Number . parseFloat ( this . cssProperties [ 'margin-left' ] ) ; } return 0 ; } set left ( value ) { if ( ! Number . isFinite ( value ) ) return ; if ( ! this . cssProperties ) this . cssProperties = { } ; if ( value === 0 ) { delete this . cssProperties [ 'margin-left' ] ; } else { this . cssProperties [ 'margin-left' ] = toString ( value , 'em' ) ; } } set right ( value ) { if ( ! Number . isFinite ( value ) ) return ; if ( ! this . cssProperties ) this . cssProperties = { } ; if ( value === 0 ) { delete this . cssProperties [ 'margin-right' ] ; } else { this . cssProperties [ 'margin-right' ] = toString ( value , 'em' ) ; } } set width ( value ) { if ( ! this . cssProperties ) this . cssProperties = { } ; this . cssProperties . width = toString ( value , 'em' ) ; } /**
     * If necessary wrap this box with another one that adjust the font-size
     * to account for a change in size between the context and its parent.
     * Also, apply color and background-color attributes.
     */ wrap ( context , options ? ) { const parent = context . parent ; if ( ! parent ) return this ; if ( context . isPhantom ) { this . setStyle ( 'opacity' , 0 ) ; } const newColor = context . computedColor === parent . computedColor ? undefined : context . color ; this . setStyle ( 'color' , newColor ) ; const newSize = context . effectiveFontSize === parent . effectiveFontSize ? undefined : context . effectiveFontSize ; const newBackgroundColor = context . computedBackgroundColor === parent . computedBackgroundColor ? undefined : context . backgroundColor ; if ( ! newSize && ! newBackgroundColor && ! ( options && ( options . classes || options . type ) ) ) { return this ; } let result ; if ( newBackgroundColor ) { result = makeStruts ( this , options ) ; result . selected ( this . isSelected ) ; result . setStyle ( 'background-color' , newBackgroundColor ) ; result . setStyle ( 'display' , 'inline-block' ) ; } else { result = new Box ( this , options ) ; } const factor = context . scalingFactor ; if ( factor !== 1.0 ) { result . setStyle ( 'font-size' , factor * 100 , '%' ) ; result . height *= factor ; result . depth *= factor ; result . italic *= factor ; result . skew *= factor ; } return result ; } /**
     * Generate the HTML markup to represent this box.
     */ toMarkup ( ) { let body = this . value ?? '' ; if ( this . children ) { for ( const box of this . children ) { body += box . toMarkup ( ) ; } } const classes = this . classes . split ( ' ' ) ; classes . push ( { latex : 'ML__latex' , placeholder : 'ML__placeholder' , error : 'ML__error' , } [ this . type ] ?? '' ) ; if ( this . caret === 'latex' ) { classes . push ( 'ML__latex-caret' ) ; } const classList = classes . length === 1 ? classes [ 0 ] : classes . filter ( ( x , e , a ) => x . length > 0 && a . indexOf ( x ) === e ) . join ( ' ' ) ; let result = '' ; if ( ( body . length > 0 && body !== '\u200B' ) || classList . length > 0 || this . cssId || this . htmlData || this . htmlStyle || this . attributes || this . cssProperties || this . svgBody || this . svgOverlay ) { let props = '' ; if ( this . cssId ) { props += ` id=${ this . cssId . replace ( / /g , '-' ) } ` ; } if ( this . htmlData ) { const entries = this . htmlData . split ( ',' ) ; for ( const entry of entries ) { const matched = entry . match ( /([^=]+)=(.+$)/ ) ; if ( matched ) { const key = matched [ 1 ] . trim ( ) . replace ( / /g , '-' ) ; if ( key ) { props += ` data-${ key }=${ matched [ 2 ] } ` ; } } else { const key = entry . trim ( ) . replace ( / /g , '-' ) ; if ( key ) { props += ` data-${ key } ` ; } } } } if ( this . htmlStyle ) { const entries = this . htmlStyle . split ( ';' ) ; let styleString = '' ; for ( const entry of entries ) { const matched = entry . match ( /([^=]+):(.+$)/ ) ; if ( matched ) { const key = matched [ 1 ] . trim ( ) . replace ( / /g , '-' ) ; if ( key ) { styleString += `${ key }:${ matched [ 2 ] };` ; } } } if ( styleString ) { props += ` style="${ styleString }"` ; } } if ( this . attributes ) { props += ' ' + Object . keys ( this . attributes ) . map ( ( x ) => `${ x }="${ this . attributes ! [ x ] }"` ) . join ( ' ' ) ; } if ( classList . length > 0 ) { props += ` class="${ classList }"` ; } if ( this . cssProperties ) { const styleString = Object . keys ( this . cssProperties ) . map ( ( x ) => `${ x }:${ this . cssProperties [ x ] }` ) . join ( ';' ) ; if ( styleString . length > 0 ) { props += ` style="${ styleString }"` ; } } let svgMarkup = '' ; if ( this . svgBody ) { svgMarkup = svgBodyToMarkup ( this . svgBody ) ; } else if ( this . svgOverlay ) { svgMarkup = '<span style="' ; svgMarkup += 'display: inline-block;' ; svgMarkup += `height:${ this . height + this . depth }em;` ; svgMarkup += `vertical-align:${ this . depth }em;` ; svgMarkup += '">' ; svgMarkup += body ; svgMarkup += '</span>' ; svgMarkup += '<svg style="position:absolute;overflow:overlay;' ; svgMarkup += `height:${ this . height + this . depth }em;` ; if ( this . cssProperties ?. padding ) { svgMarkup += `top:${ this . cssProperties . padding }em;` ; svgMarkup += `left:${ this . cssProperties . padding }em;` ; svgMarkup += `width:calc(100% - 2 * ${ this . cssProperties . padding }em );` ; } else { svgMarkup += 'top:0;left:0;width:100%;' ; } svgMarkup += 'z-index:2;' ; svgMarkup += '"' ; if ( this . svgStyle ) { svgMarkup += ` style="${ this . svgStyle }"` ; } svgMarkup += `>${ this . svgOverlay }</svg>` ; } result = `<span${ props }>${ body }${ svgMarkup }</span>` ; } if ( this . caret === 'text' ) { result += '<span class="ML__text-caret"></span>' ; } else if ( this . caret === 'math' ) { result += '<span class="ML__caret"></span>' ; } return result ; } /**
     * Can this box be coalesced with 'box'?
     * This is used to 'coalesce' (i.e. group together) a series of boxes that are
     * identical except for their value, and to avoid generating redundant boxes.
     * That is: '12' ->
     *      "<span class='crm'>12</span>"
     * rather than:
     *      "<span class='crm'>1</span><span class='crm'>2</span>"
     */ tryCoalesceWith ( box ) { if ( this . type !== box . type ) return false ; if ( ! /ML__text/ . test ( this . classes ) && ! [ 'mord' , 'mbin' , 'mrel' ] . includes ( this . type ) ) { return false ; } if ( this . svgBody || ! this . value ) return false ; if ( box . svgBody || ! box . value ) return false ; const hasChildren = this . children && this . children . length > 0 ; const boxHasChildren = box . children && box . children . length > 0 ; if ( hasChildren || boxHasChildren ) return false ; const thisStyleCount = this . cssProperties ? Object . keys ( this . cssProperties ) . length : 0 ; const boxStyleCount = box . cssProperties ? Object . keys ( box . cssProperties ) . length : 0 ; if ( thisStyleCount !== boxStyleCount ) return false ; if ( thisStyleCount > 0 ) { for ( const prop of Object . keys ( this . cssProperties ) ) { if ( this . cssProperties [ prop ] !== box . cssProperties [ prop ] ) { return false ; } } } const classes = this . classes . trim ( ) . replace ( /\s+/g , ' ' ) . split ( ' ' ) ; const boxClasses = box . classes . trim ( ) . replace ( /\s+/g , ' ' ) . split ( ' ' ) ; if ( classes . length !== boxClasses . length ) return false ; classes . sort ( ) ; boxClasses . sort ( ) ; for ( const [ i , class_ ] of classes . entries ( ) ) { if ( class_ === 'vertical-separator' ) return false ; if ( class_ !== boxClasses [ i ] ) return false ; } this . value += box . value ; this . height = Math . max ( this . height , box . height ) ; this . depth = Math . max ( this . depth , box . depth ) ; this . maxFontSize = Math . max ( this . maxFontSize , box . maxFontSize ) ; this . italic = box . italic ; return true ; } } /**
 * Attempts to coalesce (merge) boxes, for example consecutive text boxes.
 * Return a new tree with coalesced boxes.
 *
 */ function coalesceRecursive ( boxes ) { if ( ! boxes || boxes . length === 0 ) return [ ] ; boxes [ 0 ] . children = coalesceRecursive ( boxes [ 0 ] . children ) ; const result = [ boxes [ 0 ] ] ; for ( let i = 1 ; i < boxes . length ; i ++ ) { if ( ! result [ result . length - 1 ] . tryCoalesceWith ( boxes [ i ] ) ) { boxes [ i ] . children = coalesceRecursive ( boxes [ i ] . children ) ; result . push ( boxes [ i ] ) ; } } return result ; } export function coalesce ( box ) { if ( box . children ) box . children = coalesceRecursive ( box . children ) ; return box ; } /**
 *  Handle proper spacing of, e.g. "-4" vs "1-4", by adjusting some box type
 */ function adjustType ( root ) { forEachBox ( root , ( prevBox , box ) => { if ( box . type === 'mbin' && ( ! prevBox || /first|none|mbin|mop|mrel|mopen|mpunct/ . test ( prevBox . type ) ) ) { box . type = 'mord' ; } if ( prevBox && prevBox . type === 'mbin' && /mrel|mclose|mpunct|placeholder/ . test ( box . type ) ) { prevBox . type = 'mord' ; } } ) ; } function applyInterAtomSpacing ( root , scale ) { forEachBox ( root , ( prevBox , box ) => { const prevType = prevBox ?. type ?? 'none' ; const table = box . isTight ? INTER_ATOM_TIGHT_SPACING [ prevType ] ?? null : INTER_ATOM_SPACING [ prevType ] ?? null ; const hskip = table ? table [ box . type ] ?? 0 : 0 ; if ( hskip ) box . left += scale * ( hskip / 18 ) ; } ) ; } function forEachBoxRecursive ( prevBox , box , f ) { if ( box . newList ) prevBox = null ; const type = box . type ; if ( type === 'first' ) { console . assert ( box . newList === true ) ; return null ; } if ( type === 'spacing' ) { return prevBox ; } f ( prevBox , box ) ; if ( box . children ) { let childPrev = null ; if ( type === undefined || type . length === 0 ) { childPrev = prevBox ; } for ( const child of box . children ) { childPrev = forEachBoxRecursive ( childPrev , child , f ) ; } if ( type === undefined || type . length === 0 ) { prevBox = childPrev ; } } if ( type !== 'supsub' && type !== undefined && type . length > 0 ) { prevBox = box ; } return prevBox ; } function forEachBox ( box , f ) { if ( ! box ) return ; forEachBoxRecursive ( null , box , f ) ; } export function adjustInterAtomSpacing ( root , scale = 1.0 ) { adjustType ( root ) ; applyInterAtomSpacing ( root , scale ) ; return root ; } export function makeStruts ( content , options ? ) { if ( ! content ) return new Box ( null , options ) ; const topStrut = new Box ( null , { classes : 'ML__strut' } ) ; topStrut . setStyle ( 'height' , Math . max ( 0 , content . height ) , 'em' ) ; const struts = [ topStrut ] ; if ( content . depth !== 0 ) { const bottomStrut = new Box ( null , { classes : 'ML__strut--bottom' } ) ; bottomStrut . setStyle ( 'height' , content . height + content . depth , 'em' ) ; bottomStrut . setStyle ( 'vertical-align' , - content . depth , 'em' ) ; struts . push ( bottomStrut ) ; } struts . push ( content ) ; return new Box ( struts , options ) ; } /**
 * Add some SVG markup to be overlaid on top of the box
 */ export function addSVGOverlay ( body , svgMarkup , svgStyle ) { body . svgOverlay = svgMarkup ; body . svgStyle = svgStyle ; return body ; } /**
 * Create a box that consist of a (stretchy) SVG element
 */ export function makeSVGBox ( svgBodyName ) { const height = svgBodyHeight ( svgBodyName ) / 2 ; const box = new Box ( null , { height : height + 0.166 , depth : height - 0.166 , maxFontSize : 0 , } ) ; box . svgBody = svgBodyName ; return box ; }