1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-17 20:51:20 +02:00

Refactor schema (#1993)

#### Is this adding or improving a _feature_ or fixing a _bug_?

Improvement.

#### What's the new behavior?

- Tweaking the declarative schema definition syntax to make it easier to represent more complex states, as well as enable it to validate previously impossible things.
- Rename `validateNode` to `normalizeNode` for clarity.
- Introduce `validateNode`, `checkNode`, `assertNode` helpers for more advanced use cases, like front-end API validation of "invalid" fields that need to be fixed before they are sent to the server.

#### How does this change work?

The `schema.blocks/inlines/document` entries are now a shorthand for a more powerful `schema.rules` syntax. For example, this now allows for declaratively validating by a node's data, regardless of type:

```js
{
  rules: [
    {
      match: {
        data: { id: '2kd293lry' },
      },
      nodes: [
        { match: { type: 'paragraph' }},
        { match: { type: 'image' }},
      ]
    }
  ]
}
```

Previously you'd have to use `validateNode` for this, since the syntax wasn't flexible enough to validate nodes without hard-coding their `type`.

This also simplifies the "concatenation" of schema rules, because under the covers all of them are implemented using the `schema.rules` array, so they simply take effect in order, just like everything else in plugins.

#### Have you checked that...?

<!-- 
Please run through this checklist for your pull request: 
-->

* [x] The new code matches the existing patterns and styles.
* [x] The tests pass with `yarn test`.
* [x] The linter passes with `yarn lint`. (Fix errors with `yarn prettier`.)
* [x] The relevant examples still work. (Run examples with `yarn watch`.)

#### Does this fix any issues or need any specific reviewers?

Fixes: #1842
Fixes: #1923
This commit is contained in:
Ian Storm Taylor
2018-07-27 15:27:07 -07:00
committed by GitHub
parent d01c441f68
commit ded82812b0
56 changed files with 1251 additions and 811 deletions

1
.eslintignore Normal file
View File

@@ -0,0 +1 @@
*.md

View File

@@ -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')
}
}
}

View File

@@ -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) => {

View File

@@ -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`

View File

@@ -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.

View File

@@ -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)
}
}

View File

@@ -3,5 +3,5 @@ import { KeyUtils } from 'slate'
beforeEach(KeyUtils.resetGenerator)
describe('utils', () => {
require('./get-children-decorations')
// require('./get-children-decorations')
})

View File

@@ -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'

View File

@@ -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.'
)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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',
])

View File

@@ -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.
*

File diff suppressed because it is too large Load Diff

View File

@@ -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',
])

View File

@@ -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

View File

@@ -8,8 +8,12 @@ export const schema = {
blocks: {
paragraph: {},
item: {
parent: { types: ['list'] },
nodes: [{ objects: ['text'] }],
parent: { type: 'list' },
nodes: [
{
match: [{ object: 'text' }],
},
],
},
list: {},
},

View File

@@ -8,8 +8,12 @@ export const schema = {
blocks: {
paragraph: {},
item: {
parent: { types: ['list'] },
nodes: [{ objects: ['text'] }],
parent: { type: 'list' },
nodes: [
{
match: [{ object: 'text' }],
},
],
},
list: {},
},

View File

@@ -8,8 +8,12 @@ export const schema = {
blocks: {
paragraph: {},
item: {
parent: { types: ['list'] },
nodes: [{ objects: ['text'] }],
parent: { type: 'list' },
nodes: [
{
match: [{ object: 'text' }],
},
],
},
list: {},
},

View File

@@ -8,8 +8,12 @@ export const schema = {
blocks: {
paragraph: {},
item: {
parent: { types: ['list'] },
nodes: [{ objects: ['text'] }],
parent: { type: 'list' },
nodes: [
{
match: [{ object: 'text' }],
},
],
},
list: {},
},

View File

@@ -0,0 +1,52 @@
/** @jsx h */
import h from '../../helpers/h'
export const schema = {}
export const input = (
<value>
<document>
<quote>
<paragraph>one</paragraph>
<link>two</link>
</quote>
</document>
</value>
)
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: [],
},
],
},
],
},
],
},
],
},
}

View File

@@ -0,0 +1,72 @@
/** @jsx h */
import h from '../../helpers/h'
export const schema = {}
export const input = (
<value>
<document>
<quote>
<link>one</link>
<paragraph>two</paragraph>
</quote>
</document>
</value>
)
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: [],
},
],
},
],
},
],
},
}

View File

@@ -0,0 +1,44 @@
/** @jsx h */
import h from '../../helpers/h'
export const schema = {}
export const input = (
<value>
<document>
<quote>
one
<paragraph>two</paragraph>
</quote>
</document>
</value>
)
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: [],
},
],
},
],
},
],
},
}

View File

@@ -8,7 +8,7 @@ export const input = (
<value>
<document>
<paragraph>
<link>
<link key="fuck">
<paragraph>one</paragraph>
two
</link>

View File

@@ -0,0 +1,45 @@
/** @jsx h */
import h from '../../helpers/h'
export const schema = {}
export const input = (
<value>
<document>
<paragraph>
<text key="a">one</text>
<text key="b">two</text>
<text key="c">three</text>
</paragraph>
</document>
</value>
)
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: [],
},
],
},
],
},
],
},
}

View File

@@ -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')
}
},

View File

@@ -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')
}
},

View File

@@ -6,7 +6,11 @@ export const schema = {
blocks: {
paragraph: {},
quote: {
nodes: [{ objects: ['text'] }],
nodes: [
{
match: [{ object: 'text' }],
},
],
},
},
}

View File

@@ -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',

View File

@@ -6,7 +6,12 @@ export const schema = {
blocks: {
paragraph: {},
quote: {
nodes: [{ types: ['paragraph'], min: 1 }],
nodes: [
{
match: [{ type: 'paragraph' }],
min: 1,
},
],
},
},
}

View File

@@ -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')
}
},

View File

@@ -6,7 +6,11 @@ export const schema = {
blocks: {
paragraph: {},
quote: {
nodes: [{ types: ['paragraph'] }],
nodes: [
{
match: [{ type: 'paragraph' }],
},
],
},
},
}

View File

@@ -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

View File

@@ -6,7 +6,12 @@ export const schema = {
blocks: {
paragraph: {},
quote: {
nodes: [{ types: ['paragraph'], max: 1 }],
nodes: [
{
match: [{ type: 'paragraph' }],
max: 1,
},
],
},
},
}

View File

@@ -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')
}
},

View File

@@ -6,7 +6,7 @@ export const schema = {
blocks: {
paragraph: {},
quote: {
first: { objects: ['text'] },
first: [{ object: 'text' }],
},
},
}

View File

@@ -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')
}
},

View File

@@ -6,7 +6,7 @@ export const schema = {
blocks: {
paragraph: {},
quote: {
first: { types: ['paragraph'] },
first: { type: 'paragraph' },
},
},
}

View File

@@ -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')
}
},

View File

@@ -6,7 +6,7 @@ export const schema = {
blocks: {
paragraph: {},
quote: {
last: { objects: ['text'] },
last: [{ object: 'text' }],
},
},
}

View File

@@ -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')
}
},

View File

@@ -6,7 +6,7 @@ export const schema = {
blocks: {
paragraph: {},
quote: {
last: { types: ['paragraph'] },
last: { type: 'paragraph' },
},
},
}

View File

@@ -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 = (
<value>
<document>
<paragraph thing="value" />
</document>
</value>
)
export const output = (
<value>
<document />
</value>
)

View File

@@ -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 = (
<value>
<document>
<paragraph />
</document>
</value>
)
export const output = (
<value>
<document />
</value>
)

View File

@@ -0,0 +1,26 @@
/** @jsx h */
import h from '../../helpers/h'
export const schema = {
rules: [
{
match: [{ type: 'paragraph' }],
object: 'inline',
},
],
}
export const input = (
<value>
<document>
<paragraph>invalid</paragraph>
</document>
</value>
)
export const output = (
<value>
<document />
</value>
)

View File

@@ -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' } })
}
},

View File

@@ -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')
}
},

View File

@@ -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))
}
},

View File

@@ -0,0 +1,25 @@
/** @jsx h */
import h from '../../helpers/h'
export const schema = {
blocks: {
paragraph: {
object: 'inline',
},
},
}
export const input = (
<value>
<document>
<paragraph>invalid</paragraph>
</document>
</value>
)
export const output = (
<value>
<document />
</value>
)

View File

@@ -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))
}
},

View File

@@ -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 = (
<value>
<document>
<paragraph>123</paragraph>
</document>
</value>
)
export const output = (
<value>
<document>
<paragraph>123</paragraph>
</document>
</value>
)

View File

@@ -0,0 +1,25 @@
/** @jsx h */
import h from '../../helpers/h'
export const schema = {
blocks: {
paragraph: {
type: 'impossible',
},
},
}
export const input = (
<value>
<document>
<paragraph>invalid</paragraph>
</document>
</value>
)
export const output = (
<value>
<document />
</value>
)

View File

@@ -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)
}
},

View File

@@ -5,7 +5,7 @@ import h from '../../helpers/h'
export const schema = {
inlines: {
link: {
parent: { objects: ['block'] },
parent: { object: 'block' },
},
},
}

View File

@@ -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')
}
},

View File

@@ -6,7 +6,7 @@ export const schema = {
blocks: {
list: {},
item: {
parent: { types: ['list'] },
parent: { type: 'list' },
},
},
}