1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-04-15 10:52:34 +02:00

refactor schema transform logic

This commit is contained in:
Ian Storm Taylor 2016-11-17 17:46:35 -08:00
parent b9d0f1d8d2
commit 286e3620dd
11 changed files with 208 additions and 145 deletions

View File

@ -5,6 +5,7 @@ This directory contains the core logic of Slate. It's separated further into a s
- [**Constants**](./constants) — containing constants that are used in Slate's codebase.
- [**Models**](./models) — containing the models that define Slate's internal data structure.
- [**Plugins**](./plugins) — containing the plugins that ship with Slate by default.
- [**Schemas**](./schemas) - containing the schemas that ship with Slate by default.
- [**Serializers**](./serializers) — containing the serializers that ship with Slate by default.
- [**Transforms**](./transforms) — containing the transforms that are used to alter a Slate document.
- [**Utils**](./utils) — containing a few private convenience modules.

View File

@ -85,6 +85,24 @@ const Node = {
return descendant
},
/**
* Assert that a node's tree has a node by `key` and return it.
*
* @param {String} key
* @return {Node}
*/
assertNode(key) {
const node = this.getNode(key)
if (!node) {
key = Normalize.key(key)
throw new Error(`Could not find a node with key "${key}".`)
}
return node
},
/**
* Assert that a node exists at `path` and return it.
*
@ -137,10 +155,11 @@ const Node = {
*/
findDescendant(iterator) {
const found = this.nodes.find(iterator)
if (found) return found
const childFound = this.nodes.find(iterator)
if (childFound) return childFound
let descendantFound = null
this.nodes.find(node => {
if (node.kind != 'text') {
descendantFound = node.findDescendant(iterator)
@ -765,6 +784,18 @@ const Node = {
.get(1)
},
/**
* Get a node in the tree by `key`.
*
* @param {String} key
* @return {Node|Null}
*/
getNode(key) {
key = Normalize.key(key)
return this.key == key ? this : this.getDescendant(key)
},
/**
* Get the offset for a descendant text node by `key`.
*

View File

@ -1,6 +1,7 @@
import Document from './document'
import SCHEMA from '../schemas/core'
import Selection from './selection'
import Transform from './transform'
import { Record, Set, Stack, List } from 'immutable'
@ -57,7 +58,7 @@ class State extends new Record(DEFAULTS) {
const state = new State({ document, selection })
return state.transform({ normalized: false })
.normalize()
.normalize(SCHEMA)
.apply({ save: false })
}

View File

@ -53,7 +53,7 @@ function Plugin(options = {}) {
if (prevState && state.document == prevState.document) return state
const newState = state.transform()
.normalizeWith(schema)
.normalize(schema)
.apply({ save: false })
return newState
@ -748,7 +748,7 @@ function Plugin(options = {}) {
}
/**
* Extend the core schema with rendering rules.
* Add default rendering rules to the schema.
*
* @type {Object}
*/

2
src/schemas/Readme.md Normal file
View File

@ -0,0 +1,2 @@
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 joined, or that the top-level document only ever contains block nodes. It is not exposed by default, since it is only needed internally.

View File

@ -290,12 +290,12 @@ function isInlineVoid(node) {
}
/**
* The default schema.
* The core schema.
*
* @type {Schema}
*/
const schema = Schema.create({
const SCHEMA = Schema.create({
rules: [
DOCUMENT_CHILDREN_RULE,
BLOCK_CHILDREN_RULE,
@ -315,4 +315,4 @@ const schema = Schema.create({
* @type {Schema}
*/
export default schema
export default SCHEMA

View File

@ -1,7 +1,8 @@
/* eslint no-console: 0 */
import { List } from 'immutable'
import Normalize from '../utils/normalize'
import SCHEMA from '../schemas/core'
import { List } from 'immutable'
/**
* Add a new `mark` to the characters at `range`.
@ -109,10 +110,10 @@ export function deleteAtRange(transform, range, options = {}) {
}
if (normalize) {
transform.normalizeNodeByKey(ancestor.key)
transform.normalizeNodeByKey(ancestor.key, SCHEMA)
}
transform.normalizeDocument()
transform.normalizeDocument(SCHEMA)
return transform
}
@ -298,7 +299,7 @@ export function insertBlockAtRange(transform, range, block, options = {}) {
}
if (normalize) {
transform.normalizeNodeByKey(parent.key)
transform.normalizeNodeByKey(parent.key, SCHEMA)
}
return transform
@ -393,7 +394,7 @@ export function insertFragmentAtRange(transform, range, fragment, options = {})
}
if (normalize) {
transform.normalizeNodeByKey(parent.key)
transform.normalizeNodeByKey(parent.key, SCHEMA)
}
return transform
@ -434,7 +435,7 @@ export function insertInlineAtRange(transform, range, inline, options = {}) {
transform.insertNodeByKey(parent.key, index + 1, inline, { normalize: false })
if (normalize) {
transform.normalizeNodeByKey(parent.key)
transform.normalizeNodeByKey(parent.key, SCHEMA)
}
return transform
@ -467,7 +468,7 @@ export function insertTextAtRange(transform, range, text, marks, options = {}) {
transform.deleteAtRange(range, { normalize: false })
}
// Unless specified, don't normalize if only inserting text
// PERF: Unless specified, don't normalize if only inserting text.
if (normalize !== undefined) {
normalize = range.isExpanded
}
@ -756,7 +757,7 @@ export function unwrapBlockAtRange(transform, range, properties, options = {}) {
// TODO: optmize to only normalize the right block
if (normalize) {
transform.normalizeDocument()
transform.normalizeDocument(SCHEMA)
}
return transform
@ -805,7 +806,7 @@ export function unwrapInlineAtRange(transform, range, properties, options = {})
// TODO: optmize to only normalize the right block
if (normalize) {
transform.normalizeDocument()
transform.normalizeDocument(SCHEMA)
}
return transform
@ -877,7 +878,7 @@ export function wrapBlockAtRange(transform, range, block, options = {}) {
})
if (normalize) {
transform.normalizeNodeByKey(parent.key)
transform.normalizeNodeByKey(parent.key, SCHEMA)
}
return transform
@ -955,7 +956,7 @@ export function wrapInlineAtRange(transform, range, inline, options = {}) {
})
if (normalize) {
transform.normalizeNodeByKey(startBlock.key)
transform.normalizeNodeByKey(startBlock.key, SCHEMA)
}
}
@ -986,8 +987,8 @@ export function wrapInlineAtRange(transform, range, inline, options = {}) {
if (normalize) {
transform
.normalizeNodeByKey(startBlock.key)
.normalizeNodeByKey(endBlock.key)
.normalizeNodeByKey(startBlock.key, SCHEMA)
.normalizeNodeByKey(endBlock.key, SCHEMA)
}
blocks.slice(1, -1).forEach((block) => {
@ -999,7 +1000,7 @@ export function wrapInlineAtRange(transform, range, inline, options = {}) {
})
if (normalize) {
transform.normalizeNodeByKey(block.key)
transform.normalizeNodeByKey(block.key, SCHEMA)
}
})
}

View File

@ -1,4 +1,6 @@
import Normalize from '../utils/normalize'
import SCHEMA from '../schemas/core'
/**
* Add mark to text at `offset` and `length` in node by `key`.
@ -21,9 +23,10 @@ export function addMarkByKey(transform, key, offset, length, mark, options = {})
const path = document.getPath(key)
transform.addMarkOperation(path, offset, length, mark)
if (normalize) {
const parent = document.getParent(key)
transform.normalizeNodeByKey(parent.key)
transform.normalizeNodeByKey(parent.key, SCHEMA)
}
return transform
@ -48,8 +51,9 @@ export function insertNodeByKey(transform, key, index, node, options = {}) {
const path = document.getPath(key)
transform.insertNodeOperation(path, index, node)
if (normalize) {
transform.normalizeNodeByKey(key)
transform.normalizeNodeByKey(key, SCHEMA)
}
return transform
@ -75,9 +79,10 @@ export function insertTextByKey(transform, key, offset, text, marks, options = {
const path = document.getPath(key)
transform.insertTextOperation(path, offset, text, marks)
if (normalize) {
const parent = document.getParent(key)
transform.normalizeNodeByKey(parent.key)
transform.normalizeNodeByKey(parent.key, SCHEMA)
}
return transform
@ -106,9 +111,9 @@ export function joinNodeByKey(transform, key, withKey, options = {}) {
if (normalize) {
const parent = document.getCommonAncestor(key, withKey)
if (parent) {
transform.normalizeNodeByKey(parent.key)
transform.normalizeNodeByKey(parent.key, SCHEMA)
} else {
transform.normalizeDocument()
transform.normalizeDocument(SCHEMA)
}
}
@ -139,7 +144,7 @@ export function moveNodeByKey(transform, key, newKey, newIndex, options = {}) {
if (normalize) {
const parent = document.key == newKey ? document : document.getCommonAncestor(key, newKey)
transform.normalizeNodeByKey(parent.key)
transform.normalizeNodeByKey(parent.key, SCHEMA)
}
return transform
@ -166,9 +171,10 @@ export function removeMarkByKey(transform, key, offset, length, mark, options =
const path = document.getPath(key)
transform.removeMarkOperation(path, offset, length, mark)
if (normalize) {
const parent = document.getParent(key)
transform.normalizeNodeByKey(parent.key)
transform.normalizeNodeByKey(parent.key, SCHEMA)
}
return transform
@ -195,9 +201,9 @@ export function removeNodeByKey(transform, key, options = {}) {
if (normalize) {
const parent = document.getParent(key)
if (parent) {
transform.normalizeNodeByKey(parent.key)
transform.normalizeNodeByKey(parent.key, SCHEMA)
} else {
transform.normalizeDocument()
transform.normalizeDocument(SCHEMA)
}
}
@ -223,9 +229,10 @@ export function removeTextByKey(transform, key, offset, length, options = {}) {
const path = document.getPath(key)
transform.removeTextOperation(path, offset, length)
if (normalize) {
const parent = document.getParent(key)
transform.normalizeParentsByKey(parent.key)
transform.normalizeParentsByKey(parent.key, SCHEMA)
}
return transform
@ -254,9 +261,10 @@ export function setMarkByKey(transform, key, offset, length, mark, properties, o
const path = document.getPath(key)
transform.setMarkOperation(path, offset, length, mark, newMark)
if (normalize) {
const parent = document.getParent(key)
transform.normalizeNodeByKey(parent.key)
transform.normalizeNodeByKey(parent.key, SCHEMA)
}
return transform
@ -285,9 +293,9 @@ export function setNodeByKey(transform, key, properties, options = {}) {
if (normalize) {
const parent = document.getParent(key)
if (parent) {
transform.normalizeNodeByKey(parent.key)
transform.normalizeNodeByKey(parent.key, SCHEMA)
} else {
transform.normalizeDocument()
transform.normalizeDocument(SCHEMA)
}
}
@ -316,9 +324,9 @@ export function splitNodeByKey(transform, key, offset, options = {}) {
if (normalize) {
const parent = document.getParent(key)
if (parent) {
transform.normalizeNodeByKey(parent.key)
transform.normalizeNodeByKey(parent.key, SCHEMA)
} else {
transform.normalizeDocument()
transform.normalizeDocument(SCHEMA)
}
}

View File

@ -148,9 +148,6 @@ import {
import {
normalize,
normalizeWith,
normalizeNodeWith,
normalizeParentsWith,
normalizeDocument,
normalizeSelection,
normalizeNodeByKey,
@ -299,9 +296,6 @@ export default {
*/
normalize,
normalizeWith,
normalizeNodeWith,
normalizeParentsWith,
normalizeDocument,
normalizeSelection,
normalizeNodeByKey,

View File

@ -1,78 +1,69 @@
import Normalize from '../utils/normalize'
import Schema from '../models/schema'
import warn from '../utils/warn'
import { default as coreSchema } from '../plugins/schema'
/**
* Normalize the document and selection with the core schema.
*
* @param {Transform} transform
* @return {Transform}
*/
export function normalize(transform) {
return transform
.normalizeDocument()
.normalizeSelection()
}
/**
* Normalize the document with the core schema.
*
* @param {Transform} transform
* @return {Transform}
*/
export function normalizeDocument(transform) {
return transform.normalizeWith(coreSchema)
}
/**
* Normalize state with a `schema`.
* Normalize the document and selection with a `schema`.
*
* @param {Transform} transform
* @param {Schema} schema
* @return {Transform}
*/
export function normalizeWith(transform, schema) {
const { state } = transform
const { document } = state
export function normalize(transform, schema) {
assertSchema(schema)
return transform
.normalizeDocument(schema)
.normalizeSelection(schema)
}
/**
* Normalize the document with a `schema`.
*
* @param {Transform} transform
* @param {Schema} schema
* @return {Transform}
*/
export function normalizeDocument(transform, schema) {
assertSchema(schema)
// If the schema has no validation rules, there's nothing to normalize.
if (!schema.hasValidators) {
return transform
}
return transform.normalizeNodeWith(schema, document)
const { state } = transform
const { document } = state
return normalizeNodeWith(transform, document, schema)
}
/**
* Normalize a `node` and its children with a `schema`.
*
* @param {Transform} transform
* @param {Node|String} key
* @param {Schema} schema
* @param {Node} node
* @return {Transform}
*/
export function normalizeNodeWith(transform, schema, node) {
// For performance considerations, we will check if the transform was changed.
const opCount = transform.operations.length
export function normalizeNodeByKey(transform, key, schema) {
assertSchema(schema)
key = Normalize.key(key)
// Iterate over its children.
normalizeChildrenWith(transform, schema, node)
// Re-find the node reference if necessary.
if (transform.operations.length != opCount) {
node = refindNode(transform, node)
// If the schema has no validation rules, there's nothing to normalize.
if (!schema.hasValidators) {
return transform
}
// Now normalize the node itself if it still exists.
if (node) {
normalizeNodeOnly(transform, schema, node)
}
const { state } = transform
const { document } = state
const node = document.assertNode(key)
normalizeNodeWith(transform, node, schema)
return transform
}
@ -80,66 +71,25 @@ export function normalizeNodeWith(transform, schema, node) {
* Normalize a `node` and its parents with a `schema`.
*
* @param {Transform} transform
* @param {Node|String} key
* @param {Schema} schema
* @param {Node} node
* @return {Transform}
*/
export function normalizeParentsWith(transform, schema, node) {
normalizeNodeOnly(transform, schema, node)
export function normalizeParentsByKey(transform, key, schema) {
assertSchema(schema)
key = Normalize.key(key)
// Normalize went back up to the very top of the document.
if (node.kind == 'document') {
return transform
}
// Re-find the node first.
node = refindNode(transform, node)
if (!node) {
// If the schema has no validation rules, there's nothing to normalize.
if (!schema.hasValidators) {
return transform
}
const { state } = transform
const { document } = state
const parent = document.getParent(node.key)
const node = document.assertNode(key)
return normalizeParentsWith(transform, schema, parent)
}
/**
* Normalize a `node` and its children with the core schema.
*
* @param {Transform} transform
* @param {Node|String} key
* @return {Transform}
*/
export function normalizeNodeByKey(transform, key) {
key = Normalize.key(key)
const { state } = transform
const { document } = state
const node = document.key == key ? document : document.assertDescendant(key)
transform.normalizeNodeWith(coreSchema, node)
return transform
}
/**
* Normalize a `node` and its parent with the core schema.
*
* @param {Transform} transform
* @param {Node|String} key
* @return {Transform}
*/
export function normalizeParentsByKey(transform, key) {
key = Normalize.key(key)
const { state } = transform
const { document } = state
const node = document.key == key ? document : document.assertDescendant(key)
transform.normalizeParentsWith(coreSchema, node)
normalizeParentsWith(transform, node, schema)
return transform
}
@ -162,7 +112,8 @@ export function normalizeSelection(transform) {
!document.hasDescendant(selection.anchorKey) ||
!document.hasDescendant(selection.focusKey)
) {
warn('Selection was invalid and reset to start of the document')
warn('The selection was invalid and reset to start of the document.')
const firstText = document.getFirstText()
selection = selection.merge({
anchorKey: firstText.key,
@ -178,6 +129,66 @@ export function normalizeSelection(transform) {
return transform
}
/**
* Normalize a `node` and its children with a `schema`.
*
* @param {Transform} transform
* @param {Node} node
* @param {Schema} schema
* @return {Transform}
*/
function normalizeNodeWith(transform, node, schema) {
// For performance considerations, we will check if the transform was changed.
const opCount = transform.operations.length
// Iterate over its children.
normalizeChildrenWith(transform, node, schema)
// Re-find the node reference if necessary.
if (transform.operations.length != opCount) {
node = refindNode(transform, node)
}
// Now normalize the node itself if it still exists.
if (node) {
normalizeNodeOnly(transform, node, schema)
}
return transform
}
/**
* Normalize a `node` and its parents with a `schema`.
*
* @param {Transform} transform
* @param {Node} node
* @param {Schema} schema
* @return {Transform}
*/
function normalizeParentsWith(transform, node, schema) {
normalizeNodeOnly(transform, node, schema)
// Normalize went back up to the very top of the document.
if (node.kind == 'document') {
return transform
}
// Re-find the node first.
node = refindNode(transform, node)
if (!node) {
return transform
}
const { state } = transform
const { document } = state
const parent = document.getParent(node.key)
return normalizeParentsWith(transform, parent, schema)
}
/**
* Re-find a reference to a node that may have been modified or removed
* entirely by a transform.
@ -199,16 +210,16 @@ function refindNode(transform, node) {
* Normalize the children of a `node` with a `schema`.
*
* @param {Transform} transform
* @param {Schema} schema
* @param {Node} node
* @param {Schema} schema
* @return {Transform}
*/
function normalizeChildrenWith(transform, schema, node) {
function normalizeChildrenWith(transform, node, schema) {
if (node.kind == 'text') return transform
node.nodes.forEach((child) => {
transform.normalizeNodeWith(schema, child)
normalizeNodeWith(transform, child, schema)
})
return transform
@ -218,12 +229,12 @@ function normalizeChildrenWith(transform, schema, node) {
* Normalize a `node` with a `schema`, but not its children.
*
* @param {Transform} transform
* @param {Schema} schema
* @param {Node} node
* @param {Schema} schema
* @return {Transform}
*/
function normalizeNodeOnly(transform, schema, node) {
function normalizeNodeOnly(transform, node, schema) {
let max = schema.rules.length
let iterations = 0
@ -257,3 +268,19 @@ function normalizeNodeOnly(transform, schema, node) {
return iterate(transform, node)
}
/**
* Assert that a `schema` exists.
*
* @param {Schema} schema
*/
function assertSchema(schema) {
if (schema instanceof Schema) return
if (schema == null) {
throw new Error('You must pass a `schema` object.')
} else {
throw new Error(`You passed an invalid \`schema\` object: ${schema}.`)
}
}

View File

@ -1,11 +1,9 @@
import 'jsdom-global/register'
import React from 'react'
import fs from 'fs'
import readMetadata from 'read-metadata'
import strip from '../helpers/strip-dynamic'
import { Raw, Schema } from '../..'
import { mount } from 'enzyme'
import { resolve } from 'path'
import { strictEqual } from '../helpers/assert-json'
@ -32,7 +30,7 @@ describe('schema', () => {
const expected = readMetadata.sync(resolve(testDir, 'output.yaml'))
const schema = Schema.create(require(testDir))
const state = Raw.deserialize(input, { terse: true })
const normalized = state.transform().normalizeWith(schema).apply()
const normalized = state.transform().normalize(schema).apply()
const output = Raw.serialize(normalized, { terse: true })
strictEqual(strip(output), strip(expected))
})