Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add disallow rules in the Schema #15916

Merged
merged 7 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/framework/architecture/editing-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ The {@link module:engine/model/schema~Schema model's schema} defines several asp
* What attributes are allowed for a certain node. For example, `image` can have the `src` and `alt` attributes.
* Additional semantics of model nodes. For example, `image` is of the "object" type and paragraph of the "block" type.

The schema can also define which children and attributes are specifically disallowed, which is useful when nodes inherit properties from other nodes, but want to exclude some things:

* Nodes can be disallowed in certain places. For example, a custom element `specialParagraph` inherits all properties from `paragraph` but needs to disallow `imageInline`.
* Attributes can be disallowed on a certain node. For example, a custom element `specialPurposeHeading` inherits attributes from `heading2` but does not allow `alignment` attribute.

scofalik marked this conversation as resolved.
Show resolved Hide resolved
This information is then used by the features and the engine to make decisions on how to process the model. For instance, the information from the schema will affect:

* What happens with the pasted content and what is filtered out (note: in case of pasting the other important mechanism is the {@link framework/deep-dive/conversion/upcast conversion}. HTML elements and attributes that are not upcasted by any of the registered converters are filtered out before they even become model nodes, so the schema is not applied to them; the conversion will be covered later in this guide).
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/crash-course/model-and-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Let's see how the model compares to HTML.

### Schema

You cannot put everything in the model. At least not until you update the schema. The schema defines what is allowed and where, what attributes are allowed for certain nodes, and so on.
You cannot put anything you want inside the document model. At least not until you update the schema. The schema defines what is allowed and where, what attributes are allowed for certain nodes, what is not allowed, and so on.

Schema determines things like whether the given element can be enclosed in a block quote, or whether the bold button is enabled on selected content.

Expand Down
29 changes: 8 additions & 21 deletions packages/ckeditor5-code-block/src/codeblockediting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,6 @@ export default class CodeBlockEditing extends Plugin {
const schema = editor.model.schema;
const model = editor.model;
const view = editor.editing.view;
const listEditing: ListEditing | null = editor.plugins.has( 'ListEditing' ) ?
editor.plugins.get( 'ListEditing' ) : null;

const normalizedLanguagesDefs = getNormalizedAndLocalizedLanguageDefinitions( editor );

Expand Down Expand Up @@ -133,32 +131,21 @@ export default class CodeBlockEditing extends Plugin {
schema.register( 'codeBlock', {
allowWhere: '$block',
allowChildren: '$text',
isBlock: true,
allowAttributes: [ 'language' ]
// Disallow `$inlineObject` and its derivatives like `inlineWidget` inside `codeBlock` to ensure that only text,
// not other inline elements like inline images, are allowed. This maintains the semantic integrity of code blocks.
disallowChildren: '$inlineObject',
allowAttributes: [ 'language' ],
allowAttributesOf: '$listItem',
isBlock: true
} );

// Allow all list* attributes on `codeBlock` (integration with DocumentList).
// Disallow all attributes on $text inside `codeBlock`.
schema.addAttributeCheck( ( context, attributeName ) => {
if (
context.endsWith( 'codeBlock' ) &&
listEditing && listEditing.getListAttributeNames().includes( attributeName )
) {
return true;
}

// Disallow all attributes on `$text` inside `codeBlock`.
schema.addAttributeCheck( context => {
if ( context.endsWith( 'codeBlock $text' ) ) {
return false;
}
} );

// Disallow object elements inside `codeBlock`. See #9567.
editor.model.schema.addChildCheck( ( context, childDefinition ) => {
if ( context.endsWith( 'codeBlock' ) && childDefinition.isObject ) {
return false;
}
} );

// Conversion.
editor.editing.downcastDispatcher.on<DowncastInsertEvent>(
'insert:codeBlock',
Expand Down
16 changes: 7 additions & 9 deletions packages/ckeditor5-code-block/tests/codeblockediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,15 @@ describe( 'CodeBlockEditing', () => {
expect( model.schema.checkChild( [ '$root', 'codeBlock' ], 'codeBlock' ) ).to.be.false;
} );

it( 'disallows object elements in codeBlock', () => {
// Fake "inline-widget".
model.schema.register( 'inline-widget', {
inheritAllFrom: '$block',
// Allow to be a child of the `codeBlock` element.
allowIn: 'codeBlock',
// And mark as an object.
isObject: true
it( 'disallows $inlineObject', () => {
// Disallow `$inlineObject` and its derivatives like `inlineWidget` inside `codeBlock` to ensure that only text,
// not other inline elements like inline images, are allowed. This maintains the semantic integrity of code blocks.
model.schema.register( 'inlineWidget', {
inheritAllFrom: '$inlineObject'
} );

expect( model.schema.checkChild( [ '$root', 'codeBlock' ], 'inline-widget' ) ).to.be.false;
expect( model.schema.checkChild( [ '$root', 'codeBlock' ], '$inlineObject' ) ).to.be.false;
expect( model.schema.checkChild( [ '$root', 'codeBlock' ], 'inlineWidget' ) ).to.be.false;
} );

it( 'allows only for $text in codeBlock', () => {
Expand Down
64 changes: 64 additions & 0 deletions packages/ckeditor5-engine/docs/framework/deep-dive/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,70 @@ Both the `{@link module:engine/model/schema~SchemaItemDefinition#allowIn}` and `
You can read more about the format of the item definition in the {@link module:engine/model/schema~SchemaItemDefinition} API guide.
</info-box>

## Disallowing structures

The schema, in addition to allowing certain structures, can also be used to ensure some structures are explicitly disallowed. This can be achieved with the use of disallow rules.
scofalik marked this conversation as resolved.
Show resolved Hide resolved
scofalik marked this conversation as resolved.
Show resolved Hide resolved

Typically, you will use {@link module:engine/model/schema~SchemaItemDefinition#disallowChildren} property for that. It can be used to define which nodes are disallowed inside given element:

```js
schema.register( 'myElement', {
inheritAllFrom: '$block',
disallowChildren: 'imageInline'
} );
```

In the example above, a new custom element should behave like any block element (paragraph, heading, etc.) but it should not be possible to insert inline images inside it.

### Precedence over allow rules

In general, all `disallow` rules have higher priority than their `allow` counterparts. When we also take inheritance into the picture, the hierarchy of rules looks like this (from the highest priority):

1. `disallowChildren` / `disallowIn` from the element's own definition.
2. `allowChildren` / `allowIn` from the element's own definition.
3. `disallowChildren` / `disallowIn` from the inherited element's definition.
4. `allowChildren` / `allowIn` from the inherited element's definition.

### Disallow rules examples

While disallowing is easy to understand for simple cases, things might start to get unclear when more complex rules are involved. Below are some examples explaining how disallowing works when rules inheriting is involved.

```js
schema.register( 'baseChild' );
schema.register( 'baseParent', { allowChildren: [ 'baseChild' ] } );

schema.register( 'extendedChild', { inheritAllFrom: 'baseChild' } );
schema.register( 'extendedParent', { inheritAllFrom: 'baseParent', disallowChildren: [ 'baseChild' ] } );
```

In this case, `extendedChild` will be allowed in `baseParent` (thanks to inheriting from `baseChild`) and in `extendedParent` (as it inherits `baseParent`).

But `baseChild` will be allowed only in `baseParent`. Although `extendedParent` inherits all rules from `baseParent` it specifically disallows `baseChild` as the part of its definition.

Below is a different example, where instead `baseChild` is extended with `disallowIn` rule:

```js
schema.register( 'baseParent' );
schema.register( 'baseChild', { allowIn: 'baseParent' } );

schema.register( 'extendedParent', { inheritAllFrom: 'baseParent' } );
schema.register( 'extendedChild', { inheritAllFrom: 'baseChild' } );
schema.extend( 'baseChild', { disallowIn: 'extendedParent' } );
```

This changes how schema rules are resolved. `baseChild` will still be disallowed in `extendedParent` as before. But now, `extendedChild` will be disallowed in `extendedParent` as well. That's because it will inherit this rule from `baseChild`, and there is no other rule that would allow `extendedChild` in `extendedParent`.
scofalik marked this conversation as resolved.
Show resolved Hide resolved

Of course, you can mix `allowIn` with `disallowChildren` as well as `allowChildren` with `disallowIn`.

Finally, a situation may come up, when you want to inherit from an item which is already disallowed, but the new element should be re-allowed again. In this case, the definitions should look like this:

```js
schema.register( 'baseParent', { inheritAllFrom: 'paragraph', disallowChildren: [ 'imageInline' ] } );
schema.register( 'extendedParent', { inheritAllFrom: 'baseParent', allowChildren: [ 'imageInline' ] } );
```

Here, `imageInline` is allowed in paragraph, but will not be allowed in `baseParent`. However, `extendedParent` will again re-allow it, as own definitions are more important than inherited definitions.

## 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
Loading