Browse Source

Merge pull request #7346 from ckeditor/i/1239-code-autoformat

Fix (autoformat): Autoformatting should not occur inside an existing text with a model `code` attribute. Closes #1239.
Aleksander Nowodzinski 5 years ago
parent
commit
ad3562a2d3

+ 1 - 1
packages/ckeditor5-autoformat/docs/features/autoformat.md

@@ -81,7 +81,7 @@ ClassicEditor
 
 ## Creating custom autoformatters
 
-The {@link module:autoformat/autoformat~Autoformat} feature bases on {@link module:autoformat/blockautoformatediting~BlockAutoformatEditing} and {@link module:autoformat/inlineautoformatediting~InlineAutoformatEditing} tools to create the autoformatters mentioned above.
+The {@link module:autoformat/autoformat~Autoformat} feature bases on {@link module:autoformat/blockautoformatediting~blockAutoformatEditing} and {@link module:autoformat/inlineautoformatediting~inlineAutoformatEditing} tools to create the autoformatters mentioned above.
 
 You can use these tools to create your own autoformatters. Check the [`Autoformat` feature's code](https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-autoformat/src/autoformat.js) as an example.
 

+ 14 - 27
packages/ckeditor5-autoformat/src/autoformat.js

@@ -7,8 +7,8 @@
  * @module autoformat/autoformat
  */
 
-import BlockAutoformatEditing from './blockautoformatediting';
-import InlineAutoformatEditing from './inlineautoformatediting';
+import blockAutoformatEditing from './blockautoformatediting';
+import inlineAutoformatEditing from './inlineautoformatediting';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
 /**
@@ -51,13 +51,11 @@ export default class Autoformat extends Plugin {
 		const commands = this.editor.commands;
 
 		if ( commands.get( 'bulletedList' ) ) {
-			// eslint-disable-next-line no-new
-			new BlockAutoformatEditing( this.editor, /^[*-]\s$/, 'bulletedList' );
+			blockAutoformatEditing( this.editor, this, /^[*-]\s$/, 'bulletedList' );
 		}
 
 		if ( commands.get( 'numberedList' ) ) {
-			// eslint-disable-next-line no-new
-			new BlockAutoformatEditing( this.editor, /^1[.|)]\s$/, 'numberedList' );
+			blockAutoformatEditing( this.editor, this, /^1[.|)]\s$/, 'numberedList' );
 		}
 	}
 
@@ -80,39 +78,31 @@ export default class Autoformat extends Plugin {
 		const commands = this.editor.commands;
 
 		if ( commands.get( 'bold' ) ) {
-			/* eslint-disable no-new */
 			const boldCallback = getCallbackFunctionForInlineAutoformat( this.editor, 'bold' );
 
-			new InlineAutoformatEditing( this.editor, /(\*\*)([^*]+)(\*\*)$/g, boldCallback );
-			new InlineAutoformatEditing( this.editor, /(__)([^_]+)(__)$/g, boldCallback );
-			/* eslint-enable no-new */
+			inlineAutoformatEditing( this.editor, this, /(\*\*)([^*]+)(\*\*)$/g, boldCallback );
+			inlineAutoformatEditing( this.editor, this, /(__)([^_]+)(__)$/g, boldCallback );
 		}
 
 		if ( commands.get( 'italic' ) ) {
-			/* eslint-disable no-new */
 			const italicCallback = getCallbackFunctionForInlineAutoformat( this.editor, 'italic' );
 
 			// The italic autoformatter cannot be triggered by the bold markers, so we need to check the
 			// text before the pattern (e.g. `(?:^|[^\*])`).
-			new InlineAutoformatEditing( this.editor, /(?:^|[^*])(\*)([^*_]+)(\*)$/g, italicCallback );
-			new InlineAutoformatEditing( this.editor, /(?:^|[^_])(_)([^_]+)(_)$/g, italicCallback );
-			/* eslint-enable no-new */
+			inlineAutoformatEditing( this.editor, this, /(?:^|[^*])(\*)([^*_]+)(\*)$/g, italicCallback );
+			inlineAutoformatEditing( this.editor, this, /(?:^|[^_])(_)([^_]+)(_)$/g, italicCallback );
 		}
 
 		if ( commands.get( 'code' ) ) {
-			/* eslint-disable no-new */
 			const codeCallback = getCallbackFunctionForInlineAutoformat( this.editor, 'code' );
 
-			new InlineAutoformatEditing( this.editor, /(`)([^`]+)(`)$/g, codeCallback );
-			/* eslint-enable no-new */
+			inlineAutoformatEditing( this.editor, this, /(`)([^`]+)(`)$/g, codeCallback );
 		}
 
 		if ( commands.get( 'strikethrough' ) ) {
-			/* eslint-disable no-new */
 			const strikethroughCallback = getCallbackFunctionForInlineAutoformat( this.editor, 'strikethrough' );
 
-			new InlineAutoformatEditing( this.editor, /(~~)([^~]+)(~~)$/g, strikethroughCallback );
-			/* eslint-enable no-new */
+			inlineAutoformatEditing( this.editor, this, /(~~)([^~]+)(~~)$/g, strikethroughCallback );
 		}
 	}
 
@@ -137,8 +127,7 @@ export default class Autoformat extends Plugin {
 					const level = commandValue[ 7 ];
 					const pattern = new RegExp( `^(#{${ level }})\\s$` );
 
-					// eslint-disable-next-line no-new
-					new BlockAutoformatEditing( this.editor, pattern, () => {
+					blockAutoformatEditing( this.editor, this, pattern, () => {
 						if ( !command.isEnabled ) {
 							return false;
 						}
@@ -159,8 +148,7 @@ export default class Autoformat extends Plugin {
 	 */
 	_addBlockQuoteAutoformats() {
 		if ( this.editor.commands.get( 'blockQuote' ) ) {
-			// eslint-disable-next-line no-new
-			new BlockAutoformatEditing( this.editor, /^>\s$/, 'blockQuote' );
+			blockAutoformatEditing( this.editor, this, /^>\s$/, 'blockQuote' );
 		}
 	}
 
@@ -174,13 +162,12 @@ export default class Autoformat extends Plugin {
 	 */
 	_addCodeBlockAutoformats() {
 		if ( this.editor.commands.get( 'codeBlock' ) ) {
-			// eslint-disable-next-line no-new
-			new BlockAutoformatEditing( this.editor, /^```$/, 'codeBlock' );
+			blockAutoformatEditing( this.editor, this, /^```$/, 'codeBlock' );
 		}
 	}
 }
 
-// Helper function for getting `InlineAutoformatEditing` callbacks that checks if command is enabled.
+// Helper function for getting `inlineAutoformatEditing` callbacks that checks if command is enabled.
 //
 // @param {module:core/editor/editor~Editor} editor
 // @param {String} attributeKey

+ 82 - 91
packages/ckeditor5-autoformat/src/blockautoformatediting.js

@@ -2,11 +2,6 @@
  * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
-
-/**
- * @module autoformat/blockautoformatediting
- */
-
 import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
 
 /**
@@ -16,108 +11,104 @@ import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
  * 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 constructors documentation to learn how to create custom inline autoformatters. You can also use
+ * See the {@link module:autoformat/blockautoformatediting~blockAutoformatEditing `blockAutoformatEditing`} documentation
+ * to learn how to create custom block 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/blockautoformatediting
  */
-export default class BlockAutoformatEditing {
-	/**
-	 * @inheritDoc
-	 */
-	static get pluginName() {
-		return 'BlockAutoformatEditing';
+
+/**
+ * Creates a listener triggered on {@link module:engine/model/document~Document#event:change:data `change:data`} event in the document.
+ * Calls the callback when inserted text matches the regular expression or the command name
+ * if provided instead of the callback.
+ *
+ * Examples of usage:
+ *
+ * To convert a paragraph to heading 1 when `- ` is typed, using just the command name:
+ *
+ *		blockAutoformatEditing( editor, plugin, /^\- $/, 'heading1' );
+ *
+ * To convert a paragraph to heading 1 when `- ` is typed, using just the callback:
+ *
+ *		blockAutoformatEditing( editor, plugin, /^\- $/, ( context ) => {
+ *			const { match } = context;
+ *			const headingLevel = match[ 1 ].length;
+ *
+ *			editor.execute( 'heading', {
+ *				formatId: `heading${ headingLevel }`
+ *			} );
+ * 		} );
+ *
+ * @param {module:core/editor/editor~Editor} editor The editor instance.
+ * @param {module:autoformat/autoformat~Autoformat} plugin The autoformat plugin instance.
+ * @param {RegExp} pattern The regular expression to execute on just inserted text.
+ * @param {Function|String} callbackOrCommand The callback to execute or the command to run when the text is matched.
+ * In case of providing the callback, it receives the following parameter:
+ * * {Object} match RegExp.exec() result of matching the pattern to inserted text.
+ */
+export default function blockAutoformatEditing( editor, plugin, pattern, callbackOrCommand ) {
+	let callback;
+	let command = null;
+
+	if ( typeof callbackOrCommand == 'function' ) {
+		callback = callbackOrCommand;
+	} else {
+		// We assume that the actual command name was provided.
+		command = editor.commands.get( callbackOrCommand );
+
+		callback = () => {
+			editor.execute( callbackOrCommand );
+		};
 	}
 
-	/**
-	 * Creates a listener triggered on `change` event in the document.
-	 * Calls the callback when inserted text matches the regular expression or the command name
-	 * if provided instead of the callback.
-	 *
-	 * Examples of usage:
-	 *
-	 * To convert a paragraph to heading 1 when `- ` is typed, using just the command name:
-	 *
-	 *		new BlockAutoformatEditing( editor, /^\- $/, 'heading1' );
-	 *
-	 * To convert a paragraph to heading 1 when `- ` is typed, using just the callback:
-	 *
-	 *		new BlockAutoformatEditing( editor, /^\- $/, ( context ) => {
-	 *			const { match } = context;
-	 *			const headingLevel = match[ 1 ].length;
-	 *
-	 *			editor.execute( 'heading', {
-	 *				formatId: `heading${ headingLevel }`
-	 *			} );
-	 * 		} );
-	 *
-	 * @param {module:core/editor/editor~Editor} editor The editor instance.
-	 * @param {RegExp} pattern The regular expression to execute on just inserted text.
-	 * @param {Function|String} callbackOrCommand The callback to execute or the command to run when the text is matched.
-	 * In case of providing the callback, it receives the following parameter:
-	 * * {Object} match RegExp.exec() result of matching the pattern to inserted text.
-	 */
-	constructor( editor, pattern, callbackOrCommand ) {
-		let callback;
-		let command = null;
-
-		if ( typeof callbackOrCommand == 'function' ) {
-			callback = callbackOrCommand;
-		} else {
-			// We assume that the actual command name was provided.
-			command = editor.commands.get( callbackOrCommand );
-
-			callback = () => {
-				editor.execute( callbackOrCommand );
-			};
+	editor.model.document.on( 'change:data', ( evt, batch ) => {
+		if ( command && !command.isEnabled || !plugin.isEnabled ) {
+			return;
 		}
 
-		editor.model.document.on( 'change', ( evt, batch ) => {
-			if ( command && !command.isEnabled ) {
-				return;
-			}
-
-			if ( batch.type == 'transparent' ) {
-				return;
-			}
+		if ( batch.type == 'transparent' ) {
+			return;
+		}
 
-			const changes = Array.from( editor.model.document.differ.getChanges() );
-			const entry = changes[ 0 ];
+		const changes = Array.from( editor.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;
-			}
+		// Typing is represented by only a single change.
+		if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
+			return;
+		}
 
-			const blockToFormat = entry.position.parent;
+		const blockToFormat = entry.position.parent;
 
-			// Block formatting should trigger only if the entire content of a paragraph is a single text node... (see ckeditor5#5671).
-			if ( !blockToFormat.is( 'paragraph' ) || blockToFormat.childCount !== 1 ) {
-				return;
-			}
+		// Block formatting should trigger only if the entire content of a paragraph is a single text node... (see ckeditor5#5671).
+		if ( !blockToFormat.is( 'paragraph' ) || blockToFormat.childCount !== 1 ) {
+			return;
+		}
 
-			const match = pattern.exec( blockToFormat.getChild( 0 ).data );
+		const match = pattern.exec( blockToFormat.getChild( 0 ).data );
 
-			// ...and this text node's data match the pattern.
-			if ( !match ) {
-				return;
-			}
+		// ...and this text node's data match the pattern.
+		if ( !match ) {
+			return;
+		}
 
-			// Use enqueueChange to create new batch to separate typing batch from the auto-format changes.
-			editor.model.enqueueChange( writer => {
-				// Matched range.
-				const start = writer.createPositionAt( blockToFormat, 0 );
-				const end = writer.createPositionAt( blockToFormat, match[ 0 ].length );
-				const range = new LiveRange( start, end );
+		// Use enqueueChange to create new batch to separate typing batch from the auto-format changes.
+		editor.model.enqueueChange( writer => {
+			// Matched range.
+			const start = writer.createPositionAt( blockToFormat, 0 );
+			const end = writer.createPositionAt( blockToFormat, match[ 0 ].length );
+			const range = new LiveRange( start, end );
 
-				const wasChanged = callback( { match } );
+			const wasChanged = callback( { match } );
 
-				// Remove matched text.
-				if ( wasChanged !== false ) {
-					writer.remove( range );
-				}
+			// Remove matched text.
+			if ( wasChanged !== false ) {
+				writer.remove( range );
+			}
 
-				range.detach();
-			} );
+			range.detach();
 		} );
-	}
+	} );
 }

+ 165 - 175
packages/ckeditor5-autoformat/src/inlineautoformatediting.js

@@ -4,211 +4,177 @@
  */
 
 /**
- * @module autoformat/inlineautoformatediting
- */
-
-import getLastTextLine from '@ckeditor/ckeditor5-typing/src/utils/getlasttextline';
-
-/**
  * 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 constructors documentation to learn how to create custom inline autoformatters. You can also use
+ * 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
  */
-export default class InlineAutoformatEditing {
-	/**
-	 * @inheritDoc
-	 */
-	static get pluginName() {
-		return '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 change applied to the model 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 {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.
-	 *		new InlineAutoformatEditing( editor, /(\*\*)([^\*]+?)(\*\*)$/g, 'bold' );
-	 *
-	 * 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|String} attributeOrCallback The name of attribute to apply on matching text or a callback for manual
-	 * formatting. If callback is passed it should return `false` if changes should not be applied (e.g. if a command is disabled).
-	 *
-	 *		// Use attribute name:
-	 *		new InlineAutoformatEditing( editor, /(\*\*)([^\*]+?)(\*\*)$/g, 'bold' );
-	 *
-	 *		// Use formatting callback:
-	 *		new InlineAutoformatEditing( editor, /(\*\*)([^\*]+?)(\*\*)$/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 );
-	 *			}
-	 *		} );
-	 */
-	constructor( editor, testRegexpOrCallback, attributeOrCallback ) {
-		let regExp;
-		let attributeKey;
-		let testCallback;
-		let formatCallback;
-
-		if ( testRegexpOrCallback instanceof RegExp ) {
-			regExp = testRegexpOrCallback;
-		} else {
-			testCallback = testRegexpOrCallback;
-		}
+/**
+ * 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;
+	}
 
-		if ( typeof attributeOrCallback == 'string' ) {
-			attributeKey = attributeOrCallback;
-		} else {
-			formatCallback = attributeOrCallback;
-		}
+	// A test callback run on changed text.
+	testCallback = testCallback || ( text => {
+		let result;
+		const remove = [];
+		const format = [];
 
-		// 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 ] );
+		while ( ( result = regExp.exec( text ) ) !== null ) {
+			// There should be full match and 3 capture groups.
+			if ( result && result.length < 4 ) {
+				break;
 			}
 
-			return {
-				remove,
-				format
-			};
-		} );
+			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 ] );
+		}
 
-		// A format callback run on matched text.
-		formatCallback = formatCallback || ( ( writer, rangesToFormat ) => {
-			const validRanges = editor.model.schema.getValidRanges( rangesToFormat, attributeKey );
+		return {
+			remove,
+			format
+		};
+	} );
 
-			for ( const range of validRanges ) {
-				writer.setAttribute( attributeKey, true, range );
-			}
+	editor.model.document.on( 'change:data', ( evt, batch ) => {
+		if ( batch.type == 'transparent' || !plugin.isEnabled ) {
+			return;
+		}
 
-			// After applying attribute to the text, remove given attribute from the selection.
-			// This way user is able to type a text without attribute used by auto formatter.
-			writer.removeSelectionAttribute( attributeKey );
-		} );
+		const model = editor.model;
+		const selection = model.document.selection;
 
-		editor.model.document.on( 'change', ( evt, batch ) => {
-			if ( batch.type == 'transparent' ) {
-				return;
-			}
+		// Do nothing if selection is not collapsed.
+		if ( !selection.isCollapsed ) {
+			return;
+		}
 
-			const model = editor.model;
-			const selection = model.document.selection;
+		const changes = Array.from( model.document.differ.getChanges() );
+		const entry = changes[ 0 ];
 
-			// Do nothing if selection is not collapsed.
-			if ( !selection.isCollapsed ) {
-				return;
-			}
+		// Typing is represented by only a single change.
+		if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
+			return;
+		}
 
-			const changes = Array.from( model.document.differ.getChanges() );
-			const entry = changes[ 0 ];
+		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 );
 
-			// Typing is represented by only a single change.
-			if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
-				return;
-			}
+		if ( !( rangesToFormat.length && rangesToRemove.length ) ) {
+			return;
+		}
 
-			const focus = selection.focus;
-			const block = focus.parent;
-			const { text, range } = getLastTextLine( 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 );
+		// 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 );
 
-			if ( !( rangesToFormat.length && rangesToRemove.length ) ) {
+			// Strict check on `false` to have backward compatibility (when callbacks were returning `undefined`).
+			if ( hasChanged === false ) {
 				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 );
-				}
-			} );
+			// 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
+// Converts output of the test function provided to the inlineAutoformatEditing and converts it to the model ranges
 // inside provided block.
 //
 // @private
@@ -222,3 +188,27 @@ function testOutputToRanges( start, arrays, model ) {
 			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 ) };
+}

+ 146 - 0
packages/ckeditor5-autoformat/tests/autoformat.js

@@ -410,6 +410,152 @@ describe( 'Autoformat', () => {
 			expect( getData( model ) ).to.equal( '<paragraph>**foobar**[]</paragraph>' );
 		} );
 
+		it( 'should not format if the plugin is disabled', () => {
+			editor.plugins.get( 'Autoformat' ).forceDisabled( 'Test' );
+
+			setData( model, '<paragraph>**foobar*[]</paragraph>' );
+
+			model.change( writer => {
+				writer.insertText( '*', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<paragraph>**foobar**[]</paragraph>' );
+		} );
+
+		describe( 'with code element', () => {
+			describe( 'should not format', () => {
+				it( '* inside', () => {
+					setData( model, '<paragraph><$text code="true">fo*obar[]</$text></paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '*', { code: true }, doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo*obar*[]</$text></paragraph>' );
+				} );
+
+				it( '__ inside', () => {
+					setData( model, '<paragraph><$text code="true">fo__obar_[]</$text></paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '_', { code: true }, doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo__obar__[]</$text></paragraph>' );
+				} );
+
+				it( '~~ inside', () => {
+					setData( model, '<paragraph><$text code="true">fo~~obar~[]</$text></paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '~', { code: true }, doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo~~obar~~[]</$text></paragraph>' );
+				} );
+
+				it( '` inside', () => {
+					setData( model, '<paragraph><$text code="true">fo`obar[]</$text></paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '`', { code: true }, doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo`obar`[]</$text></paragraph>' );
+				} );
+			} );
+
+			describe( 'should not format', () => {
+				it( '* across', () => {
+					setData( model, '<paragraph><$text code="true">fo*o</$text>bar[]</paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '*', doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo*o</$text>bar*[]</paragraph>' );
+				} );
+				it( '__ across', () => {
+					setData( model, '<paragraph><$text code="true">fo__o</$text>bar_[]</paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '_', doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo__o</$text>bar__[]</paragraph>' );
+				} );
+				it( '~~ across', () => {
+					setData( model, '<paragraph><$text code="true">fo~~o</$text>bar~[]</paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '~', doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo~~o</$text>bar~~[]</paragraph>' );
+				} );
+				it( '` across', () => {
+					setData( model, '<paragraph><$text code="true">fo`o</$text>bar[]</paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '`', doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo`o</$text>bar`[]</paragraph>' );
+				} );
+			} );
+
+			describe( 'should format', () => {
+				it( '* after', () => {
+					setData( model, '<paragraph><$text code="true">fo*o</$text>b*ar[]</paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '*', doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo*o</$text>b<$text italic="true">ar</$text>[]</paragraph>' );
+				} );
+				it( '__ after', () => {
+					setData( model, '<paragraph><$text code="true">fo__o</$text>b__ar_[]</paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '_', doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo__o</$text>b<$text bold="true">ar</$text>[]</paragraph>' );
+				} );
+				it( '~~ after', () => {
+					setData( model, '<paragraph><$text code="true">fo~~o</$text>b~~ar~[]</paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '~', doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo~~o</$text>b<$text strikethrough="true">ar</$text>[]</paragraph>' );
+				} );
+				it( '` after', () => {
+					setData( model, '<paragraph><$text code="true">fo`o</$text>b`ar[]</paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '`', doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo`o</$text>b<$text code="true">ar</$text>[]</paragraph>' );
+				} );
+			} );
+		} );
+
 		it( 'should work with <softBreak>s in paragraph', () => {
 			setData( model, '<paragraph>foo<softBreak></softBreak>**barbaz*[]</paragraph>' );
 			model.change( writer => {

+ 28 - 17
packages/ckeditor5-autoformat/tests/blockautoformatediting.js

@@ -4,7 +4,7 @@
  */
 
 import Autoformat from '../src/autoformat';
-import BlockAutoformatEditing from '../src/blockautoformatediting';
+import blockAutoformatEditing from '../src/blockautoformatediting';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import Enter from '@ckeditor/ckeditor5-enter/src/enter';
@@ -12,8 +12,8 @@ import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import Command from '@ckeditor/ckeditor5-core/src/command';
 
-describe( 'BlockAutoformatEditing', () => {
-	let editor, model, doc;
+describe( 'blockAutoformatEditing', () => {
+	let editor, model, doc, plugin;
 
 	testUtils.createSinonSandbox();
 
@@ -26,13 +26,10 @@ describe( 'BlockAutoformatEditing', () => {
 				editor = newEditor;
 				model = editor.model;
 				doc = model.document;
+				plugin = editor.plugins.get( 'Autoformat' );
 			} );
 	} );
 
-	it( 'should have pluginName', () => {
-		expect( BlockAutoformatEditing.pluginName ).to.equal( 'BlockAutoformatEditing' );
-	} );
-
 	describe( 'command name', () => {
 		it( 'should run a command when the pattern is matched', () => {
 			const spy = testUtils.sinon.spy();
@@ -40,7 +37,7 @@ describe( 'BlockAutoformatEditing', () => {
 
 			editor.commands.add( 'testCommand', testCommand );
 
-			new BlockAutoformatEditing( editor, /^[*]\s$/, 'testCommand' ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, 'testCommand' );
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 
@@ -57,7 +54,7 @@ describe( 'BlockAutoformatEditing', () => {
 
 			editor.commands.add( 'testCommand', testCommand );
 
-			new BlockAutoformatEditing( editor, /^[*]\s$/, 'testCommand' ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, 'testCommand' );
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 
@@ -79,7 +76,7 @@ describe( 'BlockAutoformatEditing', () => {
 
 			editor.commands.add( 'testCommand', testCommand );
 
-			new BlockAutoformatEditing( editor, /^[*]\s$/, 'testCommand' ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, 'testCommand' );
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 
@@ -94,7 +91,7 @@ describe( 'BlockAutoformatEditing', () => {
 	describe( 'callback', () => {
 		it( 'should run callback when the pattern is matched', () => {
 			const spy = testUtils.sinon.spy();
-			new BlockAutoformatEditing( editor, /^[*]\s$/, spy ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, spy );
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 			model.change( writer => {
@@ -104,9 +101,23 @@ describe( 'BlockAutoformatEditing', () => {
 			sinon.assert.calledOnce( spy );
 		} );
 
+		it( 'should not call the callback when the pattern is matched but the plugin is disabled', () => {
+			const callbackSpy = testUtils.sinon.spy().named( 'callback' );
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, callbackSpy );
+
+			plugin.isEnabled = false;
+
+			setData( model, '<paragraph>*[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			sinon.assert.notCalled( callbackSpy );
+		} );
+
 		it( 'should ignore other delta operations', () => {
 			const spy = testUtils.sinon.spy();
-			new BlockAutoformatEditing( editor, /^[*]\s/, spy ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s/, spy );
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 			model.change( writer => {
@@ -118,7 +129,7 @@ describe( 'BlockAutoformatEditing', () => {
 
 		it( 'should stop if there is no text to run matching on', () => {
 			const spy = testUtils.sinon.spy();
-			new BlockAutoformatEditing( editor, /^[*]\s/, spy ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s/, spy );
 
 			setData( model, '<paragraph>[]</paragraph>' );
 			model.change( writer => {
@@ -146,7 +157,7 @@ describe( 'BlockAutoformatEditing', () => {
 				} );
 
 			const spy = testUtils.sinon.spy();
-			new BlockAutoformatEditing( editor, /^[*]\s/, spy ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s/, spy );
 
 			setData( model, '<paragraph>*<softBreak></softBreak>[]</paragraph>' );
 			model.change( writer => {
@@ -174,7 +185,7 @@ describe( 'BlockAutoformatEditing', () => {
 				} );
 
 			const spy = testUtils.sinon.spy();
-			new BlockAutoformatEditing( editor, /^[*]\s/, spy ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s/, spy );
 
 			setData( model, '<paragraph>* <softBreak></softBreak>[]</paragraph>' );
 
@@ -186,7 +197,7 @@ describe( 'BlockAutoformatEditing', () => {
 		} );
 
 		it( 'should stop if callback returned false', () => {
-			new BlockAutoformatEditing( editor, /^[*]\s$/, () => false ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, () => false );
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 			model.change( writer => {
@@ -199,7 +210,7 @@ describe( 'BlockAutoformatEditing', () => {
 
 	it( 'should ignore transparent batches', () => {
 		const spy = testUtils.sinon.spy();
-		new BlockAutoformatEditing( editor, /^[*]\s$/, spy ); // eslint-disable-line no-new
+		blockAutoformatEditing( editor, plugin, /^[*]\s$/, spy );
 
 		setData( model, '<paragraph>*[]</paragraph>' );
 		model.enqueueChange( 'transparent', writer => {

+ 35 - 25
packages/ckeditor5-autoformat/tests/inlineautoformatediting.js

@@ -4,19 +4,21 @@
  */
 
 import Autoformat from '../src/autoformat';
-import InlineAutoformatEditing from '../src/inlineautoformatediting';
+import inlineAutoformatEditing from '../src/inlineautoformatediting';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import Enter from '@ckeditor/ckeditor5-enter/src/enter';
 import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
-describe( 'InlineAutoformatEditing', () => {
-	let editor, model, doc;
+describe( 'inlineAutoformatEditing', () => {
+	let editor, model, doc, plugin, formatSpy;
 
 	testUtils.createSinonSandbox();
 
 	beforeEach( () => {
+		formatSpy = testUtils.sinon.spy().named( 'formatCallback' );
+
 		return VirtualTestEditor
 			.create( {
 				plugins: [ Enter, Paragraph, Autoformat ]
@@ -25,59 +27,55 @@ describe( 'InlineAutoformatEditing', () => {
 				editor = newEditor;
 				model = editor.model;
 				doc = model.document;
+				plugin = editor.plugins.get( 'Autoformat' );
 
 				model.schema.extend( '$text', { allowAttributes: 'testAttribute' } );
 			} );
 	} );
 
-	it( 'should have pluginName', () => {
-		expect( InlineAutoformatEditing.pluginName ).to.equal( 'InlineAutoformatEditing' );
-	} );
-
-	describe( 'attribute', () => {
-		it( 'should stop early if there are less than 3 capture groups', () => {
-			new InlineAutoformatEditing( editor, /(\*)(.+?)\*/g, 'testAttribute' ); // eslint-disable-line no-new
+	describe( 'regExp', () => {
+		it( 'should not call the formatCallback if there are less than 3 capture groups', () => {
+			inlineAutoformatEditing( editor, plugin, /(\*)(.+?)\*/g, formatSpy );
 
 			setData( model, '<paragraph>*foobar[]</paragraph>' );
 			model.change( writer => {
 				writer.insertText( '*', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( model ) ).to.equal( '<paragraph>*foobar*[]</paragraph>' );
+			sinon.assert.notCalled( formatSpy );
 		} );
 
-		it( 'should apply an attribute when the pattern is matched', () => {
-			new InlineAutoformatEditing( editor, /(\*)(.+?)(\*)/g, 'testAttribute' ); // eslint-disable-line no-new
+		it( 'should call the formatCallback when the pattern is matched', () => {
+			inlineAutoformatEditing( editor, plugin, /(\*)(.+?)(\*)/g, formatSpy );
 
 			setData( model, '<paragraph>*foobar[]</paragraph>' );
 			model.change( writer => {
 				writer.insertText( '*', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( model ) ).to.equal( '<paragraph><$text testAttribute="true">foobar</$text>[]</paragraph>' );
+			sinon.assert.calledOnce( formatSpy );
 		} );
 
-		it( 'should stop early if selection is not collapsed', () => {
-			new InlineAutoformatEditing( editor, /(\*)(.+?)\*/g, 'testAttribute' ); // eslint-disable-line no-new
+		it( 'should not call the formatCallback if the selection is not collapsed', () => {
+			inlineAutoformatEditing( editor, plugin, /(\*)(.+?)\*/g, formatSpy );
 
 			setData( model, '<paragraph>*foob[ar]</paragraph>' );
 			model.change( writer => {
 				writer.insertText( '*', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( model ) ).to.equal( '<paragraph>*foob*[ar]</paragraph>' );
+			sinon.assert.notCalled( formatSpy );
 		} );
 	} );
 
 	describe( 'callback', () => {
 		it( 'should stop when there are no format ranges returned from testCallback', () => {
-			const formatSpy = testUtils.sinon.spy();
 			const testStub = testUtils.sinon.stub().returns( {
 				format: [ [] ],
 				remove: []
 			} );
 
-			new InlineAutoformatEditing( editor, testStub, formatSpy ); // eslint-disable-line no-new
+			inlineAutoformatEditing( editor, plugin, testStub, formatSpy );
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 			model.change( writer => {
@@ -88,13 +86,12 @@ describe( 'InlineAutoformatEditing', () => {
 		} );
 
 		it( 'should stop when there are no remove ranges returned from testCallback', () => {
-			const formatSpy = testUtils.sinon.spy();
 			const testStub = testUtils.sinon.stub().returns( {
 				format: [],
 				remove: [ [] ]
 			} );
 
-			new InlineAutoformatEditing( editor, testStub, formatSpy ); // eslint-disable-line no-new
+			inlineAutoformatEditing( editor, plugin, testStub, formatSpy );
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 			model.change( writer => {
@@ -105,13 +102,12 @@ describe( 'InlineAutoformatEditing', () => {
 		} );
 
 		it( 'should stop early when there is no text', () => {
-			const formatSpy = testUtils.sinon.spy();
 			const testStub = testUtils.sinon.stub().returns( {
 				format: [],
 				remove: [ [] ]
 			} );
 
-			new InlineAutoformatEditing( editor, testStub, formatSpy ); // eslint-disable-line no-new
+			inlineAutoformatEditing( editor, plugin, testStub, formatSpy );
 
 			setData( model, '<paragraph>[]</paragraph>' );
 			model.change( writer => {
@@ -121,6 +117,19 @@ describe( 'InlineAutoformatEditing', () => {
 			sinon.assert.notCalled( formatSpy );
 		} );
 
+		it( 'should not run the formatCallback when the pattern is matched but the plugin is disabled', () => {
+			inlineAutoformatEditing( editor, plugin, /(\*)(.+?)(\*)/g, formatSpy );
+
+			plugin.isEnabled = false;
+
+			setData( model, '<paragraph>*foobar[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '*', doc.selection.getFirstPosition() );
+			} );
+
+			sinon.assert.notCalled( formatSpy );
+		} );
+
 		it( 'should not autoformat if callback returned false', () => {
 			setData( model, '<paragraph>Foobar[]</paragraph>' );
 
@@ -133,7 +142,7 @@ describe( 'InlineAutoformatEditing', () => {
 
 			const formatCallback = () => false;
 
-			new InlineAutoformatEditing( editor, testCallback, formatCallback ); // eslint-disable-line no-new
+			inlineAutoformatEditing( editor, plugin, testCallback, formatCallback );
 
 			model.change( writer => {
 				writer.insertText( ' ', doc.selection.getFirstPosition() );
@@ -144,13 +153,14 @@ describe( 'InlineAutoformatEditing', () => {
 	} );
 
 	it( 'should ignore transparent batches', () => {
-		new InlineAutoformatEditing( editor, /(\*)(.+?)(\*)/g, 'testAttribute' ); // eslint-disable-line no-new
+		inlineAutoformatEditing( editor, plugin, /(\*)(.+?)(\*)/g, formatSpy );
 
 		setData( model, '<paragraph>*foobar[]</paragraph>' );
 		model.enqueueChange( 'transparent', writer => {
 			writer.insertText( '*', doc.selection.getFirstPosition() );
 		} );
 
+		sinon.assert.notCalled( formatSpy );
 		expect( getData( model ) ).to.equal( '<paragraph>*foobar*[]</paragraph>' );
 	} );
 } );

+ 1 - 1
packages/ckeditor5-autoformat/tests/manual/autoformat.html

@@ -1,3 +1,3 @@
 <div id="editor">
-	<p>This is the editor instance.</p>
+	<p>This is the editor instance, with a <code>code_element*2;</code> inside.</p>
 </div>