ソースを参照

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

Release: v1.0.0-beta.1.
Piotrek Koszuliński 7 年 前
コミット
b7f71c8496

+ 1 - 1
packages/ckeditor5-paragraph/.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-paragraph/.travis.yml


+ 7 - 0
packages/ckeditor5-paragraph/CHANGELOG.md

@@ -1,6 +1,13 @@
 Changelog
 =========
 
+## [1.0.0-beta.1](https://github.com/ckeditor/ckeditor5-paragraph/compare/v1.0.0-alpha.2...v1.0.0-beta.1) (2018-03-15)
+
+### Other changes
+
+* Aligned feature class naming to the new scheme. ([69e98d3](https://github.com/ckeditor/ckeditor5-paragraph/commit/69e98d3))
+
+
 ## [1.0.0-alpha.2](https://github.com/ckeditor/ckeditor5-paragraph/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2017-11-14)
 
 Internal changes only (updated dependencies, documentation, etc.).

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

@@ -2,7 +2,7 @@ Software License Agreement
 ==========================
 
 **CKEditor 5 Paragraph Feature** – https://github.com/ckeditor/ckeditor5-paragraph <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-paragraph/README.md

@@ -4,7 +4,9 @@ CKEditor 5 paragraph 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-paragraph.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-paragraph)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-paragraph.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-paragraph)
-[![Test Coverage](https://codeclimate.com/github/ckeditor/ckeditor5-paragraph/badges/coverage.svg)](https://codeclimate.com/github/ckeditor/ckeditor5-paragraph/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-paragraph/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-paragraph?branch=master)
+<br>
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-paragraph/status.svg)](https://david-dm.org/ckeditor/ckeditor5-paragraph)
 [![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-paragraph/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-paragraph?type=dev)
 

+ 15 - 15
packages/ckeditor5-paragraph/package.json

@@ -1,29 +1,29 @@
 {
   "name": "@ckeditor/ckeditor5-paragraph",
-  "version": "1.0.0-alpha.2",
+  "version": "1.0.0-beta.1",
   "description": "Paragraph 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-utils": "^1.0.0-alpha.2"
+    "@ckeditor/ckeditor5-core": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-engine": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-utils": "^1.0.0-beta.1"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-basic-styles": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-clipboard": "^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-link": "^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-clipboard": "^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-link": "^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",

+ 94 - 188
packages/ckeditor5-paragraph/src/paragraph.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.
  */
 
@@ -8,13 +8,11 @@
  */
 
 import ParagraphCommand from './paragraphcommand';
-import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-
-import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
-import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
 
-import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
-import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import { SchemaContext } from '@ckeditor/ckeditor5-engine/src/model/schema';
+import Position from '@ckeditor/ckeditor5-engine/src/model/position';
+import Range from '@ckeditor/ckeditor5-engine/src/model/range';
 
 /**
  * The paragraph feature for the editor.
@@ -35,63 +33,108 @@ export default class Paragraph extends Plugin {
 	 */
 	init() {
 		const editor = this.editor;
-		const doc = editor.document;
+		const model = editor.model;
 		const data = editor.data;
-		const editing = editor.editing;
 
 		editor.commands.add( 'paragraph', new ParagraphCommand( editor ) );
 
 		// Schema.
-		doc.schema.registerItem( 'paragraph', '$block' );
-
-		// Build converter from model to view for data and editing pipelines.
-		buildModelConverter().for( data.modelToView, editing.modelToView )
-			.fromElement( 'paragraph' )
-			.toElement( 'p' );
+		model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
-		// Build converter from view to model for data pipeline.
-		buildViewConverter().for( data.viewToModel )
-			.fromElement( 'p' )
-			.toElement( 'paragraph' );
+		editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
 
 		// Content autoparagraphing. --------------------------------------------------
 
-		// Step 1.
-		// "Second chance" converters for elements and texts which were not allowed in their original locations.
-		// They check if this element/text could be converted if it was in a paragraph.
-		// Forcefully converted items will be temporarily in an invalid context. It's going to be fixed in step 2.
+		// Handles elements not converted by plugins and checks if would be converted if
+		// we wraps them by a paragraph or changes them to a paragraph.
+		data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
+			// When element is already consumed by higher priority converters then do nothing.
+			if ( !conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
+				return;
+			}
+
+			// When element is paragraph-like lets try to change it into a paragraph.
+			if ( Paragraph.paragraphLikeElements.has( data.viewItem.name ) ) {
+				if ( data.viewItem.isEmpty ) {
+					return;
+				}
+
+				const paragraph = conversionApi.writer.createElement( 'paragraph' );
 
-		// Executed after converter added by a feature, but before "default" to-model-fragment converter.
-		data.viewToModel.on( 'element', convertAutoparagraphableItem, { priority: 'low' } );
-		// Executed after default text converter.
-		data.viewToModel.on( 'text', convertAutoparagraphableItem, { priority: 'lowest' } );
+				// Find allowed parent for paragraph that we are going to insert.
+				// If current parent does not allow to insert paragraph but one of the ancestors does
+				// then split nodes to allowed parent.
+				const splitResult = conversionApi.splitToAllowedParent( paragraph, data.modelCursor );
 
-		// Step 2.
-		// After an item is "forced" to be converted by `convertAutoparagraphableItem`, we need to actually take
-		// care of adding the paragraph (assumed in `convertAutoparagraphableItem`) and wrap that item in it.
+				// When there is no split result it means that we can't insert paragraph in this position.
+				if ( !splitResult ) {
+					return;
+				}
+
+				// Insert paragraph in allowed position.
+				conversionApi.writer.insert( paragraph, splitResult.position );
+
+				// Convert children to paragraph.
+				const { modelRange } = conversionApi.convertChildren( data.viewItem, Position.createAt( paragraph ) );
+
+				// Output range starts before paragraph but ends inside it after last child.
+				// This is because we want to keep siblings inside the same paragraph as long as it is possible.
+				// When next node won't be allowed in a paragraph it will split this paragraph anyway.
+				data.modelRange = new Range( Position.createBefore( paragraph ), modelRange.end );
+				data.modelCursor = data.modelRange.end;
+
+			// When element is not paragraph-like lets try to wrap it by a paragraph.
+			} else if ( isParagraphable( data.viewItem, data.modelCursor, conversionApi.schema ) ) {
+				data = Object.assign( data, wrapInParagraph( data.viewItem, data.modelCursor, conversionApi ) );
+			}
+		}, { priority: 'low' } );
+
+		// Handles not converted text nodes and checks if would be converted if we wraps then by a paragraph.
+		data.upcastDispatcher.on( 'text', ( evt, data, conversionApi ) => {
+			// When node is already converted then do nothing.
+			if ( data.modelRange ) {
+				return;
+			}
 
-		// Executed after all converters (even default ones).
-		data.viewToModel.on( 'element', autoparagraphItems, { priority: 'lowest' } );
-		data.viewToModel.on( 'documentFragment', autoparagraphItems, { priority: 'lowest' } );
+			if ( isParagraphable( data.viewItem, data.modelCursor, conversionApi.schema ) ) {
+				data = Object.assign( data, wrapInParagraph( data.viewItem, data.modelCursor, conversionApi ) );
+			}
+		}, { priority: 'lowest' } );
 
 		// Empty roots autoparagraphing. -----------------------------------------------
 
 		// Post-fixer which takes care of adding empty paragraph elements to empty roots.
 		// Besides fixing content on #changesDone we also need to handle #dataReady because
 		// if initial data is empty or setData() wasn't even called there will be no #change fired.
-		doc.on( 'change', ( evt, type, changes, batch ) => {
-			if ( batch.type == 'transparent' ) {
-				return;
-			}
+		model.document.registerPostFixer( writer => this._autoparagraphEmptyRoots( writer ) );
 
-			findEmptyRoots( doc, batch );
-		} );
-		doc.on( 'changesDone', autoparagraphEmptyRoots, { priority: 'lowest' } );
 		editor.on( 'dataReady', () => {
-			findEmptyRoots( doc, doc.batch( 'transparent' ) );
-			autoparagraphEmptyRoots();
+			model.enqueueChange( 'transparent', writer => this._autoparagraphEmptyRoots( writer ) );
 		}, { priority: 'lowest' } );
 	}
+
+	/**
+	 * Fixes all empty roots.
+	 *
+	 * @private
+	 * @returns {Boolean} `true` if any change has been applied, `false` otherwise.
+	 */
+	_autoparagraphEmptyRoots( writer ) {
+		const model = this.editor.model;
+
+		for ( const rootName of model.document.getRootNames() ) {
+			const root = model.document.getRoot( rootName );
+
+			if ( root.isEmpty && root.rootName != '$graveyard' ) {
+				// If paragraph element is allowed in the root, create paragraph element.
+				if ( model.schema.checkChild( root, 'paragraph' ) ) {
+					writer.insertElement( 'paragraph', root );
+
+					return true;
+				}
+			}
+		}
+	}
 }
 
 /**
@@ -141,162 +184,25 @@ Paragraph.paragraphLikeElements = new Set( [
 	'td'
 ] );
 
-// This converter forces a conversion of a non-consumed view item, if that item would be allowed by schema and converted it if was
-// inside a paragraph element. The converter checks whether conversion would be possible if there was a paragraph element
-// between `data.input` item and its parent. If the conversion would be allowed, the converter adds `"paragraph"` to the
-// context and fires conversion for `data.input` again.
-function convertAutoparagraphableItem( evt, data, consumable, conversionApi ) {
-	// If the item wasn't consumed by some of the dedicated converters...
-	if ( !consumable.test( data.input, { name: data.input.name } ) ) {
-		return;
-	}
-
-	// But would be allowed if it was in a paragraph...
-	if ( !isParagraphable( data.input, data.context, conversionApi.schema, false ) ) {
-		return;
-	}
+function wrapInParagraph( input, position, conversionApi ) {
+	const paragraph = conversionApi.writer.createElement( 'paragraph' );
 
-	// Convert that item in a paragraph context.
-	data.context.push( 'paragraph' );
-	const item = conversionApi.convertItem( data.input, consumable, data );
-	data.context.pop();
-
-	data.output = item;
+	conversionApi.writer.insert( paragraph, position );
+	return conversionApi.convertItem( input, Position.createAt( paragraph ) );
 }
 
-// This converter checks all children of an element or document fragment that has been converted and wraps
-// children in a paragraph element if it is allowed by schema.
-//
-// Basically, after an item is "forced" to be converted by `convertAutoparagraphableItem`, we need to actually take
-// care of adding the paragraph (assumed in `convertAutoparagraphableItem`) and wrap that item in it.
-function autoparagraphItems( evt, data, consumable, conversionApi ) {
-	// Autoparagraph only if the element has been converted.
-	if ( !data.output ) {
-		return;
-	}
-
-	const isParagraphLike = Paragraph.paragraphLikeElements.has( data.input.name ) && !data.output.is( 'element' );
-
-	// Keep in mind that this converter is added to all elements and document fragments.
-	// This means that we have to make a smart decision in which elements (at what level) auto-paragraph should be inserted.
-	// There are three situations when it is correct to add paragraph:
-	//   -	we are converting a view document fragment: this means that we are at the top level of conversion and we should
-	//		add paragraph elements for "bare" texts (unless converting in $clipboardHolder, but this is covered by schema),
-	//   -	we are converting an element that was converted to model element: this means that it will be represented in model
-	//		and has added its context when converting children - we should add paragraph for those items that passed
-	//		in `convertAutoparagraphableItem`, because it is correct for them to be autoparagraphed,
-	//	 -	we are converting "paragraph-like" element, which children should always be autoparagraphed (if it is allowed by schema,
-	//		so we won't end up with, i.e., paragraph inside paragraph, if paragraph was in paragraph-like element).
-	const shouldAutoparagraph =
-		( data.input.is( 'documentFragment' ) ) ||
-		( data.input.is( 'element' ) && data.output.is( 'element' ) ) ||
-		isParagraphLike;
-
-	if ( !shouldAutoparagraph ) {
-		return;
-	}
-
-	// Take care of proper context. This is important for `isParagraphable` checks.
-	const needsNewContext = data.output.is( 'element' );
-
-	if ( needsNewContext ) {
-		data.context.push( data.output );
-	}
-
-	// `paragraph` element that will wrap auto-paragraphable children.
-	let autoParagraph = null;
-
-	// Check children and wrap them in a `paragraph` element if they need to be wrapped.
-	// Be smart when wrapping children and put all auto-paragraphable siblings in one `paragraph` parent:
-	// foo<$text bold="true">bar</$text><paragraph>xxx</paragraph>baz      --->
-	// <paragraph>foo<$text bold="true">bar</$text></paragraph><paragraph>xxx</paragraph><paragraph>baz</paragraph>
-	for ( let i = 0; i < data.output.childCount; i++ ) {
-		const child = data.output.getChild( i );
-
-		if ( isParagraphable( child, data.context, conversionApi.schema, isParagraphLike ) ) {
-			// If there is no wrapping `paragraph` element, create it.
-			if ( !autoParagraph ) {
-				autoParagraph = new ModelElement( 'paragraph' );
-				data.output.insertChildren( child.index, autoParagraph );
-			}
-			// Otherwise, use existing `paragraph` and just fix iterator.
-			// Thanks to reusing `paragraph` element, multiple siblings ends up in same container.
-			else {
-				i--;
-			}
-
-			child.remove();
-			autoParagraph.appendChildren( child );
-		} else {
-			// That was not a paragraphable children, reset `paragraph` wrapper - following auto-paragraphable children
-			// need to be placed in a new `paragraph` element.
-			autoParagraph = null;
-		}
-	}
-
-	if ( needsNewContext ) {
-		data.context.pop();
-	}
-}
+function isParagraphable( node, position, schema ) {
+	const context = new SchemaContext( position );
 
-function isParagraphable( node, context, schema, insideParagraphLikeElement ) {
-	const name = node.name || '$text';
-
-	// Node is paragraphable if it is inside paragraph like element, or...
-	// It is not allowed at this context...
-	if ( !insideParagraphLikeElement && schema.check( { name, inside: context } ) ) {
-		return false;
-	}
-
-	// And paragraph is allowed in this context...
-	if ( !schema.check( { name: 'paragraph', inside: context } ) ) {
+	// When paragraph is allowed in this context...
+	if ( !schema.checkChild( context, 'paragraph' ) ) {
 		return false;
 	}
 
 	// And a node would be allowed in this paragraph...
-	if ( !schema.check( { name, inside: context.concat( 'paragraph' ) } ) ) {
+	if ( !schema.checkChild( context.push( 'paragraph' ), node ) ) {
 		return false;
 	}
 
 	return true;
 }
-
-// Looks through all roots created in document and marks every empty root, saving which batch made it empty.
-const rootsToFix = new Map();
-
-function findEmptyRoots( doc, batch ) {
-	for ( const rootName of doc.getRootNames() ) {
-		const root = doc.getRoot( rootName );
-
-		if ( root.isEmpty ) {
-			if ( !rootsToFix.has( root ) ) {
-				rootsToFix.set( root, batch );
-			}
-		} else {
-			rootsToFix.delete( root );
-		}
-	}
-}
-
-// Fixes all empty roots.
-function autoparagraphEmptyRoots() {
-	for ( const [ root, batch ] of rootsToFix ) {
-		// Only empty roots are in `rootsToFix`. Even if root got content during `changesDone` event (because of, for example
-		// other feature), this will fire `findEmptyRoots` and remove that root from `rootsToFix`. So we are guaranteed
-		// to have only empty roots here.
-		const query = { name: 'paragraph', inside: [ root ] };
-		const doc = batch.document;
-		const schema = doc.schema;
-
-		// If paragraph element is allowed in the root, create paragraph element.
-		if ( schema.check( query ) ) {
-			doc.enqueueChanges( () => {
-				// Remove root from `rootsToFix` here, before executing batch, to prevent infinite loops.
-				rootsToFix.delete( root );
-
-				// Fix empty root.
-				batch.insert( ModelPosition.createAt( root ), new ModelElement( 'paragraph' ) );
-			} );
-		}
-	}
-}

+ 10 - 15
packages/ckeditor5-paragraph/src/paragraphcommand.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.
  */
 
@@ -8,7 +8,6 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import Position from '@ckeditor/ckeditor5-engine/src/model/position';
 import first from '@ckeditor/ckeditor5-utils/src/first';
 
 /**
@@ -29,11 +28,12 @@ export default class ParagraphCommand extends Command {
 	 * @inheritDoc
 	 */
 	refresh() {
-		const document = this.editor.document;
+		const model = this.editor.model;
+		const document = model.document;
 		const block = first( document.selection.getSelectedBlocks() );
 
 		this.value = !!block && block.is( 'paragraph' );
-		this.isEnabled = !!block && checkCanBecomeParagraph( block, document.schema );
+		this.isEnabled = !!block && checkCanBecomeParagraph( block, model.schema );
 	}
 
 	/**
@@ -42,21 +42,19 @@ export default class ParagraphCommand extends Command {
 	 *
 	 * @fires execute
 	 * @param {Object} [options] Options for the executed command.
-	 * @param {module:engine/model/batch~Batch} [options.batch] A batch to collect all the change steps.
-	 * A new batch will be created if this option is not set.
 	 * @param {module:engine/model/selection~Selection} [options.selection] The selection that the command should be applied to.
 	 * By default, if not provided, the command is applied to the {@link module:engine/model/document~Document#selection}.
 	 */
 	execute( options = {} ) {
-		const document = this.editor.document;
+		const model = this.editor.model;
+		const document = model.document;
 
-		document.enqueueChanges( () => {
-			const batch = options.batch || document.batch();
+		model.change( writer => {
 			const blocks = ( options.selection || document.selection ).getSelectedBlocks();
 
 			for ( const block of blocks ) {
-				if ( !block.is( 'paragraph' ) && checkCanBecomeParagraph( block, document.schema ) ) {
-					batch.rename( block, 'paragraph' );
+				if ( !block.is( 'paragraph' ) && checkCanBecomeParagraph( block, model.schema ) ) {
+					writer.rename( block, 'paragraph' );
 				}
 			}
 		} );
@@ -70,8 +68,5 @@ export default class ParagraphCommand extends Command {
 // @param {module:engine/model/schema~Schema} schema The schema of the document.
 // @returns {Boolean}
 function checkCanBecomeParagraph( block, schema ) {
-	return schema.check( {
-		name: 'paragraph',
-		inside: Position.createBefore( block )
-	} );
+	return schema.checkChild( block.parent, 'paragraph' ) && !schema.isObject( block );
 }

+ 1 - 1
packages/ckeditor5-paragraph/tests/manual/clipboard-integration.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.
  */
 

+ 29 - 33
packages/ckeditor5-paragraph/tests/paragraph-intergration.js

@@ -1,12 +1,12 @@
 /**
- * @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 Paragraph from '../src/paragraph';
 import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
-import UndoEngine from '@ckeditor/ckeditor5-undo/src/undoengine';
-import HeadingEngine from '@ckeditor/ckeditor5-heading/src/headingengine';
+import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
+import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import {
 	getData as getModelData,
@@ -21,16 +21,15 @@ describe( 'Paragraph feature – integration', () => {
 				.create( { plugins: [ Paragraph, Clipboard ] } )
 				.then( newEditor => {
 					const editor = newEditor;
-					const doc = editor.document;
 					const clipboard = editor.plugins.get( 'Clipboard' );
 
-					setModelData( doc, '<paragraph>[]</paragraph>' );
+					setModelData( editor.model, '<paragraph>[]</paragraph>' );
 
 					clipboard.fire( 'inputTransformation', {
 						content: parseView( '<h1>foo</h1><h2>bar</h2><p>bom</p>' )
 					} );
 
-					expect( getModelData( doc ) ).to.equal(
+					expect( getModelData( editor.model ) ).to.equal(
 						'<paragraph>foo</paragraph><paragraph>bar</paragraph><paragraph>bom[]</paragraph>'
 					);
 				} );
@@ -39,19 +38,18 @@ describe( 'Paragraph feature – integration', () => {
 		// Explainer: the heading feature is configured to handle h2-h4 elements, so h1 has no handler.
 		it( 'pastes h1+h2+p as p+h2+p when heading feature is present', () => {
 			return VirtualTestEditor
-				.create( { plugins: [ Paragraph, Clipboard, HeadingEngine ] } )
+				.create( { plugins: [ Paragraph, Clipboard, HeadingEditing ] } )
 				.then( newEditor => {
 					const editor = newEditor;
-					const doc = editor.document;
 					const clipboard = editor.plugins.get( 'Clipboard' );
 
-					setModelData( doc, '<paragraph>[]</paragraph>' );
+					setModelData( editor.model, '<paragraph>[]</paragraph>' );
 
 					clipboard.fire( 'inputTransformation', {
 						content: parseView( '<h1>foo</h1><h2>bar</h2><p>bom</p>' )
 					} );
 
-					expect( getModelData( doc ) ).to.equal(
+					expect( getModelData( editor.model ) ).to.equal(
 						'<paragraph>foo</paragraph><heading1>bar</heading1><paragraph>bom[]</paragraph>'
 					);
 				} );
@@ -62,16 +60,15 @@ describe( 'Paragraph feature – integration', () => {
 				.create( { plugins: [ Paragraph, Clipboard ] } )
 				.then( newEditor => {
 					const editor = newEditor;
-					const doc = editor.document;
 					const clipboard = editor.plugins.get( 'Clipboard' );
 
-					setModelData( doc, '<paragraph>[]</paragraph>' );
+					setModelData( editor.model, '<paragraph>[]</paragraph>' );
 
 					clipboard.fire( 'inputTransformation', {
 						content: parseView( '<ul><li>foo</li><li>bar</li></ul>' )
 					} );
 
-					expect( getModelData( doc ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar[]</paragraph>' );
+					expect( getModelData( editor.model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar[]</paragraph>' );
 				} );
 		} );
 
@@ -79,19 +76,18 @@ describe( 'Paragraph feature – integration', () => {
 		// handle the li element.
 		it( 'pastes ul>li>h2+h3+p as h2+h3+p when heading feature is present', () => {
 			return VirtualTestEditor
-				.create( { plugins: [ Paragraph, Clipboard, HeadingEngine ] } )
+				.create( { plugins: [ Paragraph, Clipboard, HeadingEditing ] } )
 				.then( newEditor => {
 					const editor = newEditor;
-					const doc = editor.document;
 					const clipboard = editor.plugins.get( 'Clipboard' );
 
-					setModelData( doc, '<paragraph>[]</paragraph>' );
+					setModelData( editor.model, '<paragraph>[]</paragraph>' );
 
 					clipboard.fire( 'inputTransformation', {
 						content: parseView( '<ul><li>x</li><li><h2>foo</h2><h3>bar</h3><p>bom</p></li><li>x</li></ul>' )
 					} );
 
-					expect( getModelData( doc ) ).to.equal(
+					expect( getModelData( editor.model ) ).to.equal(
 						'<paragraph>x</paragraph>' +
 						'<heading1>foo</heading1><heading2>bar</heading2><paragraph>bom</paragraph>' +
 						'<paragraph>x[]</paragraph>'
@@ -105,16 +101,15 @@ describe( 'Paragraph feature – integration', () => {
 				.create( { plugins: [ Paragraph, Clipboard ] } )
 				.then( newEditor => {
 					const editor = newEditor;
-					const doc = editor.document;
 					const clipboard = editor.plugins.get( 'Clipboard' );
 
-					setModelData( doc, '<paragraph>[]</paragraph>' );
+					setModelData( editor.model, '<paragraph>[]</paragraph>' );
 
 					clipboard.fire( 'inputTransformation', {
 						content: parseView( '<ul><li>a<ul><li>b</li><li>c</li></ul></li></ul>' )
 					} );
 
-					expect( getModelData( doc ) ).to.equal(
+					expect( getModelData( editor.model ) ).to.equal(
 						'<paragraph>a</paragraph>' +
 						'<paragraph>b</paragraph>' +
 						'<paragraph>c[]</paragraph>'
@@ -128,16 +123,15 @@ describe( 'Paragraph feature – integration', () => {
 				.create( { plugins: [ Paragraph, Clipboard ] } )
 				.then( newEditor => {
 					const editor = newEditor;
-					const doc = editor.document;
 					const clipboard = editor.plugins.get( 'Clipboard' );
 
-					setModelData( doc, '<paragraph>[]</paragraph>' );
+					setModelData( editor.model, '<paragraph>[]</paragraph>' );
 
 					clipboard.fire( 'inputTransformation', {
 						content: parseView( '<ul><li><p>a</p>b</li></ul>' )
 					} );
 
-					expect( getModelData( doc ) ).to.equal(
+					expect( getModelData( editor.model ) ).to.equal(
 						'<paragraph>a</paragraph>' +
 						'<paragraph>b[]</paragraph>'
 					);
@@ -148,10 +142,10 @@ describe( 'Paragraph feature – integration', () => {
 	describe( 'with undo', () => {
 		it( 'fixing empty roots should be transparent to undo', () => {
 			return VirtualTestEditor
-				.create( { plugins: [ Paragraph, UndoEngine ] } )
+				.create( { plugins: [ Paragraph, UndoEditing ] } )
 				.then( newEditor => {
 					const editor = newEditor;
-					const doc = editor.document;
+					const doc = editor.model.document;
 					const root = doc.getRoot();
 
 					expect( editor.getData() ).to.equal( '<p>&nbsp;</p>' );
@@ -159,8 +153,8 @@ describe( 'Paragraph feature – integration', () => {
 
 					editor.setData( '<p>Foobar.</p>' );
 
-					doc.enqueueChanges( () => {
-						doc.batch().remove( root.getChild( 0 ) );
+					editor.model.change( writer => {
+						writer.remove( root.getChild( 0 ) );
 					} );
 
 					expect( editor.getData() ).to.equal( '<p>&nbsp;</p>' );
@@ -181,20 +175,22 @@ describe( 'Paragraph feature – integration', () => {
 
 		it( 'fixing empty roots should be transparent to undo - multiple roots', () => {
 			return VirtualTestEditor
-				.create( { plugins: [ Paragraph, UndoEngine ] } )
+				.create( { plugins: [ Paragraph, UndoEditing ] } )
 				.then( newEditor => {
 					const editor = newEditor;
-					const doc = editor.document;
+					const doc = editor.model.document;
 					const root = doc.getRoot();
 					const otherRoot = doc.createRoot( '$root', 'otherRoot' );
-					editor.editing.createRoot( 'div', 'otherRoot' );
 
 					editor.data.set( '<p>Foobar.</p>', 'main' );
 					editor.data.set( '<p>Foobar.</p>', 'otherRoot' );
 
-					doc.enqueueChanges( () => {
-						doc.batch().remove( root.getChild( 0 ) );
-						doc.batch().remove( otherRoot.getChild( 0 ) );
+					editor.model.change( writer => {
+						writer.remove( root.getChild( 0 ) );
+					} );
+
+					editor.model.change( writer => {
+						writer.remove( otherRoot.getChild( 0 ) );
 					} );
 
 					expect( editor.data.get( 'main' ) ).to.equal( '<p>&nbsp;</p>' );

+ 96 - 104
packages/ckeditor5-paragraph/tests/paragraph.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.
  */
 
@@ -13,24 +13,23 @@ import {
 } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 
-import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
-import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
+import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
+import { upcastElementToElement, upcastElementToAttribute } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
 
 import ModelDocumentFragment from '@ckeditor/ckeditor5-engine/src/model/documentfragment';
-import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
 import ModelText from '@ckeditor/ckeditor5-engine/src/model/text';
-import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
 import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
 
 describe( 'Paragraph feature', () => {
-	let editor, doc, root;
+	let model, editor, doc, root;
 
 	beforeEach( () => {
 		return VirtualTestEditor
 			.create( { plugins: [ Paragraph ] } )
 			.then( newEditor => {
 				editor = newEditor;
-				doc = editor.document;
+				model = editor.model;
+				doc = model.document;
 				root = doc.getRoot();
 			} );
 	} );
@@ -40,9 +39,9 @@ describe( 'Paragraph feature', () => {
 	} );
 
 	it( 'should set proper schema rules', () => {
-		expect( doc.schema.hasItem( 'paragraph' ) ).to.be.true;
-		expect( doc.schema.check( { name: 'paragraph', inside: '$root' } ) ).to.be.true;
-		expect( doc.schema.check( { name: '$inline', inside: 'paragraph' } ) ).to.be.true;
+		expect( model.schema.isRegistered( 'paragraph' ) ).to.be.true;
+		expect( model.schema.checkChild( [ '$root' ], 'paragraph' ) ).to.be.true;
+		expect( model.schema.checkChild( [ 'paragraph' ], '$text' ) ).to.be.true;
 	} );
 
 	it( 'should have a static paragraphLikeElements property', () => {
@@ -53,21 +52,21 @@ describe( 'Paragraph feature', () => {
 		it( 'should convert paragraph', () => {
 			editor.setData( '<p>foobar</p>' );
 
-			expect( getModelData( doc, { withoutSelection: true } ) ).to.equal( '<paragraph>foobar</paragraph>' );
+			expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>foobar</paragraph>' );
 			expect( editor.getData() ).to.equal( '<p>foobar</p>' );
 		} );
 
 		it( 'should convert paragraph only', () => {
 			editor.setData( '<p>foo<b>baz</b>bar</p>' );
 
-			expect( getModelData( doc, { withoutSelection: true } ) ).to.equal( '<paragraph>foobazbar</paragraph>' );
+			expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>foobazbar</paragraph>' );
 			expect( editor.getData() ).to.equal( '<p>foobazbar</p>' );
 		} );
 
 		it( 'should convert multiple paragraphs', () => {
 			editor.setData( '<p>foo</p><p>baz</p>' );
 
-			expect( getModelData( doc, { withoutSelection: true } ) ).to.equal( '<paragraph>foo</paragraph><paragraph>baz</paragraph>' );
+			expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>foo</paragraph><paragraph>baz</paragraph>' );
 			expect( editor.getData() ).to.equal( '<p>foo</p><p>baz</p>' );
 		} );
 
@@ -75,42 +74,43 @@ describe( 'Paragraph feature', () => {
 			it( 'should autoparagraph text', () => {
 				editor.setData( 'foo' );
 
-				expect( getModelData( doc, { withoutSelection: true } ) ).to.equal( '<paragraph>foo</paragraph>' );
+				expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>foo</paragraph>' );
 				expect( editor.getData() ).to.equal( '<p>foo</p>' );
 			} );
 
 			it( 'should autoparagraph any inline element', () => {
-				editor.document.schema.registerItem( 'span', '$inline' );
-				editor.document.schema.allow( { name: '$text', inside: '$inline' } );
+				editor.model.schema.register( 'span', { allowWhere: '$text' } );
+				editor.model.schema.extend( '$text', { allowIn: 'span' } );
 
-				buildModelConverter().for( editor.editing.modelToView, editor.data.modelToView ).fromElement( 'span' ).toElement( 'span' );
-				buildViewConverter().for( editor.data.viewToModel ).fromElement( 'span' ).toElement( 'span' );
+				editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'span', view: 'span' } ) );
+				editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'span', view: 'span' } ) );
 
 				editor.setData( '<span>foo</span>' );
 
-				expect( getModelData( doc, { withoutSelection: true } ) ).to.equal( '<paragraph><span>foo</span></paragraph>' );
+				expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '<paragraph><span>foo</span></paragraph>' );
 				expect( editor.getData() ).to.equal( '<p><span>foo</span></p>' );
 			} );
 
 			it( 'should not autoparagraph text (in clipboard holder)', () => {
-				const modelFragment = editor.data.parse( 'foo', '$clipboardHolder' );
+				const modelFragment = editor.data.parse( 'foo', [ '$clipboardHolder' ] );
 
 				expect( stringifyModel( modelFragment ) )
 					.to.equal( 'foo' );
 			} );
 
 			it( 'should not autoparagraph text (in a context which does not allow paragraphs', () => {
-				doc.schema.registerItem( 'specialRoot' );
+				model.schema.register( 'specialRoot' );
 
-				const modelFragment = editor.data.parse( 'foo', 'specialRoot' );
+				const modelFragment = editor.data.parse( 'foo', [ 'specialRoot' ] );
 
 				expect( stringifyModel( modelFragment ) )
 					.to.equal( '' );
 			} );
 
 			it( 'should autoparagraph text next to allowed element', () => {
-				doc.schema.registerItem( 'heading1', '$block' );
-				buildViewConverter().for( editor.data.viewToModel ).fromElement( 'h1' ).toElement( 'heading1' );
+				model.schema.register( 'heading1', { inheritAllFrom: '$block' } );
+
+				editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'heading1', view: 'h1' } ) );
 
 				const modelFragment = editor.data.parse( '<h1>foo</h1>bar<p>bom</p>' );
 
@@ -126,18 +126,18 @@ describe( 'Paragraph feature', () => {
 			} );
 
 			it( 'should not autoparagraph 3 inline nodes (in clipboardHolder)', () => {
-				const modelFragment = editor.data.parse( 'foo<b>bar</b>bom', '$clipboardHolder' );
+				const modelFragment = editor.data.parse( 'foo<b>bar</b>bom', [ '$clipboardHolder' ] );
 
 				expect( stringifyModel( modelFragment ) )
 					.to.equal( 'foobarbom' );
 			} );
 
 			it( 'should autoparagraph text inside converted container', () => {
-				doc.schema.registerItem( 'div' );
-				doc.schema.allow( { name: 'div', inside: '$root' } );
-				doc.schema.allow( { name: 'paragraph', inside: 'div' } );
+				model.schema.register( 'div' );
+				model.schema.extend( 'div', { allowIn: '$root' } );
+				model.schema.extend( 'paragraph', { allowIn: 'div' } );
 
-				buildViewConverter().for( editor.data.viewToModel ).fromElement( 'div' ).toElement( 'div' );
+				editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'div', view: 'div' } ) );
 
 				const modelFragment = editor.data.parse( '<div>foo</div><div>bom<p>bim</p></div>' );
 
@@ -149,8 +149,9 @@ describe( 'Paragraph feature', () => {
 			} );
 
 			it( 'should autoparagraph text inside disallowed element next to allowed element', () => {
-				doc.schema.registerItem( 'heading1', '$block' );
-				buildViewConverter().for( editor.data.viewToModel ).fromElement( 'h1' ).toElement( 'heading1' );
+				model.schema.register( 'heading1', { inheritAllFrom: '$block' } );
+
+				editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'heading1', view: 'h1' } ) );
 
 				const modelFragment = editor.data.parse( '<div><h1>foo</h1>bar</div>' );
 
@@ -159,8 +160,9 @@ describe( 'Paragraph feature', () => {
 			} );
 
 			it( 'should not autoparagraph text in disallowed element', () => {
-				doc.schema.registerItem( 'heading1', '$block' );
-				buildViewConverter().for( editor.data.viewToModel ).fromElement( 'h1' ).toElement( 'heading1' );
+				model.schema.register( 'heading1', { inheritAllFrom: '$block' } );
+
+				editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'heading1', view: 'h1' } ) );
 
 				const modelFragment = editor.data.parse( '<h1><b>foo</b>bar</h1>' );
 
@@ -169,7 +171,11 @@ describe( 'Paragraph feature', () => {
 			} );
 
 			it( 'should not fail when text is not allowed in paragraph', () => {
-				doc.schema.disallow( { name: '$text', inside: [ '$root', 'paragraph' ] } );
+				model.schema.addChildCheck( ( ctx, childDef ) => {
+					if ( ctx.endsWith( '$root paragraph' ) && childDef.name == '$text' ) {
+						return false;
+					}
+				} );
 
 				const modelFragment = editor.data.parse( 'foo' );
 
@@ -186,8 +192,8 @@ describe( 'Paragraph feature', () => {
 
 			// This test was taken from the list package.
 			it( 'does not break when some converter returns nothing', () => {
-				editor.data.viewToModel.on( 'element:li', ( evt, data, consumable ) => {
-					consumable.consume( data.input, { name: true } );
+				editor.data.upcastDispatcher.on( 'element:li', ( evt, data, conversionApi ) => {
+					conversionApi.consumable.consume( data.input, { name: true } );
 				}, { priority: 'highest' } );
 
 				const modelFragment = editor.data.parse( '<ul><li></li></ul>' );
@@ -197,10 +203,9 @@ describe( 'Paragraph feature', () => {
 
 			describe( 'should not strip attribute elements when autoparagraphing texts', () => {
 				beforeEach( () => {
-					doc.schema.allow( { name: '$inline', attributes: [ 'bold' ] } );
-					buildViewConverter().for( editor.data.viewToModel )
-						.fromElement( 'b' )
-						.toAttribute( 'bold', true );
+					model.schema.extend( '$text', { allowAttributes: 'bold' } );
+
+					editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( { view: 'b', model: 'bold' } ) );
 				} );
 
 				it( 'inside document fragment', () => {
@@ -210,20 +215,15 @@ describe( 'Paragraph feature', () => {
 				} );
 
 				it( 'inside converted element', () => {
-					doc.schema.registerItem( 'blockquote' );
-					doc.schema.allow( { name: 'blockquote', inside: '$root' } );
-					doc.schema.allow( { name: '$block', inside: 'blockquote' } );
-
-					buildModelConverter().for( editor.editing.modelToView, editor.data.modelToView )
-						.fromElement( 'blockquote' )
-						.toElement( 'blockquote' );
+					model.schema.register( 'blockQuote', { allowIn: '$root' } );
+					model.schema.extend( '$block', { allowIn: 'blockQuote' } );
 
-					buildViewConverter().for( editor.data.viewToModel ).fromElement( 'blockquote' ).toElement( 'blockquote' );
+					editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'blockQuote', view: 'blockquote' } ) );
 
 					const modelFragment = editor.data.parse( '<blockquote>foo<b>bar</b>bom</blockquote>' );
 
 					expect( stringifyModel( modelFragment ) )
-						.to.equal( '<blockquote><paragraph>foo<$text bold="true">bar</$text>bom</paragraph></blockquote>' );
+						.to.equal( '<blockQuote><paragraph>foo<$text bold="true">bar</$text>bom</paragraph></blockQuote>' );
 				} );
 
 				it( 'inside paragraph-like element', () => {
@@ -244,21 +244,21 @@ describe( 'Paragraph feature', () => {
 			} );
 
 			it( 'should convert h1+h2 (in clipboard holder)', () => {
-				const modelFragment = editor.data.parse( '<h1>foo</h1><h2>bar</h2>', '$clipboardHolder' );
+				const modelFragment = editor.data.parse( '<h1>foo</h1><h2>bar</h2>', [ '$clipboardHolder' ] );
 
 				expect( stringifyModel( modelFragment ) )
 					.to.equal( '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
 			} );
 
 			it( 'should not convert h1+h2 (in a context which does not allow paragraphs)', () => {
-				doc.schema.registerItem( 'div' );
-				doc.schema.registerItem( 'specialRoot' );
-				doc.schema.allow( { name: 'div', inside: 'specialRoot' } );
-				doc.schema.allow( { name: '$text', inside: 'div' } );
+				model.schema.register( 'div' );
+				model.schema.register( 'specialRoot' );
+				model.schema.extend( 'div', { allowIn: 'specialRoot' } );
+				model.schema.extend( '$text', { allowIn: 'div' } );
 
-				buildViewConverter().for( editor.data.viewToModel ).fromElement( 'div' ).toElement( 'div' );
+				editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'div', view: 'div' } ) );
 
-				const modelFragment = editor.data.parse( '<h1>foo</h1><h2>bar</h2><div>bom</div>', 'specialRoot' );
+				const modelFragment = editor.data.parse( '<h1>foo</h1><h2>bar</h2><div>bom</div>', [ 'specialRoot' ] );
 
 				expect( stringifyModel( modelFragment ) )
 					.to.equal( '<div>bom</div>' );
@@ -272,7 +272,7 @@ describe( 'Paragraph feature', () => {
 			} );
 
 			it( 'should convert ul,ol>li (in clipboard holder)', () => {
-				const modelFragment = editor.data.parse( '<ul><li>a</li><li>b</li></ul><ol><li>c</li></ol>', '$clipboardHolder' );
+				const modelFragment = editor.data.parse( '<ul><li>a</li><li>b</li></ul><ol><li>c</li></ol>', [ '$clipboardHolder' ] );
 
 				expect( stringifyModel( modelFragment ) )
 					.to.equal( '<paragraph>a</paragraph><paragraph>b</paragraph><paragraph>c</paragraph>' );
@@ -288,7 +288,7 @@ describe( 'Paragraph feature', () => {
 			// "b" is not autoparagraphed because clipboard holder allows text nodes.
 			// There's a similar integrational test what's going to happen when pasting in paragraph-integration.js.
 			it( 'should convert ul>li>ul>li+li (in clipboard holder)', () => {
-				const modelFragment = editor.data.parse( '<ul><li>a<ul><li>b</li><li>c</li></ul></li></ul>', '$clipboardHolder' );
+				const modelFragment = editor.data.parse( '<ul><li>a<ul><li>b</li><li>c</li></ul></li></ul>', [ '$clipboardHolder' ] );
 
 				expect( stringifyModel( modelFragment ) )
 					.to.equal( '<paragraph>a</paragraph><paragraph>b</paragraph><paragraph>c</paragraph>' );
@@ -301,17 +301,17 @@ describe( 'Paragraph feature', () => {
 					.to.equal( '<paragraph>a</paragraph><paragraph>b</paragraph>' );
 			} );
 
-			// "b" is not autoparagraphed because clipboard holder allows text nodes.
-			// There's a similar integrational test what's going to happen when pasting in paragraph-integration.js.
 			it( 'should convert ul>li>p,text (in clipboard holder)', () => {
-				const modelFragment = editor.data.parse( '<ul><li><p>a</p>b</li></ul>', '$clipboardHolder' );
+				const modelFragment = editor.data.parse( '<ul><li><p>a</p>b</li></ul>', [ '$clipboardHolder' ] );
 
 				expect( stringifyModel( modelFragment ) )
 					.to.equal( '<paragraph>a</paragraph><paragraph>b</paragraph>' );
 			} );
 
 			it( 'should convert td', () => {
-				const modelFragment = editor.data.parse( '<table><tr><td>a</td><td>b</td></tr><tr><td>c</td><td>d</td></tr></table>' );
+				const modelFragment = editor.data.parse(
+					'<table><tr><td>a</td><td>b</td></tr><tr><td>c</td><td>d</td></tr></table>'
+				);
 
 				expect( stringifyModel( modelFragment ) )
 					.to.equal( '<paragraph>a</paragraph><paragraph>b</paragraph><paragraph>c</paragraph><paragraph>d</paragraph>' );
@@ -320,7 +320,7 @@ describe( 'Paragraph feature', () => {
 			it( 'should convert td (in clipboardHolder)', () => {
 				const modelFragment = editor.data.parse(
 					'<table><tr><td>a</td><td>b</td></tr><tr><td>c</td><td>d</td></tr></table>',
-					'$clipboardHolder'
+					[ '$clipboardHolder' ]
 				);
 
 				expect( stringifyModel( modelFragment ) )
@@ -328,11 +328,11 @@ describe( 'Paragraph feature', () => {
 			} );
 
 			it( 'should convert li inside converted container', () => {
-				doc.schema.registerItem( 'div' );
-				doc.schema.allow( { name: 'div', inside: '$root' } );
-				doc.schema.allow( { name: 'paragraph', inside: 'div' } );
+				model.schema.register( 'div' );
+				model.schema.extend( 'div', { allowIn: '$root' } );
+				model.schema.extend( 'paragraph', { allowIn: 'div' } );
 
-				buildViewConverter().for( editor.data.viewToModel ).fromElement( 'div' ).toElement( 'div' );
+				editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'div', view: 'div' } ) );
 
 				const modelFragment = editor.data.parse( '<div><ul><li>foo</li><li>bar</li></ul></div><div>bom<p>bim</p></div>' );
 
@@ -362,12 +362,18 @@ describe( 'Paragraph feature', () => {
 				expect( modelFragment.getChild( 0 ).childCount ).to.equal( 1 );
 				expect( modelFragment.getChild( 0 ).getChild( 0 ) ).to.be.instanceOf( ModelText );
 			} );
+
+			it( 'should not convert empty elements', () => {
+				const modelFragment = editor.data.parse( '<ul><li></li><ul>' );
+
+				expect( stringifyModel( modelFragment ) ).to.equal( '' );
+			} );
 		} );
 	} );
 
 	describe( 'editing pipeline conversion', () => {
 		it( 'should convert paragraph', () => {
-			setModelData( doc, '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
+			setModelData( model, '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
 
 			expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( '<p>foo</p><p>bar</p>' );
 		} );
@@ -377,7 +383,7 @@ describe( 'Paragraph feature', () => {
 		it( 'wraps text and place selection at the beginning of that paragraph', () => {
 			editor.setData( 'foo' );
 
-			expect( getModelData( doc ) ).to.equal( '<paragraph>[]foo</paragraph>' );
+			expect( getModelData( model ) ).to.equal( '<paragraph>[]foo</paragraph>' );
 		} );
 	} );
 
@@ -391,61 +397,47 @@ describe( 'Paragraph feature', () => {
 			editor.setData( '<p>Foobar</p>' );
 
 			// Since `setData` first removes all contents from editor and then sets content during same enqueue
-			// changes block, the line below checks whether fixing empty roots does not kick too early and does not
+			// change block, the line below checks whether fixing empty roots does not kick too early and does not
 			// fix root if it is not needed.
 			expect( editor.getData() ).to.equal( '<p>Foobar</p>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().remove( ModelRange.createIn( root ) );
+			model.change( writer => {
+				writer.remove( ModelRange.createIn( root ) );
 			} );
 
 			expect( doc.getRoot().childCount ).to.equal( 1 );
 			expect( doc.getRoot().getChild( 0 ).is( 'paragraph' ) ).to.be.true;
 		} );
 
-		it( 'should not fix root if it got content during changesDone event', () => {
-			// "Autoheading feature".
-			doc.schema.registerItem( 'heading', '$block' );
-
-			buildModelConverter().for( editor.editing.modelToView, editor.data.modelToView )
-				.fromElement( 'heading' )
-				.toElement( 'h1' );
-
-			doc.on( 'changesDone', () => {
-				const root = doc.getRoot();
-
-				if ( root.isEmpty ) {
-					doc.enqueueChanges( () => {
-						doc.batch().insert( ModelPosition.createAt( root ), new ModelElement( 'heading' ) );
-					} );
+		it( 'should not fix root which does not allow paragraph', () => {
+			model.schema.addChildCheck( ( ctx, childDef ) => {
+				if ( ctx.endsWith( '$root' ) && childDef.name == 'paragraph' ) {
+					return false;
 				}
 			} );
 
-			doc.enqueueChanges( () => {
-				doc.batch().remove( ModelRange.createIn( root ) );
+			model.change( writer => {
+				writer.remove( ModelRange.createIn( root ) );
 			} );
 
-			expect( doc.getRoot().childCount ).to.equal( 1 );
-			expect( doc.getRoot().getChild( 0 ).name ).to.equal( 'heading' );
-		} );
-
-		it( 'should not fix root which does not allow paragraph', () => {
-			doc.schema.disallow( { name: 'paragraph', inside: '$root' } );
-
-			doc.enqueueChanges( () => {
-				doc.batch().remove( ModelRange.createIn( root ) );
-			} );
 			expect( editor.getData() ).to.equal( '' );
 		} );
 
-		it( 'should not fix root if change was in transparent batch', () => {
-			editor.setData( '<p>Foobar</p>' );
+		it( 'should fix empty roots in the right batch', () => {
+			let removeBatch, attributeBatch;
 
-			doc.enqueueChanges( () => {
-				doc.batch( 'transparent' ).remove( ModelRange.createIn( root ) );
+			model.enqueueChange( writer => {
+				removeBatch = writer.batch;
+				writer.remove( ModelRange.createIn( root ) );
+
+				model.enqueueChange( writer => {
+					attributeBatch = writer.batch;
+					writer.setAttribute( 'foo', 'bar', root );
+				} );
 			} );
 
-			expect( editor.getData() ).to.equal( '' );
+			expect( Array.from( removeBatch.deltas, delta => delta.type ) ).to.include.members( [ 'insert' ] );
+			expect( Array.from( attributeBatch.deltas, delta => delta.type ) ).to.not.include.members( [ 'insert' ] );
 		} );
 	} );
 

+ 100 - 66
packages/ckeditor5-paragraph/tests/paragraphcommand.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.
  */
 
@@ -10,23 +10,24 @@ import Range from '@ckeditor/ckeditor5-engine/src/model/range';
 import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 describe( 'ParagraphCommand', () => {
-	let editor, document, command, root, schema;
+	let editor, model, document, command, root, schema;
 
 	beforeEach( () => {
 		return ModelTestEditor.create().then( newEditor => {
 			editor = newEditor;
-			document = editor.document;
-			schema = document.schema;
+			model = editor.model;
+			document = model.document;
+			schema = model.schema;
 			command = new ParagraphCommand( editor );
 			root = document.getRoot();
 
 			editor.commands.add( 'paragraph', command );
-			schema.registerItem( 'paragraph', '$block' );
-			schema.registerItem( 'heading1', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			schema.register( 'heading1', { inheritAllFrom: '$block' } );
 
-			schema.registerItem( 'notBlock' );
-			schema.allow( { name: 'notBlock', inside: '$root' } );
-			schema.allow( { name: '$text', inside: 'notBlock' } );
+			schema.register( 'notBlock' );
+			schema.extend( 'notBlock', { allowIn: '$root' } );
+			schema.extend( '$text', { allowIn: 'notBlock' } );
 		} );
 	} );
 
@@ -36,80 +37,87 @@ describe( 'ParagraphCommand', () => {
 
 	describe( 'value', () => {
 		it( 'responds to changes in selection (collapsed selection)', () => {
-			setData( document, '<heading1>foo[]bar</heading1>' );
+			setData( model, '<heading1>foo[]bar</heading1>' );
 			expect( command.value ).to.be.false;
 
-			setData( document, '<paragraph>foo[]bar</paragraph>' );
+			setData( model, '<paragraph>foo[]bar</paragraph>' );
 			expect( command.value ).to.be.true;
 		} );
 
 		it( 'responds to changes in selection (non–collapsed selection)', () => {
-			setData( document, '<heading1>[foo]</heading1><paragraph>bar</paragraph>' );
+			setData( model, '<heading1>[foo]</heading1><paragraph>bar</paragraph>' );
 			expect( command.value ).to.be.false;
 
-			setData( document, '<heading1>[foo</heading1><paragraph>bar]</paragraph>' );
+			setData( model, '<heading1>[foo</heading1><paragraph>bar]</paragraph>' );
 			expect( command.value ).to.be.false;
 
-			setData( document, '<heading1>foo</heading1>[<paragraph>bar]</paragraph>' );
+			setData( model, '<heading1>foo</heading1>[<paragraph>bar]</paragraph>' );
 			expect( command.value ).to.be.true;
 
-			setData( document, '<heading1>foo</heading1><paragraph>[bar]</paragraph>' );
+			setData( model, '<heading1>foo</heading1><paragraph>[bar]</paragraph>' );
 			expect( command.value ).to.be.true;
 
-			setData( document, '<paragraph>[bar</paragraph><heading1>foo]</heading1>' );
+			setData( model, '<paragraph>[bar</paragraph><heading1>foo]</heading1>' );
 			expect( command.value ).to.be.true;
 		} );
 
 		it( 'has proper value when inside non-block element', () => {
-			setData( document, '<notBlock>[foo]</notBlock>' );
+			setData( model, '<notBlock>[foo]</notBlock>' );
 
 			expect( command.value ).to.be.false;
 		} );
 
 		it( 'has proper value when moved from block to element that is not a block', () => {
-			setData( document, '<paragraph>[foo]</paragraph><notBlock>foo</notBlock>' );
+			setData( model, '<paragraph>[foo]</paragraph><notBlock>foo</notBlock>' );
 			const element = document.getRoot().getChild( 1 );
 
-			document.enqueueChanges( () => {
-				document.selection.setRanges( [ Range.createIn( element ) ] );
+			model.change( writer => {
+				writer.setSelection( Range.createIn( element ) );
 			} );
 
 			expect( command.value ).to.be.false;
 		} );
 
 		it( 'should be refreshed after calling refresh()', () => {
-			setData( document, '<paragraph>[foo]</paragraph><notBlock>foo</notBlock>' );
+			setData( model, '<paragraph>[foo]</paragraph><notBlock>foo</notBlock>' );
 			const element = document.getRoot().getChild( 1 );
 
-			// Purposely not putting it in `document.enqueueChanges` to update command manually.
-			document.selection.setRanges( [ Range.createIn( element ) ] );
+			model.change( writer => {
+				writer.setSelection( Range.createIn( element ) );
 
-			expect( command.value ).to.be.true;
-			command.refresh();
-			expect( command.value ).to.be.false;
+				expect( command.value ).to.be.true;
+				command.refresh();
+				expect( command.value ).to.be.false;
+			} );
 		} );
 	} );
 
 	describe( 'execute()', () => {
 		it( 'should update value after execution', () => {
-			setData( document, '<heading1>[]</heading1>' );
+			setData( model, '<heading1>[]</heading1>' );
 			command.execute();
 
-			expect( getData( document ) ).to.equal( '<paragraph>[]</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
 			expect( command.value ).to.be.true;
 		} );
 
 		// https://github.com/ckeditor/ckeditor5-paragraph/issues/24
-		it( 'should not rename blocks which cannot become paragraphs', () => {
-			document.schema.registerItem( 'restricted' );
-			document.schema.allow( { name: 'restricted', inside: '$root' } );
-			document.schema.disallow( { name: 'paragraph', inside: 'restricted' } );
+		it( 'should not rename blocks which cannot become paragraphs (paragraph is not allowed in their parent)', () => {
+			model.schema.register( 'restricted', { allowIn: '$root' } );
 
-			document.schema.registerItem( 'fooBlock', '$block' );
-			document.schema.allow( { name: 'fooBlock', inside: 'restricted' } );
+			model.schema.register( 'fooBlock', {
+				inheritAllFrom: '$block',
+				allowIn: 'restricted'
+			} );
+
+			model.schema.addChildCheck( ( ctx, childDef ) => {
+				if ( ctx.endsWith( 'restricted' ) && childDef.name == 'paragraph' ) {
+					return false;
+				}
+			} );
 
 			setData(
-				document,
+				model,
 				'<heading1>a[bc</heading1>' +
 				'<restricted><fooBlock></fooBlock></restricted>' +
 				'<heading1>de]f</heading1>'
@@ -117,44 +125,70 @@ describe( 'ParagraphCommand', () => {
 
 			command.execute();
 
-			expect( getData( document ) ).to.equal(
+			expect( getData( model ) ).to.equal(
 				'<paragraph>a[bc</paragraph>' +
 				'<restricted><fooBlock></fooBlock></restricted>' +
 				'<paragraph>de]f</paragraph>'
 			);
 		} );
 
+		it( 'should not rename blocks which cannot become paragraphs (block is an object)', () => {
+			model.schema.register( 'image', {
+				isBlock: true,
+				isObject: true,
+				allowIn: '$root'
+			} );
+
+			setData(
+				model,
+				'<heading1>a[bc</heading1>' +
+				'<image></image>' +
+				'<heading1>de]f</heading1>'
+			);
+
+			command.execute();
+
+			expect( getData( model ) ).to.equal(
+				'<paragraph>a[bc</paragraph>' +
+				'<image></image>' +
+				'<paragraph>de]f</paragraph>'
+			);
+		} );
+
 		it( 'should not rename blocks which already are pargraphs', () => {
-			const batch = editor.document.batch();
+			setData( model, '<paragraph>foo[</paragraph><heading1>bar]</heading1>' );
+
+			model.change( writer => {
+				expect( writer.batch.deltas.length ).to.equal( 0 );
 
-			setData( document, '<paragraph>foo[</paragraph><heading1>bar]</heading1>' );
-			expect( batch.deltas.length ).to.equal( 0 );
+				command.execute();
 
-			command.execute( { batch } );
-			expect( batch.deltas.length ).to.equal( 1 );
+				expect( writer.batch.deltas.length ).to.equal( 1 );
+			} );
 		} );
 
 		describe( 'custom options', () => {
-			it( 'should use provided batch', () => {
-				const batch = editor.document.batch();
+			it( 'should use parent batch', () => {
+				setData( model, '<heading1>foo[]bar</heading1>' );
+
+				model.change( writer => {
+					expect( writer.batch.deltas.length ).to.equal( 0 );
 
-				setData( document, '<heading1>foo[]bar</heading1>' );
-				expect( batch.deltas.length ).to.equal( 0 );
+					command.execute();
 
-				command.execute( { batch } );
-				expect( batch.deltas.length ).to.be.above( 0 );
+					expect( writer.batch.deltas.length ).to.above( 0 );
+				} );
 			} );
 
 			it( 'should use provided selection', () => {
-				setData( document, '<heading1>foo[]bar</heading1><heading1>baz</heading1><heading1>qux</heading1>' );
+				setData( model, '<heading1>foo[]bar</heading1><heading1>baz</heading1><heading1>qux</heading1>' );
 
 				const secondToLastHeading = root.getChild( 1 );
 				const lastHeading = root.getChild( 2 );
-				const selection = new Selection();
-				selection.addRange( Range.createFromParentsAndOffsets( secondToLastHeading, 0, lastHeading, 1 ) );
+				const selection = new Selection( Range.createFromParentsAndOffsets( secondToLastHeading, 0, lastHeading, 1 ) );
 
 				command.execute( { selection } );
-				expect( getData( document ) ).to.equal(
+				expect( getData( model ) ).to.equal(
 					'<heading1>foo[]bar</heading1><paragraph>baz</paragraph><paragraph>qux</paragraph>'
 				);
 			} );
@@ -162,61 +196,61 @@ describe( 'ParagraphCommand', () => {
 
 		describe( 'collapsed selection', () => {
 			it( 'does nothing when executed with already applied', () => {
-				setData( document, '<paragraph>foo[]bar</paragraph>' );
+				setData( model, '<paragraph>foo[]bar</paragraph>' );
 				command.execute();
 
-				expect( getData( document ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
 			} );
 
 			it( 'converts topmost blocks', () => {
-				schema.registerItem( 'inlineImage', '$inline' );
-				schema.allow( { name: '$text', inside: 'inlineImage' } );
+				schema.register( 'inlineImage', { allowWhere: '$text' } );
+				schema.extend( '$text', { allowIn: 'inlineImage' } );
 
-				setData( document, '<heading1><inlineImage>foo[]</inlineImage>bar</heading1>' );
+				setData( model, '<heading1><inlineImage>foo[]</inlineImage>bar</heading1>' );
 				command.execute();
 
-				expect( getData( document ) ).to.equal( '<paragraph><inlineImage>foo[]</inlineImage>bar</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph><inlineImage>foo[]</inlineImage>bar</paragraph>' );
 			} );
 		} );
 
 		describe( 'non-collapsed selection', () => {
 			it( 'converts all elements where selection is applied', () => {
-				schema.registerItem( 'heading2', '$block' );
+				schema.register( 'heading2', { inheritAllFrom: '$block' } );
 
-				setData( document, '<heading1>foo[</heading1><heading2>bar</heading2><heading2>baz]</heading2>' );
+				setData( model, '<heading1>foo[</heading1><heading2>bar</heading2><heading2>baz]</heading2>' );
 
 				command.execute();
-				expect( getData( document ) ).to.equal(
+				expect( getData( model ) ).to.equal(
 					'<paragraph>foo[</paragraph><paragraph>bar</paragraph><paragraph>baz]</paragraph>'
 				);
 			} );
 
 			it( 'converts all elements even if already anchored in paragraph', () => {
-				schema.registerItem( 'heading2', '$block' );
+				schema.register( 'heading2', { inheritAllFrom: '$block' } );
 
-				setData( document, '<paragraph>foo[</paragraph><heading2>bar]</heading2>' );
+				setData( model, '<paragraph>foo[</paragraph><heading2>bar]</heading2>' );
 
 				command.execute();
-				expect( getData( document ) ).to.equal( '<paragraph>foo[</paragraph><paragraph>bar]</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>foo[</paragraph><paragraph>bar]</paragraph>' );
 			} );
 		} );
 	} );
 
 	describe( 'isEnabled', () => {
 		it( 'should be enabled when inside another block', () => {
-			setData( document, '<heading1>f{}oo</heading1>' );
+			setData( model, '<heading1>f{}oo</heading1>' );
 
 			expect( command.isEnabled ).to.be.true;
 		} );
 
 		it( 'should be disabled if inside non-block', () => {
-			setData( document, '<notBlock>f{}oo</notBlock>' );
+			setData( model, '<notBlock>f{}oo</notBlock>' );
 
 			expect( command.isEnabled ).to.be.false;
 		} );
 
 		it( 'should be disabled if selection is placed on non-block element', () => {
-			setData( document, '[<notBlock>foo</notBlock>]' );
+			setData( model, '[<notBlock>foo</notBlock>]' );
 
 			expect( command.isEnabled ).to.be.false;
 		} );