1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-09-01 19:22:35 +02:00

simplify schema rules, update docs

This commit is contained in:
Ian Storm Taylor
2016-08-14 15:51:07 -07:00
parent 06af3de5e5
commit 5091587814
8 changed files with 243 additions and 488 deletions

View File

@@ -104,17 +104,17 @@ Slate encourages you to write small, reusable modules. Check out the public ones
<br/> <br/>
### Documentation ### Documentation
If you're using Slate for the first time, check out the [Getting Started](./docs/guides/installing-slate.md) guides and the [Core Concepts](./docs/concepts) to familiarize yourself with Slate's architecture and mental models. Once you've gotten familiar with those, you'll probably want to check out the full [API Reference](./docs/reference). If you're using Slate for the first time, check out the [Getting Started](./docs/walkthroughs/installing-slate.md) walkthroughs and the [Core Concepts](./docs/concepts) to familiarize yourself with Slate's architecture and mental models. Once you've gotten familiar with those, you'll probably want to check out the full [API Reference](./docs/reference).
- [**Guides**](./docs/guides) - [**Walkthroughts**](./docs/walkthroughs)
- [Installing Slate](./docs/guides/installing-slate.md) - [Installing Slate](./docs/walkthroughs/installing-slate.md)
- [Using the Bundled Source](./docs/guides/using-the-bundled-source.md) - [Using the Bundled Source](./docs/walkthroughs/using-the-bundled-source.md)
- [Adding Event Handlers](./docs/guides/adding-event-handlers.md) - [Adding Event Handlers](./docs/walkthroughs/adding-event-handlers.md)
- [Defining Custom Block Nodes](./docs/guides/defining-custom-block-nodes.md) - [Defining Custom Block Nodes](./docs/walkthroughs/defining-custom-block-nodes.md)
- [Applying Custom Formatting](./docs/guides/applying-custom-formatting.md) - [Applying Custom Formatting](./docs/walkthroughs/applying-custom-formatting.md)
- [Using Plugins](./docs/guides/using-plugins.md) - [Using Plugins](./docs/walkthroughs/using-plugins.md)
- [Saving to a Database](./docs/guides/saving-to-a-database.md) - [Saving to a Database](./docs/walkthroughs/saving-to-a-database.md)
- [Saving and Loading HTML Content](./docs/guides/saving-and-loading-html-content.md) - [Saving and Loading HTML Content](./docs/walkthroughs/saving-and-loading-html-content.md)
- [**Concepts**](./docs/concepts) - [**Concepts**](./docs/concepts)
- [Statelessness & Immutability](./docs/concepts/statelessness-and-immutability.md) - [Statelessness & Immutability](./docs/concepts/statelessness-and-immutability.md)
- [The Document Model](./docs/concepts/the-document-model.md) - [The Document Model](./docs/concepts/the-document-model.md)

View File

@@ -16,8 +16,8 @@ Slate schemas are built up of a set of rules. Every rule has a few properties:
```js ```js
{ {
match: Function || Object, match: Function || Object,
component: Component || Function || Object || String, render: Component || Function || Object || String,
decorator: Function, decorate: Function,
validate: Function || Object, validate: Function || Object,
transform: Function transform: Function
} }

View File

@@ -1,185 +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.
- [Properties](#properties)
- [`marks`](#marks)
- [`nodes`](#nodes)
- [`rules`](#rules)
- [Rule Properties](#rule-properties)
- [`component`](#component)
- [`decorator`](#decorator)
- [`match`](#match)
- [`transform`](#transform)
- [`validate`](#validate)
## Properties
```js
{
marks: Object,
nodes: Object,
rules: Array
}
```
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.
### `marks`
`Object type: Component || Function || Object || String`
```js
{
bold: props => <strong>{props.children}</strong>
}
```
```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<type, Component || Function>`
`Object<type, Rule>`
```js
{
quote: props => <blockquote {...props.attributes}>{props.children}</blockquote>
}
```
```js
{
code: {
component: props => <pre {...props.attributes}><code>{props.children}</code></pre>,
decorator: myCodeHighlighter
}
}
```
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 optionall define any other property of a schema `Rule`.
### `rules`
`Array<Rule>`
```js
[
{
match: { kind: 'block', type: 'code' },
component: props => <pre {...props.attributes}><code>{props.children}</code></pre>,
decorator: myCodeHighlighter
}
]
```
An array of rules that define the schema's behavior. Each of the rules are evaluated in order to determine a match.
Internally, the `marks` and `nodes` properties of a schema are simply converted into `rules`.
## Rule Properties
```js
{
match: Function || Object,
component: Component || Function || Object || String,
decorator: Function,
validate: Function || Object,
transform: Function
}
```
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.
### `match`
`Function`
`Object`
```js
(node) => node.kind == 'block' && node.type == 'quote'
```
```js
{
kind: 'block',
type: 'quote'
}
```
The `match` property is the only required property of a rule. It determines which nodes are matched when a rule is matched. In the simplest form it is a function which returns a boolean, but it can also be expressed in object form.
### `component`
`ReactComponent` <br/>
`Function component(props: Object) => Any` <br/>
`Object<cssProperty, cssValue>` <br/>
`String`
```js
(props) => <blockquote {...props.attributes}>{props.children}</blockquote>
```
The `component` property determines how Slate will render the node or mark that was matched by the `match` property. In addition to a React component, marks can also be rendered by supplying an object of styles, or a class name string.
### `decorator`
`Function decorate(text: Text, match: Node) => List<Character>`
The `decorator` property defines a function that can add extra marks to the text inside of a node, for example for code highlighting. It is called with the [`Text`](./text.md) node in question, and the [`Node`](./node.md) matched by the `match` property, and should return a list of [`Characters`](./character.md) with the desired marks applied.
### `validate`
`Function (match: Node) => Any` <br/>
`Object`
```js
{
nodesAnyOf: [
{ kind: 'block' }
],
nodesNoneOf: [
{ type: 'quote' }
]
}
```
The `validate` property defines a series of constraints that the [`Node`](./node.md) must abide by, for example that its children only be other [`Block`](./block.md) nodes. The full list of validations supported is:
- `kind` `String` — the
- `kinds`
- `maxLength`
- `maxNodes` `Number` — the maximum number of child nodes a node can have.
- `minLength`
- `minNodes` `Number`
- `nodesAnyOf` `Array` — an array of `match` objects that the nodes can match.
- `nodesExactlyOf` `Array` — an array of `match` objects that the nodes must all match exactly in order.
- `nodesNoneOf` `Array` — an array of `match` objects that the nodes must not match.
- `text`
- `type`
- `types`
## 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:
## 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
## Match Properties
## Validate Properties

View File

@@ -8,9 +8,9 @@ Every Slate editor has a "schema" associated with it, which contains information
- [`nodes`](#nodes) - [`nodes`](#nodes)
- [`rules`](#rules) - [`rules`](#rules)
- [Rule Properties](#rule-properties) - [Rule Properties](#rule-properties)
- [`component`](#component) - [`decorate`](#decorate)
- [`decorator`](#decorator)
- [`match`](#match) - [`match`](#match)
- [`render`](#render)
- [`transform`](#transform) - [`transform`](#transform)
- [`validate`](#validate) - [`validate`](#validate)
@@ -19,8 +19,8 @@ Every Slate editor has a "schema" associated with it, which contains information
```js ```js
{ {
marks: Object,
nodes: Object, nodes: Object,
marks: Object,
rules: Array rules: Array
} }
``` ```
@@ -62,8 +62,8 @@ An object that defines the [`Marks`](./mark.md) in the schema by `type`. Each ke
```js ```js
{ {
code: { code: {
component: props => <pre {...props.attributes}><code>{props.children}</code></pre>, render: props => <pre {...props.attributes}><code>{props.children}</code></pre>,
decorator: myCodeHighlighter decorate: myCodeHighlighter
} }
} }
``` ```
@@ -78,7 +78,7 @@ An object that defines the [`Block`](./block.md) and [`Inline`](./inline.md) nod
{ {
match: { kind: 'block', type: 'code' }, match: { kind: 'block', type: 'code' },
component: props => <pre {...props.attributes}><code>{props.children}</code></pre>, component: props => <pre {...props.attributes}><code>{props.children}</code></pre>,
decorator: myCodeHighlighter decorate: myCodeHighlighter
} }
] ]
``` ```
@@ -92,43 +92,88 @@ Internally, the `marks` and `nodes` properties of a schema are simply converted
```js ```js
{ {
match: Function || Object, match: Function,
component: Component || Function || Object || String, decorate: Function,
decorator: Function, render: Component || Function || Object || String,
validate: Function || Object, transform: Function,
transform: Function validate: Function
} }
``` ```
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 rules. Each of the properties will add certain functionality to the schema, based on the properties it defines.
### `match` ### `match`
`Object || Function` `Function match(object: Node || Mark)`
```js ```js
{ {
kind: 'block', match: (object) => object.kind == 'block' && object.type == 'code'
type: 'quote'
} }
``` ```
The `match` property is the only required property of a rule. It determines which nodes are matched when a rule is matched. The `match` property is the only required property of a rule. It determines which objects the rule applies to.
### `decorate`
`Function decorate(text: Node, object: Node) => List<Characters>`
## Matches ```js
{
decorate: (text, node) => {
let { characters } = text
let first = characters.get(0)
let { marks } = first
let mark = Mark.create({ type: 'bold' })
marks = marks.add(mark)
first = first.merge({ marks })
characters = characters.set(0, first)
return characters
}
}
```
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: The `decorate` property allows you define a function that will apply extra marks to all of the ranges of text inside a node. It is called with a [`Text`](./text.md) node and the matched node. It should return a list of characters with the desired marks, which will then be added to the text before rendering.
### `render`
`Component` <br/>
`Function` <br/>
`Object` <br/>
`String`
```js
{
render: (props) => <pre {...props.attributes}><code>{props.children}</code></pre>
}
```
## Components 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.
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 ### `transform`
`Function transform(transform: Transform, object: Node, failure: Any) => Transform`
```js
{
transform: (transform, node, invalidChildren) => {
invalidChildren.forEach((child) => {
transform = transform.removeNodeByKey(child.key)
})
return transform
}
}
```
The `transform` property is run to recover the editor's state after the `validate` property of a rule has determined that an object is invalid. It is passed a [`Transform`](./transform.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 reason validation failed.
### `validate`
`Function validate(object: Node) => Any || Void`
## Match Properties ```js
{
validate: (node) => {
const invalidChildren = node.nodes.filter(child => child.kind == 'block')
return invalidChildren.size ? invalidChildren : null
}
}
```
## Validate Properties 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 `transform` property.

View File

@@ -138,7 +138,7 @@ class Schema extends new Record(DEFAULTS) {
} }
/** /**
* Return the component for an `object`. * Return the renderer for an `object`.
* *
* This method is private, because it should always be called on one of the * 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 * often-changing immutable objects instead, since it will be memoized for
@@ -149,9 +149,9 @@ class Schema extends new Record(DEFAULTS) {
*/ */
__getComponent(object) { __getComponent(object) {
const match = this.rules.find(rule => rule.match(object) && rule.component) const match = this.rules.find(rule => rule.match(object) && rule.render)
if (!match) return if (!match) return
return match.component return match.render
} }
/** /**
@@ -167,17 +167,17 @@ class Schema extends new Record(DEFAULTS) {
__getDecorators(object) { __getDecorators(object) {
return this.rules return this.rules
.filter(rule => rule.match(object) && rule.decorator) .filter(rule => rule.match(object) && rule.decorate)
.map((rule) => { .map((rule) => {
return (text) => { return (text) => {
return rule.decorator(text, object) return rule.decorate(text, object)
} }
}) })
} }
/** /**
* Validate an `object` against the schema, returning the failing rule and * Validate an `object` against the schema, returning the failing rule and
* reason if the object is invalid, or void if it's valid. * 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 * 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 * often-changing immutable objects instead, since it will be memoized for
@@ -188,20 +188,21 @@ class Schema extends new Record(DEFAULTS) {
*/ */
__validate(object) { __validate(object) {
let reason let value
const match = this.rules.find((rule) => { const match = this.rules.find((rule) => {
if (!rule.match(object)) return if (!rule.match(object)) return
if (!rule.validate) return if (!rule.validate) return
reason = rule.validate(object)
return reason value = rule.validate(object)
return value
}) })
if (!reason) return if (!value) return
return { return {
rule: match, rule: match,
reason value,
} }
} }
@@ -227,9 +228,7 @@ function normalizeProperties(properties) {
rules = rules.concat(array) rules = rules.concat(array)
} }
return { return { rules }
rules: rules.map(normalizeRule)
}
} }
/** /**
@@ -243,23 +242,20 @@ function normalizeNodes(nodes) {
const rules = [] const rules = []
for (const key in nodes) { for (const key in nodes) {
const value = nodes[key] let rule = nodes[key]
const match = {
kinds: ['block', 'inline'], if (typeOf(rule) == 'function' || isReactComponent(rule)) {
type: key, rule = { render: rule }
} }
if (value.component || value.decorator || value.validate) { rule.match = (object) => {
rules.push({ return (
match, (object.kind == 'block' || object.kind == 'inline') &&
...value, object.type == key
}) )
} else {
rules.push({
match,
component: value
})
} }
rules.push(rule)
} }
return rules return rules
@@ -276,146 +272,37 @@ function normalizeMarks(marks) {
const rules = [] const rules = []
for (const key in marks) { for (const key in marks) {
const value = marks[key] let rule = marks[key]
const match = {
kind: 'mark', if (!rule.render && !rule.decorator && !rule.validate) {
type: key, rule = { render: rule }
} }
if (value.component || value.decorator || value.validate) { rule.render = normalizeMarkComponent(rule.render)
rules.push({ rule.match = object => object.kind == 'mark' && object.type == key
match, rules.push(rule)
...value,
component: normalizeMarkComponent(value.component),
})
} else {
rules.push({
match,
component: normalizeMarkComponent(value)
})
}
} }
return rules return rules
} }
/** /**
* Normalize a mark `component`. * Normalize a mark `render` property.
* *
* @param {Component || Function || Object || String} component * @param {Component || Function || Object || String} render
* @return {Component} * @return {Component}
*/ */
function normalizeMarkComponent(component) { function normalizeMarkComponent(render) {
if (isReactComponent(component)) return component if (isReactComponent(render)) return render
switch (typeOf(component)) { switch (typeOf(render)) {
case 'function': case 'function':
return component return render
case 'object': case 'object':
return props => <span style={component}>{props.children}</span> return props => <span style={render}>{props.children}</span>
case 'string': case 'string':
return props => <span className={component}>{props.children}</span> return props => <span className={render}>{props.children}</span>
}
}
/**
* Normalize a `rule` object.
*
* @param {Object} rule
* @return {Object}
*/
function normalizeRule(rule) {
return {
...rule,
match: normalizeMatch(rule.match),
validate: normalizeValidate(rule.validate),
transform: normalizeTransform(rule.transform),
}
}
/**
* Normalize a `match` spec.
*
* @param {Function || Object || String} match
* @return {Function}
*/
function normalizeMatch(match) {
switch (typeOf(match)) {
case 'function': return match
case 'boolean': return () => match
case 'object': return normalizeSpec(match)
default: {
throw new Error(`Invalid \`match\` spec: "${match}".`)
}
}
}
/**
* Normalize a `validate` spec.
*
* @param {Function || Object || String} validate
* @return {Function}
*/
function normalizeValidate(validate) {
switch (typeOf(validate)) {
case 'function': return validate
case 'boolean': return () => validate
case 'object': return normalizeSpec(validate, true)
}
}
/**
* Normalize a `transform` spec.
*
* @param {Function || Object || String} transform
* @return {Function}
*/
function normalizeTransform(transform) {
switch (typeOf(transform)) {
case 'function': return transform
}
}
/**
* Normalize a `spec` object.
*
* @param {Object} obj
* @param {Boolean} giveReason
* @return {Boolean}
*/
function normalizeSpec(obj, giveReason) {
const spec = { ...obj }
if (spec.exactlyOf) spec.exactlyOf = spec.exactlyOf.map(s => normalizeSpec(s))
if (spec.anyOf) spec.anyOf = spec.anyOf.map(s => normalizeSpec(s))
if (spec.noneOf) spec.noneOf = spec.noneOf.map(s => normalizeSpec(s))
return (node) => {
for (const key in CHECKS) {
const value = spec[key]
if (value == null) continue
const fail = CHECKS[key]
const failure = fail(node, value)
if (failure === undefined) continue
if (giveReason) {
return {
type: key,
value: failure
}
} else {
return false
}
}
return giveReason ? undefined : true
} }
} }

View File

@@ -373,14 +373,14 @@ class State extends new Record(DEFAULTS) {
document.filterDescendantsDeep((node) => { document.filterDescendantsDeep((node) => {
if (failure = node.validate(schema)) { if (failure = node.validate(schema)) {
const { reason, rule } = failure const { value, rule } = failure
transform = rule.transform(transform, node, reason) transform = rule.transform(transform, node, value)
} }
}) })
if (failure = document.validate(schema)) { if (failure = document.validate(schema)) {
const { reason, rule } = failure const { value, rule } = failure
transform = rule.transform(transform, document, reason) transform = rule.transform(transform, document, value)
} }
return transform.steps.size return transform.steps.size

View File

@@ -32,108 +32,6 @@ function Plugin(options = {}) {
placeholderStyle, placeholderStyle,
} = options } = options
/**
* The default block renderer.
*
* @param {Object} props
* @return {Element}
*/
function DEFAULT_BLOCK(props) {
return (
<div {...props.attributes} style={{ position: 'relative' }}>
{props.children}
{placeholder
? <Placeholder
className={placeholderClassName}
node={props.node}
parent={props.state.document}
state={props.state}
style={placeholderStyle}
>
{placeholder}
</Placeholder>
: null}
</div>
)
}
/**
* The default inline renderer.
*
* @param {Object} props
* @return {Element}
*/
function DEFAULT_INLINE(props) {
return (
<span {...props.attributes} style={{ position: 'relative' }}>
{props.children}
</span>
)
}
/**
* The default schema.
*
* @type {Object}
*/
const DEFAULT_SCHEMA = {
rules: [
{
match: {
kind: 'document'
},
validate: {
anyOf: [
{ kind: 'block' }
]
},
transform: (transform, match, reason) => {
return reason.value.reduce((tr, node) => {
return tr.removeNodeByKey(node.key)
}, transform)
}
},
{
match: {
kind: 'block'
},
validate: {
anyOf: [
{ kind: 'block' },
{ kind: 'inline' },
{ kind: 'text' },
]
},
transform: (transform, match, reason) => {
return reason.value.reduce((tr, node) => {
return tr.removeNodeByKey(node.key)
}, transform)
},
component: DEFAULT_BLOCK
},
{
match: {
kind: 'inline'
},
validate: {
anyOf: [
{ kind: 'inline' },
{ kind: 'text' },
]
},
transform: (transform, match, reason) => {
return reason.value.reduce((tr, node) => {
return tr.removeNodeByKey(node.key)
}, transform)
},
component: DEFAULT_INLINE
},
]
}
/** /**
* On before change, enforce the editor's schema. * On before change, enforce the editor's schema.
* *
@@ -672,17 +570,128 @@ function Plugin(options = {}) {
} }
/** /**
* The core `node` renderer, which uses plain `<div>` or `<span>` depending on * A default schema rule to render block nodes.
* what kind of node it is.
* *
* @param {Node} node * @type {Object}
* @return {Component} component
*/ */
function renderNode(node) { const BLOCK_RENDER_RULE = {
return node.kind == 'block' match: (node) => {
? DEFAULT_BLOCK return node.kind == 'block'
: DEFAULT_INLINE },
render: (props) => {
return (
<div {...props.attributes} style={{ position: 'relative' }}>
{props.children}
{placeholder
? <Placeholder
className={placeholderClassName}
node={props.node}
parent={props.state.document}
state={props.state}
style={placeholderStyle}
>
{placeholder}
</Placeholder>
: null}
</div>
)
}
}
/**
* A default schema rule to render inline nodes.
*
* @type {Object}
*/
const INLINE_RENDER_RULE = {
match: (node) => {
return node.kind == 'inline'
},
render: (props) => {
return (
<span {...props.attributes} style={{ position: 'relative' }}>
{props.children}
</span>
)
}
}
/**
* A default schema rule to only allow block nodes in documents.
*
* @type {Object}
*/
const DOCUMENT_CHILDREN_RULE = {
match: (node) => {
return node.kind == 'document'
},
validate: (document) => {
const { nodes } = document
const invalids = nodes.filter(n => n.kind != 'block')
return invalids.size ? invalids : null
},
transform: (transform, document, invalids) => {
return invalids.reduce((t, n) => t.removeNodeByKey(n.key), transform)
}
}
/**
* A default schema rule to only allow block, inline and text nodes in blocks.
*
* @type {Object}
*/
const BLOCK_CHILDREN_RULE = {
match: (node) => {
return node.kind == 'block'
},
validate: (block) => {
const { nodes } = block
const invalids = nodes.filter(n => n.kind != 'block' && n.kind != 'inline' && n.kind != 'text')
return invalids.size ? invalids : null
},
transform: (transform, block, invalids) => {
return invalids.reduce((t, n) => t.removeNodeByKey(n.key), transform)
}
}
/**
* A default schema rule to only allow inline and text nodes in inlines.
*
* @type {Object}
*/
const INLINE_CHILDREN_RULE = {
match: (object) => {
return object.kind == 'inline'
},
validate: (inline) => {
const { nodes } = inline
const invalids = nodes.filter(n => n.kind != 'inline' && n.kind != 'text')
return invalids.size ? invalids : null
},
transform: (transform, inline, invalids) => {
return invalids.reduce((t, n) => t.removeNodeByKey(n.key), transform)
}
}
/**
* The default schema.
*
* @type {Object}
*/
const schema = {
rules: [
BLOCK_RENDER_RULE,
INLINE_RENDER_RULE,
DOCUMENT_CHILDREN_RULE,
BLOCK_CHILDREN_RULE,
INLINE_CHILDREN_RULE,
]
} }
/** /**
@@ -699,8 +708,7 @@ function Plugin(options = {}) {
onKeyDown, onKeyDown,
onPaste, onPaste,
onSelect, onSelect,
renderNode, schema,
schema: DEFAULT_SCHEMA
} }
} }

View File

@@ -5,7 +5,7 @@ const BOLD = {
fontWeight: 'bold' fontWeight: 'bold'
} }
function decorator(text) { function decorate(text, block) {
let { characters } = text let { characters } = text
let second = characters.get(1) let second = characters.get(1)
let mark = Mark.create({ type: 'bold' }) let mark = Mark.create({ type: 'bold' })
@@ -18,7 +18,7 @@ function decorator(text) {
export const schema = { export const schema = {
nodes: { nodes: {
default: { default: {
decorator decorate
} }
}, },
marks: { marks: {