瀏覽代碼

Simplified italic autoformat regexp, updated docs and tests.

Szymon Kupś 9 年之前
父節點
當前提交
24eb6ddbb7

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

@@ -92,57 +92,7 @@ export default class Autoformat extends Feature {
 	 */
 	_addInlineAutoformats() {
 		// Bold text between `**`, e.g. `**text to bold**`.
-		new InlineAutoformatEngine( this.editor, /(\*\*)(.+?)(\*\*)$/g, 'bold' );
-		new InlineAutoformatEngine( this.editor, /(?:[^\*])+(\*)([^\*]+)(\*)$/g, 'italic' );
-		// Italicize text between `*`, e.g. `*text to italicize*`.
-		// Slightly more complicated because of the clashing with the Bold autoformat.
-		// Won't work for text shorter than 3 characters.
-		// 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 the prefix is used to calculate `start` index.
-		// 		const pattern = /(?:[^\*]|^)(\*[^\*].+?[^\*]\*)(?:[^\*]|$)/g;
-		//
-		// 		let result;
-		// 		let remove = [];
-		// 		let format = [];
-		//
-		// 		while ( ( result = pattern.exec( text ) ) !== null ) {
-		// 			// Add "prefix" 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 + delimiterLen, start + fullMatchLen - delimiterLen ] );
-		// 		}
-		//
-		// 		return {
-		// 			remove,
-		// 			format
-		// 		};
-		// 	},
-		// 	'italic'
-		// );
+		new InlineAutoformatEngine( this.editor, /(\*\*|__)([^\*_]+?)(\*\*|__)$/g, 'bold' );
+		new InlineAutoformatEngine( this.editor, /(?:^|[^\*_])(\*|_)([^\*_]+?)(\*|_)$/g, 'italic' );
 	}
 }

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

@@ -3,7 +3,8 @@
  * For licensing, see LICENSE.md.
  */
 
-import Range from '../engine/model/liverange.js';
+import LiveRange from '../engine/model/liverange.js';
+import Text from '../engine/model/text.js';
 import getSchemaValidRanges from '../core/command/helpers/getschemavalidranges.js';
 
 /**
@@ -15,51 +16,69 @@ import getSchemaValidRanges from '../core/command/helpers/getschemavalidranges.j
  */
 export default class InlineAutoformatEngine {
 	/**
-	 * Assigns to `editor` to watch for pattern (either by executing that pattern or passing the text to `testCallbackOrPattern` callback).
-	 * It formats found text by executing command `formatCallbackOrCommand` or by running `formatCallbackOrCommand` format callback.
+	 *
+	 * Enables mechanism on given {@link core.editor.Editor} instance to watch for specified pattern (either by executing
+	 * given RegExp or by passing the text to provided callback).
+	 * It formats found text by applying proper attribute or by running provided formatting callback.
+	 * Each time data model changes text from given node (from the beginning of the current node to the collapsed
+	 * selection location) will be tested.
 	 *
 	 * @param {core.editor.Editor} editor Editor instance.
-	 * @param {Function|RegExp} testRegexpOrCallback RegExp to execute on text or test callback returning Object with offsets to
-	 * remove and offsets to format.
-	 *  * Format is applied before deletion,
-	 *	* RegExp literal *must* have 3 capture groups.
+	 * @param {Function|RegExp} testRegexpOrCallback RegExp or callback to execute on text.
+	 * Provided RegExp *must* have three capture groups. First and third capture groups
+	 * should match opening/closing delimiters. Second capture group should match text to format.
+	 *
+	 *		// Matches `**bold text**` pattern.
+	 *		// There are three matching groups:
+	 *		// - first to match starting `**` delimiter,
+	 *		// - second to match text to format,
+	 *		// - third to match ending `**` delimiter.
+	 *		new InlineAutoformatEngine( this.editor, /(\*\*)([^\*]+?)(\*\*)$/g, 'bold' );
+	 *
+	 * When function is provided instead of RegExp, it will be executed with text to match as a parameter. Function
+	 * should return proper "ranges" to delete and format.
 	 *
-	 * Example of object that should be returned from test callback.
+	 *		{
+	 *			remove: [
+	 *				[ 0, 1 ],	// Remove first letter from given text.
+	 *				[ 5, 6 ]	// Remove 6th letter from given text.
+	 *			],
+	 *			format: [
+	 *				[ 1, 5 ]	// Format all letters from 2nd to 5th.
+	 *			]
+	 *		}
 	 *
-	 *	{
-	 *		remove: [
-	 *			[ 0, 1 ],
-	 *			[ 5, 6 ]
-	 *		],
-	 *		format: [
-	 *			[ 1, 5 ]
-	 *		],
-	 *	}
+	 * @param {Function|String} attributeOrCallback Name of attribute to apply on matching text or callback for manual
+	 * formatting.
 	 *
-	 * @param {Function|String} formatCallbackOrCommand Name of command to execute on matched text or format callback.
-	 * Format callback gets following parameters:
-	 *  1. {core.editor.Editor} Editor instance,
-	 *  2. {engine.model.Range} Range of matched text to format,
-	 *  3. {engine.model.Batch} Batch to group format operations.
+	 *		// Use attribute name:
+	 *		new InlineAutoformatEngine( this.editor, /(\*\*)([^\*]+?)(\*\*)$/g, 'bold' );
+	 *
+	 *		// Use formatting callback:
+	 *		new InlineAutoformatEngine( this.editor, /(\*\*)([^\*]+?)(\*\*)$/g, ( batch, validRanges ) => {
+	 *			for ( let range of validRanges ) {
+	 *				batch.setAttribute( range, command, true );
+	 *			}
+	 *		} );
 	 */
-	constructor( editor, testRegexpOrCallback, formatCallbackOrCommand ) {
+	constructor( editor, testRegexpOrCallback, attributeOrCallback ) {
 		this.editor = editor;
 
-		let pattern;
+		let regExp;
 		let command;
 		let testCallback;
 		let formatCallback;
 
 		if ( testRegexpOrCallback instanceof RegExp ) {
-			pattern = testRegexpOrCallback;
+			regExp = testRegexpOrCallback;
 		} else {
 			testCallback = testRegexpOrCallback;
 		}
 
-		if ( typeof formatCallbackOrCommand == 'string' ) {
-			command = formatCallbackOrCommand;
+		if ( typeof attributeOrCallback == 'string' ) {
+			command = attributeOrCallback;
 		} else {
-			formatCallback = formatCallbackOrCommand;
+			formatCallback = attributeOrCallback;
 		}
 
 		// A test callback run on changed text.
@@ -68,20 +87,23 @@ export default class InlineAutoformatEngine {
 			let remove = [];
 			let format = [];
 
-			while ( ( result = pattern.exec( text ) ) !== null ) {
+			while ( ( result = regExp.exec( text ) ) !== null ) {
 				// There should be full match and 3 capture groups.
 				if ( result && result.length < 4 ) {
 					break;
 				}
 
-				console.log( result );
-				const {
+				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,
@@ -118,14 +140,12 @@ export default class InlineAutoformatEngine {
 
 			const selection = this.editor.document.selection;
 
-			if ( !selection.isCollapsed || !selection.focus || !selection.focus.textNode ) {
+			if ( !selection.isCollapsed || !selection.focus || !selection.focus.parent ) {
 				return;
 			}
 
-			const textNode = selection.focus.textNode;
-			const text = textNode.data.slice( 0, selection.focus.offset + 1 );
-			const block = textNode.parent;
-
+			const block = selection.focus.parent;
+			const text = getText( block ).slice( 0, selection.focus.offset + 1 );
 			const ranges = testCallback( text );
 			const rangesToFormat = [];
 
@@ -134,7 +154,7 @@ export default class InlineAutoformatEngine {
 				if ( range[ 0 ] === undefined || range[ 1 ] === undefined ) {
 					return;
 				}
-				rangesToFormat.push( Range.createFromParentsAndOffsets(
+				rangesToFormat.push( LiveRange.createFromParentsAndOffsets(
 					block, range[ 0 ],
 					block, range[ 1 ]
 				) );
@@ -147,7 +167,7 @@ export default class InlineAutoformatEngine {
 					return;
 				}
 
-				rangesToRemove.push( Range.createFromParentsAndOffsets(
+				rangesToRemove.push( LiveRange.createFromParentsAndOffsets(
 					block, range[ 0 ],
 					block, range[ 1 ]
 				) );
@@ -172,3 +192,23 @@ export default class InlineAutoformatEngine {
 		} );
 	}
 }
+
+// Returns whole text from parent element by adding all data from text nodes together. If one of the children is not
+// an instance of {@link engine.model.Text} function will return an empty string.
+//
+// @private
+// @param {engine.model.Element} element
+// @returns {String}
+function getText( element ) {
+	let text = '';
+
+	for ( let child of element.getChildren() ) {
+		if ( child instanceof Text ) {
+			text += child.data;
+		} else {
+			return '';
+		}
+	}
+
+	return text;
+}

+ 2 - 2
packages/ckeditor5-autoformat/tests/autoformat.js

@@ -111,7 +111,7 @@ describe( 'Autoformat', () => {
 				batch.insert( doc.selection.getFirstPosition(), '*' );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<paragraph><$text bold="true">foobar[]</$text></paragraph>' );
+			expect( getData( doc ) ).to.equal( '<paragraph><$text bold="true">foobar</$text>[]</paragraph>' );
 		} );
 
 		it( 'should replace both `*` with italic', () => {
@@ -120,7 +120,7 @@ describe( 'Autoformat', () => {
 				batch.insert( doc.selection.getFirstPosition(), '*' );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<paragraph><$text italic="true">foobar[]</$text></paragraph>' );
+			expect( getData( doc ) ).to.equal( '<paragraph><$text italic="true">foobar</$text>[]</paragraph>' );
 		} );
 
 		it( 'nothing should be replaces when typing `*`', () => {

+ 17 - 70
packages/ckeditor5-autoformat/tests/inlineautoformatengine.js

@@ -9,7 +9,6 @@ import VirtualTestEditor from '/tests/core/_utils/virtualtesteditor.js';
 import Enter from '/ckeditor5/enter/enter.js';
 import { setData, getData } from '/ckeditor5/engine/dev-utils/model.js';
 import testUtils from '/tests/core/_utils/utils.js';
-import Command from '/ckeditor5/core/command/command.js';
 
 testUtils.createSinonSandbox();
 
@@ -24,61 +23,54 @@ describe( 'InlineAutoformatEngine', () => {
 			editor = newEditor;
 			doc = editor.document;
 			batch = doc.batch();
+			doc.schema.allow( { name: '$inline', attributes: [ 'testAttribute' ] } );
 		} );
 	} );
 
-	describe( 'Command name', () => {
-		it( 'should accept a string pattern', () => {
-			const spy = testUtils.sinon.spy();
-			editor.commands.set( 'testCommand', new TestCommand( editor, spy ) );
-			new InlineAutoformatEngine( editor, '(\\*)(.+?)(\\*)', 'testCommand' );
+	describe( 'attribute', () => {
+		it( 'should stop early if there are less than 3 capture groups', () => {
+			new InlineAutoformatEngine( editor, /(\*)(.+?)\*/g, 'testAttribute' );
 
 			setData( doc, '<paragraph>*foobar[]</paragraph>' );
 			doc.enqueueChanges( () => {
 				batch.insert( doc.selection.getFirstPosition(), '*' );
 			} );
 
-			sinon.assert.calledOnce( spy );
+			expect( getData( doc ) ).to.equal( '<paragraph>*foobar*[]</paragraph>' );
 		} );
 
-		it( 'should stop early if there are less than 3 capture groups', () => {
-			const spy = testUtils.sinon.spy();
-			editor.commands.set( 'testCommand', new TestCommand( editor, spy ) );
-			new InlineAutoformatEngine( editor, /(\*)(.+?)\*/g, 'testCommand' );
+		it( 'should apply an attribute when the pattern is matched', () => {
+			new InlineAutoformatEngine( editor, /(\*)(.+?)(\*)/g, 'testAttribute' );
 
 			setData( doc, '<paragraph>*foobar[]</paragraph>' );
 			doc.enqueueChanges( () => {
 				batch.insert( doc.selection.getFirstPosition(), '*' );
 			} );
 
-			sinon.assert.notCalled( spy );
+			expect( getData( doc ) ).to.equal( '<paragraph><$text testAttribute="true">foobar</$text>[]</paragraph>' );
 		} );
 
-		it( 'should run a command when the pattern is matched', () => {
-			const spy = testUtils.sinon.spy();
-			editor.commands.set( 'testCommand', new TestCommand( editor, spy ) );
-			new InlineAutoformatEngine( editor, /(\*)(.+?)(\*)/g, 'testCommand' );
+		it( 'should stop early if selection is not collapsed', () => {
+			new InlineAutoformatEngine( editor, /(\*)(.+?)\*/g, 'testAttribute' );
 
-			setData( doc, '<paragraph>*foobar[]</paragraph>' );
+			setData( doc, '<paragraph>*foob[ar]</paragraph>' );
 			doc.enqueueChanges( () => {
 				batch.insert( doc.selection.getFirstPosition(), '*' );
 			} );
 
-			sinon.assert.calledOnce( spy );
+			expect( getData( doc ) ).to.equal( '<paragraph>*foob[*ar]</paragraph>' );
 		} );
 
-		it( 'should remove found pattern', () => {
-			const spy = testUtils.sinon.spy();
-			editor.commands.set( 'testCommand', new TestCommand( editor, spy ) );
-			new InlineAutoformatEngine( editor, /(\*)(.+?)(\*)/g, 'testCommand' );
+		it( 'should stop early if there are block elements in the way', () => {
+			new InlineAutoformatEngine( editor, /(\*)(.+?)(\*)/g, 'testAttribute' );
+			doc.schema.registerItem( 'widget', '$block' );
 
-			setData( doc, '<paragraph>*foobar[]</paragraph>' );
+			setData( doc, '<paragraph>*foo<paragraph>baz</paragraph>bar[]</paragraph>' );
 			doc.enqueueChanges( () => {
 				batch.insert( doc.selection.getFirstPosition(), '*' );
 			} );
 
-			sinon.assert.calledOnce( spy );
-			expect( getData( doc ) ).to.equal( '<paragraph>foobar[]</paragraph>' );
+			expect( getData( doc ) ).to.equal( '<paragraph>*foo<paragraph>baz</paragraph>bar*[]</paragraph>' );
 		} );
 	} );
 
@@ -133,50 +125,5 @@ describe( 'InlineAutoformatEngine', () => {
 
 			sinon.assert.notCalled( formatSpy );
 		} );
-
-		it( 'takes text from nested elements', () => {
-			const formatSpy = testUtils.sinon.spy();
-			const testStub = testUtils.sinon.stub().returns( {
-				format: [],
-				remove: []
-			} );
-
-			new InlineAutoformatEngine( editor, testStub, formatSpy );
-
-			setData( doc, '<paragraph><paragraph>foobar[]</paragraph></paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
-			} );
-
-			sinon.assert.called( testStub );
-			sinon.assert.notCalled( formatSpy );
-			sinon.assert.calledWith( testStub, 'foobar' );
-		} );
 	} );
 } );
-
-/**
- * Dummy command to execute.
- */
-class TestCommand extends Command {
-	/**
-	 * Creates an instance of the command.
-	 *
-	 * @param {core.editor.Editor} editor Editor instance.
-	 * @param {Function} onExecuteCallback _doExecute call hook
-	 */
-	constructor( editor, onExecuteCallback ) {
-		super( editor );
-
-		this.onExecute = onExecuteCallback;
-	}
-
-	/**
-	 * Executes command.
-	 *
-	 * @protected
-	 */
-	_doExecute() {
-		this.onExecute();
-	}
-}