Skip to content

Commit

Permalink
Merge pull request #9428 from ckeditor/i/9261
Browse files Browse the repository at this point in the history
Features (engine): Introduced the `SchemaItemDefinition#allowChildren` property simplifying defining which other items are allowed inside this schema item definition. Closes #9261.
  • Loading branch information
niegowski committed Apr 16, 2021
2 parents 9978a9a + d33378f commit 6a37ecc
Show file tree
Hide file tree
Showing 24 changed files with 364 additions and 65 deletions.
2 changes: 1 addition & 1 deletion packages/ckeditor5-block-quote/tests/blockquotecommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ describe( 'BlockQuoteCommand', () => {

model.schema.extend( 'widget', {
allowIn: '$root',
allowChildren: '$text',
isLimit: true,
isObject: true
} );
model.schema.extend( '$text', { allowIn: 'widget' } );

editor.conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } );
editor.conversion.for( 'downcast' ).elementToElement( { model: 'heading', view: 'h' } );
Expand Down
27 changes: 17 additions & 10 deletions packages/ckeditor5-ckfinder/tests/ckfindercommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ describe( 'CKFinderCommand', () => {
} );

it( 'should be true where only image is allowed', () => {
model.schema.register( 'block', { inheritAllFrom: '$block' } );
model.schema.extend( 'paragraph', { allowIn: 'block' } );
model.schema.extend( 'image', { allowIn: 'block' } );
model.schema.register( 'block', {
inheritAllFrom: '$block',
allowChildren: [ 'paragraph', 'image' ]
} );

// Block link attribute.
model.schema.addAttributeCheck( ( ctx, attributeName ) => ( attributeName !== 'linkHref' ) );
Expand All @@ -74,8 +75,10 @@ describe( 'CKFinderCommand', () => {
} );

it( 'should be true where only link is allowed', () => {
model.schema.register( 'block', { inheritAllFrom: '$block' } );
model.schema.extend( 'paragraph', { allowIn: 'block' } );
model.schema.register( 'block', {
inheritAllFrom: '$block',
allowChildren: 'paragraph'
} );

// Block image in block.
model.schema.addChildCheck( ( context, childDefinition ) => {
Expand All @@ -92,8 +95,10 @@ describe( 'CKFinderCommand', () => {
} );

it( 'should be false where link & image are not allowed', () => {
model.schema.register( 'block', { inheritAllFrom: '$block' } );
model.schema.extend( 'paragraph', { allowIn: 'block' } );
model.schema.register( 'block', {
inheritAllFrom: '$block',
allowChildren: 'paragraph'
} );

// Block link attribute - image is not allowed in 'block'.
model.schema.addAttributeCheck( ( ctx, attributeName ) => ( attributeName !== 'linkHref' ) );
Expand Down Expand Up @@ -368,8 +373,10 @@ describe( 'CKFinderCommand', () => {
} );

it( 'should show warning notification if image cannot be inserted', done => {
model.schema.register( 'block', { inheritAllFrom: '$block' } );
model.schema.extend( 'paragraph', { allowIn: 'block' } );
model.schema.register( 'block', {
inheritAllFrom: '$block',
allowChildren: 'paragraph'
} );

// Block image in block.
model.schema.addChildCheck( ( context, childDefinition ) => {
Expand Down Expand Up @@ -403,9 +410,9 @@ describe( 'CKFinderCommand', () => {
it( 'should not insert image nor crash when image could not be inserted', () => {
model.schema.register( 'other', {
allowIn: '$root',
allowChildren: '$text',
isLimit: true
} );
model.schema.extend( '$text', { allowIn: 'other' } );

editor.conversion.for( 'downcast' ).elementToElement( { model: 'other', view: 'p' } );

Expand Down
5 changes: 1 addition & 4 deletions packages/ckeditor5-code-block/src/codeblockediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,11 @@ export default class CodeBlockEditing extends Plugin {

schema.register( 'codeBlock', {
allowWhere: '$block',
allowChildren: '$text',
isBlock: true,
allowAttributes: [ 'language' ]
} );

schema.extend( '$text', {
allowIn: 'codeBlock'
} );

// Disallow all attributes on $text inside `codeBlock`.
schema.addAttributeCheck( context => {
if ( context.endsWith( 'codeBlock $text' ) ) {
Expand Down
7 changes: 5 additions & 2 deletions packages/ckeditor5-code-block/tests/codeblockediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,11 @@ describe( 'CodeBlockEditing', () => {
} );

it( 'should execute enter command when pressing enter in an element nested inside a codeBlock', () => {
model.schema.register( 'codeBlockSub', { allowIn: 'codeBlock', isInline: true } );
model.schema.extend( '$text', { allowIn: 'codeBlockSub' } );
model.schema.register( 'codeBlockSub', {
allowIn: 'codeBlock',
allowChildren: '$text',
isInline: true
} );
editor.conversion.elementToElement( { model: 'codeBlockSub', view: 'codeBlockSub' } );

const enterCommand = editor.commands.get( 'enter' );
Expand Down
7 changes: 4 additions & 3 deletions packages/ckeditor5-editor-balloon/tests/ballooneditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,9 +330,10 @@ describe( 'BalloonEditor', () => {

const schema = editor.model.schema;

schema.register( 'heading' );
schema.extend( 'heading', { allowIn: '$root' } );
schema.extend( '$text', { allowIn: 'heading' } );
schema.register( 'heading', {
allowIn: '$root',
allowChildren: '$text'
} );

editor.conversion.for( 'upcast' ).elementToElement( { model: 'heading', view: 'heading' } );
editor.conversion.for( 'dataDowncast' ).elementToElement( { model: 'heading', view: 'heading' } );
Expand Down
7 changes: 4 additions & 3 deletions packages/ckeditor5-editor-inline/tests/inlineeditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,10 @@ describe( 'InlineEditor', () => {

const schema = editor.model.schema;

schema.register( 'heading' );
schema.extend( 'heading', { allowIn: '$root' } );
schema.extend( '$text', { allowIn: 'heading' } );
schema.register( 'heading', {
allowIn: '$root',
allowChildren: '$text'
} );

editor.conversion.for( 'upcast' ).elementToElement( { model: 'heading', view: 'heading' } );
editor.conversion.for( 'dataDowncast' ).elementToElement( { model: 'heading', view: 'heading' } );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Elements and attributes are checked by features separately by using the {@link m

## Defining allowed structures

When a feature introduces a model element, it should register it in the schema. Besides defining that such an element may exist in the model, the feature also needs to define where this element can be placed:
When a feature introduces a model element, it should register it in the schema. Besides defining that such an element may exist in the model, the feature also needs to define where this element can be placed. This information is provided by the {@link module:engine/model/schema~SchemaItemDefinition#allowIn} property of the {@link module:engine/model/schema~SchemaItemDefinition}:

```js
schema.register( 'myElement', {
Expand All @@ -37,14 +37,39 @@ In other words, this would be correct:

While this would be incorrect:

```js
```xml
<$root>
<foo>
<myElement></myElement>
</foo>
</$root>
```

To declare which nodes could be inside the registered element, the {@link module:engine/model/schema~SchemaItemDefinition#allowChildren} property could be used:

```js
schema.register( 'myElement', {
allowIn: '$root',
allowChildren: '$text'
} );
```

To allow the following structure:

```xml
<$root>
<myElement>
foobar
</myElement>
</$root>
```

Both the `{@link module:engine/model/schema~SchemaItemDefinition#allowIn}` and `{@link module:engine/model/schema~SchemaItemDefinition#allowChildren}` properties can be also inherited from other `SchemaItemDefinition` items.

<info-box>
You can read more about the format of the item definition in {@link module:engine/model/schema~SchemaItemDefinition}.
</info-box>

## Defining additional semantics

In addition to setting allowed structures, the schema can also define additional traits of model elements. By using the `is*` properties, a feature author may declare how a certain element should be treated by other features and by the engine.
Expand Down
4 changes: 2 additions & 2 deletions packages/ckeditor5-engine/src/model/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,15 @@ export default class Model {

this.schema.register( '$clipboardHolder', {
allowContentOf: '$root',
allowChildren: '$text',
isLimit: true
} );
this.schema.extend( '$text', { allowIn: '$clipboardHolder' } );

this.schema.register( '$documentFragment', {
allowContentOf: '$root',
allowChildren: '$text',
isLimit: true
} );
this.schema.extend( '$text', { allowIn: '$documentFragment' } );

// An element needed by the `upcastElementToMarker` converter.
// This element temporarily represents a marker boundary during the conversion process and is removed
Expand Down
50 changes: 50 additions & 0 deletions packages/ckeditor5-engine/src/model/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,10 @@ export default class Schema {
compiledDefinitions[ itemName ] = compileBaseItemRule( sourceRules[ itemName ], itemName );
}

for ( const itemName of itemNames ) {
compileAllowChildren( compiledDefinitions, itemName );
}

for ( const itemName of itemNames ) {
compileAllowContentOf( compiledDefinitions, itemName );
}
Expand All @@ -898,6 +902,7 @@ export default class Schema {

for ( const itemName of itemNames ) {
cleanUpAllowIn( compiledDefinitions, itemName );
setupAllowChildren( compiledDefinitions, itemName );
cleanUpAllowAttributes( compiledDefinitions, itemName );
}

Expand Down Expand Up @@ -1090,6 +1095,7 @@ mix( Schema, ObservableMixin );
* You can define the following rules:
*
* * {@link ~SchemaItemDefinition#allowIn `allowIn`} &ndash; Defines in which other items this item will be allowed.
* * {@link ~SchemaItemDefinition#allowChildren `allowChildren`} &ndash; Defines which other items are allowed inside this item.
* * {@link ~SchemaItemDefinition#allowAttributes `allowAttributes`} &ndash; Defines allowed attributes of the given item.
* * {@link ~SchemaItemDefinition#allowContentOf `allowContentOf`} &ndash; Inherits "allowed children" from other items.
* * {@link ~SchemaItemDefinition#allowWhere `allowWhere`} &ndash; Inherits "allowed in" from other items.
Expand Down Expand Up @@ -1157,6 +1163,14 @@ mix( Schema, ObservableMixin );
* isBlock: true
* } );
*
* Allow `paragraph` inside a `$root` and allow `$text` as a `paragraph` child:
*
* schema.register( 'paragraph', {
* allowIn: '$root',
* allowChildren: '$text',
* isBlock: true
* } );
*
* Make `image` a block object, which is allowed everywhere where `$block` is.
* Also, allow `src` and `alt` attributes in it:
*
Expand Down Expand Up @@ -1205,6 +1219,7 @@ mix( Schema, ObservableMixin );
* @typedef {Object} module:engine/model/schema~SchemaItemDefinition
*
* @property {String|Array.<String>} allowIn Defines in which other items this item will be allowed.
* @property {String|Array.<String>} allowChildren Defines which other items are allowed inside this item.
* @property {String|Array.<String>} allowAttributes Defines allowed attributes of the given item.
* @property {String|Array.<String>} allowContentOf Inherits "allowed children" from other items.
* @property {String|Array.<String>} allowWhere Inherits "allowed in" from other items.
Expand Down Expand Up @@ -1280,6 +1295,7 @@ mix( Schema, ObservableMixin );
* * The `name` property,
* * The `is*` properties,
* * The `allowIn` array,
* * The `allowChildren` array,
* * The `allowAttributes` array.
*
* @typedef {Object} module:engine/model/schema~SchemaCompiledItemDefinition
Expand Down Expand Up @@ -1562,6 +1578,8 @@ function compileBaseItemRule( sourceItemRules, itemName ) {
allowAttributes: [],
allowAttributesOf: [],

allowChildren: [],

inheritTypesFrom: []
};

Expand All @@ -1574,13 +1592,34 @@ function compileBaseItemRule( sourceItemRules, itemName ) {
copyProperty( sourceItemRules, itemRule, 'allowAttributes' );
copyProperty( sourceItemRules, itemRule, 'allowAttributesOf' );

copyProperty( sourceItemRules, itemRule, 'allowChildren' );

copyProperty( sourceItemRules, itemRule, 'inheritTypesFrom' );

makeInheritAllWork( sourceItemRules, itemRule );

return itemRule;
}

function compileAllowChildren( compiledDefinitions, itemName ) {
const item = compiledDefinitions[ itemName ];

for ( const allowChildrenItem of item.allowChildren ) {
const allowedChildren = compiledDefinitions[ allowChildrenItem ];

// The allowChildren property may point to an unregistered element.
if ( !allowedChildren ) {
continue;
}

allowedChildren.allowIn.push( itemName );
}

// The allowIn property already includes correct items, reset the allowChildren property
// to avoid duplicates later when setting up compilation results.
item.allowChildren.length = 0;
}

function compileAllowContentOf( compiledDefinitions, itemName ) {
for ( const allowContentOfItemName of compiledDefinitions[ itemName ].allowContentOf ) {
// The allowContentOf property may point to an unregistered element.
Expand Down Expand Up @@ -1654,6 +1693,17 @@ function cleanUpAllowIn( compiledDefinitions, itemName ) {
itemRule.allowIn = Array.from( new Set( existingItems ) );
}

// Setup allowChildren items based on allowIn.
function setupAllowChildren( compiledDefinitions, itemName ) {
const itemRule = compiledDefinitions[ itemName ];

for ( const allowedParentItemName of itemRule.allowIn ) {
const allowedParentItem = compiledDefinitions[ allowedParentItemName ];

allowedParentItem.allowChildren.push( itemName );
}
}

function cleanUpAllowAttributes( compiledDefinitions, itemName ) {
const itemRule = compiledDefinitions[ itemName ];

Expand Down
3 changes: 1 addition & 2 deletions packages/ckeditor5-engine/tests/controller/datacontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,7 @@ describe( 'DataController', () => {
it( 'should accept parsing context', () => {
modelDocument.createRoot( 'inlineRoot', 'inlineRoot' );

schema.register( 'inlineRoot' );
schema.extend( '$text', { allowIn: 'inlineRoot' } );
schema.register( 'inlineRoot', { allowChildren: '$text' } );

const viewFragment = new ViewDocumentFragment( viewDocument, [ parseView( 'foo' ) ] );

Expand Down
6 changes: 2 additions & 4 deletions packages/ckeditor5-engine/tests/dev-utils/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,7 @@ describe( 'model test utils', () => {
it( 'should work in a special root', () => {
const model = new Model();

model.schema.register( 'textOnly' );
model.schema.extend( '$text', { allowIn: 'textOnly' } );
model.schema.register( 'textOnly', { allowChildren: '$text' } );
model.document.createRoot( 'textOnly', 'textOnly' );

setData( model, 'a[b]c', { rootName: 'textOnly' } );
Expand Down Expand Up @@ -575,8 +574,7 @@ describe( 'model test utils', () => {

it( 'converts data in the specified context', () => {
const model = new Model();
model.schema.register( 'foo' );
model.schema.extend( '$text', { allowIn: 'foo' } );
model.schema.register( 'foo', { allowChildren: '$text' } );

expect( () => {
parse( 'text', model.schema, { context: [ 'foo' ] } );
Expand Down

0 comments on commit 6a37ecc

Please sign in to comment.