import { InsertOptions , Offset , OutputFormat } from '../public/mathfield' ; import { MathfieldPrivate } from './mathfield-private' ; import { serialize as serializeMathJson } from '@cortex-js/compute-engine/dist/math-json.min.esm.js' ; import { requestUpdate } from './render' ; import { range } from '../editor-model/selection-utils' ; import { ModeEditor } from './mode-editor' ; import { ModelPrivate } from '../editor-model/model-private' ; import { Atom } from '../core/atom-class' ; import { ArrayAtom } from '../core-atoms/array' ; import { parseMathString , trimModeShiftCommand , } from '../editor/parse-math-string' ; import type { Style } from '../public/core' ; import { LeftRightAtom } from '../core-atoms/leftright' ; import { RIGHT_DELIM } from '../core/delimiters' ; import { contentDidChange , selectionDidChange , placeholderDidChange , } from '../editor-model/listeners' ; import { applyStyleToUnstyledAtoms } from '../editor-model/styling' ; import { parseLatex } from '../core/parser' ; import { PlaceholderAtom } from '../core-atoms/placeholder' ; import MathfieldElement from '../public/mathfield-element' ; export class MathModeEditor extends ModeEditor { constructor ( ) { super ( 'math' ) ; } onPaste ( mathfield , ev ) { if ( ! ev . clipboardData ) return false ; let text = '' ; let format = 'auto' ; const json = ev . clipboardData . getData ( 'application/json' ) ; if ( json ) { try { text = serializeMathJson ( JSON . parse ( json ) , { } ) ; format = 'latex' ; } catch { text = '' ; } } if ( ! text ) { text = ev . clipboardData . getData ( 'text/plain' ) ; } if ( text ) { mathfield . snapshot ( ) ; if ( this . insert ( mathfield . model , text , { smartFence : mathfield . options . smartFence , colorMap : mathfield . colorMap , backgroundColorMap : mathfield . backgroundColorMap , format , } ) ) { requestUpdate ( mathfield ) ; } ev . preventDefault ( ) ; ev . stopPropagation ( ) ; } return true ; } insert ( model , text , options ) { if ( ! options . insertionMode ) options . insertionMode = 'replaceSelection' ; if ( ! options . selectionMode ) options . selectionMode = 'placeholder' ; if ( ! options . format ) options . format = 'auto' ; options . macros = options . macros ?? model . options . macros ; if ( ! ( options . smartFence ?? false ) ) { if ( options . insertionMode !== 'replaceAll' ) { const { parent } = model . at ( model . position ) ; if ( parent instanceof LeftRightAtom && parent . rightDelim === '?' && model . at ( model . position ) . isLastSibling && /^[)}\]|]$/ . test ( text ) ) { parent . rightDelim = text ; model . position += 1 ; selectionDidChange ( model ) ; contentDidChange ( model ) ; return true ; } } } else if ( model . selectionIsCollapsed && insertSmartFence ( model , text , options . style ) ) { return true ; } const { suppressChangeNotifications } = model ; if ( options . suppressChangeNotifications ) { model . suppressChangeNotifications = true ; } const contentWasChanging = model . suppressChangeNotifications ; model . suppressChangeNotifications = true ; const args = { } ; args [ 0 ] = model . getValue ( model . selection ) ; args [ '?' ] = '\\placeholder{}' ; args [ '@' ] = args [ '?' ] ; if ( options . insertionMode === 'replaceSelection' && ! model . selectionIsCollapsed ) { model . position = model . deleteAtoms ( range ( model . selection ) ) ; } else if ( options . insertionMode === 'replaceAll' ) { model . root . setChildren ( [ ] , 'body' ) ; model . position = 0 ; } else if ( options . insertionMode === 'insertBefore' ) { model . collapseSelection ( 'backward' ) ; } else if ( options . insertionMode === 'insertAfter' ) { model . collapseSelection ( 'forward' ) ; } if ( ! model . at ( model . position ) . isLastSibling && model . at ( model . position + 1 ) . type === 'placeholder' ) { model . deleteAtoms ( [ model . position , model . position + 1 ] ) ; } else if ( model . at ( model . position ) . type === 'placeholder' ) { model . deleteAtoms ( [ model . position - 1 , model . position ] ) ; model . position -= 1 ; } if ( args [ 0 ] ) { args [ '@' ] = args [ 0 ] ; } else if ( /(^|[^\\])#@/ . test ( text ) ) { const offset = getImplicitArgOffset ( model ) ; if ( offset >= 0 ) { args [ '@' ] = model . getValue ( offset , model . position ) ; model . deleteAtoms ( [ offset , model . position ] ) ; model . position = offset ; } } if ( ! args [ 0 ] ) args [ 0 ] = args [ '?' ] ; let usedArg = false ; const argFunction = ( arg ) : string => { usedArg = true ; return args [ arg ] ; } ; const [ format , newAtoms ] = convertStringToAtoms ( model , text , argFunction , options ) ; if ( ! newAtoms ) return false ; const placeholdersFound = findPlaceholders ( newAtoms ) ; const newPlaceholders = placeholdersFound . filter ( ( atom ) => atom . placeholderId && ! model . mathfield . _placeholders . has ( atom . placeholderId ) ) ; const idsFound = placeholdersFound . map ( ( atom ) => atom . placeholderId ) ; const removedPlaceholder = [ ... model . mathfield . _placeholders . keys ( ) ] . filter ( ( placeholderId ) => ! idsFound . includes ( placeholderId ) ) ; removedPlaceholder . forEach ( ( placeholderId ) => { if ( model . mathfield . _placeholders . has ( placeholderId ) ) { model . mathfield . _placeholders . get ( placeholderId ) ?. field . remove ( ) ; model . mathfield . _placeholders . delete ( placeholderId ) ; } } ) ; newPlaceholders . forEach ( ( placeholder ) => { if ( placeholder . placeholderId && ! model . mathfield . _placeholders . has ( placeholder . placeholderId ) ) { const element = new MathfieldElement ( { virtualKeyboardMode : 'onfocus' , readOnly : false , fontsDirectory : model . mathfield . options . fontsDirectory , } ) ; const container = model . mathfield . element ?. querySelector ( '.ML__placeholdercontainer' ) ; element . value = placeholder . defaultValue ?. length ? Atom . serialize ( placeholder . defaultValue , { defaultMode : 'text' } ) : '' ; element . classList . add ( 'nested-mathfield' ) ; element . style . display = 'inline-block' ; element . style . zIndex = '1001' ; element . style . position = 'absolute' ; element . style . minWidth = '30px' ; const style = document . createElement ( 'style' ) ; style . innerHTML = `.nested-mathfield {
          border: 1px solid black;
        }
          .ML__fieldcontainer{
            min-height:auto !important;
          }

          ` ; element . appendChild ( style ) ; element . addEventListener ( 'input' , ( ) => { placeholderDidChange ( model , placeholder . placeholderId ! ) ; /**
                     * this timeout give some time when is a placeholder to render properly
                     * before rendering the main field.
                     */ setTimeout ( ( ) => { requestUpdate ( model . mathfield ) ; } ) ; } ) ; container ?. appendChild ( element ) ; model . mathfield . _placeholders . set ( placeholder . placeholderId , { atom : placeholder , field : element , } ) ; } } ) ; const { parent } = model . at ( model . position ) ; if ( format !== 'latex' && model . options . removeExtraneousParentheses && parent instanceof LeftRightAtom && parent . leftDelim === '(' && parent . hasEmptyBranch ( 'body' ) && newAtoms . length === 1 && newAtoms [ 0 ] . type === 'genfrac' ) { const newParent = parent . parent ! ; const branch = parent . treeBranch ! ; newParent . removeChild ( parent ) ; newParent . setChildren ( newAtoms , branch ) ; } const hadEmptyBody = parent ! . hasEmptyBranch ( 'body' ) ; const cursor = model . at ( model . position ) ; cursor . parent ! . addChildrenAfter ( newAtoms , cursor ) ; if ( format === 'latex' ) { if ( parent ! . type === 'root' && hadEmptyBody && ! usedArg ) { parent ! . verbatimLatex = text ; } } model . suppressChangeNotifications = contentWasChanging ; const lastNewAtom = newAtoms [ newAtoms . length - 1 ] ; if ( options . selectionMode === 'placeholder' ) { const newPlaceholders = newAtoms . reduce ( ( acc , atom ) => [ ... acc , ... atom . children . filter ( ( x ) => x . type === 'placeholder' ) , ] , [ ] ) ; if ( newPlaceholders . length > 0 ) { const placeholderOffset = model . offsetOf ( newPlaceholders [ 0 ] ) ; model . setSelection ( placeholderOffset - 1 , placeholderOffset ) ; model . announce ( 'move' ) ; } else if ( lastNewAtom ) { model . position = model . offsetOf ( lastNewAtom ) ; } } else if ( options . selectionMode === 'before' ) { } else if ( options . selectionMode === 'after' ) { if ( lastNewAtom ) { model . position = model . offsetOf ( lastNewAtom ) ; } } else if ( options . selectionMode === 'item' ) { model . setSelection ( model . anchor , model . offsetOf ( lastNewAtom ) ) ; } contentDidChange ( model ) ; model . suppressChangeNotifications = suppressChangeNotifications ; return true ; } } function convertStringToAtoms ( model , s , args , options ) { let format = undefined ; let result = [ ] ; if ( options . format === 'ascii-math' ) { [ format , s ] = parseMathString ( s , { format : 'ascii-math' , inlineShortcuts : model . mathfield . options . inlineShortcuts , } ) ; result = parseLatex ( s , { parseMode : 'math' , macros : options ?. macros , onError : model . listeners . onError , colorMap : options . colorMap , backgroundColorMap : options . backgroundColorMap , } ) ; if ( format !== 'latex' && model . options . removeExtraneousParentheses ) { simplifyParen ( result ) ; } } else if ( options . format === 'auto' || options . format === 'latex' ) { if ( options . format === 'auto' ) { [ format , s ] = parseMathString ( s , { format : 'auto' , inlineShortcuts : model . mathfield . options . inlineShortcuts , } ) ; } if ( options . format === 'latex' ) [ , s ] = trimModeShiftCommand ( s ) ; result = parseLatex ( s , { parseMode : 'math' , args : args , macros : options . macros , smartFence : options . smartFence , onError : model . listeners . onError , colorMap : options . colorMap , backgroundColorMap : options . backgroundColorMap , } ) ; if ( options . format !== 'latex' && model . options . removeExtraneousParentheses ) { simplifyParen ( result ) ; } } applyStyleToUnstyledAtoms ( result , options . style ) ; return [ format ?? 'latex' , result ] ; } function removeParen ( atoms ) { if ( ! atoms ) return null ; console . assert ( atoms [ 0 ] . type === 'first' ) ; if ( atoms . length > 1 ) return null ; const atom = atoms [ 0 ] ; if ( atom instanceof LeftRightAtom && atom . leftDelim === '(' && atom . rightDelim === ')' ) { return atom . removeBranch ( 'body' ) ; } return null ; } revisit function simplifyParen ( atoms ) { if ( ! atoms ) return ; for ( let i = 0 ; atoms [ i ] ; i ++ ) { const atom = atoms [ i ] ; if ( atom instanceof LeftRightAtom && atom . leftDelim === '(' ) { let genFracCount = 0 ; let genFracIndex = 0 ; let nonGenFracCount = 0 ; for ( let j = 0 ; atom . body ! [ j ] ; j ++ ) { if ( atom . body ! [ j ] . type === 'genfrac' ) { genFracCount ++ ; genFracIndex = j ; } nonGenFracCount ++ ; } if ( nonGenFracCount === 0 && genFracCount === 1 ) { atoms [ i ] = atom . body ! [ genFracIndex ] ; } } } for ( const atom of atoms ) { for ( const branch of atom . branches ) { if ( ! atom . hasEmptyBranch ( branch ) ) { simplifyParen ( atom . branch ( branch ) ! ) ; const newChildren = removeParen ( atom . branch ( branch ) ! ) ; if ( newChildren ) atom . setChildren ( newChildren , branch ) ; } } if ( atom instanceof ArrayAtom ) { for ( const x of atom . cells ) simplifyParen ( x ) ; } } } function findPlaceholders ( atoms ) { if ( ! atoms ) return [ ] ; let result = [ ] ; for ( const atom of atoms ) { for ( const branch of atom . branches ) { if ( ! atom . hasEmptyBranch ( branch ) ) { const branchPlaceholder = findPlaceholders ( atom . branch ( branch ) ! ) ; result = result . concat ( branchPlaceholder ) ; } } if ( atom instanceof PlaceholderAtom ) { result . push ( atom ) ; } } return result ; } /**
 * Locate the offset before the insertion point that would indicate
 * a good place to select as an implicit argument.
 *
 * For example with '1+\sin(x)', if the insertion point is at the
 * end, the implicit arg offset would be after the plus. As a result,
 * inserting a fraction after the sin would yield: '1+\frac{\sin(c)}{\placeholder{}}'
 */ function getImplicitArgOffset ( model ) { let atom = model . at ( model . position ) ; if ( atom . mode === 'text' ) { while ( ! atom . isFirstSibling && atom . mode === 'text' ) { atom = atom . leftSibling ; } return model . offsetOf ( atom ) ; } if ( ! isImplicitArg ( atom ) ) { return - 1 ; } while ( ! atom . isFirstSibling && isImplicitArg ( atom ) ) { atom = atom . leftSibling ; } return model . offsetOf ( atom ) ; } /**
 *
 * Predicate returns true if the atom should be considered an implicit argument.
 *
 * Used for example when typing "/" to insert a fraction: all the atoms to
 * the left of insertion point that return true for `isImplicitArg()` will
 * be included as the numerator
 */ function isImplicitArg ( atom ) { if ( /^(mord|surd|msubsup|leftright|mop)$/ . test ( atom . type ) ) { if ( atom . isExtensibleSymbol ) return false ; return true ; } return false ; } /**
 * Insert a smart fence '(', '{', '[', etc...
 * If not handled (because `fence` wasn't a fence), return false.
 */ export function insertSmartFence ( model , fence , style ? ) { console . assert ( model . selectionIsCollapsed ) ; const atom = model . at ( model . position ) ; const { parent } = atom ; let delims = parent instanceof LeftRightAtom ? parent . leftDelim ! + parent . rightDelim ! : '' ; if ( delims === '\\lbrace\\rbrace' ) delims = '{}' ; if ( delims === '\\{\\}' ) delims = '{}' ; if ( delims === '\\lparen\\rparen' ) delims = '()' ; if ( delims === '{}' && /\||\\vert|\\Vert|\\mvert|\\mid/ . test ( fence ) ) { ModeEditor . insert ( 'math' , model , '\\,\\middle' + fence + '\\, ' , { format : 'latex' , style , } ) ; return true ; } if ( fence === '{' || fence === '\\{' ) fence = '\\lbrace' ; if ( fence === '}' || fence === '\\}' ) fence = '\\rbrace' ; if ( fence === '[' ) fence = '\\lbrack' ; if ( fence === ']' ) fence = '\\rbrack' ; const rDelim = RIGHT_DELIM [ fence ] ; if ( rDelim && ! ( parent instanceof LeftRightAtom && parent . leftDelim === '|' ) ) { let s = '' ; s = atom . isFunction ? `\\mleft${ fence }\\mright${ rDelim }` : `\\left${ fence }\\right?` ; const lastSiblingOffset = model . offsetOf ( atom . lastSibling ) ; const content = model . extractAtoms ( [ model . position , lastSiblingOffset ] ) ; ModeEditor . insert ( 'math' , model , s , { format : 'latex' , style , } ) ; model . at ( model . position ) . body = content ; model . position -= 1 ; return true ; } let lDelim = '' ; Object . keys ( RIGHT_DELIM ) . forEach ( ( delim ) => { if ( fence === RIGHT_DELIM [ delim ] ) lDelim = delim ; } ) ; if ( lDelim ) { if ( parent instanceof LeftRightAtom && atom . isLastSibling ) { parent . rightDelim = fence ; model . position += 1 ; contentDidChange ( model ) ; return true ; } const firstSibling = model . offsetOf ( atom . firstSibling ) ; let i ; for ( i = model . position ; i >= firstSibling ; i -- ) { const atom = model . at ( i ) ; if ( atom instanceof LeftRightAtom && atom . rightDelim === '?' ) { break ; } } const match = model . at ( i ) ; if ( i >= firstSibling && match instanceof LeftRightAtom ) { match . rightDelim = fence ; match . addChildren ( model . extractAtoms ( [ i , model . position ] ) , atom . treeBranch ! ) ; model . position = i ; contentDidChange ( model ) ; return true ; } if ( parent instanceof LeftRightAtom && parent . rightDelim === '?' ) { parent . rightDelim = fence ; parent . parent ! . addChildren ( model . extractAtoms ( [ model . position , model . offsetOf ( atom . lastSibling ) ] ) , parent . treeBranch ! ) ; model . position = model . offsetOf ( parent ) ; contentDidChange ( model ) ; return true ; } const grandparent = parent ! . parent ; if ( grandparent instanceof LeftRightAtom && grandparent . rightDelim === '?' && model . at ( model . position ) . isLastSibling ) { model . position = model . offsetOf ( grandparent ) ; return insertSmartFence ( model , fence , style ) ; } return false ; } return false ; } new MathModeEditor ( ) ;