From 98ed83c23b8ae8ae105e78c5b14c5ba6d9706783 Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Tue, 31 Oct 2017 21:11:05 -0700 Subject: [PATCH] Add schema first/last definitions (#1360) * add `first` and `last` validations to schema * update docs * update schema usage in images example * fix forced-layout example --- docs/reference/slate/schema.md | 70 ++++++++++++++++++- examples/forced-layout/index.js | 1 - examples/images/index.js | 40 ++++++----- packages/slate/src/models/schema.js | 36 +++++++++- .../custom/first-child-kind-invalid-custom.js | 39 +++++++++++ .../first-child-kind-invalid-default.js | 30 ++++++++ .../custom/first-child-type-invalid-custom.js | 43 ++++++++++++ .../first-child-type-invalid-default.js | 35 ++++++++++ .../custom/last-child-kind-invalid-custom.js | 39 +++++++++++ .../custom/last-child-kind-invalid-default.js | 30 ++++++++ .../custom/last-child-type-invalid-custom.js | 43 ++++++++++++ .../custom/last-child-type-invalid-default.js | 35 ++++++++++ 12 files changed, 418 insertions(+), 23 deletions(-) create mode 100644 packages/slate/test/schema/custom/first-child-kind-invalid-custom.js create mode 100644 packages/slate/test/schema/custom/first-child-kind-invalid-default.js create mode 100644 packages/slate/test/schema/custom/first-child-type-invalid-custom.js create mode 100644 packages/slate/test/schema/custom/first-child-type-invalid-default.js create mode 100644 packages/slate/test/schema/custom/last-child-kind-invalid-custom.js create mode 100644 packages/slate/test/schema/custom/last-child-kind-invalid-default.js create mode 100644 packages/slate/test/schema/custom/last-child-type-invalid-custom.js create mode 100644 packages/slate/test/schema/custom/last-child-type-invalid-default.js diff --git a/docs/reference/slate/schema.md b/docs/reference/slate/schema.md index 6c91d951a..69231d1ad 100644 --- a/docs/reference/slate/schema.md +++ b/docs/reference/slate/schema.md @@ -69,7 +69,9 @@ A dictionary of inlines by type, each with its own set of validation rules. ```js { data: Object, + first: Object, isVoid: Boolean, + last: Object, nodes: Array, normalize: Function, parent: Object, @@ -90,7 +92,18 @@ Slate schemas are built up of a set of validation rules. Each of the properties } ``` -A dictionary of +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. + +### `first` +`Object` + +```js +{ + first: { types: ['quote', 'paragraph'] }, +} +``` + +Will validate the first child node. The `first` definition can declare `kinds` and `types` properties. ### `isVoid` `Boolean` @@ -103,6 +116,17 @@ A dictionary of Will validate a node's `isVoid` property. +### `last` +`Object` + +```js +{ + last: { types: ['quote', 'paragraph'] }, +} +``` + +Will validate the last child node. The `last` definition can declare `kinds` and `types` properties. + ### `nodes` `Array` @@ -115,7 +139,9 @@ Will validate a node's `isVoid` property. } ``` -Will validate a node's children. The node definitions can declare the `kinds`, `types`, `min` and `max` properties. +Will validate a node's children. The `nodes` definitions can declare the `kinds`, `types`, `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. ### `normalize` `normalize(change: Change, reason: String, context: Object) => Void` @@ -233,6 +259,46 @@ When supplying your own `normalize` property for a schema rule, it will be calle } ``` +### `first_child_kind_invalid` + +```js +{ + child: Node, + node: Node, + rule: Object, +} +``` + +### `first_child_type_invalid` + +```js +{ + child: Node, + node: Node, + rule: Object, +} +``` + +### `last_child_kind_invalid` + +```js +{ + child: Node, + node: Node, + rule: Object, +} +``` + +### `last_child_type_invalid` + +```js +{ + child: Node, + node: Node, + rule: Object, +} +``` + ### `node_data_invalid` ```js diff --git a/examples/forced-layout/index.js b/examples/forced-layout/index.js index 37d1d9bb9..343ebec0a 100644 --- a/examples/forced-layout/index.js +++ b/examples/forced-layout/index.js @@ -74,7 +74,6 @@ class ForcedLayout extends React.Component { schema={schema} onChange={this.onChange} renderNode={this.renderNode} - validateNode={this.validateNode} /> ) diff --git a/examples/images/index.js b/examples/images/index.js index 284f989f3..83f0933b8 100644 --- a/examples/images/index.js +++ b/examples/images/index.js @@ -27,6 +27,26 @@ function insertImage(change, src, target) { }) } +/** + * A schema to enforce that there's always a paragraph as the last block. + * + * @type {Object} + */ + +const schema = { + document: { + last: { types: ['paragraph'] }, + normalize: (change, reason, { node, child }) => { + switch (reason) { + case 'last_child_type_invalid': { + const paragraph = Block.create('paragraph') + return change.insertNodeByKey(node.key, node.nodes.size, paragraph) + } + } + } + } +} + /** * The images example. * @@ -88,11 +108,11 @@ class Images extends React.Component { ) @@ -119,24 +139,6 @@ class Images extends React.Component { } } - /** - * Perform node validation on the document. - * - * @param {Node} node - * @return {Function|Void} - */ - - validateNode = (node) => { - if (node.kind != 'document') return - const last = node.nodes.last() - - if (!last || last.type != 'paragraph') { - const index = node.nodes.size - const paragraph = Block.create('paragraph') - return change => change.insertNodeByKey(node.key, index, paragraph) - } - } - /** * On change. * diff --git a/packages/slate/src/models/schema.js b/packages/slate/src/models/schema.js index 9bcaf2ace..4fd0bbd94 100644 --- a/packages/slate/src/models/schema.js +++ b/packages/slate/src/models/schema.js @@ -19,6 +19,10 @@ const CHILD_KIND_INVALID = 'child_kind_invalid' const CHILD_REQUIRED = 'child_required' const CHILD_TYPE_INVALID = 'child_type_invalid' const CHILD_UNKNOWN = 'child_unknown' +const FIRST_CHILD_KIND_INVALID = 'first_child_kind_invalid' +const FIRST_CHILD_TYPE_INVALID = 'first_child_type_invalid' +const LAST_CHILD_KIND_INVALID = 'last_child_kind_invalid' +const LAST_CHILD_TYPE_INVALID = 'last_child_type_invalid' const NODE_DATA_INVALID = 'node_data_invalid' const NODE_IS_VOID_INVALID = 'node_is_void_invalid' const NODE_MARK_INVALID = 'node_mark_invalid' @@ -204,7 +208,11 @@ class Schema extends Record(DEFAULTS) { switch (reason) { case CHILD_KIND_INVALID: case CHILD_TYPE_INVALID: - case CHILD_UNKNOWN: { + case CHILD_UNKNOWN: + case FIRST_CHILD_KIND_INVALID: + case FIRST_CHILD_TYPE_INVALID: + case LAST_CHILD_KIND_INVALID: + case LAST_CHILD_TYPE_INVALID: { const { child, node } = context return child.kind == 'text' && node.kind == 'block' && node.nodes.size == 1 ? change.removeNodeByKey(node.key) @@ -295,6 +303,30 @@ class Schema extends Record(DEFAULTS) { } } + if (rule.first != null) { + const first = node.nodes.first() + + if (rule.first.kinds != null && !rule.first.kinds.includes(first.kind)) { + return this.fail(FIRST_CHILD_KIND_INVALID, { ...ctx, child: first }) + } + + if (rule.first.types != null && !rule.first.types.includes(first.type)) { + return this.fail(FIRST_CHILD_TYPE_INVALID, { ...ctx, child: first }) + } + } + + if (rule.last != null) { + const last = node.nodes.last() + + if (rule.last.kinds != null && !rule.last.kinds.includes(last.kind)) { + return this.fail(LAST_CHILD_KIND_INVALID, { ...ctx, child: last }) + } + + if (rule.last.types != null && !rule.last.types.includes(last.type)) { + return this.fail(LAST_CHILD_TYPE_INVALID, { ...ctx, child: last }) + } + } + if (rule.nodes != null || parents != null) { const children = node.nodes.toArray() const defs = rule.nodes != null ? rule.nodes.slice() : [] @@ -470,6 +502,8 @@ function resolveNodeRule(kind, type, obj) { data: {}, isVoid: null, nodes: null, + first: null, + last: null, parent: null, text: null, ...obj, 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 new file mode 100644 index 000000000..8c836353e --- /dev/null +++ b/packages/slate/test/schema/custom/first-child-kind-invalid-custom.js @@ -0,0 +1,39 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = { + blocks: { + paragraph: {}, + quote: { + first: { kinds: ['block'] }, + normalize: (change, reason, { child }) => { + if (reason == 'first_child_kind_invalid') { + change.wrapBlockByKey(child.key, 'paragraph') + } + } + } + } +} + +export const input = ( + + + + text + + + +) + +export const output = ( + + + + + text + + + + +) 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 new file mode 100644 index 000000000..8e165d884 --- /dev/null +++ b/packages/slate/test/schema/custom/first-child-kind-invalid-default.js @@ -0,0 +1,30 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = { + blocks: { + paragraph: {}, + quote: { + first: { kinds: ['text'] }, + } + } +} + +export const input = ( + + + + + + + +) + +export const output = ( + + + + + +) 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 new file mode 100644 index 000000000..912af00f8 --- /dev/null +++ b/packages/slate/test/schema/custom/first-child-type-invalid-custom.js @@ -0,0 +1,43 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = { + blocks: { + paragraph: {}, + quote: { + first: { types: ['paragraph'] }, + normalize: (change, reason, { child }) => { + if (reason == 'first_child_type_invalid') { + change.wrapBlockByKey(child.key, 'paragraph') + } + } + } + } +} + +export const input = ( + + + + + + + + + +) + +export const output = ( + + + + + + + + + + + +) 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 new file mode 100644 index 000000000..faff1f4bc --- /dev/null +++ b/packages/slate/test/schema/custom/first-child-type-invalid-default.js @@ -0,0 +1,35 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = { + blocks: { + paragraph: {}, + quote: { + first: { types: ['paragraph'] } + } + } +} + +export const input = ( + + + + + + + + + +) + +export const output = ( + + + + + + + + +) 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 new file mode 100644 index 000000000..35fa79300 --- /dev/null +++ b/packages/slate/test/schema/custom/last-child-kind-invalid-custom.js @@ -0,0 +1,39 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = { + blocks: { + paragraph: {}, + quote: { + last: { kinds: ['block'] }, + normalize: (change, reason, { child }) => { + if (reason == 'last_child_kind_invalid') { + change.wrapBlockByKey(child.key, 'paragraph') + } + } + } + } +} + +export const input = ( + + + + text + + + +) + +export const output = ( + + + + + text + + + + +) 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 new file mode 100644 index 000000000..a8632ef0e --- /dev/null +++ b/packages/slate/test/schema/custom/last-child-kind-invalid-default.js @@ -0,0 +1,30 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = { + blocks: { + paragraph: {}, + quote: { + last: { kinds: ['text'] }, + } + } +} + +export const input = ( + + + + + + + +) + +export const output = ( + + + + + +) 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 new file mode 100644 index 000000000..8f80afe46 --- /dev/null +++ b/packages/slate/test/schema/custom/last-child-type-invalid-custom.js @@ -0,0 +1,43 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = { + blocks: { + paragraph: {}, + quote: { + last: { types: ['paragraph'] }, + normalize: (change, reason, { child }) => { + if (reason == 'last_child_type_invalid') { + change.wrapBlockByKey(child.key, 'paragraph') + } + } + } + } +} + +export const input = ( + + + + + + + + + +) + +export const output = ( + + + + + + + + + + + +) 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 new file mode 100644 index 000000000..82c0f8111 --- /dev/null +++ b/packages/slate/test/schema/custom/last-child-type-invalid-default.js @@ -0,0 +1,35 @@ +/** @jsx h */ + +import h from '../../helpers/h' + +export const schema = { + blocks: { + paragraph: {}, + quote: { + last: { types: ['paragraph'] } + } + } +} + +export const input = ( + + + + + + + + + +) + +export const output = ( + + + + + + + + +)