diff --git a/.gitignore b/.gitignore index d10c0ec0d71..f986c67c547 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,4 @@ upload # docs site/ -apps/*/coverage \ No newline at end of file +apps/*/coverage diff --git a/packages/ckeditor5-math/package.json b/packages/ckeditor5-math/package.json index 26c051daadc..7a8df5db271 100644 --- a/packages/ckeditor5-math/package.json +++ b/packages/ckeditor5-math/package.json @@ -70,6 +70,7 @@ ] }, "dependencies": { - "@ckeditor/ckeditor5-icons": "47.3.0" + "@ckeditor/ckeditor5-icons": "47.3.0", + "mathlive": "0.108.2" } } diff --git a/packages/ckeditor5-math/src/index.ts b/packages/ckeditor5-math/src/index.ts index 6d6982ae239..eac4d7b223a 100644 --- a/packages/ckeditor5-math/src/index.ts +++ b/packages/ckeditor5-math/src/index.ts @@ -1,6 +1,9 @@ import ckeditor from './../theme/icons/math.svg?raw'; import './augmentation.js'; import "../theme/mathform.css"; +import 'mathlive'; +import 'mathlive/fonts.css'; +import 'mathlive/static.css'; export { default as Math } from './math.js'; export { default as MathUI } from './mathui.js'; diff --git a/packages/ckeditor5-math/src/mathui.ts b/packages/ckeditor5-math/src/mathui.ts index 4c4a2794c59..3e3da2744dd 100644 --- a/packages/ckeditor5-math/src/mathui.ts +++ b/packages/ckeditor5-math/src/mathui.ts @@ -55,9 +55,9 @@ export default class MathUI extends Plugin { this._balloon.showStack( 'main' ); - requestAnimationFrame(() => { - this.formView?.mathInputView.fieldView.element?.focus(); - }); + requestAnimationFrame( () => { + this.formView?.mathInputView.focus(); + } ); } private _createFormView() { @@ -71,31 +71,37 @@ export default class MathUI extends Plugin { throw new CKEditorError( 'math-command' ); } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const mathConfig = editor.config.get( 'math' )!; const formView = new MainFormView( editor.locale, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - mathConfig.engine!, - mathConfig.lazyLoad, + { + engine: mathConfig.engine!, + lazyLoad: mathConfig.lazyLoad, + previewUid: this._previewUid, + previewClassName: mathConfig.previewClassName!, + katexRenderOptions: mathConfig.katexRenderOptions! + }, mathConfig.enablePreview, - this._previewUid, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - mathConfig.previewClassName!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - mathConfig.popupClassName!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - mathConfig.katexRenderOptions! + mathConfig.popupClassName! ); formView.mathInputView.bind( 'value' ).to( mathCommand, 'value' ); formView.displayButtonView.bind( 'isOn' ).to( mathCommand, 'display' ); // Form elements should be read-only when corresponding commands are disabled. - formView.mathInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', value => !value ); - formView.saveButtonView.bind( 'isEnabled' ).to( mathCommand ); - formView.displayButtonView.bind( 'isEnabled' ).to( mathCommand ); + formView.mathInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', ( value: boolean ) => !value ); + formView.saveButtonView.bind( 'isEnabled' ).to( + mathCommand, + 'isEnabled', + formView.mathInputView, + 'value', + ( commandEnabled, equation ) => { + const normalizedEquation = ( equation ?? '' ).trim(); + return commandEnabled && normalizedEquation.length > 0; + } + ); + formView.displayButtonView.bind( 'isEnabled' ).to( mathCommand, 'isEnabled' ); // Listen to submit button click this.listenTo( formView, 'submit', () => { @@ -115,24 +121,12 @@ export default class MathUI extends Plugin { } ); // Allow pressing Enter to submit changes, and use Shift+Enter to insert a new line - formView.keystrokes.set('enter', (data, cancel) => { - if (!data.shiftKey) { - formView.fire('submit'); + formView.keystrokes.set( 'enter', ( data, cancel ) => { + if ( !data.shiftKey ) { + formView.fire( 'submit' ); cancel(); } - }); - - // Allow the textarea to be resizable - formView.mathInputView.fieldView.once('render', () => { - const textarea = formView.mathInputView.fieldView.element; - if (!textarea) return; - Object.assign(textarea.style, { - resize: 'both', - height: '100px', - width: '400px', - minWidth: '100%', - }); - }); + } ); return formView; } @@ -162,14 +156,12 @@ export default class MathUI extends Plugin { } ); if ( this._balloon.visibleView === this.formView ) { - this.formView.mathInputView.fieldView.element?.select(); + this.formView.mathInputView.focus(); } - // Show preview element const previewEl = document.getElementById( this._previewUid ); - if ( previewEl && this.formView.previewEnabled ) { - // Force refresh preview - this.formView.mathView?.updateMath(); + if ( previewEl && this.formView.mathView ) { + this.formView.mathView.updateMath(); } this.formView.equation = mathCommand.value ?? ''; @@ -206,8 +198,10 @@ export default class MathUI extends Plugin { private _removeFormView() { if ( this._isFormInPanel && this.formView ) { - this.formView.saveButtonView.focus(); + // Hide virtual keyboard before removing the form + this.formView.hideKeyboard(); + this.formView.saveButtonView.focus(); this._balloon.remove( this.formView ); // Hide preview element diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index 2d1c5979383..52d3b051204 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -1,91 +1,59 @@ -import { ButtonView, createLabeledTextarea, FocusCycler, LabelView, LabeledFieldView, submitHandler, SwitchButtonView, View, ViewCollection, type TextareaView, type FocusableView, Locale, FocusTracker, KeystrokeHandler } from 'ckeditor5'; +import { ButtonView, FocusCycler, FocusTracker, KeystrokeHandler, LabelView, submitHandler, SwitchButtonView, View, ViewCollection, type FocusableView, type Locale } from 'ckeditor5'; import IconCheck from "@ckeditor/ckeditor5-icons/theme/icons/check.svg?raw"; import IconCancel from "@ckeditor/ckeditor5-icons/theme/icons/cancel.svg?raw"; import { extractDelimiters, hasDelimiters } from '../utils.js'; -import MathView from './mathview.js'; +import MathView, { type MathViewOptions } from './mathview.js'; +import MathInputView from './mathinputview.js'; import '../../theme/mathform.css'; -import type { KatexOptions } from '../typings-external.js'; - -class MathInputView extends LabeledFieldView { - public value: null | string = null; - public isReadOnly = false; - - constructor( locale: Locale ) { - super( locale, createLabeledTextarea ); - } -} export default class MainFormView extends View { public saveButtonView: ButtonView; - public mathInputView: MathInputView; - public displayButtonView: SwitchButtonView; public cancelButtonView: ButtonView; - public previewEnabled: boolean; - public previewLabel?: LabelView; + public displayButtonView: SwitchButtonView; + + public mathInputView: MathInputView; public mathView?: MathView; - public override locale: Locale = new Locale(); - public lazyLoad: undefined | ( () => Promise ); + + public focusTracker = new FocusTracker(); + public keystrokes = new KeystrokeHandler(); + private _focusables = new ViewCollection(); + private _focusCycler: FocusCycler; constructor( locale: Locale, - engine: - | 'mathjax' - | 'katex' - | ( ( - equation: string, - element: HTMLElement, - display: boolean, - ) => void ), - lazyLoad: undefined | ( () => Promise ), + mathViewOptions: MathViewOptions, previewEnabled = false, - previewUid: string, - previewClassName: Array, - popupClassName: Array, - katexRenderOptions: KatexOptions + popupClassName: Array = [] ) { super( locale ); - const t = locale.t; - // Submit button - this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', null ); - this.saveButtonView.type = 'submit'; + // Create views + this.mathInputView = new MathInputView( locale ); + this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', 'submit' ); + this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel' ); + this.cancelButtonView.delegate( 'execute' ).to( this, 'cancel' ); + this.displayButtonView = this._createDisplayButton( t ); - // Equation input - this.mathInputView = this._createMathInput(); + // Build children - // Display button - this.displayButtonView = this._createDisplayButton(); - - // Cancel button - this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel', 'cancel' ); - - this.previewEnabled = previewEnabled; + const children: Array = [ + this.mathInputView, + this.displayButtonView + ]; - let children = []; - if ( this.previewEnabled ) { - // Preview label - this.previewLabel = new LabelView( locale ); - this.previewLabel.text = t( 'Equation preview' ); + if ( previewEnabled ) { + const previewLabel = new LabelView( locale ); + previewLabel.text = t( 'Equation preview' ); - // Math element - this.mathView = new MathView( engine, lazyLoad, locale, previewUid, previewClassName, katexRenderOptions ); + this.mathView = new MathView( locale, mathViewOptions ); this.mathView.bind( 'display' ).to( this.displayButtonView, 'isOn' ); - children = [ - this.mathInputView, - this.displayButtonView, - this.previewLabel, - this.mathView - ]; - } else { - children = [ - this.mathInputView, - this.displayButtonView - ]; + children.push( previewLabel, this.mathView ); } - // Add UI elements to template + this._setupSync( previewEnabled ); + this.setTemplate( { tag: 'form', attributes: { @@ -107,10 +75,30 @@ export default class MainFormView extends View { }, children }, - this.saveButtonView, - this.cancelButtonView + { + tag: 'div', + attributes: { + class: [ + 'ck-math-button-row' + ] + }, + children: [ + this.saveButtonView, + this.cancelButtonView + ] + } ] } ); + + this._focusCycler = new FocusCycler( { + focusables: this._focusables, + focusTracker: this.focusTracker, + keystrokeHandler: this.keystrokes, + actions: { + focusPrevious: 'shift + tab', + focusNext: 'tab' + } + } ); } public override render(): void { @@ -121,103 +109,73 @@ export default class MainFormView extends View { view: this } ); - // Register form elements to focusable elements - const childViews = [ - this.mathInputView, + const focusableViews = [ + this.mathInputView.latexTextAreaView, this.displayButtonView, this.saveButtonView, this.cancelButtonView ]; - childViews.forEach( v => { + focusableViews.forEach( v => { + this._focusables.add( v ); if ( v.element ) { - this._focusables.add( v ); this.focusTracker.add( v.element ); } } ); - // Listen to keypresses inside form element + this.mathInputView.on( 'mathfieldReady', () => { + const mathfieldView = this.mathInputView.mathFieldFocusableView; + if ( mathfieldView.element ) { + if ( this._focusables.has( mathfieldView ) ) { + this._focusables.remove( mathfieldView ); + } + this._focusables.add( mathfieldView, 0 ); + this.focusTracker.add( mathfieldView.element ); + } + } ); + if ( this.element ) { this.keystrokes.listenTo( this.element ); } } - public focus(): void { - this._focusCycler.focusFirst(); - } - public get equation(): string { - return this.mathInputView.fieldView.element?.value ?? ''; + return this.mathInputView.value ?? ''; } public set equation( equation: string ) { - if ( this.mathInputView.fieldView.element ) { - this.mathInputView.fieldView.element.value = equation; - } - if ( this.previewEnabled && this.mathView ) { - this.mathView.value = equation; + const norm = equation.trim(); + this.mathInputView.value = norm.length ? norm : null; + if ( this.mathView ) { + this.mathView.value = norm; } } - public focusTracker: FocusTracker = new FocusTracker(); - public keystrokes: KeystrokeHandler = new KeystrokeHandler(); - private _focusables = new ViewCollection(); - private _focusCycler: FocusCycler = new FocusCycler( { - focusables: this._focusables, - focusTracker: this.focusTracker, - keystrokeHandler: this.keystrokes, - actions: { - focusPrevious: 'shift + tab', - focusNext: 'tab' - } - } ); - - private _createMathInput() { - const t = this.locale.t; - - // Create equation input - const mathInput = new MathInputView( this.locale ); - const fieldView = mathInput.fieldView; - mathInput.infoText = t( 'Insert equation in TeX format.' ); - - const onInput = () => { - if ( fieldView.element != null ) { - let equationInput = fieldView.element.value.trim(); - - // If input has delimiters - if ( hasDelimiters( equationInput ) ) { - // Get equation without delimiters - const params = extractDelimiters( equationInput ); + public focus(): void { + this._focusCycler.focusFirst(); + } - // Remove delimiters from input field - fieldView.element.value = params.equation; + private _setupSync( previewEnabled: boolean ): void { + this.mathInputView.on( 'change:value', () => { + let eq = ( this.mathInputView.value ?? '' ).trim(); - equationInput = params.equation; + if ( hasDelimiters( eq ) ) { + const params = extractDelimiters( eq ); + eq = params.equation; + this.displayButtonView.isOn = params.display; - // update display button and preview - this.displayButtonView.isOn = params.display; - } - if ( this.previewEnabled && this.mathView ) { - // Update preview view - this.mathView.value = equationInput; + if ( this.mathInputView.value !== eq ) { + this.mathInputView.value = eq.length ? eq : null; } - - this.saveButtonView.isEnabled = !!equationInput; } - }; - - fieldView.on( 'render', onInput ); - fieldView.on( 'input', onInput ); - return mathInput; + if ( previewEnabled && this.mathView && this.mathView.value !== eq ) { + this.mathView.value = eq; + } + } ); } - private _createButton( - label: string, - icon: string, - className: string, - eventName: string | null - ) { + private _createButton( label: string, icon: string, className: string, type?: 'submit' | 'button' ): ButtonView { const button = new ButtonView( this.locale ); button.set( { @@ -232,16 +190,14 @@ export default class MainFormView extends View { } } ); - if ( eventName ) { - button.delegate( 'execute' ).to( this, eventName ); + if ( type ) { + button.type = type; } return button; } - private _createDisplayButton() { - const t = this.locale.t; - + private _createDisplayButton( t: ( str: string ) => string ): SwitchButtonView { const switchButton = new SwitchButtonView( this.locale ); switchButton.set( { @@ -256,15 +212,13 @@ export default class MainFormView extends View { } ); switchButton.on( 'execute', () => { - // Toggle state switchButton.isOn = !switchButton.isOn; - - if ( this.previewEnabled && this.mathView ) { - // Update preview view - this.mathView.display = switchButton.isOn; - } } ); return switchButton; } + + public hideKeyboard(): void { + this.mathInputView.hideKeyboard(); + } } diff --git a/packages/ckeditor5-math/src/ui/mathinputview.ts b/packages/ckeditor5-math/src/ui/mathinputview.ts new file mode 100644 index 00000000000..a3b93f3e17f --- /dev/null +++ b/packages/ckeditor5-math/src/ui/mathinputview.ts @@ -0,0 +1,264 @@ +// Math input widget: wraps a MathLive and a LaTeX textarea +// and keeps them in sync for the CKEditor 5 math dialog. +import { View, type Locale, type FocusableView } from 'ckeditor5'; +import 'mathlive/fonts.css'; // Auto-bundles offline fonts + +declare global { + interface Window { + mathVirtualKeyboard?: { + visible: boolean; + show: () => void; + hide: () => void; + addEventListener: ( event: string, cb: () => void ) => void; + removeEventListener: ( event: string, cb: () => void ) => void; + }; + } +} + +interface MathFieldElement extends HTMLElement { + value: string; + readOnly: boolean; + mathVirtualKeyboardPolicy: string; + inlineShortcuts?: Record; + setValue?: ( value: string, options?: { silenceNotifications?: boolean } ) => void; +} + +// Wrapper for the MathLive element to make it focusable in CKEditor's UI system +export class MathFieldFocusableView extends View implements FocusableView { + public declare element: HTMLElement | null; + private _view: MathInputView; + constructor( locale: Locale, view: MathInputView ) { + super( locale ); + this._view = view; + } + public focus(): void { + this._view.mathfield?.focus(); + } + public setElement( el: HTMLElement ): void { + this.element = el; + } +} + +// Wrapper for the LaTeX textarea to make it focusable in CKEditor's UI system +export class LatexTextAreaView extends View implements FocusableView { + declare public element: HTMLTextAreaElement; + constructor( locale: Locale ) { + super( locale ); + this.setTemplate( { tag: 'textarea', attributes: { + class: [ 'ck', 'ck-textarea', 'ck-latex-textarea' ], spellcheck: 'false', tabindex: 0 + } } ); + } + public focus(): void { + this.element?.focus(); + } +} + +// Main view class for the math input +export default class MathInputView extends View { + public declare value: string | null; + public declare isReadOnly: boolean; + public mathfield: MathFieldElement | null = null; + public readonly latexTextAreaView: LatexTextAreaView; + public readonly mathFieldFocusableView: MathFieldFocusableView; + private _destroyed = false; + private _vkGeometryHandler?: () => void; + private _updating = false; + private static _configured = false; + + constructor( locale: Locale ) { + super( locale ); + this.latexTextAreaView = new LatexTextAreaView( locale ); + this.mathFieldFocusableView = new MathFieldFocusableView( locale, this ); + this.set( 'value', null ); + this.set( 'isReadOnly', false ); + this.setTemplate( { + tag: 'div', attributes: { class: [ 'ck', 'ck-math-input' ] }, + children: [ + { tag: 'div', attributes: { class: [ 'ck-mathlive-container' ] } }, + { tag: 'label', attributes: { class: [ 'ck-latex-label' ] }, children: [ locale.t( 'LaTeX' ) ] }, + { tag: 'div', attributes: { class: [ 'ck-latex-wrapper' ] }, children: [ this.latexTextAreaView ] } + ] + } ); + } + + public override render(): void { + super.render(); + const textarea = this.latexTextAreaView.element; + + // Sync changes from the LaTeX textarea to the mathfield and model + this.listenTo( textarea, 'input', () => { + if ( this._updating ) { + return; + } + this._updating = true; + const val = textarea.value; + this.value = val || null; + if ( this.mathfield ) { + if ( val === '' ) { + this.mathfield.remove(); + this.mathfield = null; + this._initMathField( false ); + } else if ( this.mathfield.value.trim() !== val.trim() ) { + this._setMathfieldValue( val ); + } + } + this._updating = false; + } ); + + // Sync changes from the model (this.value) to the UI elements + this.on( 'change:value', ( _e, _n, val ) => { + if ( this._updating ) { + return; + } + this._updating = true; + const newVal = val ?? ''; + if ( textarea.value !== newVal ) { + textarea.value = newVal; + } + if ( this.mathfield ) { + if ( this.mathfield.value.trim() !== newVal.trim() ) { + this._setMathfieldValue( newVal ); + } + } else if ( newVal !== '' ) { + this._initMathField( false ); + } + this._updating = false; + } ); + + // Handle read-only state changes + this.on( 'change:isReadOnly', ( _e, _n, val ) => { + textarea.readOnly = val; + if ( this.mathfield ) { + this.mathfield.readOnly = val; + } + } ); + + // Handle virtual keyboard geometry changes + const vk = window.mathVirtualKeyboard; + if ( vk && !this._vkGeometryHandler ) { + this._vkGeometryHandler = () => { + if ( vk.visible && this.mathfield ) { + this.mathfield.focus(); + } + }; + vk.addEventListener( 'geometrychange', this._vkGeometryHandler ); + } + + const initial = this.value ?? ''; + if ( textarea.value !== initial ) { + textarea.value = initial; + } + this._loadMathLive(); + } + + // Loads the MathLive library dynamically + private async _loadMathLive(): Promise { + try { + await import( 'mathlive' ); + await customElements.whenDefined( 'math-field' ); + if ( this._destroyed ) { + return; + } + if ( !MathInputView._configured ) { + const MathfieldClass = customElements.get( 'math-field' ) as any; + if ( MathfieldClass ) { + MathfieldClass.soundsDirectory = null; + MathfieldClass.plonkSound = null; + MathInputView._configured = true; + } + } + if ( this.element && !this._destroyed ) { + this._initMathField( true ); + } + } catch { + const c = this.element?.querySelector( '.ck-mathlive-container' ); + if ( c ) { + c.textContent = 'Math editor unavailable'; + } + } + } + + // Initializes the element + private _initMathField( shouldFocus: boolean ): void { + const container = this.element?.querySelector( '.ck-mathlive-container' ); + if ( !container ) { + return; + } + if ( this.mathfield ) { + this._setMathfieldValue( this.value ?? '' ); + return; + } + const mf = document.createElement( 'math-field' ) as MathFieldElement; + mf.mathVirtualKeyboardPolicy = 'auto'; + mf.setAttribute( 'tabindex', '0' ); + mf.value = this.value ?? ''; + mf.readOnly = this.isReadOnly; + container.appendChild( mf ); + // Set shortcuts after mounting (accessing inlineShortcuts requires mounted element) + try { + if ( mf.inlineShortcuts ) { + mf.inlineShortcuts = { ...mf.inlineShortcuts, dx: 'dx', dy: 'dy', dt: 'dt' }; + } + } catch { + // Inline shortcut configuration is optional; ignore failures to avoid breaking the math field. + } + mf.addEventListener( 'keydown', ev => { + if ( ev.key === 'Tab' && !ev.shiftKey ) { + ev.preventDefault(); + ev.stopImmediatePropagation(); + this.latexTextAreaView.focus(); + } + }, { capture: true } ); + mf.addEventListener( 'input', () => { + if ( this._updating ) { + return; + } + this._updating = true; + const textarea = this.latexTextAreaView.element; + if ( textarea.value.trim() !== mf.value.trim() ) { + textarea.value = mf.value; + } + this.value = mf.value || null; + this._updating = false; + } ); + this.mathfield = mf; + this.mathFieldFocusableView.setElement( mf ); + this.fire( 'mathfieldReady' ); + if ( shouldFocus ) { + requestAnimationFrame( () => mf.focus() ); + } + } + + // Updates the mathfield value without triggering loops + private _setMathfieldValue( value: string ): void { + if ( !this.mathfield ) { + return; + } + if ( this.mathfield.setValue ) { + this.mathfield.setValue( value, { silenceNotifications: true } ); + } else { + this.mathfield.value = value; + } + } + + public hideKeyboard(): void { + window.mathVirtualKeyboard?.hide(); + } + + public focus(): void { + this.mathfield?.focus(); + } + + public override destroy(): void { + this._destroyed = true; + const vk = window.mathVirtualKeyboard; + if ( vk && this._vkGeometryHandler ) { + vk.removeEventListener( 'geometrychange', this._vkGeometryHandler ); + this._vkGeometryHandler = undefined; + } + this.hideKeyboard(); + this.mathfield?.remove(); + this.mathfield = null; + super.destroy(); + } +} diff --git a/packages/ckeditor5-math/src/ui/mathview.ts b/packages/ckeditor5-math/src/ui/mathview.ts index fab16262e93..aa1027329ee 100644 --- a/packages/ckeditor5-math/src/ui/mathview.ts +++ b/packages/ckeditor5-math/src/ui/mathview.ts @@ -2,44 +2,44 @@ import { View, type Locale } from 'ckeditor5'; import type { KatexOptions } from '../typings-external.js'; import { renderEquation } from '../utils.js'; +/** + * Configuration options for the MathView. + */ +export interface MathViewOptions { + engine: 'mathjax' | 'katex' | ( ( equation: string, element: HTMLElement, display: boolean ) => void ); + lazyLoad: undefined | ( () => Promise ); + previewUid: string; + previewClassName: Array; + katexRenderOptions: KatexOptions; +} + export default class MathView extends View { + /** + * The LaTeX equation value to render. + * @observable + */ public declare value: string; + + /** + * Whether to render in display mode (centered) or inline. + * @observable + */ public declare display: boolean; - public previewUid: string; - public previewClassName: Array; - public katexRenderOptions: KatexOptions; - public engine: - | 'mathjax' - | 'katex' - | ( ( equation: string, element: HTMLElement, display: boolean ) => void ); - public lazyLoad: undefined | ( () => Promise ); - constructor( - engine: - | 'mathjax' - | 'katex' - | ( ( - equation: string, - element: HTMLElement, - display: boolean, - ) => void ), - lazyLoad: undefined | ( () => Promise ), - locale: Locale, - previewUid: string, - previewClassName: Array, - katexRenderOptions: KatexOptions - ) { - super( locale ); + /** + * Configuration options passed during initialization. + */ + private options: MathViewOptions; - this.engine = engine; - this.lazyLoad = lazyLoad; - this.previewUid = previewUid; - this.katexRenderOptions = katexRenderOptions; - this.previewClassName = previewClassName; + constructor( locale: Locale, options: MathViewOptions ) { + super( locale ); + this.options = options; this.set( 'value', '' ); this.set( 'display', false ); + // Update rendering when state changes. + // Checking isRendered prevents errors during initialization. this.on( 'change', () => { if ( this.isRendered ) { this.updateMath(); @@ -55,19 +55,39 @@ export default class MathView extends View { } public updateMath(): void { - if ( this.element ) { - void renderEquation( - this.value, - this.element, - this.engine, - this.lazyLoad, - this.display, - true, - this.previewUid, - this.previewClassName, - this.katexRenderOptions - ); + if ( !this.element ) { + return; + } + + // Handle empty equations + if ( !this.value || !this.value.trim() ) { + this.element.textContent = ''; + this.element.classList.remove( 'ck-math-render-error' ); + return; } + + // Clear previous render + this.element.textContent = ''; + this.element.classList.remove( 'ck-math-render-error' ); + + renderEquation( + this.value, + this.element, + this.options.engine, + this.options.lazyLoad, + this.display, + true, // isPreview + this.options.previewUid, + this.options.previewClassName, + this.options.katexRenderOptions + ).catch( error => { + console.error( 'Math rendering failed:', error ); + + if ( this.element ) { + this.element.textContent = 'Error rendering equation'; + this.element.classList.add( 'ck-math-render-error' ); + } + } ); } public override render(): void { diff --git a/packages/ckeditor5-math/tests/index.ts b/packages/ckeditor5-math/tests/index.ts index 4493420e1d5..b379ad8c680 100644 --- a/packages/ckeditor5-math/tests/index.ts +++ b/packages/ckeditor5-math/tests/index.ts @@ -3,6 +3,20 @@ import Math from '../src/math'; import AutoformatMath from '../src/autoformatmath'; import { describe, it, expect } from 'vitest'; +// Suppress MathLive errors during async cleanup in tests +if (typeof window !== 'undefined') { + window.addEventListener('unhandledrejection', event => { + if (event.reason?.message?.includes('options') || event.reason?.message?.includes('mathlive')) { + event.preventDefault(); + } + }); + window.addEventListener('error', event => { + if (event.message?.includes('options') || event.message?.includes('mathlive')) { + event.preventDefault(); + } + }); +} + describe( 'CKEditor5 Math DLL', () => { it( 'exports Math', () => { expect( MathDll ).to.equal( Math ); diff --git a/packages/ckeditor5-math/tests/lazyload.ts b/packages/ckeditor5-math/tests/lazyload.ts index 1265078502d..173fae184de 100644 --- a/packages/ckeditor5-math/tests/lazyload.ts +++ b/packages/ckeditor5-math/tests/lazyload.ts @@ -2,6 +2,20 @@ import { ClassicEditor, type EditorConfig } from 'ckeditor5'; import MathUI from '../src/mathui'; import { describe, beforeEach, it, afterEach, expect } from "vitest"; +// Suppress MathLive errors during async cleanup +if (typeof window !== 'undefined') { + window.addEventListener('unhandledrejection', event => { + if (event.reason?.message?.includes('options') || event.reason?.message?.includes('mathlive')) { + event.preventDefault(); + } + }); + window.addEventListener('error', event => { + if (event.message?.includes('options') || event.message?.includes('mathlive')) { + event.preventDefault(); + } + }); +} + describe( 'Lazy load', () => { let editorElement: HTMLDivElement; let editor: ClassicEditor; @@ -24,11 +38,14 @@ describe( 'Lazy load', () => { beforeEach( () => { editorElement = document.createElement( 'div' ); document.body.appendChild( editorElement ); - lazyLoadInvoked = false; } ); - afterEach( () => { + afterEach( async () => { + if ( mathUIFeature?.formView ) { + mathUIFeature._hideUI(); + } + await new Promise( resolve => setTimeout( resolve, 50 ) ); editorElement.remove(); return editor.destroy(); } ); @@ -37,6 +54,7 @@ describe( 'Lazy load', () => { await buildEditor( { math: { engine: 'katex', + enablePreview: true, lazyLoad: async () => { lazyLoadInvoked = true; } @@ -44,6 +62,15 @@ describe( 'Lazy load', () => { } ); mathUIFeature._showUI(); + + // Trigger render with a non-empty value to bypass empty check optimization + if ( mathUIFeature.formView ) { + mathUIFeature.formView.equation = 'x^2'; + } + + // Wait for async rendering and lazy loading + await new Promise( resolve => setTimeout( resolve, 100 ) ); + expect( lazyLoadInvoked ).to.be.true; } ); } ); diff --git a/packages/ckeditor5-math/tests/mathui.ts b/packages/ckeditor5-math/tests/mathui.ts index 5a392c0db0c..94becd000ed 100644 --- a/packages/ckeditor5-math/tests/mathui.ts +++ b/packages/ckeditor5-math/tests/mathui.ts @@ -410,7 +410,7 @@ describe( 'MathUI', () => { it( 'should bind mainFormView.mathInputView#value to math command value', () => { const command = editor.commands.get( 'math' ); - expect( formView!.mathInputView.value ).to.null; + expect( formView!.mathInputView.value ).to.be.null; command!.value = 'x^2'; expect( formView!.mathInputView.value ).to.equal( 'x^2' ); @@ -419,10 +419,18 @@ describe( 'MathUI', () => { it( 'should execute math command on mainFormView#submit event', () => { const executeSpy = vi.spyOn( editor, 'execute' ); - formView!.mathInputView.fieldView.element!.value = 'x^2'; + formView!.mathInputView.value = 'x^2'; formView!.fire( 'submit' ); - expect(executeSpy.mock.lastCall?.slice(0, 2)).toMatchObject(['math', 'x^2']); + expect( executeSpy.mock.lastCall?.slice( 0, 2 ) ).toMatchObject( [ 'math', 'x^2' ] ); + } ); + + it( 'should update equation value when mathInputView changes', () => { + formView!.mathInputView.value = 'x^2'; + expect( formView!.equation ).to.equal( 'x^2' ); + + formView!.mathInputView.value = '\\frac{1}{2}'; + expect( formView!.equation ).to.equal( '\\frac{1}{2}' ); } ); it( 'should hide the balloon on mainFormView#cancel if math command does not have a value', () => { diff --git a/packages/ckeditor5-math/theme/mathform.css b/packages/ckeditor5-math/theme/mathform.css index 3b7b4047f9b..a5d55f2f1b0 100644 --- a/packages/ckeditor5-math/theme/mathform.css +++ b/packages/ckeditor5-math/theme/mathform.css @@ -1,35 +1,220 @@ +/** + * Math Equation Editor Dialog Styles - Compact & Readable + */ + +/* === Z-INDEX: MathLive UI above CKEditor === */ +.ML__keyboard, .ML__popover, .ML__menu, .ML__suggestions, .ML__autocomplete, +.ML__tooltip, .ML__sr-only, [data-ml-root], #mathlive-suggestion-popover, +.mathlive-suggestions-popover, [data-ml-tooltip], .ML__base { + z-index: calc(var(--ck-z-panel) + 1000) !important; +} +.ML__tooltip, [role="tooltip"], .ML__popover[role="tooltip"], .popover, [data-ml-tooltip] { + z-index: calc(var(--ck-z-panel) + 2000) !important; + position: fixed !important; +} +.ck.ck-balloon-panel, .ck.ck-balloon-panel .ck-balloon-panel__content { + overflow: visible !important; +} + +/* === MAIN DIALOG === */ .ck.ck-math-form { - display: flex; - align-items: flex-start; - flex-direction: row; - flex-wrap: nowrap; - padding: var(--ck-spacing-standard); + display: flex; + flex-direction: column; + padding: var(--ck-spacing-standard); + box-sizing: border-box; + max-width: 80vw; + max-height: 80vh; + overflow: visible; + user-select: text; +} + +/* Scrollable content - vertical scroll, horizontal visible for tooltips */ +.ck-math-view { + overflow-y: auto; + overflow-x: visible; + display: flex; + flex-direction: column; + flex: 1 1 auto; + gap: var(--ck-spacing-standard); + min-height: 0; + width: 100%; +} + +/* === MATH INPUT === */ +.ck.ck-math-input { + display: flex; + flex-direction: column; + gap: var(--ck-spacing-standard); + width: fit-content; + min-width: 100%; + max-width: 100%; + flex: 1 1 auto; + min-height: 0; + overflow: visible !important; +} + +/* === MATHLIVE EDITOR === */ +.ck.ck-math-input .ck-mathlive-container { + position: relative; + width: 100%; + min-height: 50px; + padding: var(--ck-spacing-small); + border: 1px solid var(--ck-color-input-border); + border-radius: var(--ck-border-radius); + background: var(--ck-color-input-background) !important; + transition: border-color 120ms ease; + overflow: visible !important; + clip-path: none !important; +} +.ck.ck-math-input .ck-mathlive-container:focus-within { + border-color: var(--ck-color-focus-border); +} + +/* Position keyboard & menu buttons */ +.ck-mathlive-container math-field::part(virtual-keyboard-toggle), +.ck-mathlive-container math-field::part(menu-toggle) { + position: absolute; + top: 8px; +} +.ck-mathlive-container math-field::part(virtual-keyboard-toggle) { right: 40px; } +.ck-mathlive-container math-field::part(menu-toggle) { + right: 8px; + display: flex !important; + visibility: visible !important; +} + +/* Math field element */ +.ck.ck-math-form math-field { + display: block !important; + width: 100%; + font-size: 1.5em; + background: transparent !important; + color: var(--ck-color-input-text); + border: none !important; + padding: 0; + outline: none !important; + --selection-background-color: rgba(33, 150, 243, 0.2); + --selection-color: inherit; + --contains-highlight-background-color: rgba(0, 0, 0, 0.05); +} - @media screen and (max-width: 600px) { - flex-wrap: wrap; +/* === LATEX TEXTAREA === */ +.ck.ck-math-input .ck-latex-wrapper { + display: flex; + flex-direction: column; + width: fit-content; + min-width: 100%; + max-width: 100%; + padding: var(--ck-spacing-small); + border: 1px solid var(--ck-color-input-border); + border-radius: var(--ck-border-radius); + background: var(--ck-color-input-background) !important; + transition: border-color 120ms ease; + box-sizing: border-box; +} +.ck.ck-math-input .ck-latex-wrapper:focus-within { + border-color: var(--ck-color-focus-border); +} +.ck.ck-math-input .ck-latex-label { + font-size: 12px; + font-weight: 600; + color: var(--ck-color-text); + opacity: 0.8; + margin: 0 0 var(--ck-spacing-small) 0; + flex-shrink: 0; +} +.ck.ck-math-input .ck-latex-textarea { + width: fit-content; + min-width: 100%; + max-width: 100%; + min-height: 60px; + max-height: calc(80vh - 300px); + resize: both; + overflow: auto; + font-family: 'Courier New', monospace; + font-size: 0.95em; + background: transparent !important; + color: var(--ck-color-input-text); + border: none !important; + padding: 0; + outline: none !important; + box-sizing: border-box; +} - & .ck-math-view { - flex-basis: 100%; +/* === DISPLAY TOGGLE === */ +.ck-button-display-toggle { + align-self: flex-start; + padding: var(--ck-spacing-small) var(--ck-spacing-standard); + background: var(--ck-color-input-background); + color: var(--ck-color-text); + border: 1px solid var(--ck-color-input-border); + border-radius: var(--ck-border-radius); + cursor: pointer; + transition: all 0.2s ease; +} +.ck-button-display-toggle:hover { background: var(--ck-color-focus-border); } - & .ck-labeled-view { - flex-basis: 100%; - } +/* === PREVIEW === */ +.ck-math-preview, +.ck.ck-math-preview { + width: 100%; + min-height: 40px; + max-height: none !important; + height: auto !important; + padding: var(--ck-spacing-small); + background: transparent !important; + border: none !important; + display: block; + text-align: left; + overflow-x: auto !important; + overflow-y: visible !important; + flex-shrink: 0; +} - & .ck-label { - flex-basis: 100%; - } - } +/* Center equation when in display mode */ +.ck-math-preview[data-display="true"], +.ck.ck-math-preview[data-display="true"] { + text-align: center; +} - & .ck-button { - flex-basis: 50%; - } - } +.ck-math-preview.ck-error, .ck-math-render-error { + border-color: var(--ck-color-error-text); + background: var(--ck-color-base-background); + color: var(--ck-color-error-text); } -.ck-math-tex.ck-placeholder::before { - display: none !important; +/* === BUTTONS === */ +.ck-math-button-row { + display: flex; + gap: var(--ck-spacing-standard); + justify-content: flex-end; + margin-top: var(--ck-spacing-standard); +} +.ck-button-save, .ck-button-cancel { + padding: var(--ck-spacing-small) var(--ck-spacing-standard); + border: 1px solid var(--ck-color-input-border); + border-radius: var(--ck-border-radius); + cursor: pointer; + font-weight: 500; +} +.ck-button-save { + background: var(--ck-color-focus-border); + color: white; +} +.ck-button-cancel { + background: var(--ck-color-input-background); + color: var(--ck-color-text); } +.ck-button-save:hover { opacity: 0.9; } +.ck-button-cancel:hover { background: var(--ck-color-base-background); } -.ck.ck-toolbar-container { - z-index: calc(var(--ck-z-panel) + 2); +/* === OVERFLOW FIX: Allow tooltips to escape === */ +.ck.ck-balloon-panel, +.ck.ck-balloon-panel .ck-balloon-panel__content, +.ck.ck-math-form, +.ck-math-view, +.ck.ck-math-input, +.ck.ck-math-input .ck-mathlive-container { + overflow: visible !important; + clip-path: none !important; } diff --git a/packages/ckeditor5-math/vitest.config.ts b/packages/ckeditor5-math/vitest.config.ts index fb7ccfff034..47a35ced828 100644 --- a/packages/ckeditor5-math/vitest.config.ts +++ b/packages/ckeditor5-math/vitest.config.ts @@ -22,6 +22,9 @@ export default defineConfig( { include: [ 'tests/**/*.[jt]s' ], + exclude: [ + 'tests/setup.ts' + ], globals: true, watch: false, coverage: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f65f1f0538..58720177d2c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1058,6 +1058,9 @@ importers: '@ckeditor/ckeditor5-icons': specifier: 47.3.0 version: 47.3.0 + mathlive: + specifier: 0.108.2 + version: 0.108.2 devDependencies: '@ckeditor/ckeditor5-dev-build-tools': specifier: 43.1.0 @@ -2107,6 +2110,10 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} + '@cortex-js/compute-engine@0.30.2': + resolution: {integrity: sha512-Zx+iisk9WWdbxjm8EYsneIBszvjfUs7BHNwf1jBtSINIgfWGpHrTTq9vW0J59iGCFt6bOFxbmWyxNMRSmksHMA==} + engines: {node: '>=21.7.3', npm: '>=10.5.0'} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -6813,6 +6820,10 @@ packages: compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + complex-esm@2.1.1-esm1: + resolution: {integrity: sha512-IShBEWHILB9s7MnfyevqNGxV0A1cfcSnewL/4uPFiSxkcQL4Mm3FxJ0pXMtCXuWLjYz3lRRyk6OfkeDZcjD6nw==} + engines: {node: '>=16.14.2', npm: '>=8.5.0'} + component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} @@ -10025,6 +10036,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mathlive@0.108.2: + resolution: {integrity: sha512-GIZkfprGTxrbHckOvwo92ZmOOxdD018BHDzlrEwYUU+pzR5KabhqI1s43lxe/vqXdF5RLiQKgDcuk5jxEjhkYg==} + mathml-tag-names@2.1.3: resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} @@ -15209,8 +15223,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-block-quote@47.3.0': dependencies: @@ -15221,8 +15233,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-bookmark@47.3.0': dependencies: @@ -15350,6 +15360,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.3.0 '@ckeditor/ckeditor5-watchdog': 47.3.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-dev-build-tools@43.1.0(@swc/helpers@0.5.17)(tslib@2.8.1)(typescript@5.9.3)': dependencies: @@ -15457,6 +15469,8 @@ snapshots: '@ckeditor/ckeditor5-widget': 47.3.0 ckeditor5: 47.3.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-easy-image@47.3.0': dependencies: @@ -15494,6 +15508,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-inline@47.3.0': dependencies: @@ -15503,6 +15519,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-multi-root@47.3.0': dependencies: @@ -15525,6 +15543,8 @@ snapshots: '@ckeditor/ckeditor5-table': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-emoji@47.3.0': dependencies: @@ -15550,6 +15570,8 @@ snapshots: '@ckeditor/ckeditor5-core': 47.3.0 '@ckeditor/ckeditor5-engine': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-essentials@47.3.0': dependencies: @@ -15581,8 +15603,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-export-word@47.3.0': dependencies: @@ -15607,6 +15627,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-font@47.3.0': dependencies: @@ -15616,8 +15638,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-footnotes@47.3.0': dependencies: @@ -15648,8 +15668,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-heading@47.3.0': dependencies: @@ -15681,6 +15699,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.3.0 '@ckeditor/ckeditor5-widget': 47.3.0 ckeditor5: 47.3.0 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-html-embed@47.3.0': dependencies: @@ -15740,8 +15760,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-indent@47.3.0': dependencies: @@ -15776,6 +15794,8 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-link@47.3.0': dependencies: @@ -15802,6 +15822,8 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-list@47.3.0': dependencies: @@ -15854,8 +15876,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.3.0 '@ckeditor/ckeditor5-widget': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-mention@47.3.0(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)': dependencies: @@ -15865,8 +15885,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-merge-fields@47.3.0': dependencies: @@ -15879,8 +15897,6 @@ snapshots: '@ckeditor/ckeditor5-widget': 47.3.0 ckeditor5: 47.3.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-minimap@47.3.0': dependencies: @@ -15889,8 +15905,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-operations-compressor@47.3.0': dependencies: @@ -16131,8 +16145,6 @@ snapshots: '@ckeditor/ckeditor5-widget': 47.3.0 ckeditor5: 47.3.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-template@47.3.0': dependencies: @@ -16414,6 +16426,11 @@ snapshots: '@colors/colors@1.5.0': {} + '@cortex-js/compute-engine@0.30.2': + dependencies: + complex-esm: 2.1.1-esm1 + decimal.js: 10.6.0 + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -21982,6 +21999,8 @@ snapshots: compare-versions@6.1.1: {} + complex-esm@2.1.1-esm1: {} + component-emitter@1.3.1: {} compress-commons@6.0.2: @@ -22716,8 +22735,7 @@ snapshots: decimal.js@10.5.0: {} - decimal.js@10.6.0: - optional: true + decimal.js@10.6.0: {} decko@1.2.0: {} @@ -26036,6 +26054,10 @@ snapshots: math-intrinsics@1.1.0: {} + mathlive@0.108.2: + dependencies: + '@cortex-js/compute-engine': 0.30.2 + mathml-tag-names@2.1.3: {} mdast-util-find-and-replace@3.0.2: