mirror of
https://github.com/zadam/trilium.git
synced 2025-11-14 02:18:57 +01:00
122 lines
4.4 KiB
TypeScript
122 lines
4.4 KiB
TypeScript
import { type Editor, ModelText, ModelTextProxy, type ModelElement, type ModelRange, type Autoformat, inlineAutoformatEditing } from 'ckeditor5';
|
|
|
|
import { COMMANDS, ELEMENTS } from '../constants.js';
|
|
import { modelQueryElement, modelQueryElementsAll } from '../utils.js';
|
|
|
|
/**
|
|
* CKEditor's autoformatting feature (basically find and replace) has two opinionated default modes:
|
|
* block autoformatting, which replaces the entire line, and inline autoformatting,
|
|
* which expects a section to be formatted (but, importantly, not removed) surrounded by
|
|
* a pair of delimters which get removed.
|
|
*
|
|
* Neither of those are ideal for this case. We want to replace the matched text with a new element,
|
|
* without deleting the entire line.
|
|
*
|
|
* However, inlineAutoformatEditing allows for passing in a custom callback to handle
|
|
* regex matching, which also allows us to specify which sections to remove and
|
|
* which sections pass on to the formatting callback. This method removes the entire
|
|
* matched text, while passing the range of the numeric text on to the formatting callback.
|
|
*
|
|
* If 0 or more than 1 match is found, it returns empty ranges for both format and remove, which is a no-op.
|
|
*/
|
|
const regexMatchCallback = (
|
|
editor: Editor,
|
|
text: string
|
|
): {
|
|
remove: Array<[number, number]>;
|
|
format: Array<[number, number]>;
|
|
} => {
|
|
const selectionStart = editor.model.document.selection.anchor;
|
|
// get the text node containing the cursor's position, or the one ending at `the cursor's position
|
|
const surroundingText = selectionStart && ( selectionStart.textNode || selectionStart.getShiftedBy( -1 ).textNode );
|
|
|
|
if ( !selectionStart || !surroundingText ) {
|
|
return {
|
|
remove: [],
|
|
format: []
|
|
};
|
|
}
|
|
|
|
const results = text.matchAll( /\[\^([0-9]+)\]/g );
|
|
|
|
for ( const result of results || [] ) {
|
|
const removeStartIndex = text.indexOf( result[ 0 ] );
|
|
const removeEndIndex = removeStartIndex + result[ 0 ].length;
|
|
const textNodeOffset = selectionStart.parent.getChildStartOffset( surroundingText );
|
|
|
|
// if the cursor isn't at the end of the range to be replaced, do nothing
|
|
if ( textNodeOffset === null || selectionStart.offset !== textNodeOffset + removeEndIndex ) {
|
|
continue;
|
|
}
|
|
const formatStartIndex = removeStartIndex + 2;
|
|
const formatEndIndex = formatStartIndex + result[ 1 ].length;
|
|
return {
|
|
remove: [ [ removeStartIndex, removeEndIndex ] ],
|
|
format: [ [ formatStartIndex, formatEndIndex ] ]
|
|
};
|
|
}
|
|
return {
|
|
remove: [],
|
|
format: []
|
|
};
|
|
};
|
|
|
|
/**
|
|
* This callback takes in a range of text passed on by regexMatchCallback,
|
|
* and attempts to insert a corresponding footnote reference at the current location.
|
|
*
|
|
* Footnotes only get inserted if the matching range is an integer between 1
|
|
* and the number of existing footnotes + 1.
|
|
*/
|
|
const formatCallback = ( ranges: Array<ModelRange>, editor: Editor, rootElement: ModelElement ): boolean | undefined => {
|
|
const command = editor.commands.get( COMMANDS.insertFootnote );
|
|
if ( !command || !command.isEnabled ) {
|
|
return;
|
|
}
|
|
const text = [ ...ranges[ 0 ].getItems() ][ 0 ];
|
|
if ( !( text instanceof ModelTextProxy || text instanceof ModelText ) ) {
|
|
return false;
|
|
}
|
|
const match = text.data.match( /[0-9]+/ );
|
|
if ( !match ) {
|
|
return false;
|
|
}
|
|
const footnoteIndex = parseInt( match[ 0 ] );
|
|
const footnoteSection = modelQueryElement( editor, rootElement, element =>
|
|
element.is( 'element', ELEMENTS.footnoteSection )
|
|
);
|
|
if ( !footnoteSection ) {
|
|
if ( footnoteIndex !== 1 ) {
|
|
return false;
|
|
}
|
|
editor.execute( COMMANDS.insertFootnote );
|
|
return;
|
|
}
|
|
const footnoteCount = modelQueryElementsAll( editor, footnoteSection, element =>
|
|
element.is( 'element', ELEMENTS.footnoteItem )
|
|
).length;
|
|
if ( footnoteIndex === footnoteCount + 1 ) {
|
|
editor.execute( COMMANDS.insertFootnote );
|
|
return;
|
|
} else if ( footnoteIndex >= 1 && footnoteIndex <= footnoteCount ) {
|
|
editor.execute( COMMANDS.insertFootnote, { footnoteIndex } );
|
|
return;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Adds functionality to support creating footnotes using markdown syntax, e.g. `[^1]`.
|
|
*/
|
|
export const addFootnoteAutoformatting = ( editor: Editor, rootElement: ModelElement ): void => {
|
|
if ( editor.plugins.has( 'Autoformat' ) ) {
|
|
const autoformatPluginInstance = editor.plugins.get( 'Autoformat' ) as Autoformat;
|
|
inlineAutoformatEditing(
|
|
editor,
|
|
autoformatPluginInstance,
|
|
text => regexMatchCallback( editor, text ),
|
|
( _, ranges: Array<ModelRange> ) => formatCallback( ranges, editor, rootElement )
|
|
);
|
|
}
|
|
};
|