From 286e3620dd8a8da398c50e4485e96237576f5d9a Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Thu, 17 Nov 2016 17:46:35 -0800 Subject: [PATCH] refactor schema transform logic --- src/Readme.md | 1 + src/models/node.js | 35 +++- src/models/state.js | 3 +- src/plugins/core.js | 4 +- src/schemas/Readme.md | 2 + src/{plugins/schema.js => schemas/core.js} | 6 +- src/transforms/at-range.js | 29 +-- src/transforms/by-key.js | 38 ++-- src/transforms/index.js | 6 - src/transforms/normalize.js | 225 ++++++++++++--------- test/schema/index.js | 4 +- 11 files changed, 208 insertions(+), 145 deletions(-) create mode 100644 src/schemas/Readme.md rename src/{plugins/schema.js => schemas/core.js} (98%) diff --git a/src/Readme.md b/src/Readme.md index c26576908..fce57c1f5 100644 --- a/src/Readme.md +++ b/src/Readme.md @@ -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. diff --git a/src/models/node.js b/src/models/node.js index 2cf79fb08..8f4549a31 100644 --- a/src/models/node.js +++ b/src/models/node.js @@ -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`. * diff --git a/src/models/state.js b/src/models/state.js index f3874afa0..480d695e2 100644 --- a/src/models/state.js +++ b/src/models/state.js @@ -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 }) } diff --git a/src/plugins/core.js b/src/plugins/core.js index 1c835b4e0..d157dbfd5 100644 --- a/src/plugins/core.js +++ b/src/plugins/core.js @@ -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} */ diff --git a/src/schemas/Readme.md b/src/schemas/Readme.md new file mode 100644 index 000000000..5e355972c --- /dev/null +++ b/src/schemas/Readme.md @@ -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. diff --git a/src/plugins/schema.js b/src/schemas/core.js similarity index 98% rename from src/plugins/schema.js rename to src/schemas/core.js index adeabcd8d..c4b653acf 100644 --- a/src/plugins/schema.js +++ b/src/schemas/core.js @@ -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 diff --git a/src/transforms/at-range.js b/src/transforms/at-range.js index cbd4da114..c693694d8 100644 --- a/src/transforms/at-range.js +++ b/src/transforms/at-range.js @@ -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) } }) } diff --git a/src/transforms/by-key.js b/src/transforms/by-key.js index 62dcadbc3..8e2ace899 100644 --- a/src/transforms/by-key.js +++ b/src/transforms/by-key.js @@ -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) } } diff --git a/src/transforms/index.js b/src/transforms/index.js index 9c7bb514a..a1c69037d 100644 --- a/src/transforms/index.js +++ b/src/transforms/index.js @@ -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, diff --git a/src/transforms/normalize.js b/src/transforms/normalize.js index d803a1a5d..239ff8b5c 100644 --- a/src/transforms/normalize.js +++ b/src/transforms/normalize.js @@ -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}.`) + } +} diff --git a/test/schema/index.js b/test/schema/index.js index 57c00a09f..0bd24fc08 100644 --- a/test/schema/index.js +++ b/test/schema/index.js @@ -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)) })