+
diff --git a/packages/slate-react/test/rendering/fixtures/empty-block.js b/packages/slate-react/test/rendering/fixtures/empty-block.js
index 29972734b..4541c4097 100644
--- a/packages/slate-react/test/rendering/fixtures/empty-block.js
+++ b/packages/slate-react/test/rendering/fixtures/empty-block.js
@@ -2,7 +2,7 @@
import h from '../../helpers/h'
-export const schema = {}
+export const props = {}
export const state = (
@@ -15,6 +15,7 @@ export const state = (
export const output = `
+
diff --git a/packages/slate-react/test/rendering/fixtures/nested-text-direction.js b/packages/slate-react/test/rendering/fixtures/nested-text-direction.js
index f73943cd3..d3e363036 100644
--- a/packages/slate-react/test/rendering/fixtures/nested-text-direction.js
+++ b/packages/slate-react/test/rendering/fixtures/nested-text-direction.js
@@ -2,7 +2,7 @@
import h from '../../helpers/h'
-export const schema = {}
+export const props = {}
export const state = (
diff --git a/packages/slate-react/test/rendering/fixtures/readonly-custom-block-void.js b/packages/slate-react/test/rendering/fixtures/readonly-custom-block-void.js
index bbf489800..4d7c845a8 100644
--- a/packages/slate-react/test/rendering/fixtures/readonly-custom-block-void.js
+++ b/packages/slate-react/test/rendering/fixtures/readonly-custom-block-void.js
@@ -3,18 +3,19 @@
import React from 'react'
import h from '../../helpers/h'
-export const schema = {
- nodes: {
- image: (props) => {
- return (
- React.createElement('img', { src: props.node.data.get('src'), ...props.attributes })
- )
- }
+function Image(props) {
+ return React.createElement('img', { src: props.node.data.get('src'), ...props.attributes })
+}
+
+function renderNode(props) {
+ switch (props.node.type) {
+ case 'image': return Image(props)
}
}
export const props = {
readOnly: true,
+ renderNode,
}
export const state = (
diff --git a/packages/slate-react/test/rendering/fixtures/readonly-custom-inline-void.js b/packages/slate-react/test/rendering/fixtures/readonly-custom-inline-void.js
index fdca540fb..fe1a669f2 100644
--- a/packages/slate-react/test/rendering/fixtures/readonly-custom-inline-void.js
+++ b/packages/slate-react/test/rendering/fixtures/readonly-custom-inline-void.js
@@ -3,18 +3,21 @@
import React from 'react'
import h from '../../helpers/h'
-export const schema = {
- nodes: {
- emoji: (props) => {
- return (
- React.createElement('img', props.attributes)
- )
- }
+function Emoji(props) {
+ return (
+ React.createElement('img', props.attributes)
+ )
+}
+
+function renderNode(props) {
+ switch (props.node.type) {
+ case 'emoji': return Emoji(props)
}
}
export const props = {
readOnly: true,
+ renderNode,
}
export const state = (
diff --git a/packages/slate-react/test/rendering/fixtures/text-direction.js b/packages/slate-react/test/rendering/fixtures/text-direction.js
index 6a9643667..a64b56220 100644
--- a/packages/slate-react/test/rendering/fixtures/text-direction.js
+++ b/packages/slate-react/test/rendering/fixtures/text-direction.js
@@ -2,7 +2,7 @@
import h from '../../helpers/h'
-export const schema = {}
+export const props = {}
export const state = (
diff --git a/packages/slate-react/test/rendering/index.js b/packages/slate-react/test/rendering/index.js
index 93372ea40..19f475982 100644
--- a/packages/slate-react/test/rendering/index.js
+++ b/packages/slate-react/test/rendering/index.js
@@ -19,10 +19,9 @@ describe('rendering', () => {
for (const test of tests) {
it(test, async () => {
const module = require(resolve(dir, test))
- const { state, schema, output, props } = module
+ const { state, output, props } = module
const p = {
state,
- schema,
onChange() {},
...(props || {}),
}
diff --git a/packages/slate/benchmark/changes/normalize.js b/packages/slate/benchmark/changes/normalize.js
index c60593a08..9ae0c58e4 100644
--- a/packages/slate/benchmark/changes/normalize.js
+++ b/packages/slate/benchmark/changes/normalize.js
@@ -2,12 +2,11 @@
/* eslint-disable react/jsx-key */
import h from '../../test/helpers/h'
-import SCHEMA from '../../lib/schemas/core'
export default function (state) {
state
.change()
- .normalize(SCHEMA)
+ .normalize()
}
export const input = (
diff --git a/packages/slate/src/changes/at-range.js b/packages/slate/src/changes/at-range.js
index ad4741c8a..cbfffa758 100644
--- a/packages/slate/src/changes/at-range.js
+++ b/packages/slate/src/changes/at-range.js
@@ -6,7 +6,6 @@ import Inline from '../models/inline'
import Mark from '../models/mark'
import Node from '../models/node'
import String from '../utils/string'
-import SCHEMA from '../schemas/core'
/**
* Changes.
@@ -242,7 +241,7 @@ Changes.deleteAtRange = (change, range, options = {}) => {
// If we should normalize, do it now after everything.
if (normalize) {
- change.normalizeNodeByKey(ancestor.key, SCHEMA)
+ change.normalizeNodeByKey(ancestor.key)
}
}
}
@@ -667,7 +666,7 @@ Changes.insertBlockAtRange = (change, range, block, options = {}) => {
}
if (normalize) {
- change.normalizeNodeByKey(parent.key, SCHEMA)
+ change.normalizeNodeByKey(parent.key)
}
}
@@ -780,7 +779,7 @@ Changes.insertFragmentAtRange = (change, range, fragment, options = {}) => {
// Normalize if requested.
if (normalize) {
- change.normalizeNodeByKey(parent.key, SCHEMA)
+ change.normalizeNodeByKey(parent.key)
}
}
@@ -816,7 +815,7 @@ Changes.insertInlineAtRange = (change, range, inline, options = {}) => {
change.insertNodeByKey(parent.key, index + 1, inline, { normalize: false })
if (normalize) {
- change.normalizeNodeByKey(parent.key, SCHEMA)
+ change.normalizeNodeByKey(parent.key)
}
}
@@ -1111,7 +1110,7 @@ Changes.unwrapBlockAtRange = (change, range, properties, options = {}) => {
// TODO: optmize to only normalize the right block
if (normalize) {
- change.normalizeDocument(SCHEMA)
+ change.normalizeDocument()
}
}
@@ -1157,7 +1156,7 @@ Changes.unwrapInlineAtRange = (change, range, properties, options = {}) => {
// TODO: optmize to only normalize the right block
if (normalize) {
- change.normalizeDocument(SCHEMA)
+ change.normalizeDocument()
}
}
@@ -1228,7 +1227,7 @@ Changes.wrapBlockAtRange = (change, range, block, options = {}) => {
})
if (normalize) {
- change.normalizeNodeByKey(parent.key, SCHEMA)
+ change.normalizeNodeByKey(parent.key)
}
}
@@ -1300,7 +1299,7 @@ Changes.wrapInlineAtRange = (change, range, inline, options = {}) => {
})
if (normalize) {
- change.normalizeNodeByKey(startBlock.key, SCHEMA)
+ change.normalizeNodeByKey(startBlock.key)
}
}
@@ -1323,8 +1322,8 @@ Changes.wrapInlineAtRange = (change, range, inline, options = {}) => {
if (normalize) {
change
- .normalizeNodeByKey(startBlock.key, SCHEMA)
- .normalizeNodeByKey(endBlock.key, SCHEMA)
+ .normalizeNodeByKey(startBlock.key)
+ .normalizeNodeByKey(endBlock.key)
}
blocks.slice(1, -1).forEach((block) => {
@@ -1336,7 +1335,7 @@ Changes.wrapInlineAtRange = (change, range, inline, options = {}) => {
})
if (normalize) {
- change.normalizeNodeByKey(block.key, SCHEMA)
+ change.normalizeNodeByKey(block.key)
}
})
}
diff --git a/packages/slate/src/changes/by-key.js b/packages/slate/src/changes/by-key.js
index e005af91a..e98354c29 100644
--- a/packages/slate/src/changes/by-key.js
+++ b/packages/slate/src/changes/by-key.js
@@ -3,7 +3,6 @@ import Block from '../models/block'
import Inline from '../models/inline'
import Mark from '../models/mark'
import Node from '../models/node'
-import SCHEMA from '../schemas/core'
/**
* Changes.
@@ -68,7 +67,7 @@ Changes.addMarkByKey = (change, key, offset, length, mark, options = {}) => {
if (normalize) {
const parent = document.getParent(key)
- change.normalizeNodeByKey(parent.key, SCHEMA)
+ change.normalizeNodeByKey(parent.key)
}
}
@@ -91,7 +90,7 @@ Changes.insertFragmentByKey = (change, key, index, fragment, options = {}) => {
})
if (normalize) {
- change.normalizeNodeByKey(key, SCHEMA)
+ change.normalizeNodeByKey(key)
}
}
@@ -119,7 +118,7 @@ Changes.insertNodeByKey = (change, key, index, node, options = {}) => {
})
if (normalize) {
- change.normalizeNodeByKey(key, SCHEMA)
+ change.normalizeNodeByKey(key)
}
}
@@ -153,7 +152,7 @@ Changes.insertTextByKey = (change, key, offset, text, marks, options = {}) => {
if (normalize) {
const parent = document.getParent(key)
- change.normalizeNodeByKey(parent.key, SCHEMA)
+ change.normalizeNodeByKey(parent.key)
}
}
@@ -187,7 +186,7 @@ Changes.mergeNodeByKey = (change, key, options = {}) => {
if (normalize) {
const parent = document.getParent(key)
- change.normalizeNodeByKey(parent.key, SCHEMA)
+ change.normalizeNodeByKey(parent.key)
}
}
@@ -218,7 +217,7 @@ Changes.moveNodeByKey = (change, key, newKey, newIndex, options = {}) => {
if (normalize) {
const parent = document.getCommonAncestor(key, newKey)
- change.normalizeNodeByKey(parent.key, SCHEMA)
+ change.normalizeNodeByKey(parent.key)
}
}
@@ -277,7 +276,7 @@ Changes.removeMarkByKey = (change, key, offset, length, mark, options = {}) => {
if (normalize) {
const parent = document.getParent(key)
- change.normalizeNodeByKey(parent.key, SCHEMA)
+ change.normalizeNodeByKey(parent.key)
}
}
@@ -305,7 +304,7 @@ Changes.removeNodeByKey = (change, key, options = {}) => {
if (normalize) {
const parent = document.getParent(key)
- change.normalizeNodeByKey(parent.key, SCHEMA)
+ change.normalizeNodeByKey(parent.key)
}
}
@@ -362,7 +361,7 @@ Changes.removeTextByKey = (change, key, offset, length, options = {}) => {
if (normalize) {
const block = document.getClosestBlock(key)
- change.normalizeNodeByKey(block.key, SCHEMA)
+ change.normalizeNodeByKey(block.key)
}
}
@@ -387,7 +386,7 @@ Changes.replaceNodeByKey = (change, key, newNode, options = {}) => {
change.removeNodeByKey(key, { normalize: false })
change.insertNodeByKey(parent.key, index, newNode, options)
if (normalize) {
- change.normalizeNodeByKey(parent.key, SCHEMA)
+ change.normalizeNodeByKey(parent.key)
}
}
@@ -422,7 +421,7 @@ Changes.setMarkByKey = (change, key, offset, length, mark, properties, options =
if (normalize) {
const parent = document.getParent(key)
- change.normalizeNodeByKey(parent.key, SCHEMA)
+ change.normalizeNodeByKey(parent.key)
}
}
@@ -452,7 +451,7 @@ Changes.setNodeByKey = (change, key, properties, options = {}) => {
})
if (normalize) {
- change.normalizeNodeByKey(node.key, SCHEMA)
+ change.normalizeNodeByKey(node.key)
}
}
@@ -481,7 +480,7 @@ Changes.splitNodeByKey = (change, key, position, options = {}) => {
if (normalize) {
const parent = document.getParent(key)
- change.normalizeNodeByKey(parent.key, SCHEMA)
+ change.normalizeNodeByKey(parent.key)
}
}
@@ -520,7 +519,7 @@ Changes.splitDescendantsByKey = (change, key, textKey, textOffset, options = {})
if (normalize) {
const parent = document.getParent(key)
- change.normalizeNodeByKey(parent.key, SCHEMA)
+ change.normalizeNodeByKey(parent.key)
}
}
@@ -614,34 +613,11 @@ Changes.unwrapNodeByKey = (change, key, options = {}) => {
change.moveNodeByKey(key, parentParent.key, parentIndex + 1, { normalize: false })
if (normalize) {
- change.normalizeNodeByKey(parentParent.key, SCHEMA)
+ change.normalizeNodeByKey(parentParent.key)
}
}
}
-/**
- * Wrap a node in an inline with `properties`.
- *
- * @param {Change} change
- * @param {String} key The node to wrap
- * @param {Block|Object|String} inline The wrapping inline (its children are discarded)
- * @param {Object} options
- * @property {Boolean} normalize
- */
-
-Changes.wrapInlineByKey = (change, key, inline, options) => {
- inline = Inline.create(inline)
- inline = inline.set('nodes', inline.nodes.clear())
-
- const { document } = change.state
- const node = document.assertDescendant(key)
- const parent = document.getParent(node.key)
- const index = parent.nodes.indexOf(node)
-
- change.insertNodeByKey(parent.key, index, inline, { normalize: false })
- change.moveNodeByKey(node.key, inline.key, 0, options)
-}
-
/**
* Wrap a node in a block with `properties`.
*
@@ -665,6 +641,53 @@ Changes.wrapBlockByKey = (change, key, block, options) => {
change.moveNodeByKey(node.key, block.key, 0, options)
}
+/**
+ * Wrap a node in an inline with `properties`.
+ *
+ * @param {Change} change
+ * @param {String} key The node to wrap
+ * @param {Block|Object|String} inline The wrapping inline (its children are discarded)
+ * @param {Object} options
+ * @property {Boolean} normalize
+ */
+
+Changes.wrapInlineByKey = (change, key, inline, options) => {
+ inline = Inline.create(inline)
+ inline = inline.set('nodes', inline.nodes.clear())
+
+ const { document } = change.state
+ const node = document.assertDescendant(key)
+ const parent = document.getParent(node.key)
+ const index = parent.nodes.indexOf(node)
+
+ change.insertNodeByKey(parent.key, index, inline, { normalize: false })
+ change.moveNodeByKey(node.key, inline.key, 0, options)
+}
+
+/**
+ * Wrap a node by `key` with `parent`.
+ *
+ * @param {Change} change
+ * @param {String} key
+ * @param {Node|Object} parent
+ * @param {Object} options
+ */
+
+Changes.wrapNodeByKey = (change, key, parent) => {
+ parent = Node.create(parent)
+ parent = parent.set('nodes', parent.nodes.clear())
+
+ if (parent.kind == 'block') {
+ change.wrapBlockByKey(key, parent)
+ return
+ }
+
+ if (parent.kind == 'inline') {
+ change.wrapInlineByKey(key, parent)
+ return
+ }
+}
+
/**
* Export.
*
diff --git a/packages/slate/src/changes/index.js b/packages/slate/src/changes/index.js
index 712dbb780..3f3cf8a35 100644
--- a/packages/slate/src/changes/index.js
+++ b/packages/slate/src/changes/index.js
@@ -2,10 +2,10 @@
import AtCurrentRange from './at-current-range'
import AtRange from './at-range'
import ByKey from './by-key'
-import Normalize from './normalize'
import OnHistory from './on-history'
import OnSelection from './on-selection'
import OnState from './on-state'
+import WithSchema from './with-schema'
/**
* Export.
@@ -17,8 +17,8 @@ export default {
...AtCurrentRange,
...AtRange,
...ByKey,
- ...Normalize,
...OnHistory,
...OnSelection,
...OnState,
+ ...WithSchema,
}
diff --git a/packages/slate/src/changes/normalize.js b/packages/slate/src/changes/with-schema.js
similarity index 73%
rename from packages/slate/src/changes/normalize.js
rename to packages/slate/src/changes/with-schema.js
index 923f6f6e4..80eb9bfc1 100644
--- a/packages/slate/src/changes/normalize.js
+++ b/packages/slate/src/changes/with-schema.js
@@ -1,8 +1,6 @@
import { Set } from 'immutable'
-import Schema from '../models/schema'
-
/**
* Changes.
*
@@ -12,45 +10,37 @@ import Schema from '../models/schema'
const Changes = {}
/**
- * Normalize the document and selection with a `schema`.
+ * Normalize the state with its schema.
*
* @param {Change} change
- * @param {Schema} schema
*/
-Changes.normalize = (change, schema) => {
- change.normalizeDocument(schema)
+Changes.normalize = (change) => {
+ change.normalizeDocument()
}
/**
- * Normalize the document with a `schema`.
+ * Normalize the document with the state's schema.
*
* @param {Change} change
- * @param {Schema} schema
*/
-Changes.normalizeDocument = (change, schema) => {
+Changes.normalizeDocument = (change) => {
const { state } = change
const { document } = state
- change.normalizeNodeByKey(document.key, schema)
+ change.normalizeNodeByKey(document.key)
}
/**
- * Normalize a `node` and its children with a `schema`.
+ * Normalize a `node` and its children with the state's schema.
*
* @param {Change} change
* @param {Node|String} key
- * @param {Schema} schema
*/
-Changes.normalizeNodeByKey = (change, key, schema) => {
- assertSchema(schema)
-
- // If the schema has no validation rules, there's nothing to normalize.
- if (!schema.hasValidators) return
-
+Changes.normalizeNodeByKey = (change, key) => {
const { state } = change
- const { document } = state
+ const { document, schema } = state
const node = document.assertNode(key)
normalizeNodeAndChildren(change, node, schema)
}
@@ -139,20 +129,19 @@ function refindNode(change, node) {
*/
function normalizeNode(change, node, schema) {
- const max = schema.rules.length
+ const max = schema.stack.plugins.length + 1
let iterations = 0
- function iterate(t, n) {
- const failure = n.validate(schema)
- if (!failure) return
+ function iterate(c, n) {
+ const normalize = n.validate(schema)
+ if (!normalize) return
- // Run the `normalize` function for the rule with the invalid value.
- const { value, rule } = failure
- rule.normalize(t, n, value)
+ // Run the `normalize` function to fix the node.
+ normalize(c)
// Re-find the node reference, in case it was updated. If the node no longer
// exists, we're done for this branch.
- n = refindNode(t, n)
+ n = refindNode(c, n)
if (!n) return
// Increment the iterations counter, and check to make sure that we haven't
@@ -166,28 +155,12 @@ function normalizeNode(change, node, schema) {
}
// Otherwise, iterate again.
- iterate(t, n)
+ iterate(c, n)
}
iterate(change, node)
}
-/**
- * Assert that a `schema` exists.
- *
- * @param {Schema} schema
- */
-
-function assertSchema(schema) {
- if (Schema.isSchema(schema)) {
- return
- } else if (schema == null) {
- throw new Error('You must pass a `schema` object.')
- } else {
- throw new Error(`You passed an invalid \`schema\` object: ${schema}.`)
- }
-}
-
/**
* Export.
*
diff --git a/packages/slate/src/constants/core-schema-rules.js b/packages/slate/src/constants/core-schema-rules.js
new file mode 100644
index 000000000..746abfd00
--- /dev/null
+++ b/packages/slate/src/constants/core-schema-rules.js
@@ -0,0 +1,281 @@
+
+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.kind != 'document') return
+ const invalids = node.nodes.filter(n => n.kind != '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.kind != 'block') return
+ const first = node.nodes.first()
+ if (!first) return
+ const kinds = first.kind == 'block' ? ['block'] : ['inline', 'text']
+ const invalids = node.nodes.filter(n => !kinds.includes(n.kind))
+ 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.kind != 'inline') return
+ const invalids = node.nodes.filter(n => n.kind != 'inline' && n.kind != '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.kind != 'block' && node.kind != 'inline') return
+ if (node.nodes.size > 0) return
+
+ return (change) => {
+ const text = Text.create()
+ change.insertNodeByKey(node.key, 0, text, { normalize: false })
+ }
+ }
+ },
+
+ /**
+ * Ensure that void nodes contain a text node with a single space of text.
+ *
+ * @type {Object}
+ */
+
+ {
+ validateNode(node) {
+ if (!node.isVoid) return
+ if (node.kind != 'block' && node.kind != 'inline') return
+ if (node.text == ' ' && node.nodes.size == 1) return
+
+ return (change) => {
+ const text = Text.create(' ')
+ const index = node.nodes.size
+
+ change.insertNodeByKey(node.key, index, text, { normalize: false })
+
+ node.nodes.forEach((child) => {
+ change.removeNodeByKey(child.key, { normalize: false })
+ })
+ }
+ }
+ },
+
+ /**
+ * Ensure that inline nodes are never empty.
+ *
+ * This rule is applied to all blocks, because when they contain an empty
+ * inline, we need to remove the inline from that parent block. If `validate`
+ * was to be memoized, it should be against the parent node, not the inline
+ * themselves.
+ *
+ * @type {Object}
+ */
+
+ {
+ validateNode(node) {
+ if (node.kind != 'block') return
+ const invalids = node.nodes.filter(n => n.kind == 'inline' && n.text == '')
+ 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.kind != 'block' && node.kind != 'inline') return
+
+ const invalids = node.nodes.reduce((list, child, index) => {
+ if (child.kind !== '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.kind == '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.kind != 'block' && node.kind != 'inline') return
+
+ const invalids = node.nodes
+ .map((child, i) => {
+ const next = node.nodes.get(i + 1)
+ if (child.kind != 'text') return
+ if (!next || next.kind != '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.kind != 'block' && node.kind != 'inline') return
+ const { nodes } = node
+ if (nodes.size <= 1) return
+
+ const invalids = nodes.filter((desc, i) => {
+ if (desc.kind != '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.kind == 'inline') return
+
+ // It it's the last node, and the previous is an inline, preserve it.
+ if (!next && prev.kind == 'inline') return
+
+ // If it's surrounded by inlines, preserve it.
+ if (next && prev && next.kind == 'inline' && prev.kind == '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/index.js b/packages/slate/src/index.js
index 8f82e03e1..ecac2bb69 100644
--- a/packages/slate/src/index.js
+++ b/packages/slate/src/index.js
@@ -16,7 +16,6 @@ import Selection from './models/selection'
import Stack from './models/stack'
import State from './models/state'
import Text from './models/text'
-import coreSchema from './schemas/core'
import { resetKeyGenerator, setKeyGenerator } from './utils/generate-key'
/**
@@ -43,7 +42,6 @@ export {
Stack,
State,
Text,
- coreSchema,
resetKeyGenerator,
setKeyGenerator,
}
@@ -66,7 +64,6 @@ export default {
Stack,
State,
Text,
- coreSchema,
resetKeyGenerator,
setKeyGenerator,
}
diff --git a/packages/slate/src/models/node.js b/packages/slate/src/models/node.js
index 43d93d8a5..28169693c 100644
--- a/packages/slate/src/models/node.js
+++ b/packages/slate/src/models/node.js
@@ -8,6 +8,7 @@ import Block from './block'
import Data from './data'
import Document from './document'
import Inline from './inline'
+import Range from './range'
import Text from './text'
import generateKey from '../utils/generate-key'
import isIndexInRange from '../utils/is-index-in-range'
@@ -585,25 +586,16 @@ class Node {
}
/**
- * Get the component for the node from a `schema`.
+ * Get the decorations for the node from a `stack`.
*
- * @param {Schema} schema
- * @return {Component|Void}
+ * @param {Stack} stack
+ * @return {List}
*/
- getComponent(schema) {
- return schema.__getComponent(this)
- }
-
- /**
- * Get the decorations for the node from a `schema`.
- *
- * @param {Schema} schema
- * @return {Array}
- */
-
- getDecorations(schema) {
- return schema.__getDecorations(this)
+ getDecorations(stack) {
+ const decorations = stack.find('decorateNode', this)
+ const list = Range.createList(decorations || [])
+ return list
}
/**
@@ -1894,11 +1886,11 @@ class Node {
* Validate the node against a `schema`.
*
* @param {Schema} schema
- * @return {Object|Null}
+ * @return {Function|Null}
*/
validate(schema) {
- return schema.__validate(this)
+ return schema.validateNode(this)
}
/**
@@ -2124,7 +2116,6 @@ memoize(Node.prototype, [
'getClosestInline',
'getClosestVoid',
'getCommonAncestor',
- 'getComponent',
'getDecorations',
'getDepth',
'getDescendant',
diff --git a/packages/slate/src/models/schema.js b/packages/slate/src/models/schema.js
index d248e0702..f4448e66c 100644
--- a/packages/slate/src/models/schema.js
+++ b/packages/slate/src/models/schema.js
@@ -1,14 +1,38 @@
-import React from 'react'
-import find from 'lodash/find'
+import Debug from 'debug'
import isPlainObject from 'is-plain-object'
-import logger from 'slate-dev-logger'
-import typeOf from 'type-of'
+import mergeWith from 'lodash/mergeWith'
import { Record } from 'immutable'
+import CORE_SCHEMA_RULES from '../constants/core-schema-rules'
import MODEL_TYPES from '../constants/model-types'
-import Range from '../models/range'
-import isReactComponent from '../utils/is-react-component'
+import Stack from './stack'
+import memoize from '../utils/memoize'
+
+/**
+ * Validation failure reasons.
+ *
+ * @type {Object}
+ */
+
+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 NODE_DATA_INVALID = 'node_data_invalid'
+const NODE_IS_VOID_INVALID = 'node_is_void_invalid'
+const NODE_MARK_INVALID = 'node_mark_invalid'
+const NODE_TEXT_INVALID = 'node_text_invalid'
+const PARENT_KIND_INVALID = 'parent_kind_invalid'
+const PARENT_TYPE_INVALID = 'parent_type_invalid'
+
+/**
+ * Debug.
+ *
+ * @type {Function}
+ */
+
+const debug = Debug('slate:schema')
/**
* Default properties.
@@ -17,7 +41,10 @@ import isReactComponent from '../utils/is-react-component'
*/
const DEFAULTS = {
- rules: [],
+ stack: Stack.create(),
+ document: {},
+ blocks: {},
+ inlines: {},
}
/**
@@ -47,6 +74,44 @@ class Schema extends Record(DEFAULTS) {
throw new Error(`\`Schema.create\` only accepts objects or schemas, but you passed it: ${attrs}`)
}
+ /**
+ * Create a `Schema` from a JSON `object`.
+ *
+ * @param {Object} object
+ * @return {Schema}
+ */
+
+ static fromJSON(object) {
+ if (Schema.isSchema(object)) {
+ return object
+ }
+
+ let { plugins } = object
+
+ if (object.rules) {
+ throw new Error('Schemas in Slate have changed! They are no longer accept a `rules` property.')
+ }
+
+ 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 })
+ return ret
+ }
+
+ /**
+ * Alias `fromJS`.
+ */
+
+ static fromJS = Schema.fromJSON
+
/**
* Check if a `value` is a `Schema`.
*
@@ -58,25 +123,6 @@ class Schema extends Record(DEFAULTS) {
return !!(value && value[MODEL_TYPES.SCHEMA])
}
- /**
- * Create a `Schema` from a JSON `object`.
- *
- * @param {Object} object
- * @return {Schema}
- */
-
- static fromJSON(object) {
- object = normalizeProperties(object)
- const schema = new Schema(object)
- return schema
- }
-
- /**
- * Alias `fromJS`.
- */
-
- static fromJS = Schema.fromJSON
-
/**
* Get the kind.
*
@@ -88,225 +134,359 @@ class Schema extends Record(DEFAULTS) {
}
/**
- * Return true if one rule can normalize the document
- *
- * @return {Boolean}
- */
-
- get hasValidators() {
- const { rules } = this
- return rules.some(rule => rule.validate)
- }
-
- /**
- * Return true if one rule can decorate text nodes
- *
- * @return {Boolean}
- */
-
- get hasDecorators() {
- const { rules } = this
- return rules.some(rule => rule.decorate)
- }
-
- /**
- * Return the component for an `object`.
- *
- * This method is private, because it should always be called on one of the
- * often-changing immutable objects instead, since it will be memoized for
- * much better performance.
+ * Get the rule for an `object`.
*
* @param {Mixed} object
- * @return {Component|Null}
+ * @return {Object}
*/
- __getComponent(object) {
- const match = find(this.rules, rule => rule.render && rule.match(object))
- if (!match) return null
- return match.render
- }
-
- /**
- * Return the placeholder for an `object`.
- *
- * This method is private, because it should always be called on one of the
- * often-changing immutable objects instead, since it will be memoized for
- * much better performance.
- *
- * @param {Mixed} object
- * @return {Component|Null}
- */
-
- __getPlaceholder(object) {
- const match = find(this.rules, rule => rule.placeholder && rule.match(object))
- if (!match) return null
- return match.placeholder
- }
-
- /**
- * Return the decorations for an `object`.
- *
- * This method is private, because it should always be called on one of the
- * often-changing immutable objects instead, since it will be memoized for
- * much better performance.
- *
- * @param {Mixed} object
- * @return {List}
- */
-
- __getDecorations(object) {
- const array = []
-
- this.rules.forEach((rule) => {
- if (!rule.decorate) return
- if (!rule.match(object)) return
-
- const decorations = rule.decorate(object)
- if (!decorations.length) return
-
- decorations.forEach((dec) => {
- array.push(dec)
- })
- })
-
- const list = Range.createList(array)
- return list
- }
-
- /**
- * Validate an `object` against the schema, returning the failing rule and
- * value if the object is invalid, or void if it's valid.
- *
- * This method is private, because it should always be called on one of the
- * often-changing immutable objects instead, since it will be memoized for
- * much better performance.
- *
- * @param {Mixed} object
- * @return {Object|Void}
- */
-
- __validate(object) {
- let value
-
- const match = find(this.rules, (rule) => {
- if (!rule.validate) return
- if (!rule.match(object)) return
-
- value = rule.validate(object)
- return value
- })
-
- if (!value) return
-
- return {
- rule: match,
- value,
+ getRule(object) {
+ switch (object.kind) {
+ case 'document': return this.document
+ case 'block': return this.blocks[object.type]
+ case 'inline': return this.inlines[object.type]
}
}
+ /**
+ * Get a dictionary of the parent rule validations by child type.
+ *
+ * @return {Object|Null}
+ */
+
+ 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
+ }
+
+ /**
+ * Fail validation by returning a normalizing change function.
+ *
+ * @param {String} reason
+ * @param {Object} context
+ * @return {Function}
+ */
+
+ fail(reason, context) {
+ return (change) => {
+ debug(`normalizing`, { reason, context })
+ const { rule } = context
+ const count = change.operations.length
+ if (rule.normalize) rule.normalize(change, reason, context)
+ if (change.operations.length > count) return
+ this.normalize(change, reason, context)
+ }
+ }
+
+ /**
+ * Normalize an invalid state with `reason` and `context`.
+ *
+ * @param {Change} change
+ * @param {String} reason
+ * @param {Mixed} context
+ */
+
+ normalize(change, reason, context) {
+ switch (reason) {
+ case CHILD_KIND_INVALID:
+ case CHILD_TYPE_INVALID:
+ case CHILD_UNKNOWN: {
+ const { child, node } = context
+ return child.kind == 'text' && node.kind == 'block' && node.nodes.size == 1
+ ? change.removeNodeByKey(node.key)
+ : change.removeNodeByKey(child.key)
+ }
+
+ case CHILD_REQUIRED:
+ case NODE_TEXT_INVALID:
+ case PARENT_KIND_INVALID:
+ case PARENT_TYPE_INVALID: {
+ const { node } = context
+ return node.kind == '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.kind != '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
+ * invalid node, or void if the node is valid.
+ *
+ * @param {Node} node
+ * @return {Function|Void}
+ */
+
+ validateNode(node) {
+ const ret = this.stack.find('validateNode', node)
+ if (ret) return ret
+
+ if (node.kind == 'text') return
+
+ const rule = this.getRule(node) || {}
+ const parents = this.getParentRules()
+ const ctx = { node, rule }
+
+ if (rule.isVoid != null) {
+ if (node.isVoid != rule.isVoid) {
+ return this.fail(NODE_IS_VOID_INVALID, ctx)
+ }
+ }
+
+ 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) {
+ for (const def of rule.marks) {
+ if (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.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
+ }
+
+ if (rule.nodes != null) {
+ nextDef()
+ }
+
+ while (nextChild()) {
+ if (parents != null && child.kind != 'text' && child.type in parents) {
+ const r = parents[child.type]
+
+ if (r.parent.kinds != null && !r.parent.kinds.includes(node.kind)) {
+ return this.fail(PARENT_KIND_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.kinds != null && !def.kinds.includes(child.kind)) {
+ if (offset >= min && nextDef()) continue
+ return this.fail(CHILD_KIND_INVALID, { ...ctx, child, index })
+ }
+
+ if (def.types != null && !def.types.includes(child.type)) {
+ if (offset >= min && nextDef()) 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()
+ }
+ }
+ }
+ }
+
+ /**
+ * Return a JSON representation of the schema.
+ *
+ * @return {Object}
+ */
+
+ toJSON() {
+ const object = {
+ kind: this.kind,
+ document: this.document,
+ blocks: this.blocks,
+ inlines: this.inlines,
+ }
+
+ return object
+ }
+
+ /**
+ * Alias `toJS`.
+ */
+
+ toJS() {
+ return this.toJSON()
+ }
+
}
/**
- * Normalize the `properties` of a schema.
+ * Resolve a set of schema rules from an array of `plugins`.
*
- * @param {Object} properties
+ * @param {Array} plugins
* @return {Object}
*/
-function normalizeProperties(properties) {
- let { rules = [], nodes, marks } = properties
-
- if (nodes) {
- const array = normalizeNodes(nodes)
- rules = rules.concat(array)
+function resolveSchema(plugins = []) {
+ const schema = {
+ document: {},
+ blocks: {},
+ inlines: {},
}
- if (marks) {
- const array = normalizeMarks(marks)
- rules = rules.concat(array)
- }
+ plugins.slice().reverse().forEach((plugin) => {
+ if (!plugin.schema) return
- if (properties.transform) {
- logger.deprecate('0.22.0', 'The `schema.transform` property has been deprecated in favor of `schema.change`.')
- properties.change = properties.transform
- delete properties.transform
- }
+ if (plugin.schema.rules) {
+ throw new Error('Schemas in Slate have changed! They are no longer accept a `rules` property.')
+ }
- return { rules }
+ if (plugin.schema.nodes) {
+ throw new Error('Schemas in Slate have changed! They are no longer accept a `nodes` property.')
+ }
+
+ const { document = {}, blocks = {}, inlines = {}} = plugin.schema
+ const d = resolveDocumentRule(document)
+ const bs = {}
+ const is = {}
+
+ 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
}
/**
- * Normalize a `nodes` shorthand argument.
+ * Resolve a document rule `obj`.
*
- * @param {Object} nodes
- * @return {Array}
+ * @param {Object} obj
+ * @return {Object}
*/
-function normalizeNodes(nodes) {
- const rules = []
-
- for (const key in nodes) {
- let rule = nodes[key]
-
- if (typeOf(rule) == 'function' || isReactComponent(rule)) {
- rule = { render: rule }
- }
-
- rule.match = (object) => {
- return (
- (object.kind == 'block' || object.kind == 'inline') &&
- object.type == key
- )
- }
-
- rules.push(rule)
+function resolveDocumentRule(obj) {
+ return {
+ data: {},
+ nodes: null,
+ ...obj,
}
-
- return rules
}
/**
- * Normalize a `marks` shorthand argument.
+ * Resolve a node rule with `type` from `obj`.
*
- * @param {Object} marks
- * @return {Array}
+ * @param {String} kind
+ * @param {String} type
+ * @param {Object} obj
+ * @return {Object}
*/
-function normalizeMarks(marks) {
- const rules = []
-
- for (const key in marks) {
- let rule = marks[key]
-
- if (!rule.render && !rule.decorator && !rule.validate) {
- rule = { render: rule }
- }
-
- rule.render = normalizeMarkComponent(rule.render)
- rule.match = object => object.kind == 'mark' && object.type == key
- rules.push(rule)
+function resolveNodeRule(kind, type, obj) {
+ return {
+ data: {},
+ isVoid: null,
+ nodes: null,
+ parent: null,
+ text: null,
+ ...obj,
}
-
- return rules
}
/**
- * Normalize a mark `render` property.
+ * A Lodash customizer for merging `kinds` and `types` arrays.
*
- * @param {Component|Function|Object|String} render
- * @return {Component}
+ * @param {Mixed} target
+ * @param {Mixed} source
+ * @return {Array|Void}
*/
-function normalizeMarkComponent(render) {
- if (isReactComponent(render)) return render
-
- switch (typeOf(render)) {
- case 'function':
- return render
- case 'object':
- return props => {props.children}
- case 'string':
- return props => {props.children}
+function customizer(target, source, key) {
+ if (key == 'kinds' || key == 'types') {
+ return target == null ? source : target.concat(source)
}
}
@@ -316,6 +496,16 @@ function normalizeMarkComponent(render) {
Schema.prototype[MODEL_TYPES.SCHEMA] = true
+/**
+ * Memoize read methods.
+ */
+
+memoize(Schema.prototype, [
+ 'getParentRules',
+], {
+ takesArguments: true,
+})
+
/**
* Export.
*
diff --git a/packages/slate/src/models/stack.js b/packages/slate/src/models/stack.js
index c3d225bc7..a52b21e23 100644
--- a/packages/slate/src/models/stack.js
+++ b/packages/slate/src/models/stack.js
@@ -1,17 +1,8 @@
-import Debug from 'debug'
import { Record } from 'immutable'
import MODEL_TYPES from '../constants/model-types'
-import Schema from './schema'
-
-/**
- * Debug.
- *
- * @type {Function}
- */
-
-const debug = Debug('slate:stack')
+import memoize from '../utils/memoize'
/**
* Default properties.
@@ -21,7 +12,6 @@ const debug = Debug('slate:stack')
const DEFAULTS = {
plugins: [],
- schema: new Schema(),
}
/**
@@ -36,15 +26,11 @@ class Stack extends Record(DEFAULTS) {
* Constructor.
*
* @param {Object} attrs
- * @property {Array} plugins
- * @property {Schema|Object} schema
- * @property {Function} ...handlers
*/
static create(attrs = {}) {
- const { plugins } = attrs
- const schema = resolveSchema(plugins)
- const stack = new Stack({ plugins, schema })
+ const { plugins = [] } = attrs
+ const stack = new Stack({ plugins })
return stack
}
@@ -70,72 +56,91 @@ class Stack extends Record(DEFAULTS) {
}
/**
- * Invoke an event `handler` on all of the plugins, until one of them decides
- * to stop propagation.
+ * Get all plugins with `property`.
*
- * @param {String} handler
- * @param {Change} change
- * @param {Editor} editor
- * @param {Mixed} ...args
- */
-
- handle(handler, change, editor, ...args) {
- debug(handler)
-
- for (let k = 0; k < this.plugins.length; k++) {
- const plugin = this.plugins[k]
- if (!plugin[handler]) continue
- const next = plugin[handler](...args, change, editor)
- if (next != null) break
- }
- }
-
- /**
- * Invoke `render` on all of the plugins in reverse, building up a tree of
- * higher-order components.
- *
- * @param {State} state
- * @param {Editor} editor
- * @param {Object} children
- * @param {Object} props
- * @return {Component}
- */
-
- render(state, editor, props) {
- debug('render')
- const plugins = this.plugins.slice().reverse()
- let children
-
- for (let i = 0; i < plugins.length; i++) {
- const plugin = plugins[i]
- if (!plugin.render) continue
- children = plugin.render(props, state, editor)
- props.children = children
- }
-
- return children
- }
-
- /**
- * Invoke `renderPortal` on all of the plugins, building a list of portals.
- *
- * @param {State} state
- * @param {Editor} editor
+ * @param {String} property
* @return {Array}
*/
- renderPortal(state, editor) {
- debug('renderPortal')
- const portals = []
+ getPluginsWith(property) {
+ return this.plugins.filter(plugin => plugin[property] != null)
+ }
- for (let i = 0; i < this.plugins.length; i++) {
- const plugin = this.plugins[i]
- if (!plugin.renderPortal) continue
- const portal = plugin.renderPortal(state, editor)
- if (portal) portals.push(portal)
+ /**
+ * Iterate the plugins with `property`, returning the first non-null value.
+ *
+ * @param {String} property
+ * @param {Any} ...args
+ */
+
+ find(property, ...args) {
+ const plugins = this.getPluginsWith(property)
+
+ for (let i = 0; i < plugins.length; i++) {
+ const plugin = plugins[i]
+ const ret = plugin[property](...args)
+ if (ret != null) return ret
+ }
+ }
+
+ /**
+ * Iterate the plugins with `property`, returning all the non-null values.
+ *
+ * @param {String} property
+ * @param {Any} ...args
+ * @return {Array}
+ */
+
+ map(property, ...args) {
+ const plugins = this.getPluginsWith(property)
+ const array = []
+
+ for (let i = 0; i < plugins.length; i++) {
+ const plugin = plugins[i]
+ const value = plugin[property](...args)
+ if (value != null) array.push(value)
}
- return portals
+ return array
+ }
+
+ /**
+ * Iterate the plugins with `property`, breaking on any a non-null values.
+ *
+ * @param {String} property
+ * @param {Any} ...args
+ */
+
+ run(property, ...args) {
+ const plugins = this.getPluginsWith(property)
+
+ for (let i = 0; i < plugins.length; i++) {
+ const plugin = plugins[i]
+ const ret = plugin[property](...args)
+ if (ret != null) return
+ }
+ }
+
+ /**
+ * Iterate the plugins with `property`, reducing to a set of React children.
+ *
+ * @param {String} property
+ * @param {Object} props
+ * @param {Any} ...args
+ */
+
+ render(property, props, ...args) {
+ const plugins = this.getPluginsWith(property).reverse()
+ let { children = null } = props
+
+ for (let i = 0; i < plugins.length; i++) {
+ const plugin = plugins[i]
+ const value = plugin[property](props, ...args)
+ if (value == null) continue
+ props.children = children = value
+ }
+
+ return children
}
}
@@ -147,25 +152,14 @@ class Stack extends Record(DEFAULTS) {
Stack.prototype[MODEL_TYPES.STACK] = true
/**
- * Resolve a schema from a set of `plugins`.
- *
- * @param {Array} plugins
- * @return {Schema}
+ * Memoize read methods.
*/
-function resolveSchema(plugins) {
- let rules = []
-
- for (let i = 0; i < plugins.length; i++) {
- const plugin = plugins[i]
- if (plugin.schema == null) continue
- const schema = Schema.create(plugin.schema)
- rules = rules.concat(schema.rules)
- }
-
- const schema = Schema.create({ rules })
- return schema
-}
+memoize(Stack.prototype, [
+ 'getPluginsWith',
+], {
+ takesArguments: true,
+})
/**
* Export.
diff --git a/packages/slate/src/models/state.js b/packages/slate/src/models/state.js
index 59b314c37..9ba3b217e 100644
--- a/packages/slate/src/models/state.js
+++ b/packages/slate/src/models/state.js
@@ -4,11 +4,11 @@ import logger from 'slate-dev-logger'
import { Record, Set, List, Map } from 'immutable'
import MODEL_TYPES from '../constants/model-types'
-import SCHEMA from '../schemas/core'
import Data from './data'
import Document from './document'
import History from './history'
import Range from './range'
+import Schema from './schema'
/**
* Default properties.
@@ -17,11 +17,12 @@ import Range from './range'
*/
const DEFAULTS = {
- document: Document.create(),
- selection: Range.create(),
- history: History.create(),
data: new Map(),
decorations: null,
+ document: Document.create(),
+ history: History.create(),
+ schema: Schema.create(),
+ selection: Range.create(),
}
/**
@@ -64,6 +65,7 @@ class State extends Record(DEFAULTS) {
return {
data: attrs.data,
decorations: attrs.decorations,
+ schema: attrs.schema,
}
}
@@ -71,6 +73,7 @@ class State extends Record(DEFAULTS) {
const props = {}
if ('data' in attrs) props.data = Data.create(attrs.data)
if ('decorations' in attrs) props.decorations = Range.createList(attrs.decorations)
+ if ('schema' in attrs) props.schema = Schema.create(attrs.schema)
return props
}
@@ -91,12 +94,14 @@ class State extends Record(DEFAULTS) {
let {
document = {},
selection = {},
+ schema = {},
} = object
let data = new Map()
document = Document.fromJSON(document)
selection = Range.fromJSON(selection)
+ schema = Schema.fromJSON(schema)
// Allow plugins to set a default value for `data`.
if (options.plugins) {
@@ -119,14 +124,11 @@ class State extends Record(DEFAULTS) {
data,
document,
selection,
+ schema,
})
-
if (options.normalize !== false) {
- state = state
- .change({ save: false })
- .normalize(SCHEMA)
- .state
+ state = state.change({ save: false }).normalize().state
}
return state
@@ -640,10 +642,11 @@ class State extends Record(DEFAULTS) {
const object = {
kind: this.kind,
data: this.data.toJSON(),
- document: this.document.toJSON(options),
- selection: this.selection.toJSON(),
decorations: this.decorations ? this.decorations.toArray().map(d => d.toJSON()) : null,
+ document: this.document.toJSON(options),
history: this.history.toJSON(),
+ selection: this.selection.toJSON(),
+ schema: this.schema.toJSON(),
}
if ('preserveStateData' in options) {
@@ -667,6 +670,10 @@ class State extends Record(DEFAULTS) {
delete object.selection
}
+ if (!options.preserveSchema) {
+ delete object.schema
+ }
+
if (options.preserveSelection && !options.preserveKeys) {
const { document, selection } = this
object.selection.anchorPath = selection.isSet ? document.getPath(selection.anchorKey) : null
diff --git a/packages/slate/src/models/text.js b/packages/slate/src/models/text.js
index ece39a8f6..e1259ff0e 100644
--- a/packages/slate/src/models/text.js
+++ b/packages/slate/src/models/text.js
@@ -514,7 +514,7 @@ class Text extends Record(DEFAULTS) {
*/
validate(schema) {
- return schema.__validate(this)
+ return schema.validateNode(this)
}
/**
diff --git a/packages/slate/src/schemas/Readme.md b/packages/slate/src/schemas/Readme.md
deleted file mode 100644
index 9e2443e75..000000000
--- a/packages/slate/src/schemas/Readme.md
+++ /dev/null
@@ -1,2 +0,0 @@
-
-This directory contains the core schema that ships with Slate by default, which controls all of the "core" document and selection validation logic. For example, it ensures that two adjacent text nodes are always mergeed, or that the top-level document only ever contains block nodes. It is not exposed by default, since it is only needed internally.
diff --git a/packages/slate/src/schemas/core.js b/packages/slate/src/schemas/core.js
deleted file mode 100644
index 839865581..000000000
--- a/packages/slate/src/schemas/core.js
+++ /dev/null
@@ -1,311 +0,0 @@
-
-import { List } from 'immutable'
-
-import Schema from '../models/schema'
-import Text from '../models/text'
-
-/**
- * Options object with normalize set to `false`.
- *
- * @type {Object}
- */
-
-const OPTS = { normalize: false }
-
-/**
- * Define the core schema rules, order-sensitive.
- *
- * @type {Array}
- */
-
-const rules = [
-
- /**
- * Only allow block nodes in documents.
- *
- * @type {Object}
- */
-
- {
- match: (node) => {
- return node.kind == 'document'
- },
- validate: (document) => {
- const invalids = document.nodes.filter(n => n.kind != 'block')
- return invalids.size ? invalids : null
- },
- normalize: (change, document, invalids) => {
- invalids.forEach((node) => {
- change.removeNodeByKey(node.key, OPTS)
- })
- }
- },
-
- /**
- * Only allow block nodes or inline and text nodes in blocks.
- *
- * @type {Object}
- */
-
- {
- match: (node) => {
- return node.kind == 'block'
- },
- validate: (block) => {
- const first = block.nodes.first()
- if (!first) return null
-
- const kinds = first.kind == 'block'
- ? ['block']
- : ['inline', 'text']
-
- const invalids = block.nodes.filter(n => !kinds.includes(n.kind))
- return invalids.size ? invalids : null
- },
- normalize: (change, block, invalids) => {
- invalids.forEach((node) => {
- change.removeNodeByKey(node.key, OPTS)
- })
- }
- },
-
- /**
- * Only allow inline and text nodes in inlines.
- *
- * @type {Object}
- */
-
- {
- match: (object) => {
- return object.kind == 'inline'
- },
- validate: (inline) => {
- const invalids = inline.nodes.filter(n => n.kind != 'inline' && n.kind != 'text')
- return invalids.size ? invalids : null
- },
- normalize: (change, inline, invalids) => {
- invalids.forEach((node) => {
- change.removeNodeByKey(node.key, OPTS)
- })
- }
- },
-
- /**
- * Ensure that block and inline nodes have at least one text child.
- *
- * @type {Object}
- */
-
- {
- match: (object) => {
- return object.kind == 'block' || object.kind == 'inline'
- },
- validate: (node) => {
- return node.nodes.size == 0
- },
- normalize: (change, node) => {
- const text = Text.create()
- change.insertNodeByKey(node.key, 0, text, OPTS)
- }
- },
-
- /**
- * Ensure that void nodes contain a text node with a single space of text.
- *
- * @type {Object}
- */
-
- {
- match: (object) => {
- return (
- (object.kind == 'inline' || object.kind == 'block') &&
- (object.isVoid)
- )
- },
- validate: (node) => {
- return node.text !== ' ' || node.nodes.size !== 1
- },
- normalize: (change, node, result) => {
- const text = Text.create(' ')
- const index = node.nodes.size
-
- change.insertNodeByKey(node.key, index, text, OPTS)
-
- node.nodes.forEach((child) => {
- change.removeNodeByKey(child.key, OPTS)
- })
- }
- },
-
- /**
- * Ensure that inline nodes are never empty.
- *
- * This rule is applied to all blocks, because when they contain an empty
- * inline, we need to remove the inline from that parent block. If `validate`
- * was to be memoized, it should be against the parent node, not the inline
- * themselves.
- *
- * @type {Object}
- */
-
- {
- match: (object) => {
- return object.kind == 'block'
- },
- validate: (block) => {
- const invalids = block.nodes.filter(n => n.kind == 'inline' && n.text == '')
- return invalids.size ? invalids : null
- },
- normalize: (change, block, invalids) => {
- // 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 (block.nodes.size == invalids.size) {
- const text = Text.create()
- change.insertNodeByKey(block.key, 1, text, OPTS)
- }
-
- invalids.forEach((node) => {
- change.removeNodeByKey(node.key, OPTS)
- })
- }
- },
-
- /**
- * Ensure that inline void nodes are surrounded by text nodes, by adding extra
- * blank text nodes if necessary.
- *
- * @type {Object}
- */
-
- {
- match: (object) => {
- return object.kind == 'block' || object.kind == 'inline'
- },
- validate: (node) => {
- const invalids = node.nodes.reduce((list, child, index) => {
- if (child.kind !== '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.kind == 'inline')
-
- if (insertAfter || insertBefore) {
- list = list.push({ insertAfter, insertBefore, index })
- }
-
- return list
- }, new List())
-
- return invalids.size ? invalids : null
- },
- normalize: (change, block, invalids) => {
- // Shift for every text node inserted previously.
- let shift = 0
-
- invalids.forEach(({ index, insertAfter, insertBefore }) => {
- if (insertBefore) {
- change.insertNodeByKey(block.key, shift + index, Text.create(), OPTS)
- shift++
- }
-
- if (insertAfter) {
- change.insertNodeByKey(block.key, shift + index + 1, Text.create(), OPTS)
- shift++
- }
- })
- }
- },
-
- /**
- * Merge adjacent text nodes.
- *
- * @type {Object}
- */
-
- {
- match: (object) => {
- return object.kind == 'block' || object.kind == 'inline'
- },
- validate: (node) => {
- const invalids = node.nodes
- .map((child, i) => {
- const next = node.nodes.get(i + 1)
- if (child.kind != 'text') return
- if (!next || next.kind != 'text') return
- return next
- })
- .filter(Boolean)
-
- return invalids.size ? invalids : null
- },
- normalize: (change, node, invalids) => {
- // 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, OPTS)
- })
- }
- },
-
- /**
- * Prevent extra empty text nodes, except when adjacent to inline void nodes.
- *
- * @type {Object}
- */
-
- {
- match: (object) => {
- return object.kind == 'block' || object.kind == 'inline'
- },
- validate: (node) => {
- const { nodes } = node
- if (nodes.size <= 1) return
-
- const invalids = nodes.filter((desc, i) => {
- if (desc.kind != '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.kind == 'inline') return
-
- // It it's the last node, and the previous is an inline, preserve it.
- if (!next && prev.kind == 'inline') return
-
- // If it's surrounded by inlines, preserve it.
- if (next && prev && next.kind == 'inline' && prev.kind == 'inline') return
-
- // Otherwise, remove it.
- return true
- })
-
- return invalids.size ? invalids : null
- },
- normalize: (change, node, invalids) => {
- invalids.forEach((text) => {
- change.removeNodeByKey(text.key, OPTS)
- })
- }
- }
-
-]
-
-/**
- * Create the core schema.
- *
- * @type {Schema}
- */
-
-const SCHEMA = Schema.create({ rules })
-
-/**
- * Export.
- *
- * @type {Schema}
- */
-
-export default SCHEMA
diff --git a/packages/slate/test/helpers/h.js b/packages/slate/test/helpers/h.js
index a126d5ebb..938dd381d 100644
--- a/packages/slate/test/helpers/h.js
+++ b/packages/slate/test/helpers/h.js
@@ -13,6 +13,8 @@ const h = createHyperscript({
paragraph: 'paragraph',
quote: 'quote',
code: 'code',
+ list: 'list',
+ item: 'item',
image: {
type: 'image',
isVoid: true,
diff --git a/packages/slate/test/index.js b/packages/slate/test/index.js
index a70aab60b..27495f1cf 100644
--- a/packages/slate/test/index.js
+++ b/packages/slate/test/index.js
@@ -17,7 +17,7 @@ import { resetKeyGenerator } from '..'
describe('slate', () => {
require('./serializers')
- require('./schemas')
+ require('./schema')
require('./changes')
require('./history')
})
diff --git a/packages/slate/test/schemas/core/block-create-text.js b/packages/slate/test/schema/core/block-create-text.js
similarity index 100%
rename from packages/slate/test/schemas/core/block-create-text.js
rename to packages/slate/test/schema/core/block-create-text.js
diff --git a/packages/slate/test/schemas/core/document-no-inline-children.js b/packages/slate/test/schema/core/document-no-inline-children.js
similarity index 100%
rename from packages/slate/test/schemas/core/document-no-inline-children.js
rename to packages/slate/test/schema/core/document-no-inline-children.js
diff --git a/packages/slate/test/schemas/core/document-no-text-children.js b/packages/slate/test/schema/core/document-no-text-children.js
similarity index 100%
rename from packages/slate/test/schemas/core/document-no-text-children.js
rename to packages/slate/test/schema/core/document-no-text-children.js
diff --git a/packages/slate/test/schemas/core/inline-no-block-children.js b/packages/slate/test/schema/core/inline-no-block-children.js
similarity index 100%
rename from packages/slate/test/schemas/core/inline-no-block-children.js
rename to packages/slate/test/schema/core/inline-no-block-children.js
diff --git a/packages/slate/test/schemas/core/inline-text-around.js b/packages/slate/test/schema/core/inline-text-around.js
similarity index 100%
rename from packages/slate/test/schemas/core/inline-text-around.js
rename to packages/slate/test/schema/core/inline-text-around.js
diff --git a/packages/slate/test/schemas/core/remove-empty-inline.js b/packages/slate/test/schema/core/remove-empty-inline.js
similarity index 100%
rename from packages/slate/test/schemas/core/remove-empty-inline.js
rename to packages/slate/test/schema/core/remove-empty-inline.js
diff --git a/packages/slate/test/schema/custom/child-kind-invalid-custom.js b/packages/slate/test/schema/custom/child-kind-invalid-custom.js
new file mode 100644
index 000000000..75bf810cb
--- /dev/null
+++ b/packages/slate/test/schema/custom/child-kind-invalid-custom.js
@@ -0,0 +1,41 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {},
+ quote: {
+ nodes: [
+ { kinds: ['block'] },
+ ],
+ normalize: (change, reason, { child }) => {
+ if (reason == 'child_kind_invalid') {
+ change.wrapBlockByKey(child.key, 'paragraph')
+ }
+ }
+ }
+ }
+}
+
+export const input = (
+
+
+
+ text
+
+
+
+)
+
+export const output = (
+
+
+
+
+ text
+
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/child-kind-invalid-default.js b/packages/slate/test/schema/custom/child-kind-invalid-default.js
new file mode 100644
index 000000000..dfea71580
--- /dev/null
+++ b/packages/slate/test/schema/custom/child-kind-invalid-default.js
@@ -0,0 +1,32 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {},
+ quote: {
+ nodes: [
+ { kinds: ['text'] },
+ ]
+ }
+ }
+}
+
+export const input = (
+
+
+
+ text
+
+
+
+)
+
+export const output = (
+
+
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/child-required-custom.js b/packages/slate/test/schema/custom/child-required-custom.js
new file mode 100644
index 000000000..8619890e5
--- /dev/null
+++ b/packages/slate/test/schema/custom/child-required-custom.js
@@ -0,0 +1,40 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {},
+ quote: {
+ nodes: [
+ { types: ['paragraph'], min: 2 },
+ ],
+ normalize: (change, reason, { node, index }) => {
+ if (reason == 'child_required') {
+ change.insertNodeByKey(node.key, index, { kind: 'block', type: 'paragraph' })
+ }
+ }
+ }
+ }
+}
+
+export const input = (
+
+
+
+
+
+
+
+)
+
+export const output = (
+
+
+
+
+
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/child-required-default.js b/packages/slate/test/schema/custom/child-required-default.js
new file mode 100644
index 000000000..a075c0696
--- /dev/null
+++ b/packages/slate/test/schema/custom/child-required-default.js
@@ -0,0 +1,28 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {},
+ quote: {
+ nodes: [
+ { types: ['paragraph'], min: 1 },
+ ]
+ }
+ }
+}
+
+export const input = (
+
+
+
+
+
+)
+
+export const output = (
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/child-type-invalid-custom.js b/packages/slate/test/schema/custom/child-type-invalid-custom.js
new file mode 100644
index 000000000..0811a750f
--- /dev/null
+++ b/packages/slate/test/schema/custom/child-type-invalid-custom.js
@@ -0,0 +1,41 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {},
+ quote: {
+ nodes: [
+ { types: ['paragraph'] },
+ ],
+ normalize: (change, reason, { child }) => {
+ if (reason == 'child_type_invalid') {
+ change.wrapBlockByKey(child.key, 'paragraph')
+ }
+ }
+ }
+ }
+}
+
+export const input = (
+
+
+
+
+
+
+
+)
+
+export const output = (
+
+
+
+
+
+
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/child-type-invalid-default.js b/packages/slate/test/schema/custom/child-type-invalid-default.js
new file mode 100644
index 000000000..c76c9bb1a
--- /dev/null
+++ b/packages/slate/test/schema/custom/child-type-invalid-default.js
@@ -0,0 +1,30 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {},
+ quote: {
+ nodes: [
+ { types: ['paragraph'] },
+ ]
+ }
+ }
+}
+
+export const input = (
+
+
+
+
+
+
+
+)
+
+export const output = (
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/child-unknown-custom.js b/packages/slate/test/schema/custom/child-unknown-custom.js
new file mode 100644
index 000000000..92f3dae9b
--- /dev/null
+++ b/packages/slate/test/schema/custom/child-unknown-custom.js
@@ -0,0 +1,49 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {},
+ quote: {
+ nodes: [
+ { types: ['paragraph'], max: 1 },
+ ],
+ normalize: (change, reason, { node, child, index }) => {
+ if (reason == 'child_unknown') {
+ const previous = node.getPreviousSibling(child.key)
+ const offset = previous.nodes.size
+ child.nodes.forEach((n, i) => change.moveNodeByKey(n.key, previous.key, offset + i, { normalize: false }))
+ change.removeNodeByKey(child.key)
+ }
+ }
+ }
+ }
+}
+
+export const input = (
+
+
+
+
+ one
+
+
+ two
+
+
+
+
+)
+
+export const output = (
+
+
+
+
+ onetwo
+
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/child-unknown-default.js b/packages/slate/test/schema/custom/child-unknown-default.js
new file mode 100644
index 000000000..3f9ac15ca
--- /dev/null
+++ b/packages/slate/test/schema/custom/child-unknown-default.js
@@ -0,0 +1,41 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {},
+ quote: {
+ nodes: [
+ { types: ['paragraph'], max: 1 },
+ ]
+ }
+ }
+}
+
+export const input = (
+
+
+
+
+ one
+
+
+ two
+
+
+
+
+)
+
+export const output = (
+
+
+
+
+ one
+
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/node-data-invalid-custom.js b/packages/slate/test/schema/custom/node-data-invalid-custom.js
new file mode 100644
index 000000000..a11e6b4d8
--- /dev/null
+++ b/packages/slate/test/schema/custom/node-data-invalid-custom.js
@@ -0,0 +1,34 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {
+ data: {
+ thing: v => v == 'value'
+ },
+ normalize: (change, reason, { node, key }) => {
+ if (reason == 'node_data_invalid') {
+ change.setNodeByKey(node.key, { data: { thing: 'value' }})
+ }
+ }
+ }
+ }
+}
+
+export const input = (
+
+
+
+
+
+)
+
+export const output = (
+
+
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/node-data-invalid-default-undefined.js b/packages/slate/test/schema/custom/node-data-invalid-default-undefined.js
new file mode 100644
index 000000000..60b9fb0c1
--- /dev/null
+++ b/packages/slate/test/schema/custom/node-data-invalid-default-undefined.js
@@ -0,0 +1,27 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {
+ data: {
+ thing: v => v == 'value'
+ },
+ }
+ }
+}
+
+export const input = (
+
+
+
+
+
+)
+
+export const output = (
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/node-data-invalid-default.js b/packages/slate/test/schema/custom/node-data-invalid-default.js
new file mode 100644
index 000000000..7ffc0bc4d
--- /dev/null
+++ b/packages/slate/test/schema/custom/node-data-invalid-default.js
@@ -0,0 +1,29 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {
+ data: {
+ thing: v => v == null || v == 'value'
+ },
+ }
+ }
+}
+
+export const input = (
+
+
+
+
+
+)
+
+export const output = (
+
+
+
+
+
+)
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
new file mode 100644
index 000000000..d3ce966bc
--- /dev/null
+++ b/packages/slate/test/schema/custom/node-is-void-invalid-custom.js
@@ -0,0 +1,30 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {
+ isVoid: false,
+ normalize: (change, reason, { node }) => {
+ if (reason == 'node_is_void_invalid') {
+ change.removeNodeByKey(node.key, 'paragraph')
+ }
+ }
+ }
+ }
+}
+
+export const input = (
+
+
+
+
+
+)
+
+export const output = (
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/node-is-void-invalid-default.js b/packages/slate/test/schema/custom/node-is-void-invalid-default.js
new file mode 100644
index 000000000..7d637392d
--- /dev/null
+++ b/packages/slate/test/schema/custom/node-is-void-invalid-default.js
@@ -0,0 +1,29 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {
+ isVoid: false,
+ }
+ }
+}
+
+export const input = (
+
+
+
+
+
+)
+
+export const output = (
+
+
+
+ {' '}
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/node-mark-invalid-custom.js b/packages/slate/test/schema/custom/node-mark-invalid-custom.js
new file mode 100644
index 000000000..a9f47c508
--- /dev/null
+++ b/packages/slate/test/schema/custom/node-mark-invalid-custom.js
@@ -0,0 +1,34 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {
+ marks: ['bold'],
+ normalize: (change, reason, { node }) => {
+ if (reason == 'node_mark_invalid') {
+ node.nodes.forEach(n => change.removeNodeByKey(n.key))
+ }
+ }
+ }
+ }
+}
+
+export const input = (
+
+
+
+ one two three
+
+
+
+)
+
+export const output = (
+
+
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/node-mark-invalid-default.js b/packages/slate/test/schema/custom/node-mark-invalid-default.js
new file mode 100644
index 000000000..4d8d36436
--- /dev/null
+++ b/packages/slate/test/schema/custom/node-mark-invalid-default.js
@@ -0,0 +1,31 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {
+ marks: ['bold'],
+ }
+ }
+}
+
+export const input = (
+
+
+
+ one two three
+
+
+
+)
+
+export const output = (
+
+
+
+ one two three
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/node-text-invalid-custom.js b/packages/slate/test/schema/custom/node-text-invalid-custom.js
new file mode 100644
index 000000000..caa949fa6
--- /dev/null
+++ b/packages/slate/test/schema/custom/node-text-invalid-custom.js
@@ -0,0 +1,34 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {
+ text: /^\d*$/,
+ normalize: (change, reason, { node }) => {
+ if (reason == 'node_text_invalid') {
+ node.nodes.forEach(n => change.removeNodeByKey(n.key))
+ }
+ }
+ }
+ }
+}
+
+export const input = (
+
+
+
+ invalid
+
+
+
+)
+
+export const output = (
+
+
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/node-text-invalid-default.js b/packages/slate/test/schema/custom/node-text-invalid-default.js
new file mode 100644
index 000000000..843bddbbc
--- /dev/null
+++ b/packages/slate/test/schema/custom/node-text-invalid-default.js
@@ -0,0 +1,27 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ paragraph: {
+ text: /^\d*$/,
+ }
+ }
+}
+
+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
new file mode 100644
index 000000000..e00172110
--- /dev/null
+++ b/packages/slate/test/schema/custom/parent-kind-invalid-custom.js
@@ -0,0 +1,36 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ inlines: {
+ link: {
+ parent: { kinds: ['block'] },
+ normalize: (change, reason, { node }) => {
+ if (reason == 'parent_kind_invalid') {
+ change.unwrapNodeByKey(node.key)
+ }
+ }
+ }
+ }
+}
+
+export const input = (
+
+
+
+ one
+
+
+
+)
+
+export const output = (
+
+
+
+ one
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/parent-kind-invalid-default.js b/packages/slate/test/schema/custom/parent-kind-invalid-default.js
new file mode 100644
index 000000000..e62820345
--- /dev/null
+++ b/packages/slate/test/schema/custom/parent-kind-invalid-default.js
@@ -0,0 +1,29 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ inlines: {
+ link: {
+ parent: { kinds: ['block'] },
+ }
+ }
+}
+
+export const input = (
+
+
+
+ one
+
+
+
+)
+
+export const output = (
+
+
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/parent-type-invalid-custom.js b/packages/slate/test/schema/custom/parent-type-invalid-custom.js
new file mode 100644
index 000000000..97e8d319c
--- /dev/null
+++ b/packages/slate/test/schema/custom/parent-type-invalid-custom.js
@@ -0,0 +1,39 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ list: {},
+ item: {
+ parent: { types: ['list'] },
+ normalize: (change, reason, { node }) => {
+ if (reason == 'parent_type_invalid') {
+ change.wrapBlockByKey(node.key, 'list')
+ }
+ }
+ }
+ }
+}
+
+export const input = (
+
+
+
+
+
+
+
+)
+
+export const output = (
+
+
+
+
+
+
+
+
+
+)
diff --git a/packages/slate/test/schema/custom/parent-type-invalid-default.js b/packages/slate/test/schema/custom/parent-type-invalid-default.js
new file mode 100644
index 000000000..6dc540a31
--- /dev/null
+++ b/packages/slate/test/schema/custom/parent-type-invalid-default.js
@@ -0,0 +1,30 @@
+/** @jsx h */
+
+import h from '../../helpers/h'
+
+export const schema = {
+ blocks: {
+ list: {},
+ item: {
+ parent: { types: ['list'] },
+ }
+ }
+}
+
+export const input = (
+
+
+
+
+
+
+
+)
+
+export const output = (
+
+
+
+
+
+)
diff --git a/packages/slate/test/schema/index.js b/packages/slate/test/schema/index.js
new file mode 100644
index 000000000..5811513cf
--- /dev/null
+++ b/packages/slate/test/schema/index.js
@@ -0,0 +1,53 @@
+
+import assert from 'assert'
+import fs from 'fs'
+import { Schema } from '../..'
+import { basename, extname, resolve } from 'path'
+
+/**
+ * Tests.
+ */
+
+describe('schema', () => {
+ describe('core', () => {
+ const testsDir = resolve(__dirname, 'core')
+ const tests = fs.readdirSync(testsDir).filter(t => t[0] != '.').map(t => basename(t, extname(t)))
+
+ for (const test of tests) {
+ it(test, async () => {
+ const module = require(resolve(testsDir, test))
+ const { input, output, schema } = module
+ const s = Schema.create(schema)
+ const expected = output
+ const actual = input
+ .change()
+ .setState({ schema: s })
+ .normalize()
+ .state.toJSON()
+
+ assert.deepEqual(actual, expected)
+ })
+ }
+ })
+
+ describe('custom', () => {
+ const testsDir = resolve(__dirname, 'custom')
+ const tests = fs.readdirSync(testsDir).filter(t => t[0] != '.').map(t => basename(t, extname(t)))
+
+ for (const test of tests) {
+ it(test, async () => {
+ const module = require(resolve(testsDir, test))
+ const { input, output, schema } = module
+ const s = Schema.create(schema)
+ const expected = output.toJSON()
+ const actual = input
+ .change()
+ .setState({ schema: s })
+ .normalize()
+ .state.toJSON()
+
+ assert.deepEqual(actual, expected)
+ })
+ }
+ })
+})
diff --git a/packages/slate/test/schemas/index.js b/packages/slate/test/schemas/index.js
deleted file mode 100644
index 05a43b8b6..000000000
--- a/packages/slate/test/schemas/index.js
+++ /dev/null
@@ -1,27 +0,0 @@
-
-import assert from 'assert'
-import fs from 'fs'
-import { Schema } from '../..'
-import { basename, extname, resolve } from 'path'
-
-/**
- * Tests.
- */
-
-describe('schemas', () => {
- describe('core', () => {
- const innerDir = resolve(__dirname, 'core')
- const tests = fs.readdirSync(innerDir).filter(t => t[0] != '.').map(t => basename(t, extname(t)))
-
- for (const test of tests) {
- it(test, async () => {
- const module = require(resolve(innerDir, test))
- const { input, output, schema } = module
- const s = Schema.create(schema)
- const actual = input.change().normalize(s).state.toJSON()
- const expected = output
- assert.deepEqual(actual, expected)
- })
- }
- })
-})