import '../core/atom' ; import { MacroDictionary , getMacros } from '../core-definitions/definitions' ; import { AutoRenderOptions } from '../public/mathlive' ; import { ErrorListener , ParserErrorCode , Registers } from '../public/core' ; import { loadFonts } from '../core/fonts' ; import { inject as injectStylesheet } from '../common/stylesheet' ; import coreStylesheet from '../../css/core.less' ; import { parseMathString } from '../editor/parse-math-string' ; import { throwIfNotInBrowser } from '../common/capabilities' ; import { hashCode } from '../common/hash-code' ; export type AutoRenderOptionsPrivate = AutoRenderOptions & { /** A function that will convert any LaTeX found to
     * HTML markup. This is only useful to override the default MathLive renderer
     */ renderToMarkup ? : ( text : string , options : { mathstyle ? : 'displaystyle' | 'textstyle' ; letterShapeStyle ? : 'tex' | 'french' | 'iso' | 'upright' | 'auto' ; macros ? : MacroDictionary ; registers ? : Registers ; onError ? : ErrorListener < ParserErrorCode > ; format ? : string ; } ) => string ; /**
     * A function that will convert any LaTeX found to
     * MathML markup.
     */ renderToMathML ? : ( text : string , options : { mathstyle ? : string ; format ? : string ; macros ? : MacroDictionary ; registers ? : Registers ; } ) => string ; /** A function that will convert any LaTeX found to
     * speakable text markup. */ renderToSpeakableText ? : ( text : string , options : { mathstyle ? : string ; format ? : string ; macros ? : MacroDictionary ; registers ? : Registers ; } ) => string ; ignoreClassPattern ? : RegExp ; processClassPattern ? : RegExp ; processScriptTypePattern ? : RegExp ; mathstyle ? : string ; format ? : string ; } ; function findEndOfMath ( delimiter , text , startIndex ) { let index = startIndex ; let braceLevel = 0 ; const delimLength = delimiter . length ; while ( index < text . length ) { const character = text [ index ] ; if ( braceLevel <= 0 && text . slice ( index , index + delimLength ) === delimiter ) { return index ; } if ( character === '\\' ) { index ++ ; } else if ( character === '{' ) { braceLevel ++ ; } else if ( character === '}' ) { braceLevel -- ; } index ++ ; } return - 1 ; } function splitAtDelimiters ( startData , leftDelim , rightDelim , mathstyle , format = 'latex' ) { const finalData = [ ] ; for ( const startDatum of startData ) { if ( startDatum . type === 'text' ) { const text = startDatum . data ; let lookingForLeft = true ; let currIndex = 0 ; let nextIndex ; nextIndex = text . indexOf ( leftDelim ) ; if ( nextIndex !== - 1 ) { currIndex = nextIndex ; if ( currIndex > 0 ) { finalData . push ( { type : 'text' , data : text . slice ( 0 , currIndex ) , } ) ; } lookingForLeft = false ; } let done = false ; while ( ! done ) { if ( lookingForLeft ) { nextIndex = text . indexOf ( leftDelim , currIndex ) ; if ( nextIndex === - 1 ) { done = true ; break ; } if ( currIndex !== nextIndex ) { finalData . push ( { type : 'text' , data : text . slice ( currIndex , nextIndex ) , } ) ; } currIndex = nextIndex ; } else { nextIndex = findEndOfMath ( rightDelim , text , currIndex + leftDelim . length ) ; if ( nextIndex === - 1 ) { done = true ; break ; } let formula = text . slice ( currIndex + leftDelim . length , nextIndex ) ; if ( format === 'ascii-math' ) { [ , formula ] = parseMathString ( formula , { format : 'ascii-math' } ) ; } finalData . push ( { type : 'math' , data : formula , rawData : text . slice ( currIndex , nextIndex + rightDelim . length ) , mathstyle , } ) ; currIndex = nextIndex + rightDelim . length ; } lookingForLeft = ! lookingForLeft ; } if ( currIndex < text . length ) { finalData . push ( { type : 'text' , data : text . slice ( currIndex ) , } ) ; } } else { finalData . push ( startDatum ) ; } } return finalData ; } function splitWithDelimiters ( text , texDelimiters ? , mathAsciiDelimiters ? ) { let data = [ { type : 'text' , data : text } ] ; if ( texDelimiters ?. inline ) { texDelimiters . inline . forEach ( ( [ openDelim , closeDelim ] ) => { data = splitAtDelimiters ( data , openDelim , closeDelim , 'textstyle' ) ; } ) ; } if ( texDelimiters ?. display ) { texDelimiters . display . forEach ( ( [ openDelim , closeDelim ] ) => { data = splitAtDelimiters ( data , openDelim , closeDelim , 'displaystyle' ) ; } ) ; } if ( mathAsciiDelimiters ?. inline ) { mathAsciiDelimiters . inline . forEach ( ( [ openDelim , closeDelim ] ) => { data = splitAtDelimiters ( data , openDelim , closeDelim , 'textstyle' , 'ascii-math' ) ; } ) ; } if ( mathAsciiDelimiters ?. display ) { mathAsciiDelimiters . display . forEach ( ( [ openDelim , closeDelim ] ) => { data = splitAtDelimiters ( data , openDelim , closeDelim , 'displaystyle' , 'ascii-math' ) ; } ) ; } return data ; } function createMathMLNode ( latex , options ) { throwIfNotInBrowser ( ) ; const span = document . createElement ( 'span' ) ; try { const html = "<math xmlns='http://www.w3.org/1998/Math/MathML'>" + options . renderToMathML ! ( latex , options ) + '</math>' ; span . innerHTML = options . createHTML ? options . createHTML ( html ) : html ; } catch ( error ) { console . error ( "Could not convert '" + latex + "' to MathML with " , error ) ; span . textContent = latex ; } span . className = 'ML__sr-only' ; return span ; } function createMarkupNode ( text , options , mathstyle , createNodeOnFailure ) { throwIfNotInBrowser ( ) ; const element = document . createElement ( mathstyle === 'displaystyle' ? 'div' : 'span' ) ; element . setAttribute ( 'aria-hidden' , 'true' ) ; try { const html = options . renderToMarkup ! ( text , { mathstyle : mathstyle ?? 'displaystyle' , format : 'html' , macros : options . macros , } ) ; element . innerHTML = options . createHTML ? options . createHTML ( html ) : html ; } catch ( error ) { console . error ( "Could not parse'" + text + "' with " , error ) ; if ( createNodeOnFailure ) { return document . createTextNode ( text ) ; } return null ; } return element ; } function createAccessibleMarkupPair ( latex , mathstyle , options , createNodeOnFailure ) { const markupNode = createMarkupNode ( latex , options , mathstyle as 'displaystyle' | 'textstyle' , createNodeOnFailure ) ; const accessibleContent = options . renderAccessibleContent ?? '' ; if ( markupNode && /\b(mathml|speakable-text)\b/i . test ( accessibleContent ) ) { throwIfNotInBrowser ( ) ; const fragment = document . createElement ( 'span' ) ; if ( /\bmathml\b/i . test ( accessibleContent ) && options . renderToMathML ) { fragment . append ( createMathMLNode ( latex , options ) ) ; } if ( /\bspeakable-text\b/i . test ( accessibleContent ) && options . renderToSpeakableText ) { const span = document . createElement ( 'span' ) ; const html = options . renderToSpeakableText ( latex , options ) ; span . innerHTML = options . createHTML ? options . createHTML ( html ) : html ; span . className = 'ML__sr-only' ; fragment . append ( span ) ; } fragment . append ( markupNode ) ; return fragment ; } return markupNode ; } function scanText ( text , options ) { throwIfNotInBrowser ( ) ; let fragment = null ; if ( options . TeX ?. processEnvironments && /^\s*\\begin/ . test ( text ) ) { fragment = document . createDocumentFragment ( ) ; const node = createAccessibleMarkupPair ( text , '' , options , true ) ; if ( node ) fragment . appendChild ( node ) ; } else { if ( ! text . trim ( ) ) return null ; const data = splitWithDelimiters ( text , options . TeX ?. delimiters , options . asciiMath ?. delimiters ) ; if ( data . length === 1 && data [ 0 ] . type === 'text' ) { return null ; } fragment = document . createDocumentFragment ( ) ; for ( const datum of data ) { if ( datum . type === 'text' ) { fragment . appendChild ( document . createTextNode ( datum . data ) ) ; } else { const node = createAccessibleMarkupPair ( datum . data , datum . mathstyle ?? '' , options , true ) ; if ( node ) fragment . appendChild ( node ) ; } } } return fragment ; } function scanElement ( element , options ) { const originalContent = element . getAttribute ( 'data-' + options . namespace + 'original-content' ) ; if ( originalContent ) { const mathstyle = element . getAttribute ( 'data-' + options . namespace + 'mathstyle' ) ; const span = createAccessibleMarkupPair ( originalContent , mathstyle , options , false ) ; if ( span !== null ) { element . textContent = '' ; element . append ( span ) ; } return ; } if ( element . childNodes . length === 1 && element . childNodes [ 0 ] . nodeType === 3 ) { const text = element . childNodes [ 0 ] . textContent ; if ( options . TeX ?. processEnvironments && /^\s*\\begin/ . test ( text ) ) { element . textContent = '' ; element . append ( createAccessibleMarkupPair ( text , '' , options , true ) ) ; return ; } const data = splitWithDelimiters ( text , options . TeX ?. delimiters , options . asciiMath ?. delimiters ) ; if ( data . length === 1 && data [ 0 ] . type === 'math' ) { element . textContent = '' ; element . append ( createAccessibleMarkupPair ( data [ 0 ] . data , data [ 0 ] . mathstyle ?? '' , options , true ) ) ; return ; } if ( data . length === 1 && data [ 0 ] . type === 'text' ) { return ; } } for ( let i = element . childNodes . length - 1 ; i >= 0 ; i -- ) { const childNode = element . childNodes [ i ] ; if ( childNode . nodeType === 3 ) { const frag = scanText ( childNode . textContent , options ) ; if ( frag ) { i += frag . childNodes . length - 1 ; childNode . replaceWith ( frag ) ; } } else if ( childNode . nodeType === 1 ) { const tag = childNode . nodeName . toLowerCase ( ) ; if ( tag === 'script' && options . processScriptTypePattern ?. test ( childNode . type ) ) { let style = 'displaystyle' ; for ( const l of childNode . type . split ( ';' ) ) { const v = l . split ( '=' ) ; if ( v [ 0 ] . toLowerCase ( ) === 'mode' ) { style = v [ 1 ] . toLoweCase ( ) === 'display' ? 'displaystyle' : 'textstyle' ; } } const span = createAccessibleMarkupPair ( childNode . textContent , style , options , true ) ; childNode . parentNode . replaceChild ( span , childNode ) ; } else if ( tag !== 'script' ) { const shouldRender = ( options . processClassPattern ?. test ( childNode . className ) ?? false ) || ! ( ( options . skipTags ?. includes ( tag ) ?? false ) || ( options . ignoreClassPattern ?. test ( childNode . className ) ?? false ) ) ; if ( shouldRender ) { if ( element . childNodes . length === 1 && element . childNodes [ 0 ] . nodeType === 3 ) { const formula = element . textContent ; element . textContent = '' ; element . append ( createAccessibleMarkupPair ( formula , 'displaystyle' , options , true ) ) ; } else { scanElement ( childNode , options ) ; } } } } } } const DEFAULT_AUTO_RENDER_OPTIONS = { namespace : '' , skipTags : [ 'math-field' , 'noscript' , 'style' , 'textarea' , 'pre' , 'code' , 'annotation' , 'annotation-xml' , ] , processScriptType : 'math/tex' , ignoreClass : 'tex2jax_ignore' , processClass : 'tex2jax_process' , renderAccessibleContent : 'mathml' , asciiMath : { delimiters : { display : [ [ '`' , '`' ] , ] , } , } , TeX : { processEnvironments : true , delimiters : { inline : [ [ '\\(' , '\\)' ] ] , display : [ [ '$$' , '$$' ] , [ '\\[' , '\\]' ] , ] , } , } , } ; export function autoRenderMathInElement ( element , options ? ) { try { options = { ... DEFAULT_AUTO_RENDER_OPTIONS , ... options } ; options . ignoreClassPattern = new RegExp ( options . ignoreClass ?? '' ) ; options . processClassPattern = new RegExp ( options . processClass ?? '' ) ; options . processScriptTypePattern = new RegExp ( options . processScriptType ?? '' ) ; options . macros = getMacros ( options . macros ) ; if ( options . namespace ) { if ( ! /^[a-z]+-?$/ . test ( options . namespace ) ) { throw new Error ( 'options.namespace must be a string of lowercase characters only' ) ; } if ( ! options . namespace . endsWith ( '-' ) ) { options . namespace += '-' ; } } void loadFonts ( options . fontsDirectory ) ; injectStylesheet ( null , coreStylesheet , hashCode ( coreStylesheet ) . toString ( 36 ) ) ; scanElement ( element , options ) ; } catch ( error ) { if ( error instanceof Error ) { console . error ( 'renderMathInElement(): ' + error . message ) ; } else { console . error ( 'renderMathInElement(): Could not render math for element' , element ) ; } } }