mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 11:39:01 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			342 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			342 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import type { Editor, Element as CKElement, DocumentSelection, PositioningFunction } from 'ckeditor5';
 | |
| import { BalloonPanelView, CKEditorError } from 'ckeditor5';
 | |
| import type { KatexOptions, MathJax2, MathJax3 } from './typings-external.js';
 | |
| 
 | |
| export function getSelectedMathModelWidget(
 | |
| 	selection: DocumentSelection
 | |
| ): null | CKElement {
 | |
| 	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 ) {
 | |
| 							moveAndScaleElement( element, el );
 | |
| 							el.style.visibility = 'visible';
 | |
| 						}
 | |
| 					} );
 | |
| 				}
 | |
| 			);
 | |
| 		} else {
 | |
| 			selectRenderMode(
 | |
| 				element,
 | |
| 				preview,
 | |
| 				previewUid,
 | |
| 				previewClassName,
 | |
| 				el => {
 | |
| 					// Fixme: MathJax typesetting cause occasionally math processing error without asynchronous call
 | |
| 					window.setTimeout( () => {
 | |
| 						renderMathJax2( equation, el, display );
 | |
| 
 | |
| 						// Move and scale after rendering
 | |
| 						if ( preview && isMathJaxVersion2( MathJax ) ) {
 | |
| 							// eslint-disable-next-line new-cap
 | |
| 							MathJax.Hub.Queue( () => {
 | |
| 								moveAndScaleElement( element, el );
 | |
| 								el.style.visibility = 'visible';
 | |
| 							} );
 | |
| 						}
 | |
| 					} );
 | |
| 				}
 | |
| 			);
 | |
| 		}
 | |
| 	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | |
| 	} else if ( engine === 'katex' && window.katex !== undefined ) {
 | |
| 		selectRenderMode(
 | |
| 			element,
 | |
| 			preview,
 | |
| 			previewUid,
 | |
| 			previewClassName,
 | |
| 			el => {
 | |
| 				if ( katex ) {
 | |
| 					katex.render( equation, el, {
 | |
| 						throwOnError: false,
 | |
| 						displayMode: display,
 | |
| 						...katexRenderOptions
 | |
| 					} );
 | |
| 				}
 | |
| 				if ( preview ) {
 | |
| 					moveAndScaleElement( element, el );
 | |
| 					el.style.visibility = 'visible';
 | |
| 				}
 | |
| 			}
 | |
| 		);
 | |
| 	} else if ( typeof engine === 'function' ) {
 | |
| 		engine( equation, element, display );
 | |
| 	} else {
 | |
| 		if ( lazyLoad != null ) {
 | |
| 			try {
 | |
| 				window.CKEDITOR_MATH_LAZY_LOAD ??= lazyLoad();
 | |
| 				element.innerHTML = equation;
 | |
| 				await window.CKEDITOR_MATH_LAZY_LOAD;
 | |
| 				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>
 | |
| ) {
 | |
| 	let previewEl = document.getElementById( previewUid );
 | |
| 	// Create if not found
 | |
| 	if ( !previewEl ) {
 | |
| 		previewEl = document.createElement( 'div' );
 | |
| 		previewEl.setAttribute( 'id', previewUid );
 | |
| 		previewEl.classList.add( ...previewClassName );
 | |
| 		previewEl.style.visibility = 'hidden';
 | |
| 		document.body.appendChild( previewEl );
 | |
| 
 | |
| 		let ticking = false;
 | |
| 
 | |
| 		const renderTransformation = () => {
 | |
| 			if ( !ticking ) {
 | |
| 				window.requestAnimationFrame( () => {
 | |
| 					if ( previewEl ) {
 | |
| 						moveElement( element, previewEl );
 | |
| 						ticking = false;
 | |
| 					}
 | |
| 				} );
 | |
| 
 | |
| 				ticking = true;
 | |
| 			}
 | |
| 		};
 | |
| 
 | |
| 		// Create scroll listener for following
 | |
| 		window.addEventListener( 'resize', renderTransformation );
 | |
| 		window.addEventListener( 'scroll', renderTransformation );
 | |
| 	}
 | |
| 	return previewEl;
 | |
| }
 | |
| 
 | |
| function moveAndScaleElement( parent: HTMLElement, child: HTMLElement ) {
 | |
| 	// Move to right place
 | |
| 	moveElement( parent, child );
 | |
| 
 | |
| 	// Scale parent element same as preview
 | |
| 	const domRect = child.getBoundingClientRect();
 | |
| 	parent.style.width = domRect.width + 'px';
 | |
| 	parent.style.height = domRect.height + 'px';
 | |
| }
 | |
| 
 | |
| function moveElement( parent: HTMLElement, child: HTMLElement ) {
 | |
| 	const domRect = parent.getBoundingClientRect();
 | |
| 	const left = window.scrollX + domRect.left;
 | |
| 	const top = window.scrollY + domRect.top;
 | |
| 	child.style.position = 'absolute';
 | |
| 	child.style.left = left + 'px';
 | |
| 	child.style.top = top + 'px';
 | |
| 	child.style.zIndex = 'var(--ck-z-panel)';
 | |
| 	child.style.pointerEvents = 'none';
 | |
| }
 | 
