import { adjustInterAtomSpacing , makeStruts } from '../core/box' ; import { Rect , getSelectionBounds , isValidMathfield , getAtomBounds , adjustForScrolling , } from './utils' ; import type { MathfieldPrivate } from './mathfield-private' ; import { atomsToMathML } from '../addons/math-ml' ; import { Atom , Context , DEFAULT_FONT_SIZE } from '../core/core' ; import { updatePopoverPosition } from '../editor/popover' ; import { throwIfNotInBrowser } from '../common/capabilities' ; function hash ( latex ) { let result = 0 ; for ( let i = 0 ; i < latex . length ; i ++ ) { result = result * 31 + latex . charCodeAt ( i ) ; result = result | 0 ; } return Math . abs ( result ) ; } export function requestUpdate ( mathfield , options ? ) { if ( ! mathfield . dirty ) { mathfield . dirty = true ; requestAnimationFrame ( ( ) => { if ( isValidMathfield ( mathfield ) && mathfield . dirty ) { mathfield . _atomBoundsCache = new Map < string , Rect > ( ) ; render ( mathfield , options ) ; mathfield . _atomBoundsCache = undefined ; } } ) ; } } /**
 * Lay-out the mathfield and generate the DOM.
 *
 * This is usually done automatically, but if the font-size, or other geometric
 * attributes are modified, outside of MathLive, this function may need to be
 * called explicitly.
 *
 */ export function render ( mathfield , renderOptions ? ) { throwIfNotInBrowser ( ) ; if ( ! isValidMathfield ( mathfield ) ) return ; renderOptions = renderOptions ?? { } ; mathfield . dirty = false ; const { model } = mathfield ; if ( window . mathlive === undefined ) { window . mathlive = { } ; } model . root . caret = '' ; model . root . isSelected = false ; model . root . containsCaret = true ; for ( const atom of model . atoms ) { atom . caret = '' ; atom . isSelected = false ; atom . containsCaret = false ; } const hasFocus = mathfield . hasFocus ( ) && ! mathfield . options . readOnly ; if ( model . selectionIsCollapsed ) { model . at ( model . position ) . caret = hasFocus ? mathfield . mode : '' ; } else { for ( const atom of model . getAtoms ( model . selection , { includeChildren : true , } ) ) { atom . isSelected = true ; } } if ( hasFocus ) { let ancestor = model . at ( model . position ) . parent ; while ( ancestor ) { ancestor . containsCaret = true ; ancestor = ancestor . parent ; } } const base = model . root . render ( new Context ( { macros : mathfield . options . macros , registers : mathfield . options . registers , atomIdsSettings : { seed : hash ( Atom . serialize ( model . root , { expandMacro : false , defaultMode : mathfield . options . defaultMode , } ) ) , groupNumbers : renderOptions . forHighlighting ?? false , } , smartFence : mathfield . options . smartFence , renderPlaceholder : mathfield . options . readOnly ? ( context , p ) => { if ( p . placeholderId ) { const field = mathfield . getPlaceholderField ( p . placeholderId ! ) ; return p . createMathfieldBox ( context , { placeholderId : p . placeholderId ! , element : field ! , } ) ; } return p . createBox ( context ) ; } : undefined , isSelected : model . root . isSelected , } , { fontSize : DEFAULT_FONT_SIZE , letterShapeStyle : mathfield . options . letterShapeStyle , } , mathfield . options . defaultMode === 'inline-math' ? 'textstyle' : 'displaystyle' ) ) ; const wrapper = makeStruts ( adjustInterAtomSpacing ( base ! , mathfield . options . horizontalSpacingScale ) , { classes : 'ML__mathlive' , attributes : { 'translate' : 'no' , 'aria-hidden' : 'true' , } , } ) ; const field = mathfield . field ! ; const isFocused = field ! . classList . contains ( 'ML__focused' ) ; if ( isFocused && ! hasFocus ) { field ! . classList . remove ( 'ML__focused' ) ; } else if ( ! isFocused && hasFocus ) { field ! . classList . add ( 'ML__focused' ) ; } field ! . innerHTML = mathfield . options . createHTML ( wrapper . toMarkup ( ) ) ; mathfield . fieldContent = field . querySelector ( '.ML__mathlive' ) ; mathfield . accessibleNode ! . innerHTML = mathfield . options . createHTML ( '<math xmlns="http://www.w3.org/1998/Math/MathML">' + atomsToMathML ( model . root , mathfield . options ) + '</math>' ) ; renderSelection ( mathfield ) ; if ( mathfield . options . readOnly ) { mathfield . attachNestedMathfield ( ) ; } if ( ! ( renderOptions . interactive ?? false ) ) { setTimeout ( ( ) => renderSelection ( mathfield ) , 32 ) ; } } export function renderSelection ( mathfield ) { throwIfNotInBrowser ( ) ; if ( ! mathfield . field ) return ; for ( const element of mathfield . field . querySelectorAll ( '.ML__selection, .ML__contains-highlight' ) ) { element . remove ( ) ; } if ( ! mathfield . hasFocus ( ) ) return ; const model = mathfield . model ; if ( model . selectionIsCollapsed ) { setTimeout ( ( ) => updatePopoverPosition ( mathfield ) , 32 ) ; let atom = model . at ( model . position ) ; while ( atom && ! ( atom . containsCaret && atom . displayContainsHighlight ) ) { atom = atom . parent ! ; } if ( atom ?. containsCaret && atom . displayContainsHighlight ) { const bounds = adjustForScrolling ( mathfield , getAtomBounds ( mathfield , atom ) ) ; if ( bounds ) { const element = document . createElement ( 'div' ) ; element . classList . add ( 'ML__contains-highlight' ) ; element . style . position = 'absolute' ; element . style . left = `${ bounds . left }px` ; element . style . top = `${ bounds . top }px` ; element . style . width = `${ Math . ceil ( bounds . right - bounds . left ) }px` ; element . style . height = `${ Math . ceil ( bounds . bottom - bounds . top - 1 ) }px` ; mathfield . field . insertBefore ( element , mathfield . field . childNodes [ 0 ] ) ; } } return ; } for ( const x of unionRects ( getSelectionBounds ( mathfield , { excludeAtomsWithBackground : true } ) ) ) { const selectionElement = document . createElement ( 'div' ) ; selectionElement . classList . add ( 'ML__selection' ) ; selectionElement . style . position = 'absolute' ; selectionElement . style . left = `${ x . left }px` ; selectionElement . style . top = `${ x . top }px` ; selectionElement . style . width = `${ Math . ceil ( x . right - x . left ) }px` ; selectionElement . style . height = `${ Math . ceil ( x . bottom - x . top - 1 ) }px` ; mathfield . field . insertBefore ( selectionElement , mathfield . field . childNodes [ 0 ] ) ; } } /**
 * Return the rects that are not entirely contained by other rects.
 */ function unionRects ( rects ) { const result = [ ] ; for ( const rect of rects ) { let found = false ; for ( const rect2 of result ) { if ( rect . left === rect2 . left && rect . right === rect2 . right && rect . top === rect2 . top && rect . bottom === rect2 . bottom ) { found = true ; break ; } } if ( ! found ) result . push ( rect ) ; } for ( const rect of rects ) { let count = 0 ; for ( const rect2 of rects ) { if ( rect . left >= rect2 . left && rect . right <= rect2 . right && rect . top >= rect2 . top && rect . bottom <= rect2 . bottom ) { count += 1 ; if ( count > 1 ) break ; } } if ( count === 1 ) result . push ( rect ) ; } return result ; }