From 3844e2d4f93fe78c3dd12467db3c6aa6556767d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Sun, 31 Dec 2017 15:39:32 +0100 Subject: [PATCH] API docs. --- src/conversion/viewconversiondispatcher.js | 3 +- src/model/schema.js | 219 ++++++++++++++++++++- 2 files changed, 213 insertions(+), 9 deletions(-) diff --git a/src/conversion/viewconversiondispatcher.js b/src/conversion/viewconversiondispatcher.js index 7740d1021..ab7ec2137 100644 --- a/src/conversion/viewconversiondispatcher.js +++ b/src/conversion/viewconversiondispatcher.js @@ -77,8 +77,7 @@ import log from '@ckeditor/ckeditor5-utils/src/log'; * inside: data.context * }; * - * // TODO it was impossible for me to understand wheter `data.context` contains `$text`. - * if ( conversionApi.schema.checkAttribute( TODO ) ) { + * if ( conversionApi.schema.checkAttribute( [ ...data.context, '$text' ], 'link' ) ) { * item.setAttribute( 'link', data.input.getAttribute( 'href' ) ); * } * } diff --git a/src/model/schema.js b/src/model/schema.js index 2856602b2..cb6e1ceae 100644 --- a/src/model/schema.js +++ b/src/model/schema.js @@ -14,9 +14,172 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix'; import Range from './range'; /** + * The model's schema. It defines allowed and disallowed structures of nodes as well as their attributes. + * The schema rules are usually defined by features and based on them the editing framework and features + * make decisions how to process the model. + * + * The instance of schema is available in {@link module:engine/model/model~Model#schema `editor.model.schema`}. + * + * # Schema rules + * + * Schema defines allowed model structures and allowed attributes separately. They are also checked separately + * by using the {@link #checkChild} and {@link #checkAttribute} methods. + * + * ## Defining allowed structures + * + * When a feature introduces a model element it should registered it in the schema. Besides + * defining that such an element may exist in the model, the feature also needs to define where + * this element may occur: + * + * schema.register( 'myElement', { + * allowIn: '$root' + * } ); + * + * This lets the schema know that `` may be a child of the `<$root>` element. `$root` is one of generic + * node types defined by the editing framework. By default, the editor names the main root element a `<$root>`, + * so the above rule allows `` in the main editor element. + * + * In other words, this would be correct: + * + * <$root> + * + * While this would not be correct: + * + * <$root> + * + * ## Generic node types + * + * There are three basic generic node types: `$root`, `$block` and `$text`. + * They are defined as follows: + * + * this.schema.register( '$root', { + * isLimit: true + * } ); + * this.schema.register( '$block', { + * allowIn: '$root', + * isBlock: true + * } ); + * this.schema.register( '$text', { + * allowIn: '$block' + * } ); + * + * Those rules can then be reused by features to define their rules in a more extensible way. + * For example, the {@link module:paragraph/paragraph~Paragraph} feature will define its rules as: + * + * schema.register( 'paragraph', { + * inheritAllFrom: '$block' + * } ); + * + * Which translates to: + * + * schema.register( 'paragraph', { + * allowWhere: '$block', + * allowContentOf: '$block', + * allowAttributesOf: '$block', + * inheritTypesFrom: '$block' + * } ); + * + * Which can be read as: + * + * * The `` element will be allowed in elements in which `<$block>` is allowed (e.g. in `<$root>`). + * * The `` element will allow all nodes which are allowed in `<$block>` (e.g. `$text`). + * * The `` element will allow all attributes allowed on `<$block>`. + * * The `` element will inherit all `is*` properties of `<$block>` (e.g. `isBlock`). + * + * Thanks to the fact that ``'s rules are inherited from `<$block>` other features can use the `<$block>` + * type to indirectly extend ``'s rules. For example, the {@link module:block-quote/blockquote~BlockQuote} + * feature does this: + * + * schema.register( 'blockQuote', { + * allowWhere: '$block', + * allowContentOf: '$root' + * } ); + * + * Thanks to that, despite the fact that block quote and paragraph features know nothing about themselves, paragraphs + * will be allowed in block quotes and block quotes will be allowed in all places where blocks are, so if anyone will + * register a `
` element (with `allowContentOf: '$root'` rule), that `
` elements will allow + * block quotes. + * + * The side effect of such a rule inheritance is that now `
` is allowed in `
` which needs to be + * resolved by a callback which will disallow this specific structure. + * + * ## Defining advanced rules in `checkChild()`'s callbacks + * + * The {@link #checkChild} method which is the base method used to check whether some element is allowed in a given structure + * is {@link module:utils/observablemixin~ObservableMixin#decorate decorated} with the {@link #event-checkChild} event. + * It means that you can add listeners to implement your specific rules which are not limited by the declarative + * {@link module:engine/model/schema~SchemaRuleDefinition} API. + * + * The block quote feature defines such a listener to disallow nested `
` structures: + * + * schema.on( 'checkChild', ( evt, args ) => { + * // The checkChild()'s params. + * // Note that context is automatically normalized to SchemaContext instance by a highest-priority listener. + * const context = args[ 0 ]; + * const child = args[ 1 ]; + * + * // Pass the child through getRule() to normalize it (child can be passed in multiple formats). + * const childRule = schema.getRule( child ); + * + * // If checkChild() is called with a context that ends with blockQuote and blockQuote as a child + * // to check, make the method return false and stop the event so no other listener will override your decision. + * if ( childRule && childRule.name == 'blockQuote' && context.matchEnd( 'blockQuote' ) ) { + * evt.stop(); + * evt.return = false; + * } + * }, { priority: 'high' } ); + * + * ## Defining attributes + * + * TODO + * + * ## Implementing additional constraints + * + * Schema's capabilities were limited to simple (and atomic) {@link #checkChild} and {@link #checkAttribute} on purpose. + * One may imagine defining more complex rules such as "element `` must be always followed by ``". While it is + * feasible to create an API which would enable feeding the schema with such definitions, it is unrealistic to then + * expect that every editing feature will consider them when processing the model. It is also unrealistic to expect + * that it will be done automatically by the schema and the editing engine themselves. + * + * For instance, let's get back to the "element `` must be always followed by ``" rule and this initial content: + * + * <$root> + * foo + * bar[bom + * bom]bar + * + * + * Now, imagine the user presses the "block quote" button. Usually it would wrap the two selected blocks (`` and ``) + * with a `
` element: + * + * <$root> + * foo + *
+ * bar[bom + * bom]bar + *
+ * + * + * But it turns out that this creates an incorrect structure – `` is not followed by `` anymore. + * + * What should happen instead? There are at least 4 possible solutions: the block quote feature should not be + * applicable in such a context, someone should create a new `` right after ``, `` should be moved + * inside `
` together with `` or vice versa. + * + * While this is a relatively simple scenario (unlike most real-time collaboration scenarios), + * it turns out that it's already hard to say what should happen and who should react to fix this content. + * + * Therefore, if your editor needs to implement such rules, it should do that through model's post-fixers + * fixing incorrect content according to the rules that you'll define or actively prevent such situations + * (e.g. by disabling certain features). It means that those constraints will be defined specifically for your + * scenario by your code which answers the two problems that we had with generic rules – "who?" and "how?". + * * @mixes module:utils/emittermixin~ObservableMixin */ export default class Schema { + /** + * Creates schema instance. + */ constructor() { this._sourceRules = {}; @@ -33,6 +196,12 @@ export default class Schema { }, { priority: 'highest' } ); } + /** + * Registers schema item. Can only be called once for every item name. + * + * @param {String} itemName + * @param {module:engine/model/schema~SchemaRuleDefintion} rules + */ register( itemName, rules ) { if ( this._sourceRules[ itemName ] ) { // TODO docs @@ -48,6 +217,30 @@ export default class Schema { this._clearCache(); } + /** + * Extends a {@link #register registered} item's rules. + * + * Extending properties such as `allowIn` will add more items to the existing properties, + * while redefining properties such as `isBlock` will override the previously defined ones. + * + * schema.register( 'foo', { + * allowIn: '$root', + * isBlock: true; + * } ); + * schema.extend( 'foo', { + * allowIn: 'blockQuote', + * isBlock: false + * } ); + * + * schema.getRule( 'foo' ); + * // { + * // allowIn: [ '$root', 'blockQuote' ], + * // isBlock: false + * // } + * + * @param {String} itemName + * @param {module:engine/model/schema~SchemaRuleDefintion} rules + */ extend( itemName, rules ) { if ( !this._sourceRules[ itemName ] ) { // TODO docs @@ -61,6 +254,11 @@ export default class Schema { this._clearCache(); } + /** + * Returns all registered items. + * + * @returns {Object.} + */ getRules() { if ( !this._compiledRules ) { this._compile(); @@ -70,7 +268,10 @@ export default class Schema { } /** - * @param {module:engine/model/item~Item|SchemaContextItem|String} item + * Returns a definition of the given item or `undefined` if item is not registered. + * + * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item + * @returns {module:engine/model/schema~SchemaRuleDefintion} */ getRule( item ) { let itemName; @@ -80,7 +281,7 @@ export default class Schema { } else if ( item.is && ( item.is( 'text' ) || item.is( 'textProxy' ) ) ) { itemName = '$text'; } - // Element or SchemaContextItem. + // Element or module:engine/model/schema~SchemaContextItem. else { itemName = item.name; } @@ -89,14 +290,14 @@ export default class Schema { } /** - * @param {module:engine/model/item~Item|SchemaContextItem|String} item + * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item */ isRegistered( item ) { return !!this.getRule( item ); } /** - * @param {module:engine/model/item~Item|SchemaContextItem|String} item + * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item */ isBlock( item ) { const rule = this.getRule( item ); @@ -105,7 +306,7 @@ export default class Schema { } /** - * @param {module:engine/model/item~Item|SchemaContextItem|String} item + * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item */ isLimit( item ) { const rule = this.getRule( item ); @@ -114,7 +315,7 @@ export default class Schema { } /** - * @param {module:engine/model/item~Item|SchemaContextItem|String} item + * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item */ isObject( item ) { const rule = this.getRule( item ); @@ -320,6 +521,10 @@ export default class Schema { mix( Schema, ObservableMixin ); +/** + * @typedef {Object} module:engine/model/schema~SchemaRuleDefinition + */ + /** * @private */ @@ -368,7 +573,7 @@ export class SchemaContext { * TODO * * @typedef {module:engine/model/node~Node|module:engine/model/position~Position| - * Array.} SchemaContextDefinition + * Array.} module:engine/model/schema~SchemaContextDefinition */ function compileBaseItemRule( sourceItemRules, itemName ) {