浏览代码

Callback version of inline autoformat engine.

Maksymilian Barnaś 9 年之前
父节点
当前提交
b800851208

+ 82 - 2
packages/ckeditor5-autoformat/src/autoformat.js

@@ -77,7 +77,87 @@ export default class Autoformat extends Feature {
 	}
 
 	_addBoldAutoformats() {
-		// new InlineAutoformatEngine( this.editor, new RegExp( /(\*\*.+?\*\*)/g ), 'bold' );
-		new InlineAutoformatEngine( this.editor, '*', 'italic' );
+		// `Bold` autoformat.
+		new InlineAutoformatEngine(
+			this.editor,
+			( text ) => {
+				const pattern = /\*\*(.+?)\*\*/g;
+
+				let result;
+				let remove = [];
+				let format = [];
+
+				while ( ( result = pattern.exec( text ) ) !== null ) {
+					const start = result.index;
+					const fullMatchLen = result[ 0 ].length;
+					const delimiterLen = 2; // Length of '**'.
+
+					const delStart = [ start,                               start + delimiterLen ];
+					const delEnd =   [ start + fullMatchLen - delimiterLen, start + fullMatchLen ];
+
+					remove.push( delStart );
+					remove.push( delEnd );
+
+					// Calculation of offsets after text deletion is not needed.
+					format.push( [ start, start + fullMatchLen - delimiterLen ] );
+				}
+
+				return {
+					remove,
+					format
+				};
+			},
+			( editor, range, batch ) => {
+				this.editor.execute( 'bold', { ranges: [ range ], batch } );
+			}
+		);
+
+		// `Italic` autoformat.
+		new InlineAutoformatEngine(
+			this.editor,
+			( text ) => {
+				// For a text: 'Brown *fox* jumps over the lazy dog' the expression below will return following values:
+				//
+				// 	[0]: ' *fox* ',
+				// 	[1]: ' ',
+				// 	[2]: '*fox*',
+				// 	[index]: 5
+				//
+				// Value at index 1 is a "prefix". It can be empty, if the matched word is at the beginning of the line.
+				// Length of this match is used to calculate `start` index.
+				const pattern = /([^\*]|^)(\*[^\*].+?[^\*]\*)(?![^\*]|$)/g;
+
+				let result;
+				let remove = [];
+				let format = [];
+
+				while ( ( result = pattern.exec( text ) ) !== null ) {
+					// Add "prefix" match length.
+					const start = result.index + result[ 1 ].length;
+					const fullMatchLen = result[ 2 ].length;
+					const delimiterLen = 1; // Length of '*'.
+
+					const delStart = [ start,                               start + delimiterLen ];
+					const delEnd =   [ start + fullMatchLen - delimiterLen, start + fullMatchLen ];
+
+					remove.push( delStart );
+					remove.push( delEnd );
+
+					// Calculation of offsets after deletion is not needed.
+					format.push( [ start, start + fullMatchLen - delimiterLen ] );
+				}
+
+				return {
+					remove,
+					format
+				};
+			},
+			( editor, range, batch ) => {
+				this.editor.execute( 'italic', { ranges: [ range ], batch } );
+			}
+		);
+
+		// 3 capture groups: (remove)(format)(remove).
+		// new InlineAutoformatEngine( this.editor, /(\*\*.+?\*\*)/g, 'bold' );
 	}
 }

+ 40 - 78
packages/ckeditor5-autoformat/src/inlineautoformatengine.js

@@ -4,7 +4,6 @@
  */
 
 import Range from '../engine/model/range.js';
-import RootElement from '../engine/model/rootelement.js';
 
 /**
  * A paragraph feature for editor.
@@ -15,68 +14,56 @@ import RootElement from '../engine/model/rootelement.js';
  */
 export default class InlineAutoformatEngine {
 
-	constructor( editor, pattern, command, delimiterLen ) {
+	constructor( editor, testCallback, formatCallback ) {
 		this.editor = editor;
-		const doc = editor.document;
-
-		// Listen to model changes and add attributes.
-		editor.document.on( 'change', ( evt, type, data ) => {
-			if ( type === 'insert' ) {
-				const insertPosition = data.range.start;
-				const insertBlock = findTopmostBlock( insertPosition );
-
-				applyAttributes( insertBlock );
-			} else
-			if ( type === 'remove' ) {
-				const removePosition = data.sourcePosition;
-				const removeBlock = findTopmostBlock( removePosition );
-
-				if ( removeBlock !== null ) {
-					applyAttributes( removeBlock );
-				}
+
+		editor.document.on( 'change', ( evt, type ) => {
+			if ( type !== 'insert' ) {
+				return;
 			}
-		} );
 
-		function applyAttributes( block ) {
+			const batch = editor.document.batch();
+			const block = editor.document.selection.focus.parent;
 			const text = getText( block );
-			let result;
-			let index = 0;
 
-			while ( ( result = pattern.exec( text ) ) !== null ) {
-				let matched;
+			if ( block.name !== 'paragraph' ) {
+				return;
+			}
+
+			const ranges = testCallback( text );
 
-				if ( result[ 1 ] ) {
-					matched = result[ 1 ];
-				} else {
+			// Apply format before deleting text.
+			ranges.format.forEach( ( range ) => {
+				if ( !range || range[ 0 ] === undefined || range[ 1 ] === undefined ) {
 					return;
 				}
 
-				index = text.indexOf( matched, index )
-
-				doc.enqueueChanges( () => {
-					const batch = doc.batch();
-					const rangeToDeleteStart = Range.createFromParentsAndOffsets(
-						block, index,
-						block, index + delimiterLen
-					);
-					const rangeToDeleteEnd = Range.createFromParentsAndOffsets(
-						block, index + matched.length - delimiterLen,
-						block, index + matched.length
-					);
-
-					// Delete from the end to not change indices.
-					batch.remove( rangeToDeleteEnd );
-					batch.remove( rangeToDeleteStart );
-
-					const range = Range.createFromParentsAndOffsets(
-						block, index,
-						block, index + matched.length - delimiterLen * 2
-					);
-
-					batch.setAttribute( range, command, true );
+				const rangeToFormat = Range.createFromParentsAndOffsets(
+					block, range[ 0 ],
+					block, range[ 1 ]
+				);
+
+				editor.document.enqueueChanges( () => {
+					formatCallback( this.editor, rangeToFormat, batch );
 				} );
-			}
-		}
+			} );
+
+			// Reverse order of deleted ranges to not mix the positions.
+			ranges.remove.slice().reverse().forEach( ( range ) => {
+				if ( !range || range[ 0 ] === undefined || range[ 1 ] === undefined ) {
+					return;
+				}
+
+				const rangeToDelete = Range.createFromParentsAndOffsets(
+					block, range[ 0 ],
+					block, range[ 1 ]
+				);
+
+				editor.document.enqueueChanges( () => {
+					batch.remove( rangeToDelete );
+				} );
+			} );
+		} );
 	}
 }
 
@@ -93,28 +80,3 @@ function getText( element ) {
 
 	return text;
 }
-
-// Looks for topmost element from position parent to element placed in root.
-//
-// NOTE: This method does not checks schema directly - assumes that only block elements can be placed directly inside
-// root.
-//
-// @private
-// @param {engine.model.Position} position
-// @param {Boolean} [nodeAfter=true] When position is placed inside root element this will determine if element before
-// or after given position will be returned.
-// @returns {engine.model.Element}
-export function findTopmostBlock( position, nodeAfter = true ) {
-	let parent = position.parent;
-
-	// If position is placed inside root - get element after/before it.
-	if ( parent instanceof RootElement ) {
-		return nodeAfter ? position.nodeAfter : position.nodeBefore;
-	}
-
-	while ( !( parent.parent instanceof RootElement ) ) {
-		parent = parent.parent;
-	}
-
-	return parent;
-}