diff --git a/docs/concepts/schemas.md b/docs/concepts/schemas.md deleted file mode 100644 index d46899052..000000000 --- a/docs/concepts/schemas.md +++ /dev/null @@ -1,36 +0,0 @@ - -# Schemas - -Every Slate editor has a "schema" associated with it, which contains information about the structure of its content. It lets you specify how to render each different type of node. And for more advanced use cases it lets you enforce rules about what the content of the editor can and cannot be. - - -## Rules - -Slate schemas are built up of a set of rules. Every rule has a few properties: - -```js -{ - match: Function || Object, - render: Component || Function || Object || String, - decorate: Function, - validate: Function || Object, - change: Function -} -``` - -Each of the properties will add certain functionality to the schema. For example, - - -## Matches - -For any schema rule to be applied, it has to match a node in the editor's content. The most basic way to do this is to match by `kind` and `type`. For example: - -```js - - - -## Components - -The most basic use of a schema is to define which React components should be rendered for each node in the editor. For example, you might want to - -``` diff --git a/docs/guides/changes.md b/docs/guides/changes.md index 7ab1a8598..35991c14a 100644 --- a/docs/guides/changes.md +++ b/docs/guides/changes.md @@ -120,27 +120,24 @@ The `editor.change()` method will create a new [`Change`](../reference/slate/cha ### 3. From Schema Rules -The third place you may perform change operations—for more complex use cases—is from inside a custom [rule](../references/slate/schema.md#rules) in your editor's [`Schema`](../references/slate/schema.md). For example... +The third place you may perform change operations—for more complex use cases—is from inside a custom normalization rule in your editor's [`Schema`](../references/slate/schema.md). For example... ```js { - match(obj) { - return obj.kind == 'block' && obj.type == 'quote', - }, - validate(quote) { - const invalidChildren = quote.nodes.filter(n => n.kind != 'block') - if (!invalidChildren.size) return - return invalidChildren - }, - normalize(change, quote, invalidChildren) { - invalidChildren.forEach((node) => { - change.removeNodeByKey(node.key) - }) - }, + blocks: { + list: { + nodes: [{ types: ['item'] }], + normalize: (change, reason, context) => { + if (reason == 'child_type_invalid') { + change.wrapBlockByKey(context.child.key, 'item') + } + } + } + } } ``` -When a rule's validation fails, Slate passes a [`Change`](../reference/slate/change.md) object to the `normalize()` method on the rule. You can use this object to apply the changes necessary to make your document valid on the next normalization pass. +When a rule's validation fails, Slate passes a [`Change`](../reference/slate/change.md) object to the `normalize` function of the rule, if one exists. You can use this object to apply the changes necessary to make your document valid on the next normalization pass. ### 4. From Outside Slate diff --git a/docs/reference/slate-react/plugins.md b/docs/reference/slate-react/plugins.md index df25a7ec0..457266f92 100644 --- a/docs/reference/slate-react/plugins.md +++ b/docs/reference/slate-react/plugins.md @@ -127,4 +127,4 @@ The `render` property allows you to define higher-order-component-like behavior. ### `schema` `Object` -The `schema` property allows you to define a set of rules that will be added to the editor's schema. The rules from each of the schemas returned by the plugins are collected into a single schema for the editor, and the rules are applied in the same order as the plugin stack. +The `schema` property allows you to define a set of rules that will be added to the editor's schema. The rules from each of the schemas returned by the plugins are collected into a single schema for the editor. diff --git a/docs/reference/slate/schema.md b/docs/reference/slate/schema.md index a8f1a2cd2..6c91d951a 100644 --- a/docs/reference/slate/schema.md +++ b/docs/reference/slate/schema.md @@ -1,189 +1,303 @@ # `Schema` -Every Slate editor has a "schema" associated with it, which contains information about the structure of its content. It lets you specify how to render each different type of node. And for more advanced use cases it lets you enforce rules about what the content of the editor can and cannot be. +Every Slate editor has a "schema" associated with it, which contains information about the structure of its content. For the most basic cases, you'll just rely on Slate's default core schema. But for advanced use cases you can enforce rules about what the content of a Slate document can contain. ## Properties ```js { - nodes: Object, - marks: Object, - rules: Array + document: Object, + blocks: Object, + inlines: Object, } ``` -The top-level properties of a schema all give you a way to define `rules` that the schema enforces. The `nodes` and `marks` properties are just convenient ways to define the most common set of rules. +The top-level properties of a schema all give you a way to define validation "rules" that the schema enforces. -### `marks` -`Object type: Component || Function || Object || String` +### `document` +`Object` ```js { - bold: props => {props.children} -} -``` -```js -{ - bold: { - fontWeight: 'bold' - } -} -``` -```js -{ - bold: 'my-bold-class-name' -} -``` - -An object that defines the [`Marks`](./mark.md) in the schema by `type`. Each key in the object refers to a mark by its `type`. The value defines how Slate will render the mark, and can either be a React component, an object of styles, or a class name. - -### `nodes` -`Object`
-`Object` - -```js -{ - quote: props =>
{props.children}
-} -``` -```js -{ - code: { - render: props =>
{props.children}
, - decorate: myCodeHighlighter + document: { + nodes: [{ types: ['paragraph'] }] } } ``` -An object that defines the [`Block`](./block.md) and [`Inline`](./inline.md) nodes in the schema by `type`. Each key in the object refers to a node by its `type`. The values defines how Slate will render the node, and can optionally define any other property of a schema `Rule`. +A set of validation rules that apply to the top-level document. -### `rules` -`Array` +### `blocks` +`Object` ```js -[ - { - match: { kind: 'block', type: 'code' }, - render: props =>
{props.children}
, - decorate: myCodeHighlighter +{ + blocks: { + list: { + nodes: [{ types: ['item'] }] + }, + item: { + parent: { types: ['list'] } + }, } -] +} ``` -An array of rules that define the schema's behavior. Each of the rules are evaluated in order to determine a match. +A dictionary of blocks by type, each with its own set of validation rules. -Internally, the `marks` and `nodes` properties of a schema are simply converted into `rules`. +### `inlines` +`Object` + +```js +{ + inlines: { + emoji: { + isVoid: true, + nodes: [{ kinds: ['text'] }] + }, + } +} +``` + +A dictionary of inlines by type, each with its own set of validation rules. ## Rule Properties ```js { - match: Function, - decorate: Function, + data: Object, + isVoid: Boolean, + nodes: Array, normalize: Function, - placeholder: Component || Function, - render: Component || Function || Object || String, - validate: Function + parent: Object, + text: RegExp, } ``` -Slate schemas are built up of a set of rules. Each of the properties will add certain functionality to the schema, based on the properties it defines. +Slate schemas are built up of a set of validation rules. Each of the properties will validate certain pieces of the document based on the properties it defines. -### `match` -`Function match(object: Node || Mark)` +### `data` +`Object` ```js { - match: (object) => object.kind == 'block' && object.type == 'code' -} -``` - -The `match` property is the only required property of a rule. It determines which objects the rule applies to. - -### `decorate` -`Function decorate(node: Node) => List|Array` - -```js -{ - decorate: (node) => { - const text = node.getFirstText() - - return [{ - anchorKey: text.key, - anchorOffset: 0, - focusKey: text.key, - focusOffset: 1, - marks: [{ type: 'bold' }] - }] + data: { + href: v => isUrl(v), } } ``` -The `decorate` property allows you define a function that will apply extra marks to ranges of text inside a node. It is called with a [`Node`](./node.md). It should return a list of [`Range`](./range.md) objects with the desired marks, which will then be added to the text before rendering. +A dictionary of + +### `isVoid` +`Boolean` + +```js +{ + isVoid: true, +} +``` + +Will validate a node's `isVoid` property. + +### `nodes` +`Array` + +```js +{ + nodes: [ + { types: ['image', 'video'], min: 1, max: 3 }, + { types: ['paragraph'], min: 0 }, + ] +} +``` + +Will validate a node's children. The node definitions can declare the `kinds`, `types`, `min` and `max` properties. ### `normalize` -`Function normalize(change: Change, object: Node, failure: Any) => Change` +`normalize(change: Change, reason: String, context: Object) => Void` ```js { - normalize: (change, node, invalidChildren) => { - invalidChildren.forEach((child) => { - change.removeNodeByKey(child.key) - }) - - return change + normalize: (change, reason, context) => { + case 'child_kind_invalid': + change.wrapBlockByKey(context.child.key, 'paragraph') + return + case 'child_type_invalid': + change.setNodeByKey(context.child.key, 'paragraph') + return } } ``` -The `normalize` property is a function to run that recovers the editor's state after the `validate` property of a rule has determined that an object is invalid. It is passed a [`Change`](./change.md) that it can use to make modifications. It is also passed the return value of the `validate` function, which makes it easy to quickly determine the failure reason from the validation. +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. -### `placeholder` -`Component`
-`Function` +For more information on the arguments passed to `normalize`, see the [Invalid Reasons](#invalid-reasons) reference. + +### `parent` +`Array` ```js { - placeholder: (props) => {props.editor.props.placeholder} + parent: { types: ['list'] } } ``` -The `placeholder` property determines which React component Slate will use to render a placeholder for the editor. +Will validate a node's parent. The parent definition can declare the `kinds` and/or `types` properties. -### `render` -`Component`
-`Function`
-`Object`
-`String` +### `text` +`Array` ```js { - render: (props) =>
{props.children}
+ text: /^\w+$/ } ``` -The `render` property determines which React component Slate will use to render a [`Node`](./node.md) or [`Mark`](./mark.md). Mark renderers can also be defined as an object of styles or a class name string for convenience. +Will validate a node's text. -### `validate` -`Function validate(object: Node) => Any || Void` - -```js -{ - validate: (node) => { - const invalidChildren = node.nodes.filter(child => child.kind == 'block') - return invalidChildren.size ? invalidChildren : null - } -} -``` - -The `validate` property allows you to define a constraint that the matching object must abide by. It should return either `Void` if the object is valid, or any non-void value if it is invalid. This makes it easy to return the exact reason that the object is invalid, which makes it simple to recover from the invalid state with the `normalize` property. ## Static Methods +### `Schema.create` +`Schema.create(properties: Object) => Schema` + +Create a new `Schema` instance with `properties`. + +### `Schema.fromJSON` +`Schema.fromJSON(object: Object) => Schema` + +Create a schema from a JSON `object`. + ### `Schema.isSchema` `Schema.isSchema(maybeSchema: Any) => Boolean` Returns a boolean if the passed in argument is a `Schema`. + + +## Instance Methods + +### `toJSON` +`toJSON() => Object` + +Returns a JSON representation of the schema. + + +## Invalid Reasons + +When supplying your own `normalize` property for a schema rule, it will be called with `(change, reason, context)`. The `reason` will be one of a set of reasons, and `context` will vary depending on the reason. Here's the full set: + +### `child_kind_invalid` + +```js +{ + child: Node, + index: Number, + node: Node, + rule: Object, +} +``` + +### `child_required` + +```js +{ + index: Number, + node: Node, + rule: Object, +} +``` + +### `child_type_invalid` + +```js +{ + child: Node, + index: Number, + node: Node, + rule: Object, +} +``` + +### `child_unknown` + +```js +{ + child: Node, + index: Number, + node: Node, + rule: Object, +} +``` + +### `node_data_invalid` + +```js +{ + key: String, + node: Node, + rule: Object, + value: Mixed, +} +``` + +### `node_is_void_invalid` + +```js +{ + node: Node, + rule: Object, +} +``` + +### `node_kind_invalid` + +```js +{ + node: Node, + rule: Object, +} +``` + +### `node_mark_invalid` + +```js +{ + mark: Mark, + node: Node, + rule: Object, +} +``` + +### `node_text_invalid` + +```js +{ + text: String, + node: Node, + rule: Object, +} +``` + +### `parent_kind_invalid` + +```js +{ + node: Node, + parent: Node, + rule: Object, +} +``` + +### `parent_type_invalid` + +```js +{ + node: Node, + parent: Node, + rule: Object, +} +``` diff --git a/docs/walkthroughs/applying-custom-formatting.md b/docs/walkthroughs/applying-custom-formatting.md index 7f5e7668d..a47a38c61 100644 --- a/docs/walkthroughs/applying-custom-formatting.md +++ b/docs/walkthroughs/applying-custom-formatting.md @@ -16,11 +16,6 @@ class App extends React.Component { state = { state: initialState, - schema: { - nodes: { - code: CodeNode - } - } } onChange = ({ state }) => { @@ -40,12 +35,18 @@ class App extends React.Component { return ( ) } + + renderNode = (props) => { + switch (props.node.type) { + case 'code': return + } + } } ``` @@ -57,11 +58,6 @@ class App extends React.Component { state = { state: initialState, - schema: { - nodes: { - code: CodeNode - } - } } onChange = ({ state }) => { @@ -92,13 +88,19 @@ class App extends React.Component { render() { return ( ) } + + renderNode = (props) => { + switch (props.node.type) { + case 'code': return + } + } } ``` @@ -116,9 +118,7 @@ function BoldMark(props) { Pretty simple, right? -And now, let's tell Slate about that mark. -To do that, we'll add it to the `schema` object under a `marks` property. -Also, let's allow our mark to be toggled by changing `addMark` to `toggleMark`. +And now, let's tell Slate about that mark. To do that, we'll pass in the `renderMark` prop to our editor. Also, let's allow our mark to be toggled by changing `addMark` to `toggleMark`. ```js function BoldMark(props) { @@ -129,15 +129,6 @@ class App extends React.Component { state = { state: initialState, - schema: { - nodes: { - code: CodeNode - }, - // Add our "bold" mark to the schema... - marks: { - bold: BoldMark - } - } } onChange = ({ state }) => { @@ -165,13 +156,28 @@ class App extends React.Component { render() { return ( ) } + + renderNode = (props) => { + switch (props.node.type) { + case 'code': return + } + } + + // Add a `renderMark` method to render marks. + renderMark = (props) => { + switch (props.mark.type) { + case 'bold': return + } + } } ``` diff --git a/docs/walkthroughs/defining-custom-block-nodes.md b/docs/walkthroughs/defining-custom-block-nodes.md index 4ac01092d..521428dd3 100644 --- a/docs/walkthroughs/defining-custom-block-nodes.md +++ b/docs/walkthroughs/defining-custom-block-nodes.md @@ -72,12 +72,6 @@ class App extends React.Component { state = { state: initialState, - // Add a "schema" to our app's state that we can pass to the Editor. - schema: { - nodes: { - code: CodeNode - } - } } onChange = ({ state }) => { @@ -93,16 +87,23 @@ class App extends React.Component { render() { return ( - // Pass in the `schema` property... + // Pass in the `renderNode` prop... ) } + // Add a `renderNode` method to render a `CodeNode` for code blocks. + renderNode = (props) => { + switch (props.node.type) { + case 'code': return + } + } + } ``` @@ -117,11 +118,6 @@ class App extends React.Component { state = { state: initialState, - schema: { - nodes: { - code: CodeNode - } - } } onChange = ({ state }) => { @@ -143,14 +139,20 @@ class App extends React.Component { render() { return ( ) } + renderNode = (props) => { + switch (props.node.type) { + case 'code': return + } + } + } ``` @@ -167,11 +169,6 @@ class App extends React.Component { state = { state: initialState, - schema: { - nodes: { - code: CodeNode - } - } } onChange = ({ state }) => { @@ -194,13 +191,19 @@ class App extends React.Component { render() { return ( ) } + + renderNode = (props) => { + switch (props.node.type) { + case 'code': return + } + } } ``` diff --git a/docs/walkthroughs/saving-and-loading-html-content.md b/docs/walkthroughs/saving-and-loading-html-content.md index f64b665e7..d93f5e69f 100644 --- a/docs/walkthroughs/saving-and-loading-html-content.md +++ b/docs/walkthroughs/saving-and-loading-html-content.md @@ -219,19 +219,6 @@ class App extends React.Component { state = { state: html.deserialize(initialState), - // Add a schema with our nodes and marks... - schema: { - nodes: { - code: props =>
{props.children}
, - paragraph: props =>

{props.children}

, - quote: props =>
{props.children}
, - }, - marks: { - bold: props => {props.children}, - italic: props => {props.children}, - underline: props => {props.children}, - } - } } onChange = ({ state }) => { @@ -247,12 +234,31 @@ class App extends React.Component { render() { return ( ) } + + renderNode = (props) => { + switch (props.node.type) { + case 'code': return
{props.children}
+ case 'code': return

{props.children}

+ case 'quote': return
{props.children}
+ } + } + + // Add a `renderMark` method to render marks. + renderMark = (props) => { + switch (props.mark.type) { + case 'bold': return {props.children} + case 'italic': return {props.children} + case 'underline': return {props.children} + } + } } ``` diff --git a/docs/walkthroughs/using-plugins.md b/docs/walkthroughs/using-plugins.md index 3aae1825e..8dc5d0abb 100644 --- a/docs/walkthroughs/using-plugins.md +++ b/docs/walkthroughs/using-plugins.md @@ -18,11 +18,6 @@ class App extends React.Component { state = { state: initialState, - schema: { - marks: { - bold: props => {props.children} - } - } } onChange = ({ state }) => { @@ -39,14 +34,20 @@ class App extends React.Component { render() { return ( ) } + renderMark = (props) => { + switch (props.mark.type) { + case 'bold': return {props.children} + } + } + } ``` @@ -106,11 +107,6 @@ class App extends React.Component { state = { state: initialState, - schema: { - marks: { - bold: props => {props.children} - } - } } onChange = ({ state }) => { @@ -122,12 +118,18 @@ class App extends React.Component { // Add the `plugins` property to the editor, and remove `onKeyDown`. ) } + + renderMark = (props) => { + switch (props.mark.type) { + case 'bold': return {props.children} + } + } } ``` @@ -150,16 +152,6 @@ class App extends React.Component { state = { state: initialState, - schema: { - marks: { - bold: props => {props.children}, - // Add our new mark renderers... - code: props => {props.children}, - italic: props => {props.children}, - strikethrough: props => {props.children}, - underline: props => {props.children}, - } - } } onChange = ({ state }) => { @@ -170,12 +162,23 @@ class App extends React.Component { return ( ) } + + renderMark = (props) => { + switch (props.mark.type) { + case 'bold': return {props.children} + // Add our new mark renderers... + case 'code': return {props.children} + case 'italic': return {props.children} + case 'strikethrough': return {props.children} + case 'underline': return {props.children} + } + } } ``` diff --git a/examples/check-lists/index.js b/examples/check-lists/index.js index 78e04cf6f..fa8e16721 100644 --- a/examples/check-lists/index.js +++ b/examples/check-lists/index.js @@ -57,18 +57,6 @@ class CheckListItem extends React.Component { } -/** - * Define a schema. - * - * @type {Object} - */ - -const schema = { - nodes: { - 'check-list-item': CheckListItem, - }, -} - /** * The rich text example. * @@ -146,16 +134,29 @@ class CheckLists extends React.Component { ) } + /** + * Render a Slate node. + * + * @param {Object} props + * @return {Element} + */ + + renderNode = (props) => { + switch (props.node.type) { + case 'check-list-item': return + } + } + } /** diff --git a/examples/code-highlighting/index.js b/examples/code-highlighting/index.js index acf170e45..d4f6a5c5b 100644 --- a/examples/code-highlighting/index.js +++ b/examples/code-highlighting/index.js @@ -46,94 +46,6 @@ function CodeBlockLine(props) { ) } -/** - * Define a Prism.js decorator for code blocks. - * - * @param {Block} block - * @return {Array} - */ - -function codeBlockDecorator(block) { - const language = block.data.get('language') - const texts = block.getTexts().toArray() - const string = texts.map(t => t.text).join('\n') - const grammar = Prism.languages[language] - const tokens = Prism.tokenize(string, grammar) - const decorations = [] - let startText = texts.shift() - let endText = startText - let startOffset = 0 - let endOffset = 0 - let start = 0 - - for (const token of tokens) { - startText = endText - startOffset = endOffset - - const content = typeof token == 'string' ? token : token.content - const newlines = content.split('\n').length - 1 - const length = content.length - newlines - const end = start + length - - let available = startText.text.length - startOffset - let remaining = length - - endOffset = startOffset + remaining - - while (available < remaining) { - endText = texts.shift() - remaining = length - available - available = endText.text.length - endOffset = remaining - } - - if (typeof token != 'string') { - const range = { - anchorKey: startText.key, - anchorOffset: startOffset, - focusKey: endText.key, - focusOffset: endOffset, - marks: [{ type: `highlight-${token.type}` }], - } - - decorations.push(range) - } - - start = end - } - - return decorations -} - -/** - * Define a schema. - * - * @type {Object} - */ - -const schema = { - nodes: { - code: { - render: CodeBlock, - decorate: codeBlockDecorator, - }, - code_line: { - render: CodeBlockLine, - }, - }, - marks: { - 'highlight-comment': { - opacity: '0.33' - }, - 'highlight-keyword': { - fontWeight: 'bold' - }, - 'highlight-punctuation': { - opacity: '0.75' - } - } -} - /** * The code highlighting example. * @@ -186,20 +98,113 @@ class CodeHighlighting extends React.Component { * @return {Component} */ - render() { + render = () => { return (
) } + /** + * Render a Slate node. + * + * @param {Object} props + * @return {Element} + */ + + renderNode = (props) => { + switch (props.node.type) { + case 'code': return + case 'code_line': return + } + } + + /** + * Render a Slate mark. + * + * @param {Object} props + * @return {Element} + */ + + renderMark = (props) => { + const { children, mark } = props + switch (mark.type) { + case 'comment': return {children} + case 'keyword': return {children} + case 'punctuation': return {children} + } + } + + /** + * Decorate code blocks with Prism.js highlighting. + * + * @param {Node} node + * @return {Array} + */ + + decorateNode = (node) => { + if (node.type != 'code') return + + const language = node.data.get('language') + const texts = node.getTexts().toArray() + const string = texts.map(t => t.text).join('\n') + const grammar = Prism.languages[language] + const tokens = Prism.tokenize(string, grammar) + const decorations = [] + let startText = texts.shift() + let endText = startText + let startOffset = 0 + let endOffset = 0 + let start = 0 + + for (const token of tokens) { + startText = endText + startOffset = endOffset + + const content = typeof token == 'string' ? token : token.content + const newlines = content.split('\n').length - 1 + const length = content.length - newlines + const end = start + length + + let available = startText.text.length - startOffset + let remaining = length + + endOffset = startOffset + remaining + + while (available < remaining) { + endText = texts.shift() + remaining = length - available + available = endText.text.length + endOffset = remaining + } + + if (typeof token != 'string') { + const range = { + anchorKey: startText.key, + anchorOffset: startOffset, + focusKey: endText.key, + focusOffset: endOffset, + marks: [{ type: token.type }], + } + + decorations.push(range) + } + + start = end + } + + return decorations + } + } /** diff --git a/examples/embeds/index.js b/examples/embeds/index.js index aaaa4f682..823dea2c8 100644 --- a/examples/embeds/index.js +++ b/examples/embeds/index.js @@ -6,18 +6,6 @@ import React from 'react' import Video from './video' import initialState from './state.json' -/** - * Define a schema. - * - * @type {Object} - */ - -const schema = { - nodes: { - video: Video - } -} - /** * The images example. * @@ -57,14 +45,27 @@ class Embeds extends React.Component {
) } + /** + * Render a Slate node. + * + * @param {Object} props + * @return {Element} + */ + + renderNode = (props) => { + switch (props.node.type) { + case 'video': return