浏览代码

Added: bold inline autoformatting - early version.

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

+ 0 - 11
packages/ckeditor5-autoformat/src/autoformat.js

@@ -4,7 +4,6 @@
  */
 
 import AutoformatEngine from './autoformatengine.js';
-import InlineEngine from './inlineengine.js';
 import Feature from '../core/feature.js';
 import HeadingEngine from '../heading/headingengine.js';
 import ListEngine from '../list/listengine.js';
@@ -33,7 +32,6 @@ export default class Autoformat extends Feature {
 	init() {
 		this._addListAutoformats();
 		this._addHeadingAutoformats();
-		this._addInlineBold();
 	}
 
 	/**
@@ -75,13 +73,4 @@ export default class Autoformat extends Feature {
 			} );
 		} );
 	}
-
-	_addInlineBold() {
-		new InlineEngine( this.editor, /\*\*/, ( context ) => {
-			const { batch, range } = context;
-
-			batch.remove( range );
-			this.editor.execute( 'bold' );
-		} );
-	}
 }

+ 145 - 0
packages/ckeditor5-autoformat/src/inline.js

@@ -0,0 +1,145 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Feature from '../core/feature.js';
+import Range from '../engine/model/range.js';
+import RootElement from '../engine/model/rootelement.js';
+
+/**
+ * A paragraph feature for editor.
+ * Introduces `<paragraph>` element in the model which renders as `<p>` in the DOM and data.
+ *
+ * @memberOf paragraph
+ * @extends ckeditor5.Feature
+ */
+export default class Inline extends Feature {
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const doc = editor.document;
+		let blockEvents = false;
+
+		// Listen to model changes and add attributes.
+		this.listenTo( doc, 'change', ( evt, type, data ) => {
+			if ( type === 'insert' && !blockEvents ) {
+				const insertPosition = data.range.start;
+				const insertBlock = findTopmostBlock( insertPosition );
+
+				applyAttributes( insertBlock );
+			} else
+			if ( type === 'remove' && !blockEvents ) {
+				const removePosition = data.sourcePosition;
+				const removeBlock = findTopmostBlock( removePosition );
+
+				if ( removeBlock !== null ) {
+					applyAttributes( removeBlock );
+				}
+			} else
+			if ( type === 'move' ) {
+				const movePosition = data.sourcePosition;
+				const moveBlock = findTopmostBlock( movePosition );
+
+				applyAttributes( moveBlock );
+
+				const destPosition = data.range.start;
+				const destBlock = findTopmostBlock( destPosition );
+
+				applyAttributes( destBlock );
+			}
+		} );
+
+		function applyAttributes( block ) {
+			const text = getText( block );
+			const regexp = new RegExp( /(\*\*.+?\*\*)|(\*.+?\*)/g );
+
+			let delimiterLen;
+			let result;
+
+			while ( ( result = regexp.exec( text ) ) !== null ) {
+				let matched;
+				let attr;
+
+				if ( result[ 1 ] ) {
+					matched = result[ 1 ];
+					attr = 'bold';
+					delimiterLen = 2;
+				} else {
+					return;
+				}
+
+				// if ( result[ 2 ] ) {
+				// 	matched = result[ 2 ];
+				// 	attr = 'italic';
+				// 	delimiterLen = 1;
+				// }
+
+				const index = result.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
+					);
+
+					batch.remove( rangeToDeleteEnd );
+					batch.remove( rangeToDeleteStart );
+
+					const range = Range.createFromParentsAndOffsets(
+						block, index,
+						block, index + matched.length - delimiterLen * 2
+					);
+
+					batch.setAttribute( range, attr, true );
+				} );
+			}
+		}
+	}
+}
+
+function getText( element ) {
+	let text = '';
+
+	for ( let child of element.getChildren() ) {
+		if ( child.data ) {
+			text += child.data;
+		} else if ( child.name ) {
+			text += getText( child );
+		}
+	}
+
+	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;
+}

+ 0 - 75
packages/ckeditor5-autoformat/src/inlineengine.js

@@ -1,75 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Range from '../engine/model/range.js';
-
-export default class InlineEngine {
-	constructor( editor, pattern, callbackOrCommand ) {
-		let callback;
-
-		if ( typeof callbackOrCommand == 'function' ) {
-			callback = callbackOrCommand;
-		} else {
-			// We assume that the actual command name was provided.
-			const command = callbackOrCommand;
-
-			callback = ( context ) => {
-				const { batch } = context;
-
-				// Create new batch for removal and command execution.
-				editor.execute( command, { batch } );
-			};
-		}
-
-		let previousPosition;
-		let previousChar;
-
-		editor.document.on( 'change', ( event, type, changes ) => {
-			if ( type != 'insert' ) {
-				return;
-			}
-
-			// TODO: Consider storing whole range objects instead of just end-position.
-			const currentPosition = changes.range.end;
-
-			// Set previous position if it isn't set, or parents are different (selection was moved to different parent).
-			if ( !previousPosition || previousPosition.parent !== currentPosition.parent ) {
-				previousPosition = currentPosition;
-				previousChar = changes.range.getItems().next().value.data;
-
-				return;
-			}
-
-			// Ignore first character, wait for more.
-			// TODO: This must be connected with pattern.
-			if ( currentPosition.offset < 2 ) {
-				return;
-			}
-
-			const isCharacterAfter = currentPosition.isAfter( previousPosition );
-			let currentChar = changes.range.getItems().next().value.data;
-
-			// FIXME: Hardcoded character for testing. This should be regex matching.
-			if ( isCharacterAfter &&
-				currentChar === '*' &&
-				previousChar === currentChar
-			) {
-				// We shift because `previousPosition` is end-position, but we want start position.
-				const range = new Range( previousPosition.getShiftedBy( -1 ), currentPosition );
-
-				editor.document.enqueueChanges( () => {
-					// Create new batch to separate typing batch from the Autoformat changes.
-					const batch = editor.document.batch();
-
-					callback( { batch, range } );
-				} );
-			}
-
-			// Last stage, when going out of this listener is to update previous values.
-			previousPosition = currentPosition;
-			previousChar = changes.range.getItems().next().value.data;
-		} );
-	}
-}