ckeditor5-image-upload-base64
Version:
The development environment of CKEditor 5 – the best browser-based rich text editor.
215 lines (187 loc) • 7.72 kB
JavaScript
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* The inline autoformatting engine. It allows to format various inline patterns. For example,
* it can be configured to make "foo" bold when typed `**foo**` (the `**` markers will be removed).
*
* The autoformatting operation is integrated with the undo manager,
* so the autoformatting step can be undone if the user's intention was not to format the text.
*
* See the {@link module:autoformat/inlineautoformatediting~inlineAutoformatEditing `inlineAutoformatEditing`} documentation
* to learn how to create custom inline autoformatters. You can also use
* the {@link module:autoformat/autoformat~Autoformat} feature which enables a set of default autoformatters
* (lists, headings, bold and italic).
*
* @module autoformat/inlineautoformatediting
*/
/**
* Enables autoformatting mechanism for a given {@link module:core/editor/editor~Editor}.
*
* It formats the matched text by applying the given model attribute or by running the provided formatting callback.
* On every {@link module:engine/model/document~Document#event:change:data data change} in the model document
* the autoformatting engine checks the text on the left of the selection
* and executes the provided action if the text matches given criteria (regular expression or callback).
*
* @param {module:core/editor/editor~Editor} editor The editor instance.
* @param {module:autoformat/autoformat~Autoformat} plugin The autoformat plugin instance.
* @param {Function|RegExp} testRegexpOrCallback The regular expression or callback to execute on text.
* Provided regular expression *must* have three capture groups. The first and the third capture group
* should match opening and closing delimiters. The second capture group should match the text to format.
*
* // Matches the `**bold text**` pattern.
* // There are three capturing groups:
* // - The first to match the starting `**` delimiter.
* // - The second to match the text to format.
* // - The third to match the ending `**` delimiter.
* inlineAutoformatEditing( editor, plugin, /(\*\*)([^\*]+?)(\*\*)$/g, formatCallback );
*
* When a function is provided instead of the regular expression, it will be executed with the text to match as a parameter.
* The function should return proper "ranges" to delete and format.
*
* {
* remove: [
* [ 0, 1 ], // Remove the first letter from the given text.
* [ 5, 6 ] // Remove the 6th letter from the given text.
* ],
* format: [
* [ 1, 5 ] // Format all letters from 2nd to 5th.
* ]
* }
*
* @param {Function} formatCallback A callback to apply actual formatting.
* It should return `false` if changes should not be applied (e.g. if a command is disabled).
*
* inlineAutoformatEditing( editor, plugin, /(\*\*)([^\*]+?)(\*\*)$/g, ( writer, rangesToFormat ) => {
* const command = editor.commands.get( 'bold' );
*
* if ( !command.isEnabled ) {
* return false;
* }
*
* const validRanges = editor.model.schema.getValidRanges( rangesToFormat, 'bold' );
*
* for ( let range of validRanges ) {
* writer.setAttribute( 'bold', true, range );
* }
* } );
*/
export default function inlineAutoformatEditing( editor, plugin, testRegexpOrCallback, formatCallback ) {
let regExp;
let testCallback;
if ( testRegexpOrCallback instanceof RegExp ) {
regExp = testRegexpOrCallback;
} else {
testCallback = testRegexpOrCallback;
}
// A test callback run on changed text.
testCallback = testCallback || ( text => {
let result;
const remove = [];
const format = [];
while ( ( result = regExp.exec( text ) ) !== null ) {
// There should be full match and 3 capture groups.
if ( result && result.length < 4 ) {
break;
}
let {
index,
'1': leftDel,
'2': content,
'3': rightDel
} = result;
// Real matched string - there might be some non-capturing groups so we need to recalculate starting index.
const found = leftDel + content + rightDel;
index += result[ 0 ].length - found.length;
// Start and End offsets of delimiters to remove.
const delStart = [
index,
index + leftDel.length
];
const delEnd = [
index + leftDel.length + content.length,
index + leftDel.length + content.length + rightDel.length
];
remove.push( delStart );
remove.push( delEnd );
format.push( [ index + leftDel.length, index + leftDel.length + content.length ] );
}
return {
remove,
format
};
} );
editor.model.document.on( 'change:data', ( evt, batch ) => {
if ( batch.type == 'transparent' || !plugin.isEnabled ) {
return;
}
const model = editor.model;
const selection = model.document.selection;
// Do nothing if selection is not collapsed.
if ( !selection.isCollapsed ) {
return;
}
const changes = Array.from( model.document.differ.getChanges() );
const entry = changes[ 0 ];
// Typing is represented by only a single change.
if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
return;
}
const focus = selection.focus;
const block = focus.parent;
const { text, range } = getTextAfterCode( model.createRange( model.createPositionAt( block, 0 ), focus ), model );
const testOutput = testCallback( text );
const rangesToFormat = testOutputToRanges( range.start, testOutput.format, model );
const rangesToRemove = testOutputToRanges( range.start, testOutput.remove, model );
if ( !( rangesToFormat.length && rangesToRemove.length ) ) {
return;
}
// Use enqueueChange to create new batch to separate typing batch from the auto-format changes.
model.enqueueChange( writer => {
// Apply format.
const hasChanged = formatCallback( writer, rangesToFormat );
// Strict check on `false` to have backward compatibility (when callbacks were returning `undefined`).
if ( hasChanged === false ) {
return;
}
// Remove delimiters - use reversed order to not mix the offsets while removing.
for ( const range of rangesToRemove.reverse() ) {
writer.remove( range );
}
} );
} );
}
// Converts output of the test function provided to the inlineAutoformatEditing and converts it to the model ranges
// inside provided block.
//
// @private
// @param {module:engine/model/position~Position} start
// @param {Array.<Array>} arrays
// @param {module:engine/model/model~Model} model
function testOutputToRanges( start, arrays, model ) {
return arrays
.filter( array => ( array[ 0 ] !== undefined && array[ 1 ] !== undefined ) )
.map( array => {
return model.createRange( start.getShiftedBy( array[ 0 ] ), start.getShiftedBy( array[ 1 ] ) );
} );
}
// Returns the last text line after the last code element from the given range.
// It is similar to {@link module:typing/utils/getlasttextline.getLastTextLine `getLastTextLine()`},
// but it ignores any text before the last `code`.
//
// @param {module:engine/model/range~Range} range
// @param {module:engine/model/model~Model} model
// @returns {module:typing/utils/getlasttextline~LastTextLineData}
function getTextAfterCode( range, model ) {
let start = range.start;
const text = Array.from( range.getItems() ).reduce( ( rangeText, node ) => {
// Trim text to a last occurrence of an inline element and update range start.
if ( !( node.is( '$text' ) || node.is( '$textProxy' ) ) || node.getAttribute( 'code' ) ) {
start = model.createPositionAfter( node );
return '';
}
return rangeText + node.data;
}, '' );
return { text, range: model.createRange( start, range.end ) };
}