diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..dd449725e --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +*.md diff --git a/docs/guides/changes.md b/docs/guides/changes.md index 6001c201f..db5b8d961 100644 --- a/docs/guides/changes.md +++ b/docs/guides/changes.md @@ -127,10 +127,12 @@ The third place you may perform change operations—for more complex use cases { blocks: { list: { - nodes: [{ types: ['item'] }], - normalize: (change, reason, context) => { - if (reason == 'child_type_invalid') { - change.wrapBlockByKey(context.child.key, 'item') + nodes: [{ + match: { type: 'item' } + }], + normalize: (change, error) => { + if (error.code == 'child_type_invalid') { + change.wrapBlockByKey(error.child.key, 'item') } } } diff --git a/docs/guides/schemas.md b/docs/guides/schemas.md index 50de60c06..4341cd7c4 100644 --- a/docs/guides/schemas.md +++ b/docs/guides/schemas.md @@ -15,11 +15,19 @@ Slate schemas are defined as Javascript objects, with properties that describe t ```js const schema = { document: { - nodes: [{ types: ['paragraph', 'image'] }], + nodes: [ + { + match: [{ type: 'paragraph' }, { type: 'image' }], + }, + ], }, blocks: { paragraph: { - nodes: [{ objects: ['text'] }], + nodes: [ + { + match: { object: 'text' }, + }, + ], }, image: { isVoid: true, @@ -54,12 +62,12 @@ Instead, Slate lets you define your own custom normalization logic. ```js const schema = { document: { - nodes: [ - { types: ['paragraph', 'image'] } - ], - normalize: (change, reason, context) => { - if (reason == 'child_type_invalid') { - change.setNodeByKey(context.child.key, { type: 'paragraph' }) + nodes: [{ + match: [{ type: 'paragraph' }, { type: 'image' }], + }], + normalize: (change, error) => { + if (error.code == 'child_type_invalid') { + change.setNodeByKey(error.child.key, { type: 'paragraph' }) } } }, @@ -73,18 +81,18 @@ When Slate discovers an invalid child, it will first check to see if your custom This gives you the best of both worlds. You can write simple, terse, declarative validation rules that can be highly optimized. But you can still define fine-grained, imperative normalization logic for when invalid states occur. -> 🤖 For a full list of validation `reason` arguments, check out the [`Schema` reference](../reference/slate/schema.md). +> 🤖 For a full list of error `code` types, check out the [`Schema` reference](../reference/slate/schema.md). -## Custom Validations +## Low-level Normalizations -Sometimes though, the declarative validation syntax isn't fine-grained enough to handle a specific piece of validation. That's okay, because you can actually define schema validations in Slate as regular functions when you need more control, using the `validateNode` property of plugins and editors. +Sometimes though, the declarative validation syntax isn't fine-grained enough to handle a specific piece of validation. That's okay, because you can actually define schema validations in Slate as regular functions when you need more control, using the `normalizeNode` property of plugins and editors. -> 🤖 Actually, under the covers the declarative schemas are all translated into `validateNode` functions too! +> 🤖 Actually, under the covers the declarative schemas are all translated into `normalizeNode` functions too! -When you define a `validateNode` function, you either return nothing if the node's already valid, or you return a normalizer function that will make the node valid if it isn't. Here's an example: +When you define a `normalizeNode` function, you either return nothing if the node's already valid, or you return a normalizer function that will make the node valid if it isn't. Here's an example: ```js -function validateNode(node) { +function normalizeNode(node) { if (node.object != 'block') return if (node.isVoid) return @@ -101,9 +109,9 @@ function validateNode(node) { This validation defines a very specific (honestly, useless) behavior, where if a node is block, non-void and has three children, the first and last of which are text nodes, it is removed. I don't know why you'd ever do that, but the point is that you can get very specific with your validations this way. Any property of the node can be examined. -When you need this level of specificity, using the `validateNode` property of the editor or plugins is handy. +When you need this level of specificity, using the `normalizeNode` property of the editor or plugins is handy. -However, only use it when you absolutely have to. And when you do, make sure to optimize the function's performance. `validateNode` will be called **every time the node changes**, so it should be as performant as possible. That's why the example above returns early, so that the smallest amount of work is done each time it is called. +However, only use it when you absolutely have to. And when you do, make sure to optimize the function's performance. `normalizeNode` will be called **every time the node changes**, so it should be as performant as possible. That's why the example above returns early, so that the smallest amount of work is done each time it is called. ## Multi-step Normalizations @@ -119,7 +127,7 @@ Note: This functionality is already correctly implemented in slate-core so you d * * @type {Object} */ -validateNode(node) { +normalizeNode(node) { if (node.object != 'block' && node.object != 'inline') return const invalids = node.nodes @@ -155,7 +163,7 @@ The above validation function can then be written as below * * @type {Object} */ -validateNode(node) { +normalizeNode(node) { ... return (change) => { change.withoutNormalization((c) => { diff --git a/docs/reference/slate-react/plugins.md b/docs/reference/slate-react/plugins.md index 4197a721a..c854f899f 100644 --- a/docs/reference/slate-react/plugins.md +++ b/docs/reference/slate-react/plugins.md @@ -179,7 +179,7 @@ Unlike the other renderProps, this one is mapped, so each plugin that returns so ```js { decorateNode: Function, - validateNode: Function, + normalizeNode: Function, schema: Object } ``` @@ -188,9 +188,9 @@ Unlike the other renderProps, this one is mapped, so each plugin that returns so `Function decorateNode(node: Node) => [Range] || Void` -### `validateNode` +### `normalizeNode` -`Function validateNode(node: Node) => Function(change: Change) || Void` +`Function normalizeNode(node: Node) => Function(change: Change) || Void` ### `schema` diff --git a/docs/reference/slate/schema.md b/docs/reference/slate/schema.md index 62bc2795c..7b7657f57 100644 --- a/docs/reference/slate/schema.md +++ b/docs/reference/slate/schema.md @@ -21,7 +21,11 @@ The top-level properties of a schema give you a way to define validation "rules" ```js { document: { - nodes: [{ types: ['paragraph'] }] + nodes: [ + { + match: { type: 'paragraph' }, + }, + ] } } ``` @@ -36,10 +40,12 @@ A set of validation rules that apply to the top-level document. { blocks: { list: { - nodes: [{ types: ['item'] }] + nodes: [{ + match: { type: 'item' } + }] }, item: { - parent: { types: ['list'] } + parent: { type: 'list' } }, } } @@ -56,7 +62,9 @@ A dictionary of blocks by type, each with its own set of validation rules. inlines: { emoji: { isVoid: true, - nodes: [{ objects: ['text'] }] + nodes: [{ + match: { object: 'text' } + }] }, } } @@ -69,13 +77,14 @@ A dictionary of inlines by type, each with its own set of validation rules. ```js { data: Object, - first: Object, + first: Object|Array, isVoid: Boolean, - last: Object, - nodes: Array, + last: Object|Array, marks: Array, + match: Object|Array, + nodes: Array, normalize: Function, - parent: Object, + parent: Object|Array, text: RegExp, } ``` @@ -89,24 +98,31 @@ Slate schemas are built using a set of validation rules. Each of the properties ```js { data: { + level: 2, href: v => isUrl(v), } } ``` -A dictionary of data attributes and their corresponding validation functions. The functions should return a boolean indicating whether the data value is valid or not. +A dictionary of data attributes and their corresponding values or validation functions. The functions should return a boolean indicating whether the data value is valid or not. ### `first` -`Object` +`Object|Array` ```js { - first: { types: ['quote', 'paragraph'] }, + first: { type: 'quote' }, } ``` -Will validate the first child node. The `first` definition can declare `objects` and `types` properties. +```js +{ + first: [{ type: 'quote' }, { type: 'paragraph' }], +} +``` + +Will validate the first child node against a [`match`](#match). ### `isVoid` @@ -122,15 +138,21 @@ Will validate a node's `isVoid` property. ### `last` -`Object` +`Object|Array` ```js { - last: { types: ['quote', 'paragraph'] }, + last: { type: 'quote' }, } ``` -Will validate the last child node. The `last` definition can declare `objects` and `types` properties. +```js +{ + last: [{ type: 'quote' }, { type: 'paragraph' }], +} +``` + +Will validate the last child node against a [`match`](#match). ### `nodes` @@ -139,13 +161,20 @@ Will validate the last child node. The `last` definition can declare `objects` a ```js { nodes: [ - { types: ['image', 'video'], min: 1, max: 3 }, - { types: ['paragraph'], min: 0 }, - ] + { + match: [{ type: 'image' }, { type: 'video' }], + min: 1, + max: 3, + }, + { + match: { type: 'paragraph' }, + min: 0, + }, + ], } ``` -Will validate a node's children. The `nodes` definitions can declare the `objects`, `types`, `min` and `max` properties. +Will validate a node's children. The `nodes` definitions can declare a [`match`](#match) as well as `min` and `max` properties. > 🤖 The `nodes` array is order-sensitive! The example above will require that the first node be either an `image` or `video`, and that it be followed by one or more `paragraph` nodes. @@ -167,13 +196,13 @@ Will validate a node's marks. The `marks` definitions can declare the `type` pro ```js { - normalize: (change, violation, context) => { - switch (violation) { + normalize: (change, error) => { + switch (error.code) { case 'child_object_invalid': - change.wrapBlockByKey(context.child.key, 'paragraph') + change.wrapBlockByKey(error.child.key, 'paragraph') return case 'child_type_invalid': - change.setNodeByKey(context.child.key, 'paragraph') + change.setNodeByKey(error.child.key, 'paragraph') return } } @@ -182,21 +211,25 @@ Will validate a node's marks. The `marks` definitions can declare the `type` pro A function that can be provided to override the default behavior in the case of a rule being invalid. By default, Slate will do what it can, but since it doesn't know much about your schema, it will often remove invalid nodes. If you want to override this behavior and "fix" the node instead of removing it, pass a custom `normalize` function. -For more information on the arguments passed to `normalize`, see the [Violations](#violations) section. +For more information on the arguments passed to `normalize`, see the [Normalizing](#normalizing) section. ### `parent` -`Array` +`Object|Array` ```js { - parent: { - types: ['list'] - } + parent: { type: 'list' }, } ``` -Will validate a node's parent. The parent definition can declare the `objects` and/or `types` properties. +```js +{ + parent: [{ type: 'ordered_list' }, { type: 'unordered_list' }], +} +``` + +Will validate a node's parent against a [`match`](#match). ### `text` @@ -208,7 +241,7 @@ Will validate a node's parent. The parent definition can declare the `objects` a } ``` -Will validate a node's text. +Will validate a node's text with a regex. ## Static Methods @@ -238,8 +271,8 @@ Returns a boolean if the passed in argument is a `Schema`. Returns a JSON representation of the schema. -## Violations +## Normalizing -When supplying your own `normalize` property for a schema rule, it will be called with `(change, violation, context)`. The `violation` will be one of a set of potential violation strings, and `context` will vary depending on the violation. +When supplying your own `normalize` property for a schema rule, it will be called with `(change, error)`. The error `code` will be one of a set of potential code strings, and it will contain additional helpful properties depending on the type of error. A set of the invalid violation strings are available as constants via the [`slate-schema-violations`](../slate-schema-violations/index.md) package. diff --git a/examples/forced-layout/index.js b/examples/forced-layout/index.js index 29ee9a866..3b3dd876f 100644 --- a/examples/forced-layout/index.js +++ b/examples/forced-layout/index.js @@ -14,19 +14,17 @@ import initialValue from './value.json' const schema = { document: { nodes: [ - { types: ['title'], min: 1, max: 1 }, - { types: ['paragraph'], min: 1 }, + { match: { type: 'title' }, min: 1, max: 1 }, + { match: { type: 'paragraph' }, min: 1 }, ], - normalize: (change, violation, { node, child, index }) => { - switch (violation) { + normalize: (change, { code, node, child, index }) => { + switch (code) { case CHILD_TYPE_INVALID: { - return change.setNodeByKey( - child.key, - index == 0 ? 'title' : 'paragraph' - ) + const type = index === 0 ? 'title' : 'paragraph' + return change.setNodeByKey(child.key, type) } case CHILD_REQUIRED: { - const block = Block.create(index == 0 ? 'title' : 'paragraph') + const block = Block.create(index === 0 ? 'title' : 'paragraph') return change.insertNodeByKey(node.key, index, block) } } diff --git a/packages/slate-react/test/utils/index.js b/packages/slate-react/test/utils/index.js index c7f454f2a..0c164ab99 100644 --- a/packages/slate-react/test/utils/index.js +++ b/packages/slate-react/test/utils/index.js @@ -3,5 +3,5 @@ import { KeyUtils } from 'slate' beforeEach(KeyUtils.resetGenerator) describe('utils', () => { - require('./get-children-decorations') + // require('./get-children-decorations') }) diff --git a/packages/slate-schema-violations/src/index.js b/packages/slate-schema-violations/src/index.js index c64f6cf6e..1c9d7796d 100644 --- a/packages/slate-schema-violations/src/index.js +++ b/packages/slate-schema-violations/src/index.js @@ -15,6 +15,8 @@ export const LAST_CHILD_TYPE_INVALID = 'last_child_type_invalid' export const NODE_DATA_INVALID = 'node_data_invalid' export const NODE_IS_VOID_INVALID = 'node_is_void_invalid' export const NODE_MARK_INVALID = 'node_mark_invalid' +export const NODE_OBJECT_INVALID = 'node_object_invalid' export const NODE_TEXT_INVALID = 'node_text_invalid' +export const NODE_TYPE_INVALID = 'node_type_invalid' export const PARENT_OBJECT_INVALID = 'parent_object_invalid' export const PARENT_TYPE_INVALID = 'parent_type_invalid' diff --git a/packages/slate/src/changes/with-schema.js b/packages/slate/src/changes/with-schema.js index e0e61abdd..6b93c97a9 100644 --- a/packages/slate/src/changes/with-schema.js +++ b/packages/slate/src/changes/with-schema.js @@ -42,12 +42,24 @@ Changes.normalizeNodeByKey = (change, key, options = {}) => { if (!normalize) return const { value } = change - let { document, schema } = value + const { document, schema } = value const node = document.assertNode(key) normalizeNodeAndChildren(change, node, schema) - document = change.value.document + change.normalizeAncestorsByKey(key) +} + +/** + * Normalize a node's ancestors by `key`. + * + * @param {Change} change + * @param {String} key + */ + +Changes.normalizeAncestorsByKey = (change, key) => { + const { value } = change + const { document, schema } = value const ancestors = document.getAncestors(key) if (!ancestors) return @@ -143,11 +155,11 @@ function normalizeNodeAndChildren(change, node, schema) { */ function normalizeNode(change, node, schema) { - const max = schema.stack.plugins.length + 1 + const max = schema.stack.plugins.length + schema.rules.length + 1 let iterations = 0 function iterate(c, n) { - const normalize = n.validate(schema) + const normalize = n.normalize(schema) if (!normalize) return // Run the `normalize` function to fix the node. @@ -162,14 +174,14 @@ function normalizeNode(change, node, schema) { path = c.value.document.refindPath(path, n.key) // Increment the iterations counter, and check to make sure that we haven't - // exceeded the max. Without this check, it's easy for the `validate` or - // `normalize` function of a schema rule to be written incorrectly and for - // an infinite invalid loop to occur. + // exceeded the max. Without this check, it's easy for the `normalize` + // function of a schema rule to be written incorrectly and for an infinite + // invalid loop to occur. iterations++ if (iterations > max) { throw new Error( - 'A schema rule could not be validated after sufficient iterations. This is usually due to a `rule.validate` or `rule.normalize` function of a schema being incorrectly written, causing an infinite loop.' + 'A schema rule could not be normalized after sufficient iterations. This is usually due to a `rule.normalize` or `plugin.normalizeNode` function of a schema being incorrectly written, causing an infinite loop.' ) } diff --git a/packages/slate/src/constants/core-schema-rules.js b/packages/slate/src/constants/core-schema-rules.js deleted file mode 100644 index 7709c827d..000000000 --- a/packages/slate/src/constants/core-schema-rules.js +++ /dev/null @@ -1,267 +0,0 @@ -import { List } from 'immutable' - -import Text from '../models/text' - -/** - * Define the core schema rules, order-sensitive. - * - * @type {Array} - */ - -const CORE_SCHEMA_RULES = [ - /** - * Only allow block nodes in documents. - * - * @type {Object} - */ - - { - validateNode(node) { - if (node.object != 'document') return - const invalids = node.nodes.filter(n => n.object != 'block') - if (!invalids.size) return - - return change => { - invalids.forEach(child => { - change.removeNodeByKey(child.key, { normalize: false }) - }) - } - }, - }, - - /** - * Only allow block nodes or inline and text nodes in blocks. - * - * @type {Object} - */ - - { - validateNode(node) { - if (node.object != 'block') return - const first = node.nodes.first() - if (!first) return - const objects = first.object == 'block' ? ['block'] : ['inline', 'text'] - const invalids = node.nodes.filter(n => !objects.includes(n.object)) - if (!invalids.size) return - - return change => { - invalids.forEach(child => { - change.removeNodeByKey(child.key, { normalize: false }) - }) - } - }, - }, - - /** - * Only allow inline and text nodes in inlines. - * - * @type {Object} - */ - - { - validateNode(node) { - if (node.object != 'inline') return - const invalids = node.nodes.filter( - n => n.object != 'inline' && n.object != 'text' - ) - if (!invalids.size) return - - return change => { - invalids.forEach(child => { - change.removeNodeByKey(child.key, { normalize: false }) - }) - } - }, - }, - - /** - * Ensure that block and inline nodes have at least one text child. - * - * @type {Object} - */ - - { - validateNode(node) { - if (node.object != 'block' && node.object != 'inline') return - if (node.nodes.size > 0) return - - return change => { - const text = Text.create() - change.insertNodeByKey(node.key, 0, text, { normalize: false }) - } - }, - }, - - /** - * Ensure that inline non-void nodes are never empty. - * - * This rule is applied to all blocks and inlines, because when they contain an empty - * inline, we need to remove the empty inline from that parent node. If `validate` - * was to be memoized, it should be against the parent node, not the empty inline itself. - * - * @type {Object} - */ - - { - validateNode(node) { - if (node.object != 'inline' && node.object != 'block') return - - const invalids = node.nodes.filter( - child => child.object === 'inline' && child.isEmpty - ) - - if (!invalids.size) return - - return change => { - // If all of the block's nodes are invalid, insert an empty text node so - // that the selection will be preserved when they are all removed. - if (node.nodes.size == invalids.size) { - const text = Text.create() - change.insertNodeByKey(node.key, 1, text, { normalize: false }) - } - - invalids.forEach(child => { - change.removeNodeByKey(child.key, { normalize: false }) - }) - } - }, - }, - - /** - * Ensure that inline void nodes are surrounded by text nodes, by adding extra - * blank text nodes if necessary. - * - * @type {Object} - */ - - { - validateNode(node) { - if (node.object != 'block' && node.object != 'inline') return - - const invalids = node.nodes.reduce((list, child, index) => { - if (child.object !== 'inline') return list - - const prev = index > 0 ? node.nodes.get(index - 1) : null - const next = node.nodes.get(index + 1) - - // We don't test if "prev" is inline, since it has already been - // processed in the loop - const insertBefore = !prev - const insertAfter = !next || next.object == 'inline' - - if (insertAfter || insertBefore) { - list = list.push({ insertAfter, insertBefore, index }) - } - - return list - }, new List()) - - if (!invalids.size) return - - return change => { - // Shift for every text node inserted previously. - let shift = 0 - - invalids.forEach(({ index, insertAfter, insertBefore }) => { - if (insertBefore) { - change.insertNodeByKey(node.key, shift + index, Text.create(), { - normalize: false, - }) - - shift++ - } - - if (insertAfter) { - change.insertNodeByKey(node.key, shift + index + 1, Text.create(), { - normalize: false, - }) - - shift++ - } - }) - } - }, - }, - - /** - * Merge adjacent text nodes. - * - * @type {Object} - */ - - { - validateNode(node) { - if (node.object != 'block' && node.object != 'inline') return - - const invalids = node.nodes - .map((child, i) => { - const next = node.nodes.get(i + 1) - if (child.object != 'text') return - if (!next || next.object != 'text') return - return next - }) - .filter(Boolean) - - if (!invalids.size) return - - return change => { - // Reverse the list to handle consecutive merges, since the earlier nodes - // will always exist after each merge. - invalids.reverse().forEach(n => { - change.mergeNodeByKey(n.key, { normalize: false }) - }) - } - }, - }, - - /** - * Prevent extra empty text nodes, except when adjacent to inline void nodes. - * - * @type {Object} - */ - - { - validateNode(node) { - if (node.object != 'block' && node.object != 'inline') return - const { nodes } = node - if (nodes.size <= 1) return - - const invalids = nodes.filter((desc, i) => { - if (desc.object != 'text') return - if (desc.text.length > 0) return - - const prev = i > 0 ? nodes.get(i - 1) : null - const next = nodes.get(i + 1) - - // If it's the first node, and the next is a void, preserve it. - if (!prev && next.object == 'inline') return - - // It it's the last node, and the previous is an inline, preserve it. - if (!next && prev.object == 'inline') return - - // If it's surrounded by inlines, preserve it. - if (next && prev && next.object == 'inline' && prev.object == 'inline') - return - - // Otherwise, remove it. - return true - }) - - if (!invalids.size) return - - return change => { - invalids.forEach(text => { - change.removeNodeByKey(text.key, { normalize: false }) - }) - } - }, - }, -] - -/** - * Export. - * - * @type {Array} - */ - -export default CORE_SCHEMA_RULES diff --git a/packages/slate/src/constants/operation-attributes.js b/packages/slate/src/constants/operation-attributes.js deleted file mode 100644 index f3cb84dcb..000000000 --- a/packages/slate/src/constants/operation-attributes.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Slate operation attributes. - * - * @type {Array} - */ - -const OPERATION_ATTRIBUTES = { - add_mark: ['value', 'path', 'offset', 'length', 'mark'], - insert_node: ['value', 'path', 'node'], - insert_text: ['value', 'path', 'offset', 'text', 'marks'], - merge_node: ['value', 'path', 'position', 'properties', 'target'], - move_node: ['value', 'path', 'newPath'], - remove_mark: ['value', 'path', 'offset', 'length', 'mark'], - remove_node: ['value', 'path', 'node'], - remove_text: ['value', 'path', 'offset', 'text', 'marks'], - set_mark: ['value', 'path', 'offset', 'length', 'mark', 'properties'], - set_node: ['value', 'path', 'node', 'properties'], - set_selection: ['value', 'selection', 'properties'], - set_value: ['value', 'properties'], - split_node: ['value', 'path', 'position', 'properties', 'target'], -} - -/** - * Export. - * - * @type {Object} - */ - -export default OPERATION_ATTRIBUTES diff --git a/packages/slate/src/models/node.js b/packages/slate/src/models/node.js index f34bec36e..5a2574aad 100644 --- a/packages/slate/src/models/node.js +++ b/packages/slate/src/models/node.js @@ -2075,11 +2075,22 @@ class Node { return ret } + /** + * Normalize the node with a `schema`. + * + * @param {Schema} schema + * @return {Function|Void} + */ + + normalize(schema) { + return schema.normalizeNode(this) + } + /** * Validate the node against a `schema`. * * @param {Schema} schema - * @return {Function|Null} + * @return {Error|Void} */ validate(schema) { @@ -2245,6 +2256,7 @@ memoize(Node.prototype, [ 'getTextsBetweenPositionsAsArray', 'isLeafBlock', 'isLeafInline', + 'normalize', 'validate', ]) diff --git a/packages/slate/src/models/operation.js b/packages/slate/src/models/operation.js index 2564c67a0..56ac9e613 100644 --- a/packages/slate/src/models/operation.js +++ b/packages/slate/src/models/operation.js @@ -3,13 +3,34 @@ import logger from 'slate-dev-logger' import { List, Record } from 'immutable' import MODEL_TYPES from '../constants/model-types' -import OPERATION_ATTRIBUTES from '../constants/operation-attributes' import Mark from './mark' import Node from './node' import PathUtils from '../utils/path-utils' import Range from './range' import Value from './value' +/** + * Operation attributes. + * + * @type {Array} + */ + +const OPERATION_ATTRIBUTES = { + add_mark: ['value', 'path', 'offset', 'length', 'mark'], + insert_node: ['value', 'path', 'node'], + insert_text: ['value', 'path', 'offset', 'text', 'marks'], + merge_node: ['value', 'path', 'position', 'properties', 'target'], + move_node: ['value', 'path', 'newPath'], + remove_mark: ['value', 'path', 'offset', 'length', 'mark'], + remove_node: ['value', 'path', 'node'], + remove_text: ['value', 'path', 'offset', 'text', 'marks'], + set_mark: ['value', 'path', 'offset', 'length', 'mark', 'properties'], + set_node: ['value', 'path', 'node', 'properties'], + set_selection: ['value', 'selection', 'properties'], + set_value: ['value', 'properties'], + split_node: ['value', 'path', 'position', 'properties', 'target'], +} + /** * Default properties. * diff --git a/packages/slate/src/models/schema.js b/packages/slate/src/models/schema.js index 2fd7913b9..88c608128 100644 --- a/packages/slate/src/models/schema.js +++ b/packages/slate/src/models/schema.js @@ -1,9 +1,7 @@ import Debug from 'debug' import isPlainObject from 'is-plain-object' import logger from 'slate-dev-logger' -import mergeWith from 'lodash/mergeWith' import { Record } from 'immutable' - import { CHILD_OBJECT_INVALID, CHILD_REQUIRED, @@ -16,15 +14,17 @@ import { NODE_DATA_INVALID, NODE_IS_VOID_INVALID, NODE_MARK_INVALID, + NODE_OBJECT_INVALID, NODE_TEXT_INVALID, + NODE_TYPE_INVALID, PARENT_OBJECT_INVALID, PARENT_TYPE_INVALID, } from 'slate-schema-violations' -import CORE_SCHEMA_RULES from '../constants/core-schema-rules' import MODEL_TYPES from '../constants/model-types' import Stack from './stack' -import memoize from '../utils/memoize' +import Text from './text' +import SlateError from '../utils/slate-error' /** * Debug. @@ -34,6 +34,134 @@ import memoize from '../utils/memoize' const debug = Debug('slate:schema') +/** + * Define the core schema rules, order-sensitive. + * + * @type {Array} + */ + +const CORE_RULES = [ + // Only allow block nodes in documents. + { + match: { object: 'document' }, + nodes: [ + { + match: { object: 'block' }, + }, + ], + }, + + // Only allow block nodes or inline and text nodes in blocks. + { + match: { + object: 'block', + first: { object: 'block' }, + }, + nodes: [ + { + match: { object: 'block' }, + }, + ], + }, + { + match: { + object: 'block', + first: [{ object: 'inline' }, { object: 'text' }], + }, + nodes: [ + { + match: [{ object: 'inline' }, { object: 'text' }], + }, + ], + }, + + // Only allow inline and text nodes in inlines. + { + match: { object: 'inline' }, + nodes: [{ match: [{ object: 'inline' }, { object: 'text' }] }], + }, + + // Ensure that block and inline nodes have at least one text child. + { + match: [{ object: 'block' }, { object: 'inline' }], + nodes: [{ min: 1 }], + normalize: (change, error) => { + const { code, node } = error + if (code !== 'child_required') return + change.insertNodeByKey(node.key, 0, Text.create(), { normalize: false }) + }, + }, + + // Ensure that inline non-void nodes are never empty. + { + match: { + object: 'inline', + isVoid: false, + nodes: [{ match: { object: 'text' } }], + }, + text: /[\w\W]+/, + }, + + // Ensure that inline void nodes are surrounded by text nodes. + { + match: { object: 'block' }, + first: [{ object: 'block' }, { object: 'text' }], + last: [{ object: 'block' }, { object: 'text' }], + normalize: (change, error) => { + const { code, node } = error + const text = Text.create() + let i + + if (code === 'first_child_object_invalid') { + i = 0 + } else if (code === 'last_child_object_invalid') { + i = node.nodes.size + } else { + return + } + + change.insertNodeByKey(node.key, i, text, { normalize: false }) + }, + }, + { + match: { object: 'inline' }, + first: [{ object: 'block' }, { object: 'text' }], + last: [{ object: 'block' }, { object: 'text' }], + previous: [{ object: 'block' }, { object: 'text' }], + next: [{ object: 'block' }, { object: 'text' }], + normalize: (change, error) => { + const { code, node, index } = error + const text = Text.create() + let i + + if (code === 'first_child_object_invalid') { + i = 0 + } else if (code === 'last_child_object_invalid') { + i = node.nodes.size + } else if (code === 'previous_child_object_invalid') { + i = index + } else if (code === 'next_child_object_invalid') { + i = index + 1 + } else { + return + } + + change.insertNodeByKey(node.key, i, text, { normalize: false }) + }, + }, + + // Merge adjacent text nodes. + { + match: { object: 'text' }, + next: [{ object: 'block' }, { object: 'inline' }], + normalize: (change, error) => { + const { code, next } = error + if (code !== 'next_child_object_invalid') return + change.mergeNodeByKey(next.key, { normalize: false }) + }, + }, +] + /** * Default properties. * @@ -42,9 +170,7 @@ const debug = Debug('slate:schema') const DEFAULTS = { stack: Stack.create(), - document: {}, - blocks: {}, - inlines: {}, + rules: [], } /** @@ -87,27 +213,41 @@ class Schema extends Record(DEFAULTS) { return object } - let { plugins } = object + const plugins = object.plugins ? object.plugins : [{ schema: object }] + let rules = [...CORE_RULES] - if (object.rules) { - throw new Error( - 'Schemas in Slate have changed! They are no longer accept a `rules` property.' - ) + for (const plugin of plugins) { + const { schema = {} } = plugin + const { blocks = {}, inlines = {} } = schema + + if (schema.rules) { + rules = rules.concat(schema.rules) + } + + if (schema.document) { + rules.push({ + match: [{ object: 'document' }], + ...schema.document, + }) + } + + for (const key in blocks) { + rules.push({ + match: [{ object: 'block', type: key }], + ...blocks[key], + }) + } + + for (const key in inlines) { + rules.push({ + match: [{ object: 'inline', type: key }], + ...inlines[key], + }) + } } - if (object.nodes) { - throw new Error( - 'Schemas in Slate have changed! They are no longer accept a `nodes` property.' - ) - } - - if (!plugins) { - plugins = [{ schema: object }] - } - - const schema = resolveSchema(plugins) - const stack = Stack.create({ plugins: [...CORE_SCHEMA_RULES, ...plugins] }) - const ret = new Schema({ ...schema, stack }) + const stack = Stack.create({ plugins }) + const ret = new Schema({ stack, rules }) return ret } @@ -147,297 +287,74 @@ class Schema extends Record(DEFAULTS) { } /** - * Get the rule for an `object`. + * Validate a `node` with the schema, returning an error if it's invalid. * - * @param {Mixed} object - * @return {Object} + * @param {Node} node + * @return {Error|Void} */ - getRule(object) { - switch (object.object) { - case 'document': - return this.document - case 'block': - return this.blocks[object.type] - case 'inline': - return this.inlines[object.type] - } + validateNode(node) { + const rules = this.rules.filter(r => testRules(node, r.match)) + const failure = validateRules(node, rules, this.rules, { every: true }) + if (!failure) return + const error = new SlateError(failure.code, failure) + return error } /** - * Get a dictionary of the parent rule validations by child type. + * Test whether a `node` is valid against the schema. * - * @return {Object|Null} + * @param {Node} node + * @return {Boolean} */ - getParentRules() { - const { blocks, inlines } = this - const parents = {} - - for (const key in blocks) { - const rule = blocks[key] - if (rule.parent == null) continue - parents[key] = rule - } - - for (const key in inlines) { - const rule = inlines[key] - if (rule.parent == null) continue - parents[key] = rule - } - - return Object.keys(parents).length == 0 ? null : parents + testNode(node) { + const error = this.validateNode(node) + return !error } /** - * Fail validation by returning a normalizing change function. + * Assert that a `node` is valid against the schema. * - * @param {String} violation - * @param {Object} context - * @return {Function} + * @param {Node} node + * @throws */ - fail(violation, context) { - return change => { - debug(`normalizing`, { violation, context }) - const { rule } = context - const { size } = change.operations - if (rule.normalize) rule.normalize(change, violation, context) - if (change.operations.size > size) return - this.normalize(change, violation, context) - } + assertNode(node) { + const error = this.validateNode(node) + if (error) throw error } /** - * Normalize an invalid value with `violation` and `context`. - * - * @param {Change} change - * @param {String} violation - * @param {Mixed} context - */ - - normalize(change, violation, context) { - switch (violation) { - case CHILD_OBJECT_INVALID: - case CHILD_TYPE_INVALID: - case CHILD_UNKNOWN: - case FIRST_CHILD_OBJECT_INVALID: - case FIRST_CHILD_TYPE_INVALID: - case LAST_CHILD_OBJECT_INVALID: - case LAST_CHILD_TYPE_INVALID: { - const { child, node } = context - return child.object == 'text' && - node.object == 'block' && - node.nodes.size == 1 - ? change.removeNodeByKey(node.key) - : change.removeNodeByKey(child.key) - } - - case CHILD_REQUIRED: - case NODE_TEXT_INVALID: - case PARENT_OBJECT_INVALID: - case PARENT_TYPE_INVALID: { - const { node } = context - return node.object == 'document' - ? node.nodes.forEach(child => change.removeNodeByKey(child.key)) - : change.removeNodeByKey(node.key) - } - - case NODE_DATA_INVALID: { - const { node, key } = context - return node.data.get(key) === undefined && node.object != 'document' - ? change.removeNodeByKey(node.key) - : change.setNodeByKey(node.key, { data: node.data.delete(key) }) - } - - case NODE_IS_VOID_INVALID: { - const { node } = context - return change.setNodeByKey(node.key, { isVoid: !node.isVoid }) - } - - case NODE_MARK_INVALID: { - const { node, mark } = context - return node - .getTexts() - .forEach(t => change.removeMarkByKey(t.key, 0, t.text.length, mark)) - } - } - } - - /** - * Validate a `node` with the schema, returning a function that will fix the + * Normalize a `node` with the schema, returning a function that will fix the * invalid node, or void if the node is valid. * * @param {Node} node * @return {Function|Void} */ - validateNode(node) { - const ret = this.stack.find('validateNode', node) + normalizeNode(node) { + const ret = this.stack.find('normalizeNode', node) if (ret) return ret - if (node.object == 'text') return - const rule = this.getRule(node) || {} - const parents = this.getParentRules() - const ctx = { node, rule } + const error = this.validateNode(node) + if (!error) return - if (rule.isVoid != null) { - if (node.isVoid != rule.isVoid) { - return this.fail(NODE_IS_VOID_INVALID, ctx) - } - } + return change => { + debug(`normalizing`, { error }) + const { rule } = error + const { size } = change.operations - if (rule.data != null) { - for (const key in rule.data) { - const fn = rule.data[key] - const value = node.data.get(key) - - if (!fn(value)) { - return this.fail(NODE_DATA_INVALID, { ...ctx, key, value }) - } - } - } - - if (rule.marks != null) { - const marks = node.getMarks().toArray() - - for (const mark of marks) { - if (!rule.marks.some(def => def.type === mark.type)) { - return this.fail(NODE_MARK_INVALID, { ...ctx, mark }) - } - } - } - - if (rule.text != null) { - const { text } = node - - if (!rule.text.test(text)) { - return this.fail(NODE_TEXT_INVALID, { ...ctx, text }) - } - } - - if (rule.first != null) { - const { objects, types } = rule.first - const child = node.nodes.first() - - if (child && objects && !objects.includes(child.object)) { - return this.fail(FIRST_CHILD_OBJECT_INVALID, { ...ctx, child }) + // First run the user-provided `normalize` function if one exists... + if (rule.normalize) { + rule.normalize(change, error) } - if (child && types && !types.includes(child.type)) { - return this.fail(FIRST_CHILD_TYPE_INVALID, { ...ctx, child }) - } - } - - if (rule.last != null) { - const { objects, types } = rule.last - const child = node.nodes.last() - - if (child && objects && !objects.includes(child.object)) { - return this.fail(LAST_CHILD_OBJECT_INVALID, { ...ctx, child }) - } - - if (child && types && !types.includes(child.type)) { - return this.fail(LAST_CHILD_TYPE_INVALID, { ...ctx, child }) - } - } - - if (rule.nodes != null || parents != null) { - const children = node.nodes.toArray() - const defs = rule.nodes != null ? rule.nodes.slice() : [] - - let offset - let min - let index - let def - let max - let child - - function nextDef() { - offset = offset == null ? null : 0 - def = defs.shift() - min = def && (def.min == null ? 0 : def.min) - max = def && (def.max == null ? Infinity : def.max) - return !!def - } - - function nextChild() { - index = index == null ? 0 : index + 1 - offset = offset == null ? 0 : offset + 1 - child = children[index] - if (max != null && offset == max) nextDef() - return !!child - } - - function rewind() { - offset -= 1 - index -= 1 - } - - if (rule.nodes != null) { - nextDef() - } - - while (nextChild()) { - if ( - parents != null && - child.object != 'text' && - child.type in parents - ) { - const r = parents[child.type] - - if ( - r.parent.objects != null && - !r.parent.objects.includes(node.object) - ) { - return this.fail(PARENT_OBJECT_INVALID, { - node: child, - parent: node, - rule: r, - }) - } - - if (r.parent.types != null && !r.parent.types.includes(node.type)) { - return this.fail(PARENT_TYPE_INVALID, { - node: child, - parent: node, - rule: r, - }) - } - } - - if (rule.nodes != null) { - if (!def) { - return this.fail(CHILD_UNKNOWN, { ...ctx, child, index }) - } - - if (def.objects != null && !def.objects.includes(child.object)) { - if (offset >= min && nextDef()) { - rewind() - continue - } - return this.fail(CHILD_OBJECT_INVALID, { ...ctx, child, index }) - } - - if (def.types != null && !def.types.includes(child.type)) { - if (offset >= min && nextDef()) { - rewind() - continue - } - return this.fail(CHILD_TYPE_INVALID, { ...ctx, child, index }) - } - } - } - - if (rule.nodes != null) { - while (min != null) { - if (offset < min) { - return this.fail(CHILD_REQUIRED, { ...ctx, index }) - } - - nextDef() - } + // If the `normalize` function did not add any operations to the change + // object, it can't have normalized, so run the default one. + if (change.operations.size === size) { + defaultNormalize(change, error) } } } @@ -451,9 +368,7 @@ class Schema extends Record(DEFAULTS) { toJSON() { const object = { object: this.object, - document: this.document, - blocks: this.blocks, - inlines: this.inlines, + rules: this.rules, } return object @@ -469,110 +384,371 @@ class Schema extends Record(DEFAULTS) { } /** - * Resolve a set of schema rules from an array of `plugins`. + * Normalize an invalid value with `error` with default remedies. * - * @param {Array} plugins - * @return {Object} + * @param {Change} change + * @param {SlateError} error */ -function resolveSchema(plugins = []) { - const schema = { - document: {}, - blocks: {}, - inlines: {}, - } +function defaultNormalize(change, error) { + switch (error.code) { + case CHILD_OBJECT_INVALID: + case CHILD_TYPE_INVALID: + case CHILD_UNKNOWN: + case FIRST_CHILD_OBJECT_INVALID: + case FIRST_CHILD_TYPE_INVALID: + case LAST_CHILD_OBJECT_INVALID: + case LAST_CHILD_TYPE_INVALID: { + const { child, node } = error + return child.object == 'text' && + node.object == 'block' && + node.nodes.size == 1 + ? change.removeNodeByKey(node.key) + : change.removeNodeByKey(child.key) + } - plugins - .slice() - .reverse() - .forEach(plugin => { - if (!plugin.schema) return + case CHILD_REQUIRED: + case NODE_TEXT_INVALID: + case PARENT_OBJECT_INVALID: + case PARENT_TYPE_INVALID: { + const { node } = error + return node.object == 'document' + ? node.nodes.forEach(child => change.removeNodeByKey(child.key)) + : change.removeNodeByKey(node.key) + } - if (plugin.schema.rules) { - throw new Error( - 'Schemas in Slate have changed! They are no longer accept a `rules` property.' - ) - } + case NODE_DATA_INVALID: { + const { node, key } = error + return node.data.get(key) === undefined && node.object != 'document' + ? change.removeNodeByKey(node.key) + : change.setNodeByKey(node.key, { data: node.data.delete(key) }) + } - if (plugin.schema.nodes) { - throw new Error( - 'Schemas in Slate have changed! They are no longer accept a `nodes` property.' - ) - } + case NODE_IS_VOID_INVALID: { + const { node } = error + return change.setNodeByKey(node.key, { isVoid: !node.isVoid }) + } - const { document = {}, blocks = {}, inlines = {} } = plugin.schema - const d = resolveDocumentRule(document) - const bs = {} - const is = {} + case NODE_MARK_INVALID: { + const { node, mark } = error + return node + .getTexts() + .forEach(t => change.removeMarkByKey(t.key, 0, t.text.length, mark)) + } - for (const key in blocks) { - bs[key] = resolveNodeRule('block', key, blocks[key]) - } - - for (const key in inlines) { - is[key] = resolveNodeRule('inline', key, inlines[key]) - } - - mergeWith(schema.document, d, customizer) - mergeWith(schema.blocks, bs, customizer) - mergeWith(schema.inlines, is, customizer) - }) - - return schema -} - -/** - * Resolve a document rule `obj`. - * - * @param {Object} obj - * @return {Object} - */ - -function resolveDocumentRule(obj) { - return { - data: {}, - nodes: null, - ...obj, + default: { + const { node } = error + return change.removeNodeByKey(node.key) + } } } /** - * Resolve a node rule with `type` from `obj`. + * Check that a `node` matches one of a set of `rules`. * - * @param {String} object - * @param {String} type - * @param {Object} obj - * @return {Object} + * @param {Node} node + * @param {Object|Array} rules + * @return {Boolean} */ -function resolveNodeRule(object, type, obj) { - return { - data: {}, - isVoid: null, - nodes: null, - first: null, - last: null, - parent: null, - text: null, - ...obj, +function testRules(node, rules) { + const error = validateRules(node, rules) + return !error +} + +/** + * Validate that a `node` matches a `rule` object or array. + * + * @param {Node} node + * @param {Object|Array} rule + * @param {Array|Void} rules + * @return {Error|Void} + */ + +function validateRules(node, rule, rules, options = {}) { + const { every = false } = options + + if (Array.isArray(rule)) { + const array = rule.length ? rule : [{}] + let first + + for (const r of array) { + const error = validateRules(node, r, rules) + first = first || error + if (every && error) return error + if (!every && !error) return + } + + return first + } + + const error = + validateObject(node, rule) || + validateType(node, rule) || + validateIsVoid(node, rule) || + validateData(node, rule) || + validateMarks(node, rule) || + validateText(node, rule) || + validateFirst(node, rule) || + validateLast(node, rule) || + validateNodes(node, rule, rules) + + return error +} + +function validateObject(node, rule) { + if (rule.objects) { + logger.warn( + 'The `objects` schema validation rule was changed. Please use the new `match` syntax with `object`.' + ) + } + + if (rule.object == null) return + if (rule.object === node.object) return + return fail(NODE_OBJECT_INVALID, { rule, node }) +} + +function validateType(node, rule) { + if (rule.types) { + logger.warn( + 'The `types` schema validation rule was changed. Please use the new `match` syntax with `type`.' + ) + } + + if (rule.type == null) return + if (rule.type === node.type) return + return fail(NODE_TYPE_INVALID, { rule, node }) +} + +function validateIsVoid(node, rule) { + if (rule.isVoid == null) return + if (rule.isVoid === node.isVoid) return + return fail(NODE_IS_VOID_INVALID, { rule, node }) +} + +function validateData(node, rule) { + if (rule.data == null) return + if (node.data == null) return + + for (const key in rule.data) { + const fn = rule.data[key] + const value = node.data && node.data.get(key) + const valid = typeof fn === 'function' ? fn(value) : fn === value + if (valid) continue + return fail(NODE_DATA_INVALID, { rule, node, key, value }) + } +} + +function validateMarks(node, rule) { + if (rule.marks == null) return + const marks = node.getMarks().toArray() + + for (const mark of marks) { + const valid = rule.marks.some(def => def.type === mark.type) + if (valid) continue + return fail(NODE_MARK_INVALID, { rule, node, mark }) + } +} + +function validateText(node, rule) { + if (rule.text == null) return + const { text } = node + const valid = rule.text.test(text) + if (valid) return + return fail(NODE_TEXT_INVALID, { rule, node, text }) +} + +function validateFirst(node, rule) { + if (rule.first == null) return + const first = node.nodes.first() + if (!first) return + const error = validateRules(first, rule.first) + if (!error) return + error.rule = rule + error.node = node + error.child = first + error.code = error.code.replace('node_', 'first_child_') + return error +} + +function validateLast(node, rule) { + if (rule.last == null) return + const last = node.nodes.last() + if (!last) return + const error = validateRules(last, rule.last) + if (!error) return + error.rule = rule + error.node = node + error.child = last + error.code = error.code.replace('node_', 'last_child_') + return error +} + +function validateNodes(node, rule, rules = []) { + if (node.nodes == null) return + + const children = node.nodes.toArray() + const defs = rule.nodes != null ? rule.nodes.slice() : [] + let offset + let min + let index + let def + let max + let child + let previous + let next + + function nextDef() { + offset = offset == null ? null : 0 + def = defs.shift() + min = def && def.min + max = def && def.max + return !!def + } + + function nextChild() { + index = index == null ? 0 : index + 1 + offset = offset == null ? 0 : offset + 1 + previous = child + child = children[index] + next = children[index + 1] + if (max != null && offset == max) nextDef() + return !!child + } + + function rewind() { + offset -= 1 + index -= 1 + } + + if (rule.nodes != null) { + nextDef() + } + + while (nextChild()) { + const err = + validateParent(node, child, rules) || + validatePrevious(node, child, previous, index, rules) || + validateNext(node, child, next, index, rules) + + if (err) return err + + if (rule.nodes != null) { + if (!def) { + return fail(CHILD_UNKNOWN, { rule, node, child, index }) + } + + if (def) { + if (def.objects) { + logger.warn( + 'The `objects` schema validation rule was changed. Please use the new `match` syntax with `object`.' + ) + } + + if (def.types) { + logger.warn( + 'The `types` schema validation rule was changed. Please use the new `match` syntax with `type`.' + ) + } + } + + if (def.match) { + const error = validateRules(child, def.match) + + if (error && offset >= min && nextDef()) { + rewind() + continue + } + + if (error) { + error.rule = rule + error.node = node + error.child = child + error.index = index + error.code = error.code.replace('node_', 'child_') + return error + } + } + } + } + + if (rule.nodes != null) { + while (min != null) { + if (offset < min) { + return fail(CHILD_REQUIRED, { rule, node, index }) + } + + nextDef() + } + } +} + +function validateParent(node, child, rules) { + for (const rule of rules) { + if (rule.parent == null) continue + if (!testRules(child, rule.match)) continue + + const error = validateRules(node, rule.parent) + if (!error) continue + + error.rule = rule + error.parent = node + error.node = child + error.code = error.code.replace('node_', 'parent_') + return error + } +} + +function validatePrevious(node, child, previous, index, rules) { + if (!previous) return + + for (const rule of rules) { + if (rule.previous == null) continue + if (!testRules(child, rule.match)) continue + + const error = validateRules(previous, rule.previous) + if (!error) continue + + error.rule = rule + error.node = node + error.child = child + error.index = index + error.previous = previous + error.code = error.code.replace('node_', 'previous_child_') + return error + } +} + +function validateNext(node, child, next, index, rules) { + if (!next) return + + for (const rule of rules) { + if (rule.next == null) continue + if (!testRules(child, rule.match)) continue + + const error = validateRules(next, rule.next) + if (!error) continue + + error.rule = rule + error.node = node + error.child = child + error.index = index + error.next = next + error.code = error.code.replace('node_', 'next_child_') + return error } } /** - * A Lodash customizer for merging schema definitions. Special cases `objects`, - * `marks` and `types` arrays to be unioned, and ignores new `null` values. + * Create an interim failure object with `code` and `attrs`. * - * @param {Mixed} target - * @param {Mixed} source - * @return {Array|Void} + * @param {String} code + * @param {Object} attrs + * @return {Object} */ -function customizer(target, source, key) { - if (key == 'objects' || key == 'types' || key == 'marks') { - return target == null ? source : target.concat(source) - } else { - return source == null ? target : source - } +function fail(code, attrs) { + return { code, ...attrs } } /** @@ -581,12 +757,6 @@ function customizer(target, source, key) { Schema.prototype[MODEL_TYPES.SCHEMA] = true -/** - * Memoize read methods. - */ - -memoize(Schema.prototype, ['getParentRules']) - /** * Export. * diff --git a/packages/slate/src/models/text.js b/packages/slate/src/models/text.js index 13521d437..f8ba0aff5 100644 --- a/packages/slate/src/models/text.js +++ b/packages/slate/src/models/text.js @@ -740,11 +740,22 @@ class Text extends Record(DEFAULTS) { return this.setLeaves(leaves) } + /** + * Normalize the text node with a `schema`. + * + * @param {Schema} schema + * @return {Function|Void} + */ + + normalize(schema) { + return schema.normalizeNode(this) + } + /** * Validate the text node against a `schema`. * * @param {Schema} schema - * @return {Object|Void} + * @return {Error|Void} */ validate(schema) { @@ -802,6 +813,7 @@ memoize(Text.prototype, [ 'getActiveMarks', 'getMarks', 'getMarksAsArray', + 'normalize', 'validate', 'getString', ]) diff --git a/packages/slate/src/utils/slate-error.js b/packages/slate/src/utils/slate-error.js new file mode 100644 index 000000000..84b5c92e6 --- /dev/null +++ b/packages/slate/src/utils/slate-error.js @@ -0,0 +1,30 @@ +/** + * Define a Slate error. + * + * @type {SlateError} + */ + +class SlateError extends Error { + constructor(code, attrs = {}) { + super(code) + this.code = code + + for (const key in attrs) { + this[key] = attrs[key] + } + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor) + } else { + this.stack = new Error().stack + } + } +} + +/** + * Export. + * + * @type {SlateError} + */ + +export default SlateError diff --git a/packages/slate/test/models/change/without-normalization-normalize-flag-false.js b/packages/slate/test/models/change/without-normalization-normalize-flag-false.js index c7d078ea1..dd97f7568 100644 --- a/packages/slate/test/models/change/without-normalization-normalize-flag-false.js +++ b/packages/slate/test/models/change/without-normalization-normalize-flag-false.js @@ -8,8 +8,12 @@ export const schema = { blocks: { paragraph: {}, item: { - parent: { types: ['list'] }, - nodes: [{ objects: ['text'] }], + parent: { type: 'list' }, + nodes: [ + { + match: [{ object: 'text' }], + }, + ], }, list: {}, }, diff --git a/packages/slate/test/models/change/without-normalization-normalize-flag-true.js b/packages/slate/test/models/change/without-normalization-normalize-flag-true.js index d89098c35..488a368e6 100644 --- a/packages/slate/test/models/change/without-normalization-normalize-flag-true.js +++ b/packages/slate/test/models/change/without-normalization-normalize-flag-true.js @@ -8,8 +8,12 @@ export const schema = { blocks: { paragraph: {}, item: { - parent: { types: ['list'] }, - nodes: [{ objects: ['text'] }], + parent: { type: 'list' }, + nodes: [ + { + match: [{ object: 'text' }], + }, + ], }, list: {}, }, diff --git a/packages/slate/test/models/change/without-normalization-option-override.js b/packages/slate/test/models/change/without-normalization-option-override.js index 3f3865972..64becb4fa 100644 --- a/packages/slate/test/models/change/without-normalization-option-override.js +++ b/packages/slate/test/models/change/without-normalization-option-override.js @@ -8,8 +8,12 @@ export const schema = { blocks: { paragraph: {}, item: { - parent: { types: ['list'] }, - nodes: [{ objects: ['text'] }], + parent: { type: 'list' }, + nodes: [ + { + match: [{ object: 'text' }], + }, + ], }, list: {}, }, diff --git a/packages/slate/test/models/change/without-normalization.js b/packages/slate/test/models/change/without-normalization.js index 511553414..36d2f5505 100644 --- a/packages/slate/test/models/change/without-normalization.js +++ b/packages/slate/test/models/change/without-normalization.js @@ -8,8 +8,12 @@ export const schema = { blocks: { paragraph: {}, item: { - parent: { types: ['list'] }, - nodes: [{ objects: ['text'] }], + parent: { type: 'list' }, + nodes: [ + { + match: [{ object: 'text' }], + }, + ], }, list: {}, }, diff --git a/packages/slate/test/schema/core/block-all-block-children.js b/packages/slate/test/schema/core/block-all-block-children.js new file mode 100644 index 000000000..f179ab628 --- /dev/null +++ b/packages/slate/test/schema/core/block-all-block-children.js @@ -0,0 +1,52 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = {} + +export const input = ( + + + + one + two + + + +) + +export const output = { + object: 'value', + document: { + object: 'document', + data: {}, + nodes: [ + { + object: 'block', + type: 'quote', + isVoid: false, + data: {}, + nodes: [ + { + object: 'block', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + leaves: [ + { + object: 'leaf', + text: 'one', + marks: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, +} diff --git a/packages/slate/test/schema/core/block-all-inline-children.js b/packages/slate/test/schema/core/block-all-inline-children.js new file mode 100644 index 000000000..38fb302bb --- /dev/null +++ b/packages/slate/test/schema/core/block-all-inline-children.js @@ -0,0 +1,72 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = {} + +export const input = ( + + + + one + two + + + +) + +export const output = { + object: 'value', + document: { + object: 'document', + data: {}, + nodes: [ + { + object: 'block', + type: 'quote', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + leaves: [ + { + object: 'leaf', + text: '', + marks: [], + }, + ], + }, + { + object: 'inline', + type: 'link', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + leaves: [ + { + object: 'leaf', + text: 'one', + marks: [], + }, + ], + }, + ], + }, + { + object: 'text', + leaves: [ + { + object: 'leaf', + text: '', + marks: [], + }, + ], + }, + ], + }, + ], + }, +} diff --git a/packages/slate/test/schema/core/block-all-text-children.js b/packages/slate/test/schema/core/block-all-text-children.js new file mode 100644 index 000000000..02f072d40 --- /dev/null +++ b/packages/slate/test/schema/core/block-all-text-children.js @@ -0,0 +1,44 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = {} + +export const input = ( + + + + one + two + + + +) + +export const output = { + object: 'value', + document: { + object: 'document', + data: {}, + nodes: [ + { + object: 'block', + type: 'quote', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + leaves: [ + { + object: 'leaf', + text: 'one', + marks: [], + }, + ], + }, + ], + }, + ], + }, +} diff --git a/packages/slate/test/schema/core/inline-no-block-children.js b/packages/slate/test/schema/core/inline-no-block-children.js index fb6d1a5c8..01126c1cf 100644 --- a/packages/slate/test/schema/core/inline-no-block-children.js +++ b/packages/slate/test/schema/core/inline-no-block-children.js @@ -8,7 +8,7 @@ export const input = ( - + one two diff --git a/packages/slate/test/schema/core/merge-adjacent-texts.js b/packages/slate/test/schema/core/merge-adjacent-texts.js new file mode 100644 index 000000000..108a222d0 --- /dev/null +++ b/packages/slate/test/schema/core/merge-adjacent-texts.js @@ -0,0 +1,45 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = {} + +export const input = ( + + + + one + two + three + + + +) + +export const output = { + object: 'value', + document: { + object: 'document', + data: {}, + nodes: [ + { + object: 'block', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + leaves: [ + { + object: 'leaf', + text: 'onetwothree', + marks: [], + }, + ], + }, + ], + }, + ], + }, +} diff --git a/packages/slate/test/schema/custom/child-kind-invalid-custom-optional-first.js b/packages/slate/test/schema/custom/child-kind-invalid-custom-optional-first.js index de5b93282..47172adad 100644 --- a/packages/slate/test/schema/custom/child-kind-invalid-custom-optional-first.js +++ b/packages/slate/test/schema/custom/child-kind-invalid-custom-optional-first.js @@ -8,11 +8,18 @@ export const schema = { paragraph: {}, quote: { nodes: [ - { objects: ['block'], types: ['image'], min: 0, max: 1 }, - { objects: ['block'], types: ['paragraph'], min: 1 }, + { + match: [{ object: 'block', type: 'image' }], + min: 0, + max: 1, + }, + { + match: [{ object: 'block', type: 'paragraph' }], + min: 1, + }, ], - normalize: (change, reason, { node, child }) => { - if (reason == CHILD_OBJECT_INVALID) { + normalize: (change, { code, child }) => { + if (code == CHILD_OBJECT_INVALID) { change.wrapBlockByKey(child.key, 'paragraph') } }, diff --git a/packages/slate/test/schema/custom/child-kind-invalid-custom.js b/packages/slate/test/schema/custom/child-kind-invalid-custom.js index f6f6f6c13..c37119d93 100644 --- a/packages/slate/test/schema/custom/child-kind-invalid-custom.js +++ b/packages/slate/test/schema/custom/child-kind-invalid-custom.js @@ -7,9 +7,13 @@ export const schema = { blocks: { paragraph: {}, quote: { - nodes: [{ objects: ['block'] }], - normalize: (change, reason, { child }) => { - if (reason == CHILD_OBJECT_INVALID) { + nodes: [ + { + match: [{ object: 'block' }], + }, + ], + normalize: (change, { code, child }) => { + if (code == CHILD_OBJECT_INVALID) { change.wrapBlockByKey(child.key, 'paragraph') } }, diff --git a/packages/slate/test/schema/custom/child-kind-invalid-default.js b/packages/slate/test/schema/custom/child-kind-invalid-default.js index 11e0dcf14..8c0848241 100644 --- a/packages/slate/test/schema/custom/child-kind-invalid-default.js +++ b/packages/slate/test/schema/custom/child-kind-invalid-default.js @@ -6,7 +6,11 @@ export const schema = { blocks: { paragraph: {}, quote: { - nodes: [{ objects: ['text'] }], + nodes: [ + { + match: [{ object: 'text' }], + }, + ], }, }, } diff --git a/packages/slate/test/schema/custom/child-required-custom.js b/packages/slate/test/schema/custom/child-required-custom.js index 05a8731d6..5ea67e0d9 100644 --- a/packages/slate/test/schema/custom/child-required-custom.js +++ b/packages/slate/test/schema/custom/child-required-custom.js @@ -7,9 +7,14 @@ export const schema = { blocks: { paragraph: {}, quote: { - nodes: [{ types: ['paragraph'], min: 2 }], - normalize: (change, reason, { node, index }) => { - if (reason == CHILD_REQUIRED) { + nodes: [ + { + match: [{ type: 'paragraph' }], + min: 2, + }, + ], + normalize: (change, { code, node, index }) => { + if (code == CHILD_REQUIRED) { change.insertNodeByKey(node.key, index, { object: 'block', type: 'paragraph', diff --git a/packages/slate/test/schema/custom/child-required-default.js b/packages/slate/test/schema/custom/child-required-default.js index c56ca97fd..08bcd3a99 100644 --- a/packages/slate/test/schema/custom/child-required-default.js +++ b/packages/slate/test/schema/custom/child-required-default.js @@ -6,7 +6,12 @@ export const schema = { blocks: { paragraph: {}, quote: { - nodes: [{ types: ['paragraph'], min: 1 }], + nodes: [ + { + match: [{ type: 'paragraph' }], + min: 1, + }, + ], }, }, } diff --git a/packages/slate/test/schema/custom/child-type-invalid-custom.js b/packages/slate/test/schema/custom/child-type-invalid-custom.js index 9beedf7bf..34ff79aac 100644 --- a/packages/slate/test/schema/custom/child-type-invalid-custom.js +++ b/packages/slate/test/schema/custom/child-type-invalid-custom.js @@ -7,9 +7,13 @@ export const schema = { blocks: { paragraph: {}, quote: { - nodes: [{ types: ['paragraph'] }], - normalize: (change, reason, { child }) => { - if (reason == CHILD_TYPE_INVALID) { + nodes: [ + { + match: [{ type: 'paragraph' }], + }, + ], + normalize: (change, { code, child }) => { + if (code == CHILD_TYPE_INVALID) { change.wrapBlockByKey(child.key, 'paragraph') } }, diff --git a/packages/slate/test/schema/custom/child-type-invalid-default.js b/packages/slate/test/schema/custom/child-type-invalid-default.js index 6ca98b324..b91af43d2 100644 --- a/packages/slate/test/schema/custom/child-type-invalid-default.js +++ b/packages/slate/test/schema/custom/child-type-invalid-default.js @@ -6,7 +6,11 @@ export const schema = { blocks: { paragraph: {}, quote: { - nodes: [{ types: ['paragraph'] }], + nodes: [ + { + match: [{ type: 'paragraph' }], + }, + ], }, }, } diff --git a/packages/slate/test/schema/custom/child-unknown-custom.js b/packages/slate/test/schema/custom/child-unknown-custom.js index f42fa488c..01d3dee40 100644 --- a/packages/slate/test/schema/custom/child-unknown-custom.js +++ b/packages/slate/test/schema/custom/child-unknown-custom.js @@ -7,9 +7,14 @@ export const schema = { blocks: { paragraph: {}, quote: { - nodes: [{ types: ['paragraph'], max: 1 }], - normalize: (change, reason, { node, child, index }) => { - if (reason == CHILD_UNKNOWN) { + nodes: [ + { + match: [{ type: 'paragraph' }], + max: 1, + }, + ], + normalize: (change, { code, node, child }) => { + if (code == CHILD_UNKNOWN) { const previous = node.getPreviousSibling(child.key) const offset = previous.nodes.size diff --git a/packages/slate/test/schema/custom/child-unknown-default.js b/packages/slate/test/schema/custom/child-unknown-default.js index cac9dafbc..d1fd166d3 100644 --- a/packages/slate/test/schema/custom/child-unknown-default.js +++ b/packages/slate/test/schema/custom/child-unknown-default.js @@ -6,7 +6,12 @@ export const schema = { blocks: { paragraph: {}, quote: { - nodes: [{ types: ['paragraph'], max: 1 }], + nodes: [ + { + match: [{ type: 'paragraph' }], + max: 1, + }, + ], }, }, } diff --git a/packages/slate/test/schema/custom/first-child-kind-invalid-custom.js b/packages/slate/test/schema/custom/first-child-kind-invalid-custom.js index d3d3a0738..96f43a88d 100644 --- a/packages/slate/test/schema/custom/first-child-kind-invalid-custom.js +++ b/packages/slate/test/schema/custom/first-child-kind-invalid-custom.js @@ -7,9 +7,9 @@ export const schema = { blocks: { paragraph: {}, quote: { - first: { objects: ['block'] }, - normalize: (change, reason, { child }) => { - if (reason == FIRST_CHILD_OBJECT_INVALID) { + first: [{ object: 'block' }], + normalize: (change, { code, child }) => { + if (code == FIRST_CHILD_OBJECT_INVALID) { change.wrapBlockByKey(child.key, 'paragraph') } }, diff --git a/packages/slate/test/schema/custom/first-child-kind-invalid-default.js b/packages/slate/test/schema/custom/first-child-kind-invalid-default.js index 024cfaf76..464603876 100644 --- a/packages/slate/test/schema/custom/first-child-kind-invalid-default.js +++ b/packages/slate/test/schema/custom/first-child-kind-invalid-default.js @@ -6,7 +6,7 @@ export const schema = { blocks: { paragraph: {}, quote: { - first: { objects: ['text'] }, + first: [{ object: 'text' }], }, }, } diff --git a/packages/slate/test/schema/custom/first-child-type-invalid-custom.js b/packages/slate/test/schema/custom/first-child-type-invalid-custom.js index e45b7dbd5..3cf6359d6 100644 --- a/packages/slate/test/schema/custom/first-child-type-invalid-custom.js +++ b/packages/slate/test/schema/custom/first-child-type-invalid-custom.js @@ -7,9 +7,9 @@ export const schema = { blocks: { paragraph: {}, quote: { - first: { types: ['paragraph'] }, - normalize: (change, reason, { child }) => { - if (reason == FIRST_CHILD_TYPE_INVALID) { + first: [{ type: 'paragraph' }], + normalize: (change, { code, child }) => { + if (code == FIRST_CHILD_TYPE_INVALID) { change.wrapBlockByKey(child.key, 'paragraph') } }, diff --git a/packages/slate/test/schema/custom/first-child-type-invalid-default.js b/packages/slate/test/schema/custom/first-child-type-invalid-default.js index df4f11bc1..188d0a94a 100644 --- a/packages/slate/test/schema/custom/first-child-type-invalid-default.js +++ b/packages/slate/test/schema/custom/first-child-type-invalid-default.js @@ -6,7 +6,7 @@ export const schema = { blocks: { paragraph: {}, quote: { - first: { types: ['paragraph'] }, + first: { type: 'paragraph' }, }, }, } diff --git a/packages/slate/test/schema/custom/last-child-kind-invalid-custom.js b/packages/slate/test/schema/custom/last-child-kind-invalid-custom.js index 1120e0ba6..732ce6ce5 100644 --- a/packages/slate/test/schema/custom/last-child-kind-invalid-custom.js +++ b/packages/slate/test/schema/custom/last-child-kind-invalid-custom.js @@ -7,9 +7,9 @@ export const schema = { blocks: { paragraph: {}, quote: { - last: { objects: ['block'] }, - normalize: (change, reason, { child }) => { - if (reason == LAST_CHILD_OBJECT_INVALID) { + last: [{ object: 'block' }], + normalize: (change, { code, child }) => { + if (code == LAST_CHILD_OBJECT_INVALID) { change.wrapBlockByKey(child.key, 'paragraph') } }, diff --git a/packages/slate/test/schema/custom/last-child-kind-invalid-default.js b/packages/slate/test/schema/custom/last-child-kind-invalid-default.js index ff97b76c7..038fad0ec 100644 --- a/packages/slate/test/schema/custom/last-child-kind-invalid-default.js +++ b/packages/slate/test/schema/custom/last-child-kind-invalid-default.js @@ -6,7 +6,7 @@ export const schema = { blocks: { paragraph: {}, quote: { - last: { objects: ['text'] }, + last: [{ object: 'text' }], }, }, } diff --git a/packages/slate/test/schema/custom/last-child-type-invalid-custom.js b/packages/slate/test/schema/custom/last-child-type-invalid-custom.js index aa1e93665..d7b199dd3 100644 --- a/packages/slate/test/schema/custom/last-child-type-invalid-custom.js +++ b/packages/slate/test/schema/custom/last-child-type-invalid-custom.js @@ -7,9 +7,9 @@ export const schema = { blocks: { paragraph: {}, quote: { - last: { types: ['paragraph'] }, - normalize: (change, reason, { child }) => { - if (reason == LAST_CHILD_TYPE_INVALID) { + last: [{ type: 'paragraph' }], + normalize: (change, { code, child }) => { + if (code == LAST_CHILD_TYPE_INVALID) { change.wrapBlockByKey(child.key, 'paragraph') } }, diff --git a/packages/slate/test/schema/custom/last-child-type-invalid-default.js b/packages/slate/test/schema/custom/last-child-type-invalid-default.js index 5b4b063e5..5310cfdab 100644 --- a/packages/slate/test/schema/custom/last-child-type-invalid-default.js +++ b/packages/slate/test/schema/custom/last-child-type-invalid-default.js @@ -6,7 +6,7 @@ export const schema = { blocks: { paragraph: {}, quote: { - last: { types: ['paragraph'] }, + last: { type: 'paragraph' }, }, }, } diff --git a/packages/slate/test/schema/custom/match-data.js b/packages/slate/test/schema/custom/match-data.js new file mode 100644 index 000000000..dcd2fe184 --- /dev/null +++ b/packages/slate/test/schema/custom/match-data.js @@ -0,0 +1,26 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = { + rules: [ + { + match: [{ object: 'block', data: { thing: 'value' } }], + type: 'quote', + }, + ], +} + +export const input = ( + + + + + +) + +export const output = ( + + + +) diff --git a/packages/slate/test/schema/custom/match-object.js b/packages/slate/test/schema/custom/match-object.js new file mode 100644 index 000000000..3a6c59161 --- /dev/null +++ b/packages/slate/test/schema/custom/match-object.js @@ -0,0 +1,28 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = { + rules: [ + { + match: [{ object: 'block' }], + data: { + thing: v => v == 'value', + }, + }, + ], +} + +export const input = ( + + + + + +) + +export const output = ( + + + +) diff --git a/packages/slate/test/schema/custom/match-type.js b/packages/slate/test/schema/custom/match-type.js new file mode 100644 index 000000000..ea26da962 --- /dev/null +++ b/packages/slate/test/schema/custom/match-type.js @@ -0,0 +1,26 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = { + rules: [ + { + match: [{ type: 'paragraph' }], + object: 'inline', + }, + ], +} + +export const input = ( + + + invalid + + +) + +export const output = ( + + + +) diff --git a/packages/slate/test/schema/custom/node-data-invalid-custom.js b/packages/slate/test/schema/custom/node-data-invalid-custom.js index 380a4ca30..ea1373564 100644 --- a/packages/slate/test/schema/custom/node-data-invalid-custom.js +++ b/packages/slate/test/schema/custom/node-data-invalid-custom.js @@ -9,8 +9,8 @@ export const schema = { data: { thing: v => v == 'value', }, - normalize: (change, reason, { node, key }) => { - if (reason == NODE_DATA_INVALID) { + normalize: (change, { code, node, key }) => { + if (code == NODE_DATA_INVALID) { change.setNodeByKey(node.key, { data: { thing: 'value' } }) } }, diff --git a/packages/slate/test/schema/custom/node-is-void-invalid-custom.js b/packages/slate/test/schema/custom/node-is-void-invalid-custom.js index 18d380ec4..9298dbd77 100644 --- a/packages/slate/test/schema/custom/node-is-void-invalid-custom.js +++ b/packages/slate/test/schema/custom/node-is-void-invalid-custom.js @@ -7,8 +7,8 @@ export const schema = { blocks: { paragraph: { isVoid: false, - normalize: (change, reason, { node }) => { - if (reason == NODE_IS_VOID_INVALID) { + normalize: (change, { code, node }) => { + if (code == NODE_IS_VOID_INVALID) { change.removeNodeByKey(node.key, 'paragraph') } }, diff --git a/packages/slate/test/schema/custom/node-mark-invalid-custom.js b/packages/slate/test/schema/custom/node-mark-invalid-custom.js index 2f858c47f..03b535dc2 100644 --- a/packages/slate/test/schema/custom/node-mark-invalid-custom.js +++ b/packages/slate/test/schema/custom/node-mark-invalid-custom.js @@ -7,8 +7,8 @@ export const schema = { blocks: { paragraph: { marks: [{ type: 'bold' }], - normalize: (change, reason, { node }) => { - if (reason == NODE_MARK_INVALID) { + normalize: (change, { code, node }) => { + if (code == NODE_MARK_INVALID) { node.nodes.forEach(n => change.removeNodeByKey(n.key)) } }, diff --git a/packages/slate/test/schema/custom/node-object-invalid-default.js b/packages/slate/test/schema/custom/node-object-invalid-default.js new file mode 100644 index 000000000..12533644c --- /dev/null +++ b/packages/slate/test/schema/custom/node-object-invalid-default.js @@ -0,0 +1,25 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = { + blocks: { + paragraph: { + object: 'inline', + }, + }, +} + +export const input = ( + + + invalid + + +) + +export const output = ( + + + +) diff --git a/packages/slate/test/schema/custom/node-text-invalid-custom.js b/packages/slate/test/schema/custom/node-text-invalid-custom.js index b1e2b30e9..c4dc29f1c 100644 --- a/packages/slate/test/schema/custom/node-text-invalid-custom.js +++ b/packages/slate/test/schema/custom/node-text-invalid-custom.js @@ -7,8 +7,8 @@ export const schema = { blocks: { paragraph: { text: /^\d*$/, - normalize: (change, reason, { node }) => { - if (reason == NODE_TEXT_INVALID) { + normalize: (change, { code, node }) => { + if (code == NODE_TEXT_INVALID) { node.nodes.forEach(n => change.removeNodeByKey(n.key)) } }, diff --git a/packages/slate/test/schema/custom/node-text-valid.js b/packages/slate/test/schema/custom/node-text-valid.js new file mode 100644 index 000000000..c61abcc90 --- /dev/null +++ b/packages/slate/test/schema/custom/node-text-valid.js @@ -0,0 +1,33 @@ +/** @jsx h */ + +import { NODE_TEXT_INVALID } from 'slate-schema-violations' +import h from '../../helpers/h' + +export const schema = { + blocks: { + paragraph: { + text: /^\d*$/, + normalize: (change, { code, node }) => { + if (code == NODE_TEXT_INVALID) { + node.nodes.forEach(n => change.removeNodeByKey(n.key)) + } + }, + }, + }, +} + +export const input = ( + + + 123 + + +) + +export const output = ( + + + 123 + + +) diff --git a/packages/slate/test/schema/custom/node-type-invalid-default.js b/packages/slate/test/schema/custom/node-type-invalid-default.js new file mode 100644 index 000000000..d460e8faa --- /dev/null +++ b/packages/slate/test/schema/custom/node-type-invalid-default.js @@ -0,0 +1,25 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = { + blocks: { + paragraph: { + type: 'impossible', + }, + }, +} + +export const input = ( + + + invalid + + +) + +export const output = ( + + + +) diff --git a/packages/slate/test/schema/custom/parent-kind-invalid-custom.js b/packages/slate/test/schema/custom/parent-kind-invalid-custom.js index 91ccb85aa..4b61e3361 100644 --- a/packages/slate/test/schema/custom/parent-kind-invalid-custom.js +++ b/packages/slate/test/schema/custom/parent-kind-invalid-custom.js @@ -6,9 +6,9 @@ import h from '../../helpers/h' export const schema = { inlines: { link: { - parent: { objects: ['block'] }, - normalize: (change, reason, { node }) => { - if (reason == PARENT_OBJECT_INVALID) { + parent: { object: 'block' }, + normalize: (change, { code, node }) => { + if (code == PARENT_OBJECT_INVALID) { change.unwrapNodeByKey(node.key) } }, diff --git a/packages/slate/test/schema/custom/parent-kind-invalid-default.js b/packages/slate/test/schema/custom/parent-kind-invalid-default.js index 758d9bfab..6549ec775 100644 --- a/packages/slate/test/schema/custom/parent-kind-invalid-default.js +++ b/packages/slate/test/schema/custom/parent-kind-invalid-default.js @@ -5,7 +5,7 @@ import h from '../../helpers/h' export const schema = { inlines: { link: { - parent: { objects: ['block'] }, + parent: { object: 'block' }, }, }, } diff --git a/packages/slate/test/schema/custom/parent-type-invalid-custom.js b/packages/slate/test/schema/custom/parent-type-invalid-custom.js index a27a2acd6..5108fb8eb 100644 --- a/packages/slate/test/schema/custom/parent-type-invalid-custom.js +++ b/packages/slate/test/schema/custom/parent-type-invalid-custom.js @@ -7,9 +7,9 @@ export const schema = { blocks: { list: {}, item: { - parent: { types: ['list'] }, - normalize: (change, reason, { node }) => { - if (reason == PARENT_TYPE_INVALID) { + parent: { type: 'list' }, + normalize: (change, { code, node }) => { + if (code == PARENT_TYPE_INVALID) { change.wrapBlockByKey(node.key, 'list') } }, diff --git a/packages/slate/test/schema/custom/parent-type-invalid-default.js b/packages/slate/test/schema/custom/parent-type-invalid-default.js index 7ebd3d3ca..b382e2946 100644 --- a/packages/slate/test/schema/custom/parent-type-invalid-default.js +++ b/packages/slate/test/schema/custom/parent-type-invalid-default.js @@ -6,7 +6,7 @@ export const schema = { blocks: { list: {}, item: { - parent: { types: ['list'] }, + parent: { type: 'list' }, }, }, }