Преглед изворни кода

Merge pull request #4 from ckeditor/t/3

Add inline formatting capabilities to the editor.
Piotrek Koszuliński пре 9 година
родитељ
комит
187d368a22

+ 65 - 22
packages/ckeditor5-autoformat/src/autoformat.js

@@ -3,16 +3,45 @@
  * For licensing, see LICENSE.md.
  */
 
-import AutoformatEngine from './autoformatengine.js';
+import BlockAutoformatEngine from './blockautoformatengine.js';
+import InlineAutoformatEngine from './inlineautoformatengine.js';
 import Feature from '../core/feature.js';
 import HeadingEngine from '../heading/headingengine.js';
 import ListEngine from '../list/listengine.js';
+import BoldEngine from '../basic-styles/boldengine.js';
+import ItalicEngine from '../basic-styles/italicengine.js';
 
 /**
- * Includes set of predefined Autoformatting actions:
- * * Bulleted list,
- * * Numbered list,
- * * Headings.
+ * Includes a set of predefined autoformatting actions.
+ *
+ * ## Bulleted list
+ *
+ * You can create a bulleted list by staring a line with:
+ *
+ * * `* `
+ * * `- `
+ *
+ * ## Numbered list
+ *
+ * You can create a numbered list by staring a line with:
+ *
+ * * `1. `
+ * * `1) `
+ *
+ * ## Headings
+ *
+ * You can create a heading by starting a line with:
+ *
+ * `# ` – will create Heading 1,
+ * `## ` – will create Heading 2,
+ * `### ` – will create Heading 3.
+ *
+ * ## Bold and italic
+ *
+ * You can apply bold or italic to a text by typing Markdown formatting:
+ *
+ * * `**foo bar**` or `__foo bar__` – will bold the text,
+ * * `*foo bar*` or `_foo bar_` – will italicize the text,
  *
  * @memberOf autoformat
  * @extends core.Feature
@@ -22,7 +51,7 @@ export default class Autoformat extends Feature {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ HeadingEngine, ListEngine ];
+		return [ HeadingEngine, ListEngine, BoldEngine, ItalicEngine ];
 	}
 
 	/**
@@ -31,38 +60,31 @@ export default class Autoformat extends Feature {
 	init() {
 		this._addListAutoformats();
 		this._addHeadingAutoformats();
+		this._addInlineAutoformats();
 	}
 
 	/**
-	 * Add autoformats related to ListEngine commands.
+	 * Adds autoformatting related to ListEngine commands.
 	 *
 	 * When typed:
-	 *
-	 * 	`* ` or `- `
-	 *		Paragraph will be changed to a bulleted list.
-	 *
-	 * 	`1. ` or `1) `
-	 *		Paragraph will be changed to a numbered list (1 can be any digit or list of digits).
+	 * - `* ` or `- ` - paragraph will be changed to a bulleted list,
+	 * - `1. ` or `1) ` - paragraph will be changed to a numbered list (1 can be any digit or list of digits).
 	 *
 	 * @private
 	 */
 	_addListAutoformats() {
-		new AutoformatEngine( this.editor, /^[\*\-]\s$/, 'bulletedList' );
-		new AutoformatEngine( this.editor, /^\d+[\.|)]?\s$/, 'numberedList' );
+		new BlockAutoformatEngine( this.editor, /^[\*\-]\s$/, 'bulletedList' );
+		new BlockAutoformatEngine( this.editor, /^\d+[\.|)]?\s$/, 'numberedList' );
 	}
 
 	/**
-	 * Add autoformats related to HeadingEngine commands.
-	 *
-	 * When typed:
-	 *
-	 * 	`#` or `##` or `###`
-	 *		Paragraph will be changed to a corresponding heading level.
+	 * Adds autoformatting related to HeadingEngine commands.
+	 * When typed `# ` or `## ` or `### ` paragraph will be changed to a corresponding heading level.
 	 *
 	 * @private
 	 */
 	_addHeadingAutoformats() {
-		new AutoformatEngine( this.editor, /^(#{1,3})\s$/, ( context ) => {
+		new BlockAutoformatEngine( this.editor, /^(#{1,3})\s$/, ( context ) => {
 			const { batch, match } = context;
 			const headingLevel = match[ 1 ].length;
 
@@ -72,4 +94,25 @@ export default class Autoformat extends Feature {
 			} );
 		} );
 	}
+
+	/**
+	 * Adds inline autoformatting capabilities to the editor.
+	 *
+	 * When typed:
+	 * - `**foobar**`: `**` characters are removed, and `foobar` is set to bold,
+	 * - `__foobar__`: `__` characters are removed, and `foobar` is set to bold,
+	 * - `*foobar*`: `*` characters are removed, and `foobar` is set to italic,
+	 * - `_foobar_`: `_` characters are removed, and `foobar` is set to italic.
+	 *
+	 * @private
+	 */
+	_addInlineAutoformats() {
+		new InlineAutoformatEngine( this.editor, /(\*\*)([^\*]+)(\*\*)$/g, 'bold' );
+		new InlineAutoformatEngine( this.editor, /(__)([^_]+)(__)$/g, 'bold' );
+
+		// The italic autoformatter cannot be triggered by the bold markers, so we need to check the
+		// text before the pattern (e.g. `(?:^|[^\*])`).
+		new InlineAutoformatEngine( this.editor, /(?:^|[^\*])(\*)([^\*_]+)(\*)$/g, 'italic' );
+		new InlineAutoformatEngine( this.editor, /(?:^|[^_])(_)([^_]+)(_)$/g, 'italic' );
+	}
 }

+ 16 - 4
packages/ckeditor5-autoformat/src/autoformatengine.js → packages/ckeditor5-autoformat/src/blockautoformatengine.js

@@ -6,7 +6,19 @@
 import Range from '../engine/model/range.js';
 import TextProxy from '../engine/model/textproxy.js';
 
-export default class AutoformatEngine {
+/**
+ * The block autoformatting engine. Allows to format various block patterns. For example,
+ * it can be configured to make a paragraph starting with "* " a list item.
+ *
+ * The autoformatting operation is integrated with the undo manager,
+ * so the autoformatting step can be undone, if the user's intention wasn't to format the text.
+ *
+ * See the constructors documentation to learn how to create custom inline autoformatters. You can also use
+ * the {@link autoformat.Autoformat} feature which enables a set of default autoformatters (lists, headings, bold and italic).
+ *
+ * @memberOf autoformat
+ */
+export default class BlockAutoformatEngine {
 	/**
 	 * Creates listener triggered on `change` event in document.
 	 * Calls callback when inserted text matches regular expression or command name
@@ -16,11 +28,11 @@ export default class AutoformatEngine {
 	 *
 	 * To convert paragraph to heading1 when `- ` is typed, using just commmand name:
 	 *
-	 *		createAutoformat( editor, /^\- $/, 'heading1');
+	 *		new BlockAutoformatEngine( editor, /^\- $/, 'heading1' );
 	 *
 	 * To convert paragraph to heading1 when `- ` is typed, using just callback:
 	 *
-	 *		createAutoformat( editor, /^\- $/, ( context ) => {
+	 *		new BlockAutoformatEngine( editor, /^\- $/, ( context ) => {
 	 *			const { batch, match } = context;
 	 *			const headingLevel = match[ 1 ].length;
 	 *
@@ -31,7 +43,7 @@ export default class AutoformatEngine {
 	 * 		} );
 	 *
 	 * @param {core.editor.Editor} editor Editor instance.
-	 * @param {Regex} pattern Regular expression to exec on just inserted text.
+	 * @param {RegExp} pattern Regular expression to exec on just inserted text.
 	 * @param {Function|String} callbackOrCommand Callback to execute or command to run when text is matched.
 	 * In case of providing callback it receives following parameters:
 	 * * {engine.model.Batch} batch Newly created batch for autoformat changes.

+ 209 - 0
packages/ckeditor5-autoformat/src/inlineautoformatengine.js

@@ -0,0 +1,209 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import LiveRange from '../engine/model/liverange.js';
+import getSchemaValidRanges from '../core/command/helpers/getschemavalidranges.js';
+
+/**
+ * The inline autoformatting engine. 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 wasn't to format the text.
+ *
+ * See the constructors documentation to learn how to create custom inline autoformatters. You can also use
+ * the {@link autoformat.Autoformat} feature which enables a set of default autoformatters (lists, headings, bold and italic).
+ *
+ * @memberOf autoformat
+ */
+export default class InlineAutoformatEngine {
+	/**
+	 * Enables autoformatting mechanism on a given {@link core.editor.Editor}.
+	 *
+	 * It formats the matched text by applying given model attribute or by running the 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 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 capturing 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.
+	 *
+	 *		{
+	 *			remove: [
+	 *				[ 0, 1 ],	// Remove first letter from the given text.
+	 *				[ 5, 6 ]	// Remove 6th letter from the given text.
+	 *			],
+	 *			format: [
+	 *				[ 1, 5 ]	// Format all letters from 2nd to 5th.
+	 *			]
+	 *		}
+	 *
+	 * @param {Function|String} attributeOrCallback Name of attribute to apply on matching text or callback for manual
+	 * formatting.
+	 *
+	 *		// 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, attributeOrCallback ) {
+		this.editor = editor;
+
+		let regExp;
+		let command;
+		let testCallback;
+		let formatCallback;
+
+		if ( testRegexpOrCallback instanceof RegExp ) {
+			regExp = testRegexpOrCallback;
+		} else {
+			testCallback = testRegexpOrCallback;
+		}
+
+		if ( typeof attributeOrCallback == 'string' ) {
+			command = attributeOrCallback;
+		} else {
+			formatCallback = attributeOrCallback;
+		}
+
+		// A test callback run on changed text.
+		testCallback = testCallback || ( ( text ) => {
+			let result;
+			let remove = [];
+			let 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
+			};
+		} );
+
+		// A format callback run on matched text.
+		formatCallback = formatCallback || ( ( batch, validRanges ) => {
+			for ( let range of validRanges ) {
+				batch.setAttribute( range, command, true );
+			}
+		} );
+
+		editor.document.on( 'change', ( evt, type ) => {
+			if ( type !== 'insert' ) {
+				return;
+			}
+
+			const selection = this.editor.document.selection;
+
+			if ( !selection.isCollapsed || !selection.focus || !selection.focus.parent ) {
+				return;
+			}
+
+			const block = selection.focus.parent;
+			const text = getText( block ).slice( 0, selection.focus.offset + 1 );
+			const ranges = testCallback( text );
+			const rangesToFormat = [];
+
+			// Apply format before deleting text.
+			ranges.format.forEach( ( range ) => {
+				if ( range[ 0 ] === undefined || range[ 1 ] === undefined ) {
+					return;
+				}
+
+				rangesToFormat.push( LiveRange.createFromParentsAndOffsets(
+					block, range[ 0 ],
+					block, range[ 1 ]
+				) );
+			} );
+
+			const rangesToRemove = [];
+
+			// Reverse order to not mix the offsets while removing.
+			ranges.remove.slice().reverse().forEach( ( range ) => {
+				if ( range[ 0 ] === undefined || range[ 1 ] === undefined ) {
+					return;
+				}
+
+				rangesToRemove.push( LiveRange.createFromParentsAndOffsets(
+					block, range[ 0 ],
+					block, range[ 1 ]
+				) );
+			} );
+
+			if ( !( rangesToFormat.length && rangesToRemove.length ) ) {
+				return;
+			}
+
+			const batch = editor.document.batch();
+
+			editor.document.enqueueChanges( () => {
+				const validRanges = getSchemaValidRanges( command, rangesToFormat, editor.document.schema );
+
+				// Apply format.
+				formatCallback( batch, validRanges );
+
+				// Remove delimiters.
+				for ( let range of rangesToRemove ) {
+					batch.remove( range );
+				}
+			} );
+		} );
+	}
+}
+
+// Returns whole text from parent element by adding all data from text nodes together.
+//
+// @private
+// @param {engine.model.Element} element
+// @returns {String}
+function getText( element ) {
+	return Array.from( element.getChildren() ).reduce( ( a, b ) => a + b.data, '' );
+}

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

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

+ 7 - 7
packages/ckeditor5-autoformat/tests/autoformatengine.js → packages/ckeditor5-autoformat/tests/blockautoformatengine.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import AutoformatEngine from '/ckeditor5/autoformat/autoformatengine.js';
+import BlockAutoformatEngine from '/ckeditor5/autoformat/blockautoformatengine.js';
 import Paragraph from '/ckeditor5/paragraph/paragraph.js';
 import VirtualTestEditor from '/tests/core/_utils/virtualtesteditor.js';
 import Enter from '/ckeditor5/enter/enter.js';
@@ -13,7 +13,7 @@ import Command from '/ckeditor5/core/command/command.js';
 
 testUtils.createSinonSandbox();
 
-describe( 'AutoformatEngine', () => {
+describe( 'BlockAutoformatEngine', () => {
 	let editor, doc, batch;
 
 	beforeEach( () => {
@@ -31,7 +31,7 @@ describe( 'AutoformatEngine', () => {
 		it( 'should run a command when the pattern is matched', () => {
 			const spy = testUtils.sinon.spy();
 			editor.commands.set( 'testCommand', new TestCommand( editor, spy ) );
-			new AutoformatEngine( editor, /^[\*]\s$/, 'testCommand' );
+			new BlockAutoformatEngine( editor, /^[\*]\s$/, 'testCommand' );
 
 			setData( doc, '<paragraph>*[]</paragraph>' );
 			doc.enqueueChanges( () => {
@@ -44,7 +44,7 @@ describe( 'AutoformatEngine', () => {
 		it( 'should remove found pattern', () => {
 			const spy = testUtils.sinon.spy();
 			editor.commands.set( 'testCommand', new TestCommand( editor, spy ) );
-			new AutoformatEngine( editor, /^[\*]\s$/, 'testCommand' );
+			new BlockAutoformatEngine( editor, /^[\*]\s$/, 'testCommand' );
 
 			setData( doc, '<paragraph>*[]</paragraph>' );
 			doc.enqueueChanges( () => {
@@ -59,7 +59,7 @@ describe( 'AutoformatEngine', () => {
 	describe( 'Callback', () => {
 		it( 'should run callback when the pattern is matched', () => {
 			const spy = testUtils.sinon.spy();
-			new AutoformatEngine( editor, /^[\*]\s$/, spy );
+			new BlockAutoformatEngine( editor, /^[\*]\s$/, spy );
 
 			setData( doc, '<paragraph>*[]</paragraph>' );
 			doc.enqueueChanges( () => {
@@ -71,7 +71,7 @@ describe( 'AutoformatEngine', () => {
 
 		it( 'should ignore other delta operations', () => {
 			const spy = testUtils.sinon.spy();
-			new AutoformatEngine( editor, /^[\*]\s/, spy );
+			new BlockAutoformatEngine( editor, /^[\*]\s/, spy );
 
 			setData( doc, '<paragraph>*[]</paragraph>' );
 			doc.enqueueChanges( () => {
@@ -83,7 +83,7 @@ describe( 'AutoformatEngine', () => {
 
 		it( 'should stop if there is no text to run matching on', () => {
 			const spy = testUtils.sinon.spy();
-			new AutoformatEngine( editor, /^[\*]\s/, spy );
+			new BlockAutoformatEngine( editor, /^[\*]\s/, spy );
 
 			setData( doc, '<paragraph>[]</paragraph>' );
 			doc.enqueueChanges( () => {

+ 117 - 0
packages/ckeditor5-autoformat/tests/inlineautoformatengine.js

@@ -0,0 +1,117 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import InlineAutoformatEngine from '/ckeditor5/autoformat/inlineautoformatengine.js';
+import Paragraph from '/ckeditor5/paragraph/paragraph.js';
+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';
+
+testUtils.createSinonSandbox();
+
+describe( 'InlineAutoformatEngine', () => {
+	let editor, doc, batch;
+
+	beforeEach( () => {
+		return VirtualTestEditor.create( {
+			features: [ Enter, Paragraph ]
+		} )
+		.then( newEditor => {
+			editor = newEditor;
+			doc = editor.document;
+			batch = doc.batch();
+			doc.schema.allow( { name: '$inline', attributes: [ 'testAttribute' ] } );
+		} );
+	} );
+
+	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(), '*' );
+			} );
+
+			expect( getData( doc ) ).to.equal( '<paragraph>*foobar*[]</paragraph>' );
+		} );
+
+		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(), '*' );
+			} );
+
+			expect( getData( doc ) ).to.equal( '<paragraph><$text testAttribute="true">foobar</$text>[]</paragraph>' );
+		} );
+
+		it( 'should stop early if selection is not collapsed', () => {
+			new InlineAutoformatEngine( editor, /(\*)(.+?)\*/g, 'testAttribute' );
+
+			setData( doc, '<paragraph>*foob[ar]</paragraph>' );
+			doc.enqueueChanges( () => {
+				batch.insert( doc.selection.getFirstPosition(), '*' );
+			} );
+
+			expect( getData( doc ) ).to.equal( '<paragraph>*foob[*ar]</paragraph>' );
+		} );
+	} );
+
+	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 InlineAutoformatEngine( editor, testStub, formatSpy );
+
+			setData( doc, '<paragraph>*[]</paragraph>' );
+			doc.enqueueChanges( () => {
+				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			} );
+
+			sinon.assert.notCalled( formatSpy );
+		} );
+
+		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 InlineAutoformatEngine( editor, testStub, formatSpy );
+
+			setData( doc, '<paragraph>*[]</paragraph>' );
+			doc.enqueueChanges( () => {
+				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			} );
+
+			sinon.assert.notCalled( formatSpy );
+		} );
+
+		it( 'should stop early when there is no text', () => {
+			const formatSpy = testUtils.sinon.spy();
+			const testStub = testUtils.sinon.stub().returns( {
+				format: [],
+				remove: [ [] ]
+			} );
+
+			new InlineAutoformatEngine( editor, testStub, formatSpy );
+
+			setData( doc, '<paragraph>[]</paragraph>' );
+			doc.enqueueChanges( () => {
+				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			} );
+
+			sinon.assert.notCalled( formatSpy );
+		} );
+	} );
+} );

+ 9 - 5
packages/ckeditor5-autoformat/tests/manual/autoformat.md

@@ -3,13 +3,17 @@
 
 ## Autoformat
 
-1. Type `#` and press space to replace current paragraph with the heading.
+1. Type `#` and press space in empty paragraph to replace it with the heading.
 
-2. Type `*` or `-` and press space to replace current paragraph with list item.
+2. Type `*` or `-` and press space in empty paragraph to replace it with list item.
 
-3. Type number from the range **1-3** to replace current paragraph with numbered list item.
+3. Type number from the range **1-3** to replace empty paragraph with numbered list item.
 
-4. For every autoformat pattern: Undo until you'll see just the pattern (e.g. `- `). Typing should be then possible  without triggering autoformatting again.
+4. Type `*foobar*`/`_foobar_` to italicize `foobar`. `*`/`_` should be removed.
 
-5. Typing a different pattern in already converted block **must not** trigger autoformatting. For example, typing `- ` in heading should not convert heading to list.
+5. Type `**foobar**`/`__foobar__` to bold `foobar`. `**`/`__` should be removed.
+
+6. For every autoformat pattern: Undo until you'll see just the pattern (e.g. `- `). Typing should be then possible  without triggering autoformatting again.
+
+7. Typing a different pattern in already converted block **must not** trigger autoformatting. For example, typing `- ` in heading should not convert heading to list.