mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-07-31 12:30:11 +02:00
change schema plugin to be returned from function (#3252)
* change schema plugin to be returned from function * fix forced-layout example
This commit is contained in:
150
packages/slate-schema/src/define-schema.ts
Normal file
150
packages/slate-schema/src/define-schema.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { Editor, Text, NodeEntry } from 'slate'
|
||||||
|
|
||||||
|
import { NodeRule, SchemaRule } from './rules'
|
||||||
|
import { NodeError } from './errors'
|
||||||
|
import { checkNode, checkAncestor } from './checkers'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The schema plugin augments an editor to ensure that its content is normalized
|
||||||
|
* to always obey a schema after operations are applied.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const defineSchema = (
|
||||||
|
rules: SchemaRule[] = []
|
||||||
|
): ((editor: Editor) => Editor) => {
|
||||||
|
const nodeRules: NodeRule[] = rules
|
||||||
|
const parentRules = rules.filter(rule => {
|
||||||
|
return (
|
||||||
|
'parent' in rule.validate ||
|
||||||
|
'next' in rule.validate ||
|
||||||
|
'previous' in rule.validate
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (editor: Editor): Editor => {
|
||||||
|
const { normalizeNode } = editor
|
||||||
|
|
||||||
|
editor.normalizeNode = (entry: NodeEntry) => {
|
||||||
|
const [n, p] = entry
|
||||||
|
let error: NodeError | undefined
|
||||||
|
let rule: NodeRule | undefined
|
||||||
|
|
||||||
|
for (const r of nodeRules) {
|
||||||
|
error = checkNode(editor, [n, p], r, nodeRules)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
rule = r
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Text.isText(n)) {
|
||||||
|
const failure = checkAncestor(editor, [n, p], r, parentRules)
|
||||||
|
|
||||||
|
if (failure) {
|
||||||
|
rule = failure[0]
|
||||||
|
error = failure[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error == null) {
|
||||||
|
return normalizeNode(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevLength = editor.operations.length
|
||||||
|
|
||||||
|
// First run the user-provided `normalize` function if one exists...
|
||||||
|
if (rule != null && rule.normalize) {
|
||||||
|
rule.normalize(editor, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the `normalize` function did add any operations to the editor,
|
||||||
|
// we assume that it fully handled the normalization and exit.
|
||||||
|
if (editor.operations.length > prevLength) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (error.code) {
|
||||||
|
case 'first_child_invalid':
|
||||||
|
case 'last_child_invalid': {
|
||||||
|
const { path } = error
|
||||||
|
const [parent, parentPath] = Editor.parent(editor, path)
|
||||||
|
|
||||||
|
if (parent.children.length > 1) {
|
||||||
|
Editor.removeNodes(editor, { at: path })
|
||||||
|
} else if (parentPath.length === 0) {
|
||||||
|
const range = Editor.range(editor, parentPath)
|
||||||
|
Editor.removeNodes(editor, {
|
||||||
|
at: range,
|
||||||
|
match: ([, p]) => p.length === 1,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Editor.removeNodes(editor, { at: parentPath })
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'child_max_invalid': {
|
||||||
|
const { path } = error
|
||||||
|
const [parent, parentPath] = Editor.parent(editor, path)
|
||||||
|
|
||||||
|
if (parent.children.length === 1 && parentPath.length !== 0) {
|
||||||
|
Editor.removeNodes(editor, { at: parentPath })
|
||||||
|
} else {
|
||||||
|
Editor.removeNodes(editor, { at: path })
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'child_min_invalid': {
|
||||||
|
const { path } = error
|
||||||
|
const [, parentPath] = Editor.parent(editor, path)
|
||||||
|
|
||||||
|
if (parentPath.length === 0) {
|
||||||
|
const range = Editor.range(editor, parentPath)
|
||||||
|
Editor.removeNodes(editor, {
|
||||||
|
at: range,
|
||||||
|
match: ([, p]) => p.length === 1,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Editor.removeNodes(editor, { at: parentPath })
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'child_invalid':
|
||||||
|
case 'next_sibling_invalid':
|
||||||
|
case 'node_leaf_invalid':
|
||||||
|
case 'node_property_invalid':
|
||||||
|
case 'node_text_invalid':
|
||||||
|
case 'previous_sibling_invalid': {
|
||||||
|
const { path } = error
|
||||||
|
Editor.removeNodes(editor, { at: path })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'parent_invalid': {
|
||||||
|
const { path, index } = error
|
||||||
|
const childPath = path.concat(index)
|
||||||
|
Editor.removeNodes(editor, { at: childPath })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
const _: never = error
|
||||||
|
throw new Error(
|
||||||
|
`Cannot normalize unknown validation error: "${JSON.stringify(
|
||||||
|
error
|
||||||
|
)}"`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return editor
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +1,3 @@
|
|||||||
export * from './errors'
|
export * from './errors'
|
||||||
export * from './with-schema'
|
export * from './define-schema'
|
||||||
export * from './rules'
|
export * from './rules'
|
||||||
|
@@ -1,152 +0,0 @@
|
|||||||
import { Editor, Text, NodeEntry } from 'slate'
|
|
||||||
|
|
||||||
import { NodeRule, SchemaRule } from './rules'
|
|
||||||
import { NodeError } from './errors'
|
|
||||||
import { checkNode, checkAncestor } from './checkers'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `withSchema` plugin augments an editor to ensure that its content is
|
|
||||||
* normalized to always obey a schema after operations are applied.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const withSchema = (
|
|
||||||
editor: Editor,
|
|
||||||
rules: SchemaRule[] = []
|
|
||||||
): Editor => {
|
|
||||||
const { normalizeNode } = editor
|
|
||||||
const nodeRules: NodeRule[] = rules
|
|
||||||
const parentRules: NodeRule[] = []
|
|
||||||
|
|
||||||
for (const rule of rules) {
|
|
||||||
if (
|
|
||||||
'parent' in rule.validate ||
|
|
||||||
'next' in rule.validate ||
|
|
||||||
'previous' in rule.validate
|
|
||||||
) {
|
|
||||||
parentRules.push(rule)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.normalizeNode = (entry: NodeEntry) => {
|
|
||||||
const [n, p] = entry
|
|
||||||
let error: NodeError | undefined
|
|
||||||
let rule: NodeRule | undefined
|
|
||||||
|
|
||||||
for (const r of nodeRules) {
|
|
||||||
error = checkNode(editor, [n, p], r, nodeRules)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
rule = r
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Text.isText(n)) {
|
|
||||||
const failure = checkAncestor(editor, [n, p], r, parentRules)
|
|
||||||
|
|
||||||
if (failure) {
|
|
||||||
rule = failure[0]
|
|
||||||
error = failure[1]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error == null) {
|
|
||||||
return normalizeNode(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevLength = editor.operations.length
|
|
||||||
|
|
||||||
// First run the user-provided `normalize` function if one exists...
|
|
||||||
if (rule != null && rule.normalize) {
|
|
||||||
rule.normalize(editor, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the `normalize` function did add any operations to the editor,
|
|
||||||
// we assume that it fully handled the normalization and exit.
|
|
||||||
if (editor.operations.length > prevLength) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (error.code) {
|
|
||||||
case 'first_child_invalid':
|
|
||||||
case 'last_child_invalid': {
|
|
||||||
const { path } = error
|
|
||||||
const [parent, parentPath] = Editor.parent(editor, path)
|
|
||||||
|
|
||||||
if (parent.children.length > 1) {
|
|
||||||
Editor.removeNodes(editor, { at: path })
|
|
||||||
} else if (parentPath.length === 0) {
|
|
||||||
const range = Editor.range(editor, parentPath)
|
|
||||||
Editor.removeNodes(editor, {
|
|
||||||
at: range,
|
|
||||||
match: ([, p]) => p.length === 1,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Editor.removeNodes(editor, { at: parentPath })
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'child_max_invalid': {
|
|
||||||
const { path } = error
|
|
||||||
const [parent, parentPath] = Editor.parent(editor, path)
|
|
||||||
|
|
||||||
if (parent.children.length === 1 && parentPath.length !== 0) {
|
|
||||||
Editor.removeNodes(editor, { at: parentPath })
|
|
||||||
} else {
|
|
||||||
Editor.removeNodes(editor, { at: path })
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'child_min_invalid': {
|
|
||||||
const { path } = error
|
|
||||||
const [, parentPath] = Editor.parent(editor, path)
|
|
||||||
|
|
||||||
if (parentPath.length === 0) {
|
|
||||||
const range = Editor.range(editor, parentPath)
|
|
||||||
Editor.removeNodes(editor, {
|
|
||||||
at: range,
|
|
||||||
match: ([, p]) => p.length === 1,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Editor.removeNodes(editor, { at: parentPath })
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'child_invalid':
|
|
||||||
case 'next_sibling_invalid':
|
|
||||||
case 'node_leaf_invalid':
|
|
||||||
case 'node_property_invalid':
|
|
||||||
case 'node_text_invalid':
|
|
||||||
case 'previous_sibling_invalid': {
|
|
||||||
const { path } = error
|
|
||||||
Editor.removeNodes(editor, { at: path })
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'parent_invalid': {
|
|
||||||
const { path, index } = error
|
|
||||||
const childPath = path.concat(index)
|
|
||||||
Editor.removeNodes(editor, { at: childPath })
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
const _: never = error
|
|
||||||
throw new Error(
|
|
||||||
`Cannot normalize unknown validation error: "${JSON.stringify(
|
|
||||||
error
|
|
||||||
)}"`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return editor
|
|
||||||
}
|
|
@@ -1,12 +1,13 @@
|
|||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
import { fixtures } from '../../../support/fixtures'
|
import { fixtures } from '../../../support/fixtures'
|
||||||
import { Editor } from 'slate'
|
import { Editor } from 'slate'
|
||||||
import { withSchema } from '..'
|
import { defineSchema } from '..'
|
||||||
|
|
||||||
describe('slate-schema', () => {
|
describe('slate-schema', () => {
|
||||||
fixtures(__dirname, 'validations', ({ module }) => {
|
fixtures(__dirname, 'validations', ({ module }) => {
|
||||||
const { input, schema, output } = module
|
const { input, schema, output } = module
|
||||||
const editor = withSchema(input, schema)
|
const withSchema = defineSchema(schema)
|
||||||
|
const editor = withSchema(input)
|
||||||
Editor.normalize(editor, { force: true })
|
Editor.normalize(editor, { force: true })
|
||||||
assert.deepEqual(editor.children, output.children)
|
assert.deepEqual(editor.children, output.children)
|
||||||
})
|
})
|
||||||
|
@@ -2,9 +2,9 @@ import React, { useState, useCallback, useMemo } from 'react'
|
|||||||
import { Slate, Editable, withReact } from 'slate-react'
|
import { Slate, Editable, withReact } from 'slate-react'
|
||||||
import { Editor, createEditor } from 'slate'
|
import { Editor, createEditor } from 'slate'
|
||||||
import { withHistory } from 'slate-history'
|
import { withHistory } from 'slate-history'
|
||||||
import { withSchema } from 'slate-schema'
|
import { defineSchema } from 'slate-schema'
|
||||||
|
|
||||||
const schema = [
|
const withSchema = defineSchema([
|
||||||
{
|
{
|
||||||
for: 'node',
|
for: 'node',
|
||||||
match: 'editor',
|
match: 'editor',
|
||||||
@@ -36,14 +36,14 @@ const schema = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
])
|
||||||
|
|
||||||
const ForcedLayoutExample = () => {
|
const ForcedLayoutExample = () => {
|
||||||
const [value, setValue] = useState(initialValue)
|
const [value, setValue] = useState(initialValue)
|
||||||
const [selection, setSelection] = useState(null)
|
const [selection, setSelection] = useState(null)
|
||||||
const renderElement = useCallback(props => <Element {...props} />, [])
|
const renderElement = useCallback(props => <Element {...props} />, [])
|
||||||
const editor = useMemo(
|
const editor = useMemo(
|
||||||
() => withSchema(withHistory(withReact(createEditor())), schema),
|
() => withSchema(withHistory(withReact(createEditor()))),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
|
Reference in New Issue
Block a user