8
0
Prechádzať zdrojové kódy

Merge tag 'v1.0.0-beta.1' into stable

Release: v1.0.0-beta.1.
Piotrek Koszuliński 7 rokov pred
rodič
commit
ade7c536ea

+ 1 - 1
packages/ckeditor5-autoformat/.eslintrc.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 15 - 11
packages/ckeditor5-autoformat/.travis.yml

@@ -1,24 +1,28 @@
 sudo: required
 dist: trusty
 addons:
+  firefox: "latest"
   apt:
     sources:
-      - google-chrome
+    - google-chrome
     packages:
-      - google-chrome-stable
+    - google-chrome-stable
 language: node_js
 node_js:
-  - "6"
+- '6'
 cache:
-  - node_modules
+- node_modules
 before_install:
-  - export DISPLAY=:99.0
-  - sh -e /etc/init.d/xvfb start
+- export DISPLAY=:99.0
+- sh -e /etc/init.d/xvfb start
 install:
-  - npm install @ckeditor/ckeditor5-dev-tests
-  - ckeditor5-dev-tests-install-dependencies
+- npm install @ckeditor/ckeditor5-dev-tests
+- ckeditor5-dev-tests-install-dependencies
 script:
-  - ckeditor5-dev-tests-travis
+- ckeditor5-dev-tests-travis
 after_success:
-  - codeclimate-test-reporter < coverage/lcov.info
-  - ckeditor5-dev-tests-save-revision
+- ckeditor5-dev-tests-save-revision
+env:
+  global:
+  - secure: dtGScAwEaAfM7zFD7nGEMatgArTONe6cWmP1xzVFqevps1l9dx3hIZNsaa+hwQgi/mxUaOT/ieTyVtdmOMEB6GKVJsxBWAF/QSsigoWgYTvPpslUV3E7w7dhSuegWvhR9RUCpceK2tz+OsQD6uWgsdq9m58e/K/Xf+n+5q0/X1waFJmxdusTV3kJrh3SfvkjUPwueqgz/l/UzX6rn4GaNYuNtH+ZLOpmZuWVDJSpY7sDyfjMY9RPf9qOheJ6KBPEfoZK8mweaQi5lvt+c2+SHAxWdZnGX3tVj+NZ3nNlM6YhwDWiz9fonL9xuRdUS8kmt8cbvpXE/l8QDtj8PZLEKpDOwMSxxnmqUJiumeL/gOUbS8D3u6SoMcnMKITWdzJA1FmZE/IlPiFrGZBNl8nmZ7qfPI86kJiE37cqE4mfVy3N5LTB6ReJ4vOCj8QqHB4yxwL6cV0D/G2cL1aC4Ug5c07edgRoLUmKeeDAbbyTFx+W17/4LBPO8ZTediCM1leejFK5LbWpbe6Qp6GTNyWzzhT9alsZrrp6ET5/9Sl9f+65Rx1aQgCsSN94nTGlIZ1RaJip7NkFP/LU/c6hQOfqhbFOBu0ZP1oOycBZPDPD0CBo0xxUIWi+Nx6CfDv1kMj6csmGhdJyb6XEd2hYbU/bSAhFZMfyAAhE+uJevgeJuV4=
+  - secure: pbWumuIQKqm12PfvvAlC5MChkiVxBm6C2tKfXoGp1BJ1BT1+WU3IIT9HeKAfgNtUUwrV+leLD2eIBeq5KP0hugm2dfgAes9qZ8LZJw0lUnLmiD6edGBCWNgxnQVBbIqdhoSlG3xyI8c2xGzpviIyojDGu48uNhGgAM+sKxgS2VDodn7aQAVqRbtr6LtKCp13AXOa/Pap0PT1mh/PpYu/hM3K1nmXEXyKZurGbPT/E6XoDYi4AvWfRBOWKkn6TtyoIbS+Bb4zMGgQCzVN4elEhOMgcdCRKpnLLG0Speb0b8pL6yFDxtMc5xnLtBu7jwziFI1Pbta7q0ZuWd3hRZq3VrHN+oaz5v+t4ssTqaq3ssOmd+6oYdSqgv6PghH9Hn55fc0MjrLWXX8ba+3tyqkgBU0YyV3XGebrsKx+G6aS1bS10gXoM2rfFWeq5ZlKVbhVW8MFmeY+boX+Ak5oQKTAHwh0O2vsLpi1mCePaPU1upOODQrAI1v/kFwUGXlemk38kfZuPELtCDuCaKt6hR+88hhv7oqWzj/jgPk/UXvb5Is8iUUrgVdLdK/1SsJWxh46j8cdTKU0BbNzdRIgwN3SLrAlwkVqhGIPpsYYRVcH5qrlF/L1KbnE5Az1uTUbg9KwofGqOx/xL5b+OMHRa0GkfelK3DygH8jpgxnZpNdLwk4=

+ 19 - 0
packages/ckeditor5-autoformat/CHANGELOG.md

@@ -1,6 +1,25 @@
 Changelog
 =========
 
+## [1.0.0-beta.1](https://github.com/ckeditor/ckeditor5-autoformat/compare/v1.0.0-alpha.2...v1.0.0-beta.1) (2018-03-15)
+
+### Bug fixes
+
+* Fixed integration with undo. Closes [#53](https://github.com/ckeditor/ckeditor5-autoformat/issues/53). ([f5d68f4](https://github.com/ckeditor/ckeditor5-autoformat/commit/f5d68f4))
+* Ordered list will now be triggered by a numer and `.` or `)`. Closes [#42](https://github.com/ckeditor/ckeditor5-autoformat/issues/42). ([bcc4e3b](https://github.com/ckeditor/ckeditor5-autoformat/commit/bcc4e3b))
+
+  Thanks to [@vladikoff](https://github.com/vladikoff)!
+
+### Other changes
+
+* Aligned feature class naming to the new scheme. ([5f5b4a9](https://github.com/ckeditor/ckeditor5-autoformat/commit/5f5b4a9))
+
+
+## 0.0.1 (2017-10-27)
+
+Internal changes only (updated dependencies, documentation, etc.).
+
+
 ## [1.0.0-alpha.2](https://github.com/ckeditor/ckeditor5-autoformat/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2017-11-14)
 
 ### Bug fixes

+ 1 - 1
packages/ckeditor5-autoformat/LICENSE.md

@@ -2,7 +2,7 @@ Software License Agreement
 ==========================
 
 **ckeditor5-autoformat** – https://github.com/ckeditor/ckeditor5-autoformat <br>
-Copyright (c) 2003-2017, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
+Copyright (c) 2003-2018, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
 
 Licensed under the terms of any of the following licenses at your choice:
 

+ 3 - 1
packages/ckeditor5-autoformat/README.md

@@ -4,7 +4,9 @@ CKEditor 5 autoformat feature
 [![Join the chat at https://gitter.im/ckeditor/ckeditor5](https://badges.gitter.im/ckeditor/ckeditor5.svg)](https://gitter.im/ckeditor/ckeditor5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-autoformat.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-autoformat)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-autoformat.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-autoformat)
-[![Test Coverage](https://codeclimate.com/github/ckeditor/ckeditor5-autoformat/badges/coverage.svg)](https://codeclimate.com/github/ckeditor/ckeditor5-autoformat/coverage)
+[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)](https://www.browserstack.com/automate/public-build/d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)
+[![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-autoformat/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-autoformat?branch=master)
+<br>
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-autoformat/status.svg)](https://david-dm.org/ckeditor/ckeditor5-autoformat)
 [![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-autoformat/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-autoformat?type=dev)
 

+ 6 - 3
packages/ckeditor5-autoformat/docs/_snippets/features/autoformat.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -7,13 +7,15 @@
 
 import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
 import Code from '@ckeditor/ckeditor5-basic-styles/src/code';
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloudservices/tests/_utils/cloudservices-config';
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-autoformat' ), {
 		plugins: ClassicEditor.build.plugins.concat( [ Code ] ),
 		toolbar: {
 			items: [
-				'headings',
+				'heading',
+				'|',
 				'bold',
 				'italic',
 				'code',
@@ -25,7 +27,8 @@ ClassicEditor
 				'redo'
 			],
 			viewportTopOffset: 60
-		}
+		},
+		cloudServices: CS_CONFIG
 	} )
 	.then( editor => {
 		window.editorBasic = editor;

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

@@ -68,7 +68,7 @@ ClassicEditor
 
 ## Creating custom autoformatters
 
-The {@link module:autoformat/autoformat~Autoformat} feature bases on {@link module:autoformat/blockautoformatengine~BlockAutoformatEngine} and {@link module:autoformat/inlineautoformatengine~InlineAutoformatEngine} 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-autoformat/blob/master/src/autoformat.js) as an example.
 

+ 16 - 15
packages/ckeditor5-autoformat/package.json

@@ -1,29 +1,30 @@
 {
   "name": "@ckeditor/ckeditor5-autoformat",
-  "version": "1.0.0-alpha.2",
+  "version": "1.0.0-beta.1",
   "description": "Autoformatting feature for CKEditor 5.",
   "keywords": [
     "ckeditor5",
     "ckeditor5-feature"
   ],
   "dependencies": {
-    "@ckeditor/ckeditor5-core": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-engine": "^1.0.0-alpha.2"
+    "@ckeditor/ckeditor5-core": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-engine": "^1.0.0-beta.1"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-basic-styles": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-block-quote": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-editor-classic": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-enter": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-heading": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-list": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-paragraph": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-typing": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-undo": "^1.0.0-alpha.2",
-    "eslint": "^4.8.0",
-    "eslint-config-ckeditor5": "^1.0.6",
+    "@ckeditor/ckeditor5-basic-styles": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-block-quote": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-cloudservices": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-editor-classic": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-enter": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-heading": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-list": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-paragraph": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-typing": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-undo": "^1.0.0-beta.1",
+    "eslint": "^4.15.0",
+    "eslint-config-ckeditor5": "^1.0.7",
     "husky": "^0.14.3",
-    "lint-staged": "^4.2.3"
+    "lint-staged": "^6.0.0"
   },
   "engines": {
     "node": ">=6.0.0",

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -7,8 +7,8 @@
  * @module autoformat/autoformat
  */
 
-import BlockAutoformatEngine from './blockautoformatengine';
-import InlineAutoformatEngine from './inlineautoformatengine';
+import BlockAutoformatEditing from './blockautoformatediting';
+import InlineAutoformatEditing from './inlineautoformatediting';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
 /**
@@ -49,12 +49,12 @@ export default class Autoformat extends Plugin {
 
 		if ( commands.get( 'bulletedList' ) ) {
 			// eslint-disable-next-line no-new
-			new BlockAutoformatEngine( this.editor, /^[*-]\s$/, 'bulletedList' );
+			new BlockAutoformatEditing( this.editor, /^[*-]\s$/, 'bulletedList' );
 		}
 
 		if ( commands.get( 'numberedList' ) ) {
 			// eslint-disable-next-line no-new
-			new BlockAutoformatEngine( this.editor, /^\d+[.|)]?\s$/, 'numberedList' );
+			new BlockAutoformatEditing( this.editor, /^\d+[.|)]\s$/, 'numberedList' );
 		}
 	}
 
@@ -76,8 +76,8 @@ export default class Autoformat extends Plugin {
 
 		if ( commands.get( 'bold' ) ) {
 			/* eslint-disable no-new */
-			new InlineAutoformatEngine( this.editor, /(\*\*)([^*]+)(\*\*)$/g, 'bold' );
-			new InlineAutoformatEngine( this.editor, /(__)([^_]+)(__)$/g, 'bold' );
+			new InlineAutoformatEditing( this.editor, /(\*\*)([^*]+)(\*\*)$/g, 'bold' );
+			new InlineAutoformatEditing( this.editor, /(__)([^_]+)(__)$/g, 'bold' );
 			/* eslint-enable no-new */
 		}
 
@@ -86,14 +86,14 @@ export default class Autoformat extends Plugin {
 			// text before the pattern (e.g. `(?:^|[^\*])`).
 
 			/* eslint-disable no-new */
-			new InlineAutoformatEngine( this.editor, /(?:^|[^*])(\*)([^*_]+)(\*)$/g, 'italic' );
-			new InlineAutoformatEngine( this.editor, /(?:^|[^_])(_)([^_]+)(_)$/g, 'italic' );
+			new InlineAutoformatEditing( this.editor, /(?:^|[^*])(\*)([^*_]+)(\*)$/g, 'italic' );
+			new InlineAutoformatEditing( this.editor, /(?:^|[^_])(_)([^_]+)(_)$/g, 'italic' );
 			/* eslint-enable no-new */
 		}
 
 		if ( commands.get( 'code' ) ) {
 			/* eslint-disable no-new */
-			new InlineAutoformatEngine( this.editor, /(`)([^`]+)(`)$/g, 'code' );
+			new InlineAutoformatEditing( this.editor, /(`)([^`]+)(`)$/g, 'code' );
 			/* eslint-enable no-new */
 		}
 	}
@@ -103,26 +103,28 @@ export default class Autoformat extends Plugin {
 	 *
 	 * It is using a number at the end of the command name to associate it with the proper trigger:
 	 *
-	 * * `heading1` will be executed when typing `#`,
-	 * * `heading2` will be executed when typing `##`,
+	 * * `heading` with value `heading1` will be executed when typing `#`,
+	 * * `heading` with value `heading2` will be executed when typing `##`,
 	 * * ... up to `heading6` and `######`.
 	 *
 	 * @private
 	 */
 	_addHeadingAutoformats() {
-		Array.from( this.editor.commands.names() )
-			.filter( name => name.match( /^heading[1-6]$/ ) )
-			.forEach( commandName => {
-				const level = commandName[ 7 ];
-				const pattern = new RegExp( `^(#{${ level }})\\s$` );
-
-				// eslint-disable-next-line no-new
-				new BlockAutoformatEngine( this.editor, pattern, context => {
-					const { batch } = context;
-
-					this.editor.execute( commandName, { batch } );
+		const command = this.editor.commands.get( 'heading' );
+
+		if ( command ) {
+			command.modelElements
+				.filter( name => name.match( /^heading[1-6]$/ ) )
+				.forEach( commandValue => {
+					const level = commandValue[ 7 ];
+					const pattern = new RegExp( `^(#{${ level }})\\s$` );
+
+					// eslint-disable-next-line no-new
+					new BlockAutoformatEditing( this.editor, pattern, () => {
+						this.editor.execute( 'heading', { value: commandValue } );
+					} );
 				} );
-			} );
+		}
 	}
 
 	/**
@@ -136,7 +138,7 @@ export default class Autoformat extends Plugin {
 	_addBlockQuoteAutoformats() {
 		if ( this.editor.commands.get( 'blockQuote' ) ) {
 			// eslint-disable-next-line no-new
-			new BlockAutoformatEngine( this.editor, /^>\s$/, 'blockQuote' );
+			new BlockAutoformatEditing( this.editor, /^>\s$/, 'blockQuote' );
 		}
 	}
 }

+ 22 - 41
packages/ckeditor5-autoformat/src/blockautoformatengine.js → packages/ckeditor5-autoformat/src/blockautoformatediting.js

@@ -1,14 +1,13 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 /**
- * @module autoformat/blockautoformatengine
+ * @module autoformat/blockautoformatediting
  */
 
 import Range from '@ckeditor/ckeditor5-engine/src/model/range';
-import TextProxy from '@ckeditor/ckeditor5-engine/src/model/textproxy';
 
 /**
  * The block autoformatting engine. It allows to format various block patterns. For example,
@@ -21,7 +20,7 @@ import TextProxy from '@ckeditor/ckeditor5-engine/src/model/textproxy';
  * the {@link module:autoformat/autoformat~Autoformat} feature which enables a set of default autoformatters
  * (lists, headings, bold and italic).
  */
-export default class BlockAutoformatEngine {
+export default class BlockAutoformatEditing {
 	/**
 	 * Creates a listener triggered on `change` event in the document.
 	 * Calls the callback when inserted text matches the regular expression or the command name
@@ -31,16 +30,15 @@ export default class BlockAutoformatEngine {
 	 *
 	 * To convert a paragraph to heading 1 when `- ` is typed, using just the commmand name:
 	 *
-	 *		new BlockAutoformatEngine( editor, /^\- $/, 'heading1' );
+	 *		new BlockAutoformatEditing( editor, /^\- $/, 'heading1' );
 	 *
 	 * To convert a paragraph to heading 1 when `- ` is typed, using just the callback:
 	 *
-	 *		new BlockAutoformatEngine( editor, /^\- $/, ( context ) => {
-	 *			const { batch, match } = context;
+	 *		new BlockAutoformatEditing( editor, /^\- $/, ( context ) => {
+	 *			const { match } = context;
 	 *			const headingLevel = match[ 1 ].length;
 	 *
 	 *			editor.execute( 'heading', {
-	 *				batch,
 	 *				formatId: `heading${ headingLevel }`
 	 *			} );
 	 * 		} );
@@ -48,8 +46,7 @@ export default class BlockAutoformatEngine {
 	 * @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 parameters:
-	 * * {module:engine/model/batch~Batch} batch Newly created batch for autoformat changes.
+	 * 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 ) {
@@ -61,56 +58,40 @@ export default class BlockAutoformatEngine {
 			// 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 } );
+			callback = () => {
+				editor.execute( command );
 			};
 		}
 
-		editor.document.on( 'change', ( event, type, changes, batch ) => {
-			if ( batch.type == 'transparent' ) {
-				return;
-			}
+		editor.model.document.on( 'change', () => {
+			const changes = Array.from( editor.model.document.differ.getChanges() );
+			const entry = changes[ 0 ];
 
-			if ( type != 'insert' ) {
+			// Typing is represented by only a single change.
+			if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
 				return;
 			}
+			const item = entry.position.textNode || entry.position.nodeAfter;
 
-			// Take the first element. Typing shouldn't add more than one element at once.
-			// And if it is not typing (e.g. paste), Autoformat should not be fired.
-			const value = changes.range.getItems().next().value;
-
-			if ( !( value instanceof TextProxy ) ) {
+			if ( !item.parent.is( 'paragraph' ) ) {
 				return;
 			}
 
-			const textNode = value.textNode;
-			const text = textNode.data;
-
-			// Run matching only on non-empty paragraphs.
-			if ( textNode.parent.name !== 'paragraph' || !text ) {
-				return;
-			}
-
-			const match = pattern.exec( text );
+			const match = pattern.exec( item.data );
 
 			if ( !match ) {
 				return;
 			}
 
-			editor.document.enqueueChanges( () => {
-				// Create new batch to separate typing batch from the Autoformat changes.
-				const fixBatch = editor.document.batch();
-
+			// Use enqueueChange to create new batch to separate typing batch from the auto-format changes.
+			editor.model.enqueueChange( writer => {
 				// Matched range.
-				const range = Range.createFromParentsAndOffsets( textNode.parent, 0, textNode.parent, match[ 0 ].length );
+				const range = Range.createFromParentsAndOffsets( item.parent, 0, item.parent, match[ 0 ].length );
 
 				// Remove matched text.
-				fixBatch.remove( range );
+				writer.remove( range );
 
-				callback( { fixBatch, match } );
+				callback( { match } );
 			} );
 		} );
 	}

+ 46 - 63
packages/ckeditor5-autoformat/src/inlineautoformatengine.js → packages/ckeditor5-autoformat/src/inlineautoformatediting.js

@@ -1,13 +1,13 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 /**
- * @module autoformat/inlineautoformatengine
+ * @module autoformat/inlineautoformatediting
  */
 
-import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
+import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
 
 /**
  * The inline autoformatting engine. It allows to format various inline patterns. For example,
@@ -20,7 +20,7 @@ import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
  * the {@link module:autoformat/autoformat~Autoformat} feature which enables a set of default autoformatters
  * (lists, headings, bold and italic).
  */
-export default class InlineAutoformatEngine {
+export default class InlineAutoformatEditing {
 	/**
 	 * Enables autoformatting mechanism for a given {@link module:core/editor/editor~Editor}.
 	 *
@@ -38,7 +38,7 @@ export default class InlineAutoformatEngine {
 	 *		// - The first to match the starting `**` delimiter.
 	 *		// - The second to match the text to format.
 	 *		// - The third to match the ending `**` delimiter.
-	 *		new InlineAutoformatEngine( editor, /(\*\*)([^\*]+?)(\*\*)$/g, 'bold' );
+	 *		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.
@@ -57,18 +57,18 @@ export default class InlineAutoformatEngine {
 	 * formatting.
 	 *
 	 *		// Use attribute name:
-	 *		new InlineAutoformatEngine( editor, /(\*\*)([^\*]+?)(\*\*)$/g, 'bold' );
+	 *		new InlineAutoformatEditing( editor, /(\*\*)([^\*]+?)(\*\*)$/g, 'bold' );
 	 *
 	 *		// Use formatting callback:
-	 *		new InlineAutoformatEngine( editor, /(\*\*)([^\*]+?)(\*\*)$/g, ( batch, validRanges ) => {
+	 *		new InlineAutoformatEditing( editor, /(\*\*)([^\*]+?)(\*\*)$/g, ( writer, validRanges ) => {
 	 *			for ( let range of validRanges ) {
-	 *				batch.setAttribute( range, command, true );
+	 *				writer.setAttribute( command, true, range );
 	 *			}
 	 *		} );
 	 */
 	constructor( editor, testRegexpOrCallback, attributeOrCallback ) {
 		let regExp;
-		let command;
+		let attributeKey;
 		let testCallback;
 		let formatCallback;
 
@@ -79,7 +79,7 @@ export default class InlineAutoformatEngine {
 		}
 
 		if ( typeof attributeOrCallback == 'string' ) {
-			command = attributeOrCallback;
+			attributeKey = attributeOrCallback;
 		} else {
 			formatCallback = attributeOrCallback;
 		}
@@ -130,81 +130,52 @@ export default class InlineAutoformatEngine {
 		} );
 
 		// A format callback run on matched text.
-		formatCallback = formatCallback || ( ( batch, validRanges ) => {
+		formatCallback = formatCallback || ( ( writer, validRanges ) => {
 			for ( const range of validRanges ) {
-				batch.setAttribute( range, command, true );
+				writer.setAttribute( attributeKey, true, range );
 			}
+
+			// 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 );
 		} );
 
-		editor.document.on( 'change', ( evt, type, changes, batch ) => {
-			if ( batch.type == 'transparent' ) {
-				return;
-			}
+		editor.model.document.on( 'change', () => {
+			const selection = editor.model.document.selection;
 
-			if ( type !== 'insert' ) {
+			// Do nothing if selection is not collapsed.
+			if ( !selection.isCollapsed ) {
 				return;
 			}
 
-			const selection = editor.document.selection;
+			const changes = Array.from( editor.model.document.differ.getChanges() );
+			const entry = changes[ 0 ];
 
-			if ( !selection.isCollapsed || !selection.focus || !selection.focus.parent ) {
+			// Typing is represented by only a single change.
+			if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
 				return;
 			}
 
 			const block = selection.focus.parent;
 			const text = getText( block ).slice( 0, selection.focus.offset );
-			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 ]
-				) );
-			} );
+			const testOutput = testCallback( text );
+			const rangesToFormat = testOutputToRanges( block, testOutput.format );
+			const rangesToRemove = testOutputToRanges( block, testOutput.remove );
 
 			if ( !( rangesToFormat.length && rangesToRemove.length ) ) {
 				return;
 			}
 
-			editor.document.enqueueChanges( () => {
-				// Create new batch to separate typing batch from the Autoformat changes.
-				const fixBatch = editor.document.batch();
-
-				const validRanges = editor.document.schema.getValidRanges( rangesToFormat, command );
+			// Use enqueueChange to create new batch to separate typing batch from the auto-format changes.
+			editor.model.enqueueChange( writer => {
+				const validRanges = editor.model.schema.getValidRanges( rangesToFormat, attributeKey );
 
 				// Apply format.
-				formatCallback( fixBatch, validRanges );
-
-				// Detach ranges used to apply Autoformat. Prevents memory leaks. #39
-				rangesToFormat.forEach( range => range.detach() );
-
-				// Remove delimiters.
-				for ( const range of rangesToRemove ) {
-					fixBatch.remove( range );
+				formatCallback( writer, validRanges );
 
-					// Prevents memory leaks.
-					// https://github.com/ckeditor/ckeditor5-autoformat/issues/39
-					range.detach();
+				// Remove delimiters - use reversed order to not mix the offsets while removing.
+				for ( const range of rangesToRemove.reverse() ) {
+					writer.remove( range );
 				}
 			} );
 		} );
@@ -219,3 +190,15 @@ export default class InlineAutoformatEngine {
 function getText( element ) {
 	return Array.from( element.getChildren() ).reduce( ( a, b ) => a + b.data, '' );
 }
+
+// Converts output of the test function provided to the InlineAutoformatEditing and converts it to the model ranges
+// inside provided block.
+//
+// @private
+// @param {module:engine/model/element~Element} block
+// @param {Array.<Array>} arrays
+function testOutputToRanges( block, arrays ) {
+	return arrays
+		.filter( array => ( array[ 0 ] !== undefined && array[ 1 ] !== undefined ) )
+		.map( array => ModelRange.createFromParentsAndOffsets( block, array[ 0 ], block, array[ 1 ] ) );
+}

+ 169 - 157
packages/ckeditor5-autoformat/tests/autoformat.js

@@ -1,17 +1,17 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 import Autoformat from '../src/autoformat';
 
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import ListEngine from '@ckeditor/ckeditor5-list/src/listengine';
-import HeadingEngine from '@ckeditor/ckeditor5-heading/src/headingengine';
-import BoldEngine from '@ckeditor/ckeditor5-basic-styles/src/boldengine';
-import CodeEngine from '@ckeditor/ckeditor5-basic-styles/src/codeengine';
-import ItalicEngine from '@ckeditor/ckeditor5-basic-styles/src/italicengine';
-import BlockQuoteEngine from '@ckeditor/ckeditor5-block-quote/src/blockquoteengine';
+import ListEditing from '@ckeditor/ckeditor5-list/src/listediting';
+import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting';
+import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
+import CodeEditing from '@ckeditor/ckeditor5-basic-styles/src/code/codeediting';
+import ItalicEditing from '@ckeditor/ckeditor5-basic-styles/src/italic/italicediting';
+import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting';
 import Enter from '@ckeditor/ckeditor5-enter/src/enter';
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
@@ -19,12 +19,12 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtest
 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';
+import HeadingCommand from '@ckeditor/ckeditor5-heading/src/headingcommand';
 
 testUtils.createSinonSandbox();
 
 describe( 'Autoformat', () => {
-	let editor, doc, batch;
+	let editor, model, doc;
 
 	beforeEach( () => {
 		return VirtualTestEditor
@@ -33,18 +33,18 @@ describe( 'Autoformat', () => {
 					Enter,
 					Paragraph,
 					Autoformat,
-					ListEngine,
-					HeadingEngine,
-					BoldEngine,
-					ItalicEngine,
-					CodeEngine,
-					BlockQuoteEngine
+					ListEditing,
+					HeadingEditing,
+					BoldEditing,
+					ItalicEditing,
+					CodeEditing,
+					BlockQuoteEditing
 				]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
-				doc = editor.document;
-				batch = doc.batch();
+				model = editor.model;
+				doc = model.document;
 			} );
 	} );
 
@@ -54,99 +54,106 @@ describe( 'Autoformat', () => {
 
 	describe( 'Bulleted list', () => {
 		it( 'should replace asterisk with bulleted list item', () => {
-			setData( doc, '<paragraph>*[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			setData( model, '<paragraph>*[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<listItem indent="0" type="bulleted">[]</listItem>' );
+			expect( getData( model ) ).to.equal( '<listItem indent="0" type="bulleted">[]</listItem>' );
 		} );
 
 		it( 'should replace minus character with bulleted list item', () => {
-			setData( doc, '<paragraph>-[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			setData( model, '<paragraph>-[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<listItem indent="0" type="bulleted">[]</listItem>' );
+			expect( getData( model ) ).to.equal( '<listItem indent="0" type="bulleted">[]</listItem>' );
 		} );
 
 		it( 'should not replace minus character when inside bulleted list item', () => {
-			setData( doc, '<listItem indent="0" type="bulleted">-[]</listItem>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			setData( model, '<listItem indent="0" type="bulleted">-[]</listItem>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<listItem indent="0" type="bulleted">- []</listItem>' );
+			expect( getData( model ) ).to.equal( '<listItem indent="0" type="bulleted">- []</listItem>' );
 		} );
 	} );
 
 	describe( 'Numbered list', () => {
-		it( 'should replace digit with numbered list item', () => {
-			setData( doc, '<paragraph>1.[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+		it( 'should replace digit with numbered list item using the dot format', () => {
+			setData( model, '<paragraph>1.[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<listItem indent="0" type="numbered">[]</listItem>' );
+			expect( getData( model ) ).to.equal( '<listItem indent="0" type="numbered">[]</listItem>' );
+		} );
+
+		it( 'should replace digit with numbered list item using the parenthesis format', () => {
+			setData( model, '<paragraph>1)[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<listItem indent="0" type="numbered">[]</listItem>' );
+		} );
+
+		it( 'should not replace digit character when there is no . or ) in the format', () => {
+			setData( model, '<paragraph>1[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<paragraph>1 []</paragraph>' );
 		} );
 
 		it( 'should not replace digit character when inside numbered list item', () => {
-			setData( doc, '<listItem indent="0" type="numbered">1.[]</listItem>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			setData( model, '<listItem indent="0" type="numbered">1.[]</listItem>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<listItem indent="0" type="numbered">1. []</listItem>' );
+			expect( getData( model ) ).to.equal( '<listItem indent="0" type="numbered">1. []</listItem>' );
 		} );
 	} );
 
 	describe( 'Heading', () => {
 		it( 'should replace hash character with heading', () => {
-			setData( doc, '<paragraph>#[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			setData( model, '<paragraph>#[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<heading1>[]</heading1>' );
+			expect( getData( model ) ).to.equal( '<heading1>[]</heading1>' );
 		} );
 
 		it( 'should replace two hash characters with heading level 2', () => {
-			setData( doc, '<paragraph>##[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			setData( model, '<paragraph>##[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<heading2>[]</heading2>' );
+			expect( getData( model ) ).to.equal( '<heading2>[]</heading2>' );
 		} );
 
 		it( 'should not replace hash character when inside heading', () => {
-			setData( doc, '<heading1>#[]</heading1>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			setData( model, '<heading1>#[]</heading1>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<heading1># []</heading1>' );
+			expect( getData( model ) ).to.equal( '<heading1># []</heading1>' );
 		} );
 
 		it( 'should work with heading1-heading6 commands regardless of the config of the heading feature', () => {
-			const spy1 = sinon.spy();
-			const spy6 = sinon.spy();
+			const command = new HeadingCommand( editor, [ 'heading1', 'heading6' ] );
 
-			class Heading6 extends Command {
-				execute() {
-					spy6();
-				}
-			}
-			class Heading1 extends Command {
-				execute() {
-					spy1();
-				}
-			}
+			const spy = sinon.spy( command, 'execute' );
 
 			function HeadingPlugin( editor ) {
-				editor.commands.add( 'heading1', new Heading1( editor ) );
-				editor.commands.add( 'heading6', new Heading6( editor ) );
+				editor.commands.add( 'heading', command );
 			}
 
 			return VirtualTestEditor
@@ -156,21 +163,26 @@ describe( 'Autoformat', () => {
 					]
 				} )
 				.then( editor => {
-					const doc = editor.document;
+					const model = editor.model;
+					const doc = model.document;
 
-					setData( doc, '<paragraph>#[]</paragraph>' );
-					doc.enqueueChanges( () => {
-						doc.batch().insert( doc.selection.getFirstPosition(), ' ' );
+					setData( model, '<paragraph>#[]</paragraph>' );
+					model.change( writer => {
+						writer.insertText( ' ', doc.selection.getFirstPosition() );
 					} );
 
-					expect( spy1.calledOnce ).to.be.true;
+					sinon.assert.calledOnce( spy );
+					sinon.assert.calledWithExactly( spy, { value: 'heading1' } );
+
+					spy.resetHistory();
 
-					setData( doc, '<paragraph>######[]</paragraph>' );
-					doc.enqueueChanges( () => {
-						doc.batch().insert( doc.selection.getFirstPosition(), ' ' );
+					setData( model, '<paragraph>######[]</paragraph>' );
+					model.change( writer => {
+						writer.insertText( ' ', doc.selection.getFirstPosition() );
 					} );
 
-					expect( spy6.calledOnce ).to.be.true;
+					sinon.assert.calledOnce( spy );
+					sinon.assert.calledWithExactly( spy, { value: 'heading6' } );
 
 					return editor.destroy();
 				} );
@@ -178,87 +190,87 @@ describe( 'Autoformat', () => {
 	} );
 
 	describe( 'Block quote', () => {
-		it( 'should replace greater-than character with heading', () => {
-			setData( doc, '<paragraph>>[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+		it( 'should replace greater-than character with block quote', () => {
+			setData( model, '<paragraph>>[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<blockQuote><paragraph>[]</paragraph></blockQuote>' );
+			expect( getData( model ) ).to.equal( '<blockQuote><paragraph>[]</paragraph></blockQuote>' );
 		} );
 
 		it( 'should not replace greater-than character when inside heading', () => {
-			setData( doc, '<heading1>>[]</heading1>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			setData( model, '<heading1>>[]</heading1>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<heading1>> []</heading1>' );
+			expect( getData( model ) ).to.equal( '<heading1>> []</heading1>' );
 		} );
 
 		it( 'should not replace greater-than character when inside numbered list', () => {
-			setData( doc, '<listItem indent="0" type="numbered">1. >[]</listItem>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			setData( model, '<listItem indent="0" type="numbered">1. >[]</listItem>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<listItem indent="0" type="numbered">1. > []</listItem>' );
+			expect( getData( model ) ).to.equal( '<listItem indent="0" type="numbered">1. > []</listItem>' );
 		} );
 
 		it( 'should not replace greater-than character when inside buletted list', () => {
-			setData( doc, '<listItem indent="0" type="bulleted">1. >[]</listItem>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			setData( model, '<listItem indent="0" type="bulleted">1. >[]</listItem>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<listItem indent="0" type="bulleted">1. > []</listItem>' );
+			expect( getData( model ) ).to.equal( '<listItem indent="0" type="bulleted">1. > []</listItem>' );
 		} );
 	} );
 
 	describe( 'Inline autoformat', () => {
 		it( 'should replace both "**" with bold', () => {
-			setData( doc, '<paragraph>**foobar*[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), '*' );
+			setData( model, '<paragraph>**foobar*[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '*', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<paragraph><$text bold="true">foobar</$text>[]</paragraph>' );
+			expect( getData( model ) ).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(), '*' );
+			setData( model, '<paragraph>*foobar[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '*', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<paragraph><$text italic="true">foobar</$text>[]</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph><$text italic="true">foobar</$text>[]</paragraph>' );
 		} );
 
 		it( 'should replace both "`" with code', () => {
-			setData( doc, '<paragraph>`foobar[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), '`' );
+			setData( model, '<paragraph>`foobar[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '`', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<paragraph><$text code="true">foobar</$text>[]</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph><$text code="true">foobar</$text>[]</paragraph>' );
 		} );
 
 		it( 'nothing should be replaces when typing "*"', () => {
-			setData( doc, '<paragraph>foobar[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), '*' );
+			setData( model, '<paragraph>foobar[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '*', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<paragraph>foobar*[]</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>foobar*[]</paragraph>' );
 		} );
 
 		it( 'should format inside the text', () => {
-			setData( doc, '<paragraph>foo **bar*[] baz</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), '*' );
+			setData( model, '<paragraph>foo **bar*[] baz</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '*', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<paragraph>foo <$text bold="true">bar</$text>[] baz</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>foo <$text bold="true">bar</$text>[] baz</paragraph>' );
 		} );
 	} );
 
@@ -270,113 +282,113 @@ describe( 'Autoformat', () => {
 				} )
 				.then( newEditor => {
 					editor = newEditor;
-					doc = editor.document;
-					batch = doc.batch();
+					model = editor.model;
+					doc = model.document;
 				} );
 		} );
 
 		it( 'should not replace asterisk with bulleted list item', () => {
-			setData( doc, '<paragraph>*[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			setData( model, '<paragraph>*[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<paragraph>* []</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>* []</paragraph>' );
 		} );
 
 		it( 'should not replace minus character with bulleted list item', () => {
-			setData( doc, '<paragraph>-[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			setData( model, '<paragraph>-[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<paragraph>- []</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>- []</paragraph>' );
 		} );
 
 		it( 'should not replace digit with numbered list item', () => {
-			setData( doc, '<paragraph>1.[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			setData( model, '<paragraph>1.[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<paragraph>1. []</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>1. []</paragraph>' );
 		} );
 
 		it( 'should not replace hash character with heading', () => {
-			setData( doc, '<paragraph>#[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			setData( model, '<paragraph>#[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<paragraph># []</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph># []</paragraph>' );
 		} );
 
 		it( 'should not replace two hash characters with heading level 2', () => {
-			setData( doc, '<paragraph>##[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			setData( model, '<paragraph>##[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<paragraph>## []</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>## []</paragraph>' );
 		} );
 
 		it( 'should not replace both "**" with bold', () => {
-			setData( doc, '<paragraph>**foobar*[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), '*' );
+			setData( model, '<paragraph>**foobar*[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '*', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<paragraph>**foobar**[]</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>**foobar**[]</paragraph>' );
 		} );
 
 		it( 'should not replace both "*" with italic', () => {
-			setData( doc, '<paragraph>*foobar[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), '*' );
+			setData( model, '<paragraph>*foobar[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '*', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<paragraph>*foobar*[]</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>*foobar*[]</paragraph>' );
 		} );
 
 		it( 'should not replace both "`" with code', () => {
-			setData( doc, '<paragraph>`foobar[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), '`' );
+			setData( model, '<paragraph>`foobar[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '`', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<paragraph>`foobar`[]</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>`foobar`[]</paragraph>' );
 		} );
 
 		it( 'should not replace ">" with block quote', () => {
-			setData( doc, '<paragraph>>[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
+			setData( model, '<paragraph>>[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( doc ) ).to.equal( '<paragraph>> []</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>> []</paragraph>' );
 		} );
 
 		it( 'should use only configured headings', () => {
 			return VirtualTestEditor
 				.create( {
-					plugins: [ Enter, Paragraph, Autoformat, ListEngine, HeadingEngine ],
+					plugins: [ Enter, Paragraph, Autoformat, ListEditing, HeadingEditing ],
 					heading: {
 						options: [
-							{ modelElement: 'paragraph' },
-							{ modelElement: 'heading1', viewElement: 'h2' }
+							{ model: 'paragraph' },
+							{ model: 'heading1', view: 'h2' }
 						]
 					}
 				} )
 				.then( editor => {
-					doc = editor.document;
-					batch = doc.batch();
+					model = editor.model;
+					doc = model.document;
 
-					setData( doc, '<paragraph>##[]</paragraph>' );
-					doc.enqueueChanges( () => {
-						batch.insert( doc.selection.getFirstPosition(), ' ' );
+					setData( model, '<paragraph>##[]</paragraph>' );
+					model.change( writer => {
+						writer.insertText( ' ', doc.selection.getFirstPosition() );
 					} );
 
-					expect( getData( doc ) ).to.equal( '<paragraph>## []</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>## []</paragraph>' );
 				} );
 		} );
 	} );

+ 123 - 0
packages/ckeditor5-autoformat/tests/blockautoformatediting.js

@@ -0,0 +1,123 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+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';
+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';
+
+testUtils.createSinonSandbox();
+
+describe( 'BlockAutoformatEditing', () => {
+	let editor, model, doc;
+
+	beforeEach( () => {
+		return VirtualTestEditor
+			.create( {
+				plugins: [ Enter, Paragraph ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				doc = model.document;
+			} );
+	} );
+
+	describe( 'Command name', () => {
+		it( 'should run a command when the pattern is matched', () => {
+			const spy = testUtils.sinon.spy();
+			editor.commands.add( 'testCommand', new TestCommand( editor, spy ) );
+			new BlockAutoformatEditing( editor, /^[*]\s$/, 'testCommand' ); // eslint-disable-line no-new
+
+			setData( model, '<paragraph>*[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			sinon.assert.calledOnce( spy );
+		} );
+
+		it( 'should remove found pattern', () => {
+			const spy = testUtils.sinon.spy();
+			editor.commands.add( 'testCommand', new TestCommand( editor, spy ) );
+			new BlockAutoformatEditing( editor, /^[*]\s$/, 'testCommand' ); // eslint-disable-line no-new
+
+			setData( model, '<paragraph>*[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			sinon.assert.calledOnce( spy );
+			expect( getData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
+		} );
+	} );
+
+	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
+
+			setData( model, '<paragraph>*[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			sinon.assert.calledOnce( spy );
+		} );
+
+		it( 'should ignore other delta operations', () => {
+			const spy = testUtils.sinon.spy();
+			new BlockAutoformatEditing( editor, /^[*]\s/, spy ); // eslint-disable-line no-new
+
+			setData( model, '<paragraph>*[]</paragraph>' );
+			model.change( writer => {
+				writer.remove( doc.selection.getFirstRange() );
+			} );
+
+			sinon.assert.notCalled( spy );
+		} );
+
+		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
+
+			setData( model, '<paragraph>[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			sinon.assert.notCalled( spy );
+		} );
+	} );
+} );
+
+/**
+ * Dummy command to execute.
+ */
+class TestCommand extends Command {
+	/**
+	 * Creates an instance of the command.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor instance.
+	 * @param {Function} onExecuteCallback execute call hook
+	 */
+	constructor( editor, onExecuteCallback ) {
+		super( editor );
+
+		this.onExecute = onExecuteCallback;
+	}
+
+	/**
+	 * Executes command.
+	 *
+	 * @protected
+	 */
+	execute() {
+		this.onExecute();
+	}
+}

+ 0 - 148
packages/ckeditor5-autoformat/tests/blockautoformatengine.js

@@ -1,148 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import BlockAutoformatEngine from '../src/blockautoformatengine';
-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';
-import Command from '@ckeditor/ckeditor5-core/src/command';
-
-testUtils.createSinonSandbox();
-
-describe( 'BlockAutoformatEngine', () => {
-	let editor, doc, batch;
-
-	beforeEach( () => {
-		return VirtualTestEditor
-			.create( {
-				plugins: [ Enter, Paragraph ]
-			} )
-			.then( newEditor => {
-				editor = newEditor;
-				doc = editor.document;
-				batch = doc.batch();
-			} );
-	} );
-
-	describe( 'Command name', () => {
-		it( 'should run a command when the pattern is matched', () => {
-			const spy = testUtils.sinon.spy();
-			editor.commands.add( 'testCommand', new TestCommand( editor, spy ) );
-			new BlockAutoformatEngine( editor, /^[*]\s$/, 'testCommand' ); // eslint-disable-line no-new
-
-			setData( doc, '<paragraph>*[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
-			} );
-
-			sinon.assert.calledOnce( spy );
-		} );
-
-		it( 'should not run a command when changes are in transparent batch', () => {
-			const spy = testUtils.sinon.spy();
-			editor.commands.add( 'testCommand', new TestCommand( editor, spy ) );
-			new BlockAutoformatEngine( editor, /^[*]\s$/, 'testCommand' ); // eslint-disable-line no-new
-
-			setData( doc, '<paragraph>*[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				doc.batch( 'transparent' ).insert( doc.selection.getFirstPosition(), ' ' );
-			} );
-
-			sinon.assert.notCalled( spy );
-		} );
-
-		it( 'should remove found pattern', () => {
-			const spy = testUtils.sinon.spy();
-			editor.commands.add( 'testCommand', new TestCommand( editor, spy ) );
-			new BlockAutoformatEngine( editor, /^[*]\s$/, 'testCommand' ); // eslint-disable-line no-new
-
-			setData( doc, '<paragraph>*[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
-			} );
-
-			sinon.assert.calledOnce( spy );
-			expect( getData( doc ) ).to.equal( '<paragraph>[]</paragraph>' );
-		} );
-	} );
-
-	describe( 'Callback', () => {
-		it( 'should run callback when the pattern is matched', () => {
-			const spy = testUtils.sinon.spy();
-			new BlockAutoformatEngine( editor, /^[*]\s$/, spy ); // eslint-disable-line no-new
-
-			setData( doc, '<paragraph>*[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
-			} );
-
-			sinon.assert.calledOnce( spy );
-		} );
-
-		it( 'should not run a callback when changes are in transparent batch', () => {
-			const spy = testUtils.sinon.spy();
-			new BlockAutoformatEngine( editor, /^[*]\s$/, spy ); // eslint-disable-line no-new
-
-			setData( doc, '<paragraph>*[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				doc.batch( 'transparent' ).insert( doc.selection.getFirstPosition(), ' ' );
-			} );
-
-			sinon.assert.notCalled( spy );
-		} );
-
-		it( 'should ignore other delta operations', () => {
-			const spy = testUtils.sinon.spy();
-			new BlockAutoformatEngine( editor, /^[*]\s/, spy ); // eslint-disable-line no-new
-
-			setData( doc, '<paragraph>*[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.remove( doc.selection.getFirstRange() );
-			} );
-
-			sinon.assert.notCalled( spy );
-		} );
-
-		it( 'should stop if there is no text to run matching on', () => {
-			const spy = testUtils.sinon.spy();
-			new BlockAutoformatEngine( editor, /^[*]\s/, spy ); // eslint-disable-line no-new
-
-			setData( doc, '<paragraph>[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), '' );
-			} );
-
-			sinon.assert.notCalled( spy );
-		} );
-	} );
-} );
-
-/**
- * Dummy command to execute.
- */
-class TestCommand extends Command {
-	/**
-	 * Creates an instance of the command.
-	 *
-	 * @param {module:core/editor~Editor} editor Editor instance.
-	 * @param {Function} onExecuteCallback execute call hook
-	 */
-	constructor( editor, onExecuteCallback ) {
-		super( editor );
-
-		this.onExecute = onExecuteCallback;
-	}
-
-	/**
-	 * Executes command.
-	 *
-	 * @protected
-	 */
-	execute() {
-		this.onExecute();
-	}
-}

+ 119 - 0
packages/ckeditor5-autoformat/tests/inlineautoformatediting.js

@@ -0,0 +1,119 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+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';
+
+testUtils.createSinonSandbox();
+
+describe( 'InlineAutoformatEditing', () => {
+	let editor, model, doc;
+
+	beforeEach( () => {
+		return VirtualTestEditor
+			.create( {
+				plugins: [ Enter, Paragraph ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				doc = model.document;
+
+				model.schema.extend( '$text', { allowAttributes: 'testAttribute' } );
+			} );
+	} );
+
+	describe( 'attribute', () => {
+		it( 'should stop early if there are less than 3 capture groups', () => {
+			new InlineAutoformatEditing( editor, /(\*)(.+?)\*/g, 'testAttribute' ); // eslint-disable-line no-new
+
+			setData( model, '<paragraph>*foobar[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '*', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<paragraph>*foobar*[]</paragraph>' );
+		} );
+
+		it( 'should apply an attribute when the pattern is matched', () => {
+			new InlineAutoformatEditing( editor, /(\*)(.+?)(\*)/g, 'testAttribute' ); // eslint-disable-line no-new
+
+			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>' );
+		} );
+
+		it( 'should stop early if selection is not collapsed', () => {
+			new InlineAutoformatEditing( editor, /(\*)(.+?)\*/g, 'testAttribute' ); // eslint-disable-line no-new
+
+			setData( model, '<paragraph>*foob[ar]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '*', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).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 InlineAutoformatEditing( editor, testStub, formatSpy ); // eslint-disable-line no-new
+
+			setData( model, '<paragraph>*[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', 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 InlineAutoformatEditing( editor, testStub, formatSpy ); // eslint-disable-line no-new
+
+			setData( model, '<paragraph>*[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', 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 InlineAutoformatEditing( editor, testStub, formatSpy ); // eslint-disable-line no-new
+
+			setData( model, '<paragraph>[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			sinon.assert.notCalled( formatSpy );
+		} );
+	} );
+} );

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

@@ -1,168 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import InlineAutoformatEngine from '../src/inlineautoformatengine';
-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';
-
-testUtils.createSinonSandbox();
-
-describe( 'InlineAutoformatEngine', () => {
-	let editor, doc, batch;
-
-	beforeEach( () => {
-		return VirtualTestEditor
-			.create( {
-				plugins: [ 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' ); // eslint-disable-line no-new
-
-			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' ); // eslint-disable-line no-new
-
-			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 not apply an attribute when changes are in transparent batch', () => {
-			new InlineAutoformatEngine( editor, /(\*)(.+?)(\*)/g, 'testAttribute' ); // eslint-disable-line no-new
-
-			setData( doc, '<paragraph>*foobar[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				doc.batch( 'transparent' ).insert( doc.selection.getFirstPosition(), '*' );
-			} );
-
-			expect( getData( doc ) ).to.equal( '<paragraph>*foobar*[]</paragraph>' );
-		} );
-
-		it( 'should stop early if selection is not collapsed', () => {
-			new InlineAutoformatEngine( editor, /(\*)(.+?)\*/g, 'testAttribute' ); // eslint-disable-line no-new
-
-			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 not run a callback when changes are in transparent batch', () => {
-			const spy = testUtils.sinon.spy();
-			new InlineAutoformatEngine( editor, /(\*)(.+?)(\*)/g, spy ); // eslint-disable-line no-new
-
-			setData( doc, '<paragraph>*foobar[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				doc.batch( 'transparent' ).insert( doc.selection.getFirstPosition(), '*' );
-			} );
-
-			sinon.assert.notCalled( spy );
-		} );
-
-		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 ); // eslint-disable-line no-new
-
-			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 ); // eslint-disable-line no-new
-
-			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 ); // eslint-disable-line no-new
-
-			setData( doc, '<paragraph>[]</paragraph>' );
-			doc.enqueueChanges( () => {
-				batch.insert( doc.selection.getFirstPosition(), ' ' );
-			} );
-
-			sinon.assert.notCalled( formatSpy );
-		} );
-
-		it( 'should detach removed ranges', () => {
-			const detachSpies = [];
-			const callback = fixBatch => testUtils.sinon.stub( fixBatch, 'remove' ).callsFake( saveDetachSpy );
-			testUtils.sinon.stub( editor.document.schema, 'getValidRanges' )
-				.callThrough()
-				.callsFake( ranges => ranges.map( saveDetachSpy ) );
-
-			new InlineAutoformatEngine( editor, /(\*)(.+?)(\*)/g, callback ); // eslint-disable-line no-new
-
-			setData( doc, '<paragraph>*foobar[]</paragraph>' );
-
-			doc.enqueueChanges( () => {
-				doc.batch().insert( doc.selection.getFirstPosition(), '*' );
-			} );
-
-			// There should be two removed ranges and one range used to apply autoformat.
-			expect( detachSpies ).to.have.length( 3 );
-
-			for ( const spy of detachSpies ) {
-				testUtils.sinon.assert.calledOnce( spy );
-			}
-
-			function saveDetachSpy( range ) {
-				detachSpies.push( testUtils.sinon.spy( range, 'detach' ) );
-			}
-		} );
-	} );
-} );

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -21,7 +21,7 @@ import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 		plugins: [ Enter, Typing, Paragraph, Undo, Bold, Italic, Code, Heading, List, Autoformat, BlockQuote ],
-		toolbar: [ 'headings', 'numberedList', 'bulletedList', 'blockQuote', 'bold', 'italic', 'code', 'undo', 'redo' ]
+		toolbar: [ 'heading', '|', 'numberedList', 'bulletedList', 'blockQuote', 'bold', 'italic', 'code', 'undo', 'redo' ]
 	} )
 	.then( editor => {
 		window.editor = editor;

+ 3 - 1
packages/ckeditor5-autoformat/tests/manual/autoformat.md

@@ -6,7 +6,9 @@
 
 1. Type `>` and press the space in an empty paragraph to replace it with a block quote.
 
-1. Type a number from the range **1-3** to replace an empty paragraph with a numbered list item.
+1. Type a number from the range **1-3** followed by a `.` and press space to replace an empty paragraph with a numbered list item.
+
+1. Type a number from the range **1-3** followed by a `)` and press space to replace an empty paragraph with a numbered list item.
 
 1. Type `*foobar*`/`_foobar_` to italicize `foobar`. `*`/`_` should be removed.
 

+ 193 - 0
packages/ckeditor5-autoformat/tests/undointegration.js

@@ -0,0 +1,193 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Autoformat from '../src/autoformat';
+
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import ListEditing from '@ckeditor/ckeditor5-list/src/listediting';
+import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting';
+import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
+import CodeEditing from '@ckeditor/ckeditor5-basic-styles/src/code/codeediting';
+import ItalicEditing from '@ckeditor/ckeditor5-basic-styles/src/italic/italicediting';
+import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting';
+import Enter from '@ckeditor/ckeditor5-enter/src/enter';
+import Undo from '@ckeditor/ckeditor5-undo/src/undoediting';
+
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+
+import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+testUtils.createSinonSandbox();
+
+describe( 'Autoformat', () => {
+	let editor, model, doc;
+
+	beforeEach( () => {
+		return VirtualTestEditor
+			.create( {
+				plugins: [
+					Enter,
+					Undo,
+					Paragraph,
+					Autoformat,
+					ListEditing,
+					HeadingEditing,
+					BoldEditing,
+					ItalicEditing,
+					CodeEditing,
+					BlockQuoteEditing
+				]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				doc = model.document;
+			} );
+	} );
+
+	afterEach( () => {
+		return editor.destroy();
+	} );
+
+	describe( 'inline', () => {
+		it( 'should undo replacing "**" with bold', () => {
+			setData( model, '<paragraph>**foobar*[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '*', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<paragraph><$text bold="true">foobar</$text>[]</paragraph>' );
+			editor.execute( 'undo' );
+			expect( getData( model ) ).to.equal( '<paragraph>**foobar**[]</paragraph>' );
+		} );
+
+		it( 'should undo replacing "__" with bold', () => {
+			setData( model, '<paragraph>__foobar_[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '_', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<paragraph><$text bold="true">foobar</$text>[]</paragraph>' );
+			editor.execute( 'undo' );
+			expect( getData( model ) ).to.equal( '<paragraph>__foobar__[]</paragraph>' );
+		} );
+
+		it( 'should undo replacing "*" with italic', () => {
+			setData( model, '<paragraph>*foobar[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '*', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<paragraph><$text italic="true">foobar</$text>[]</paragraph>' );
+			editor.execute( 'undo' );
+			expect( getData( model ) ).to.equal( '<paragraph>*foobar*[]</paragraph>' );
+		} );
+
+		it( 'should undo replacing "_" with italic', () => {
+			setData( model, '<paragraph>_foobar[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '_', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<paragraph><$text italic="true">foobar</$text>[]</paragraph>' );
+			editor.execute( 'undo' );
+			expect( getData( model ) ).to.equal( '<paragraph>_foobar_[]</paragraph>' );
+		} );
+
+		it( 'should undo replacing "`" with code', () => {
+			setData( model, '<paragraph>`foobar[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '`', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<paragraph><$text code="true">foobar</$text>[]</paragraph>' );
+			editor.execute( 'undo' );
+			expect( getData( model ) ).to.equal( '<paragraph>`foobar`[]</paragraph>' );
+		} );
+	} );
+
+	describe( 'block', () => {
+		it( 'should work when replacing asterisk', () => {
+			setData( model, '<paragraph>*[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<listItem indent="0" type="bulleted">[]</listItem>' );
+
+			editor.execute( 'undo' );
+			expect( getData( model ) ).to.equal( '<paragraph>* []</paragraph>' );
+		} );
+
+		it( 'should work when replacing minus character', () => {
+			setData( model, '<paragraph>-[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<listItem indent="0" type="bulleted">[]</listItem>' );
+
+			editor.execute( 'undo' );
+			expect( getData( model ) ).to.equal( '<paragraph>- []</paragraph>' );
+		} );
+
+		it( 'should work when replacing digit with numbered list item using the dot format', () => {
+			setData( model, '<paragraph>1.[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<listItem indent="0" type="numbered">[]</listItem>' );
+
+			editor.execute( 'undo' );
+			expect( getData( model ) ).to.equal( '<paragraph>1. []</paragraph>' );
+		} );
+
+		it( 'should work when replacing digit with numbered list item using the parenthesis format', () => {
+			setData( model, '<paragraph>1)[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<listItem indent="0" type="numbered">[]</listItem>' );
+			editor.execute( 'undo' );
+			expect( getData( model ) ).to.equal( '<paragraph>1) []</paragraph>' );
+		} );
+
+		it( 'should work when replacing hash character with heading', () => {
+			setData( model, '<paragraph>#[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<heading1>[]</heading1>' );
+			editor.execute( 'undo' );
+			expect( getData( model ) ).to.equal( '<paragraph># []</paragraph>' );
+		} );
+
+		it( 'should work when replacing two hash characters with heading level 2', () => {
+			setData( model, '<paragraph>##[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<heading2>[]</heading2>' );
+			editor.execute( 'undo' );
+			expect( getData( model ) ).to.equal( '<paragraph>## []</paragraph>' );
+		} );
+
+		it( 'should work when replacing greater-than character with block quote', () => {
+			setData( model, '<paragraph>>[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<blockQuote><paragraph>[]</paragraph></blockQuote>' );
+			editor.execute( 'undo' );
+			expect( getData( model ) ).to.equal( '<paragraph>> []</paragraph>' );
+		} );
+	} );
+} );