| 
									
										
										
										
											2025-07-12 19:26:52 +03:00
										 |  |  | import type { Editor, ModelElement, DocumentSelection, PositioningFunction } from 'ckeditor5'; | 
					
						
							| 
									
										
										
										
											2025-05-04 22:16:32 +03:00
										 |  |  | import { BalloonPanelView, CKEditorError } from 'ckeditor5'; | 
					
						
							|  |  |  | import type { KatexOptions, MathJax2, MathJax3 } from './typings-external.js'; | 
					
						
							| 
									
										
										
										
											2024-03-14 21:33:24 -03:00
										 |  |  | 
 | 
					
						
							|  |  |  | export function getSelectedMathModelWidget( | 
					
						
							|  |  |  | 	selection: DocumentSelection | 
					
						
							| 
									
										
										
										
											2025-07-12 19:26:52 +03:00
										 |  |  | ): null | ModelElement { | 
					
						
							| 
									
										
										
										
											2024-03-14 21:33:24 -03:00
										 |  |  | 	const selectedElement = selection.getSelectedElement(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if ( | 
					
						
							|  |  |  | 		selectedElement && | 
					
						
							|  |  |  | 		( selectedElement.is( 'element', 'mathtex-inline' ) || | 
					
						
							|  |  |  | 			selectedElement.is( 'element', 'mathtex-display' ) ) | 
					
						
							|  |  |  | 	) { | 
					
						
							|  |  |  | 		return selectedElement; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return null; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Simple MathJax 3 version check
 | 
					
						
							|  |  |  | export function isMathJaxVersion3( MathJax: unknown ): MathJax is MathJax3 { | 
					
						
							|  |  |  | 	return ( | 
					
						
							|  |  |  | 		MathJax != null && typeof MathJax == 'object' && 'version' in MathJax && typeof MathJax.version == 'string' && | 
					
						
							|  |  |  | 		MathJax.version.split( '.' ).length === 3 && | 
					
						
							|  |  |  | 		MathJax.version.split( '.' )[ 0 ] === '3' | 
					
						
							|  |  |  | 	); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Simple MathJax 2 version check
 | 
					
						
							|  |  |  | export function isMathJaxVersion2( MathJax: unknown ): MathJax is MathJax2 { | 
					
						
							|  |  |  | 	return ( | 
					
						
							|  |  |  | 		MathJax != null && typeof MathJax == 'object' && 'Hub' in MathJax ); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Check if equation has delimiters.
 | 
					
						
							|  |  |  | export function hasDelimiters( text: string ): RegExpMatchArray | null { | 
					
						
							|  |  |  | 	return text.match( /^(\\\[.*?\\\]|\\\(.*?\\\))$/ ); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Find delimiters count
 | 
					
						
							|  |  |  | export function delimitersCounts( text: string ): number | undefined { | 
					
						
							|  |  |  | 	return text.match( /(\\\[|\\\]|\\\(|\\\))/g )?.length; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Extract delimiters and figure display mode for the model
 | 
					
						
							|  |  |  | export function extractDelimiters( equation: string ): { | 
					
						
							|  |  |  | 	equation: string; | 
					
						
							|  |  |  | 	display: boolean; | 
					
						
							|  |  |  | } { | 
					
						
							|  |  |  | 	equation = equation.trim(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Remove delimiters (e.g. \( \) or \[ \])
 | 
					
						
							|  |  |  | 	const hasInlineDelimiters = | 
					
						
							|  |  |  | 		equation.includes( '\\(' ) && equation.includes( '\\)' ); | 
					
						
							|  |  |  | 	const hasDisplayDelimiters = | 
					
						
							|  |  |  | 		equation.includes( '\\[' ) && equation.includes( '\\]' ); | 
					
						
							|  |  |  | 	if ( hasInlineDelimiters || hasDisplayDelimiters ) { | 
					
						
							|  |  |  | 		equation = equation.substring( 2, equation.length - 2 ).trim(); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return { | 
					
						
							|  |  |  | 		equation, | 
					
						
							|  |  |  | 		display: hasDisplayDelimiters | 
					
						
							|  |  |  | 	}; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export async function renderEquation( | 
					
						
							|  |  |  | 	equation: string, | 
					
						
							|  |  |  | 	element: HTMLElement, | 
					
						
							|  |  |  | 	engine: | 
					
						
							|  |  |  | 		| 'katex' | 
					
						
							|  |  |  | 		| 'mathjax' | 
					
						
							|  |  |  | 		| undefined | 
					
						
							|  |  |  | 		| ( ( | 
					
						
							|  |  |  | 			equation: string, | 
					
						
							|  |  |  | 			element: HTMLElement, | 
					
						
							|  |  |  | 			display: boolean, | 
					
						
							|  |  |  | 		) => void ) = 'katex', | 
					
						
							|  |  |  | 	lazyLoad?: () => Promise<void>, | 
					
						
							|  |  |  | 	display = false, | 
					
						
							|  |  |  | 	preview = false, | 
					
						
							|  |  |  | 	previewUid = '', | 
					
						
							|  |  |  | 	previewClassName: Array<string> = [], | 
					
						
							|  |  |  | 	katexRenderOptions: KatexOptions = {} | 
					
						
							|  |  |  | ): Promise<void> { | 
					
						
							|  |  |  | 	if ( engine == 'mathjax' ) { | 
					
						
							|  |  |  | 		if ( isMathJaxVersion3( MathJax ) ) { | 
					
						
							|  |  |  | 			selectRenderMode( | 
					
						
							|  |  |  | 				element, | 
					
						
							|  |  |  | 				preview, | 
					
						
							|  |  |  | 				previewUid, | 
					
						
							|  |  |  | 				previewClassName, | 
					
						
							|  |  |  | 				el => { | 
					
						
							|  |  |  | 					renderMathJax3( equation, el, display, () => { | 
					
						
							|  |  |  | 						if ( preview ) { | 
					
						
							|  |  |  | 							el.style.visibility = 'visible'; | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 					} ); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			); | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			selectRenderMode( | 
					
						
							|  |  |  | 				element, | 
					
						
							|  |  |  | 				preview, | 
					
						
							|  |  |  | 				previewUid, | 
					
						
							|  |  |  | 				previewClassName, | 
					
						
							|  |  |  | 				el => { | 
					
						
							|  |  |  | 					// Fixme: MathJax typesetting cause occasionally math processing error without asynchronous call
 | 
					
						
							| 
									
										
										
										
											2025-03-15 00:24:12 +02:00
										 |  |  | 					window.setTimeout( () => { | 
					
						
							| 
									
										
										
										
											2024-03-14 21:33:24 -03:00
										 |  |  | 						renderMathJax2( equation, el, display ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						// Move and scale after rendering
 | 
					
						
							|  |  |  | 						if ( preview && isMathJaxVersion2( MathJax ) ) { | 
					
						
							|  |  |  | 							// eslint-disable-next-line new-cap
 | 
					
						
							|  |  |  | 							MathJax.Hub.Queue( () => { | 
					
						
							|  |  |  | 								el.style.visibility = 'visible'; | 
					
						
							|  |  |  | 							} ); | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 					} ); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
					
						
							| 
									
										
										
										
											2024-09-11 21:04:41 +03:00
										 |  |  | 	} else if ( engine === 'katex' && window.katex !== undefined ) { | 
					
						
							| 
									
										
										
										
											2024-03-14 21:33:24 -03:00
										 |  |  | 		selectRenderMode( | 
					
						
							|  |  |  | 			element, | 
					
						
							|  |  |  | 			preview, | 
					
						
							|  |  |  | 			previewUid, | 
					
						
							|  |  |  | 			previewClassName, | 
					
						
							|  |  |  | 			el => { | 
					
						
							|  |  |  | 				if ( katex ) { | 
					
						
							|  |  |  | 					katex.render( equation, el, { | 
					
						
							|  |  |  | 						throwOnError: false, | 
					
						
							|  |  |  | 						displayMode: display, | 
					
						
							|  |  |  | 						...katexRenderOptions | 
					
						
							|  |  |  | 					} ); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				if ( preview ) { | 
					
						
							|  |  |  | 					el.style.visibility = 'visible'; | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		); | 
					
						
							|  |  |  | 	} else if ( typeof engine === 'function' ) { | 
					
						
							|  |  |  | 		engine( equation, element, display ); | 
					
						
							|  |  |  | 	} else { | 
					
						
							|  |  |  | 		if ( lazyLoad != null ) { | 
					
						
							|  |  |  | 			try { | 
					
						
							| 
									
										
										
										
											2025-03-15 00:24:12 +02:00
										 |  |  | 				window.CKEDITOR_MATH_LAZY_LOAD ??= lazyLoad(); | 
					
						
							| 
									
										
										
										
											2024-03-14 21:33:24 -03:00
										 |  |  | 				element.innerHTML = equation; | 
					
						
							| 
									
										
										
										
											2025-03-15 00:24:12 +02:00
										 |  |  | 				await window.CKEDITOR_MATH_LAZY_LOAD; | 
					
						
							| 
									
										
										
										
											2024-03-14 21:33:24 -03:00
										 |  |  | 				await renderEquation( | 
					
						
							|  |  |  | 					equation, | 
					
						
							|  |  |  | 					element, | 
					
						
							|  |  |  | 					engine, | 
					
						
							|  |  |  | 					undefined, | 
					
						
							|  |  |  | 					display, | 
					
						
							|  |  |  | 					preview, | 
					
						
							|  |  |  | 					previewUid, | 
					
						
							|  |  |  | 					previewClassName, | 
					
						
							|  |  |  | 					katexRenderOptions | 
					
						
							|  |  |  | 				); | 
					
						
							|  |  |  | 			} catch ( err ) { | 
					
						
							|  |  |  | 				element.innerHTML = equation; | 
					
						
							|  |  |  | 				console.error( | 
					
						
							|  |  |  | 					`math-tex-typesetting-lazy-load-failed: Lazy load failed: ${ String( err ) }` | 
					
						
							|  |  |  | 				); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			element.innerHTML = equation; | 
					
						
							|  |  |  | 			console.warn( | 
					
						
							|  |  |  | 				`math-tex-typesetting-missing: Missing the mathematical typesetting engine (${ String( engine ) }) for tex.` | 
					
						
							|  |  |  | 			); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function getBalloonPositionData( editor: Editor ): { | 
					
						
							|  |  |  | 	target: Range | HTMLElement; | 
					
						
							|  |  |  | 	positions: Array<PositioningFunction>; | 
					
						
							|  |  |  | } { | 
					
						
							|  |  |  | 	const view = editor.editing.view; | 
					
						
							|  |  |  | 	const defaultPositions = BalloonPanelView.defaultPositions; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	const selectedElement = view.document.selection.getSelectedElement(); | 
					
						
							|  |  |  | 	if ( selectedElement ) { | 
					
						
							|  |  |  | 		return { | 
					
						
							|  |  |  | 			target: view.domConverter.viewToDom( selectedElement ), | 
					
						
							|  |  |  | 			positions: [ | 
					
						
							|  |  |  | 				defaultPositions.southArrowNorth, | 
					
						
							|  |  |  | 				defaultPositions.southArrowNorthWest, | 
					
						
							|  |  |  | 				defaultPositions.southArrowNorthEast | 
					
						
							|  |  |  | 			] | 
					
						
							|  |  |  | 		}; | 
					
						
							|  |  |  | 	} else { | 
					
						
							|  |  |  | 		const viewDocument = view.document; | 
					
						
							|  |  |  | 		const firstRange = viewDocument.selection.getFirstRange(); | 
					
						
							|  |  |  | 		if ( !firstRange ) { | 
					
						
							|  |  |  | 			/** | 
					
						
							|  |  |  | 			* Missing first range. | 
					
						
							|  |  |  | 			* @error math-missing-range | 
					
						
							|  |  |  | 					*/ | 
					
						
							|  |  |  | 			throw new CKEditorError( 'math-missing-range' ); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return { | 
					
						
							|  |  |  | 			target: view.domConverter.viewRangeToDom( | 
					
						
							|  |  |  | 				firstRange | 
					
						
							|  |  |  | 			), | 
					
						
							|  |  |  | 			positions: [ | 
					
						
							|  |  |  | 				defaultPositions.southArrowNorth, | 
					
						
							|  |  |  | 				defaultPositions.southArrowNorthWest, | 
					
						
							|  |  |  | 				defaultPositions.southArrowNorthEast | 
					
						
							|  |  |  | 			] | 
					
						
							|  |  |  | 		}; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function selectRenderMode( | 
					
						
							|  |  |  | 	element: HTMLElement, | 
					
						
							|  |  |  | 	preview: boolean, | 
					
						
							|  |  |  | 	previewUid: string, | 
					
						
							|  |  |  | 	previewClassName: Array<string>, | 
					
						
							|  |  |  | 	cb: ( previewEl: HTMLElement ) => void | 
					
						
							|  |  |  | ) { | 
					
						
							|  |  |  | 	if ( preview ) { | 
					
						
							|  |  |  | 		createPreviewElement( | 
					
						
							|  |  |  | 			element, | 
					
						
							|  |  |  | 			previewUid, | 
					
						
							|  |  |  | 			previewClassName, | 
					
						
							|  |  |  | 			previewEl => { | 
					
						
							|  |  |  | 				cb( previewEl ); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		); | 
					
						
							|  |  |  | 	} else { | 
					
						
							|  |  |  | 		cb( element ); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function renderMathJax3( equation: string, element: HTMLElement, display: boolean, cb: () => void ) { | 
					
						
							|  |  |  | 	let promiseFunction: undefined | ( ( input: string, options: { display: boolean } ) => Promise<HTMLElement> ) = undefined; | 
					
						
							|  |  |  | 	if ( !isMathJaxVersion3( MathJax ) ) { | 
					
						
							|  |  |  | 		return; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if ( MathJax.tex2chtmlPromise ) { | 
					
						
							|  |  |  | 		promiseFunction = MathJax.tex2chtmlPromise; | 
					
						
							|  |  |  | 	} else if ( MathJax.tex2svgPromise ) { | 
					
						
							|  |  |  | 		promiseFunction = MathJax.tex2svgPromise; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if ( promiseFunction != null ) { | 
					
						
							|  |  |  | 		void promiseFunction( equation, { display } ).then( ( node: Element ) => { | 
					
						
							|  |  |  | 			if ( element.firstChild ) { | 
					
						
							|  |  |  | 				element.removeChild( element.firstChild ); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			element.appendChild( node ); | 
					
						
							|  |  |  | 			cb(); | 
					
						
							|  |  |  | 		} ); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function renderMathJax2( equation: string, element: HTMLElement, display?: boolean ) { | 
					
						
							|  |  |  | 	if ( isMathJaxVersion2( MathJax ) ) { | 
					
						
							|  |  |  | 		if ( display ) { | 
					
						
							|  |  |  | 			element.innerHTML = '\\[' + equation + '\\]'; | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			element.innerHTML = '\\(' + equation + '\\)'; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		// eslint-disable-next-line
 | 
					
						
							|  |  |  | 		MathJax.Hub.Queue(['Typeset', MathJax.Hub, element]); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function createPreviewElement( | 
					
						
							|  |  |  | 	element: HTMLElement, | 
					
						
							|  |  |  | 	previewUid: string, | 
					
						
							|  |  |  | 	previewClassName: Array<string>, | 
					
						
							|  |  |  | 	render: ( previewEl: HTMLElement ) => void | 
					
						
							|  |  |  | ): void { | 
					
						
							|  |  |  | 	const previewEl = getPreviewElement( element, previewUid, previewClassName ); | 
					
						
							|  |  |  | 	render( previewEl ); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function getPreviewElement( | 
					
						
							|  |  |  | 	element: HTMLElement, | 
					
						
							|  |  |  | 	previewUid: string, | 
					
						
							|  |  |  | 	previewClassName: Array<string> | 
					
						
							|  |  |  | ) { | 
					
						
							| 
									
										
										
										
											2025-03-15 00:24:12 +02:00
										 |  |  | 	let previewEl = document.getElementById( previewUid ); | 
					
						
							| 
									
										
										
										
											2024-03-14 21:33:24 -03:00
										 |  |  | 	// Create if not found
 | 
					
						
							|  |  |  | 	if ( !previewEl ) { | 
					
						
							| 
									
										
										
										
											2025-03-15 00:24:12 +02:00
										 |  |  | 		previewEl = document.createElement( 'div' ); | 
					
						
							| 
									
										
										
										
											2024-03-14 21:33:24 -03:00
										 |  |  | 		previewEl.setAttribute( 'id', previewUid ); | 
					
						
							|  |  |  | 		previewEl.classList.add( ...previewClassName ); | 
					
						
							|  |  |  | 		previewEl.style.visibility = 'hidden'; | 
					
						
							| 
									
										
										
										
											2025-05-23 17:03:07 +08:00
										 |  |  | 		element.appendChild( previewEl ); | 
					
						
							| 
									
										
										
										
											2024-03-14 21:33:24 -03:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	return previewEl; | 
					
						
							|  |  |  | } |