From a51061f0947a887b5e23d5bd16e75e53daf6829a Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Wed, 19 Oct 2016 11:52:55 +0200 Subject: [PATCH] Improve normalize and validation to be recursive --- src/plugins/core.js | 4 +- src/plugins/schema.js | 52 +++++++++++++++++-- src/transforms/apply-operation.js | 16 +++++- src/transforms/index.js | 6 +-- src/transforms/normalize.js | 84 ++++++++++++++++--------------- 5 files changed, 110 insertions(+), 52 deletions(-) diff --git a/src/plugins/core.js b/src/plugins/core.js index fe6a18fc4..5220818c8 100644 --- a/src/plugins/core.js +++ b/src/plugins/core.js @@ -7,7 +7,7 @@ import React from 'react' import String from '../utils/string' import getWindow from 'get-window' import { IS_MAC } from '../constants/environment' -import { rules } from './schema' +import { default as defaultSchema } from './schema' /** * Debug. @@ -741,7 +741,7 @@ function Plugin(options = {}) { rules: [ BLOCK_RENDER_RULE, INLINE_RENDER_RULE, - ...rules + ...defaultSchema.rules ] } diff --git a/src/plugins/schema.js b/src/plugins/schema.js index cf24a1b74..78bba43e2 100644 --- a/src/plugins/schema.js +++ b/src/plugins/schema.js @@ -1,4 +1,5 @@ import Schema from '../models/schema' +import Text from '../models/text' /* This module contains the default schema to normalize documents @@ -84,6 +85,45 @@ const INLINE_CHILDREN_RULE = { } } +/** + * A default schema rule to ensure that inline void nodes are surrounded with text nodes + * + * @type {Object} + */ + +const INLINE_VOID_TEXT_RULE = { + match: (object) => { + return object.kind == 'block' + }, + validate: (block) => { + const invalids = block.nodes.reduce((accu, child, index) => { + if (child.kind === 'block' || !child.isVoid) { + return accu + } + + const prevNode = index > 0 ? block.nodes.get(index - 1) : null + const nextNode = block.nodes.get(index + 1) + + const prev = (!prevNode || prevNode.kind !== 'text') + const next = (!nextNode || nextNode.kind !== 'text') + + if (next || prev) { + accu.push({ next, prev, index }) + } + + return accu + }, []) + + return invalids.length ? invalids : null + }, + normalize: (transform, block, invalids) => { + return invalids.reduce((t, { index, next, prev }) => { + if (prev) t = transform.insertNodeByKey(block.key, index, Text.create()) + if (next) t = transform.insertNodeByKey(block.key, index + 1, Text.create()) + return t + }, transform) + } +} /** * Join adjacent text nodes. @@ -111,10 +151,13 @@ const NO_ADJACENT_TEXT_RULE = { return invalids.size ? invalids : null }, normalize: (transform, node, pairs) => { - return pairs.reduce((t, pair) => { - const [ first, second ] = pair - return t.joinNodeByKey(second.key, first.key) - }, transform) + return pairs + // We reverse the list since we want to handle 3 consecutive text nodes + .reverse() + .reduce((t, pair) => { + const [ first, second ] = pair + return t.joinNodeByKey(second.key, first.key) + }, transform) } } @@ -130,6 +173,7 @@ const schema = Schema.create({ BLOCK_CHILDREN_RULE, MIN_TEXT_RULE, INLINE_CHILDREN_RULE, + INLINE_VOID_TEXT_RULE, NO_ADJACENT_TEXT_RULE ] }) diff --git a/src/transforms/apply-operation.js b/src/transforms/apply-operation.js index 167279950..ae8ea5368 100644 --- a/src/transforms/apply-operation.js +++ b/src/transforms/apply-operation.js @@ -1,6 +1,7 @@ import Debug from 'debug' import uid from '../utils/uid' +import { default as defaultSchema } from '../plugins/schema' /** * Debug. @@ -33,6 +34,8 @@ const OPERATIONS = { split_node: splitNode, // Selection operations. set_selection: setSelection, + // Normalize + normalize } /** @@ -48,7 +51,6 @@ export function applyOperation(transform, operation) { const { type } = operation const fn = OPERATIONS[type] - console.log('apply op', type, operation); if (!fn) { throw new Error(`Unknown operation type: "${type}".`) } @@ -57,9 +59,21 @@ export function applyOperation(transform, operation) { transform.state = fn(state, operation) transform.operations = operations.concat([operation]) + return transform } +/** + * Normalize the state with core rules + * + * @param {State} state + * @return {State} + */ + +function normalize(state) { + return state.normalize(defaultSchema) +} + /** * Add mark to text at `offset` and `length` in node by `path`. * diff --git a/src/transforms/index.js b/src/transforms/index.js index 38df2acc4..da7192a44 100644 --- a/src/transforms/index.js +++ b/src/transforms/index.js @@ -146,8 +146,7 @@ import { import { normalize, - normalizeWith, - normalizeDocument, + normalizeWith } from './normalize' /** @@ -290,7 +289,6 @@ export default { */ normalize, - normalizeWith, - normalizeDocument, + normalizeWith } diff --git a/src/transforms/normalize.js b/src/transforms/normalize.js index c8ee6322b..e93c83b0e 100644 --- a/src/transforms/normalize.js +++ b/src/transforms/normalize.js @@ -1,52 +1,54 @@ import Schema from '../models/schema' import { default as defaultSchema } from '../plugins/schema' - /** -* Normalize a node using a schema, by pushing operations to a transform. -* "prevNode" can be used to prevent iterating over all children. -* -* @param {Transform} transform -* @param {Node} node -* @param {Node} prevNode? -* @return {Transform} - */ - -function normalizeNode(transform, schema, node, prevNode) { - if (prevNode === node) { - return transform - } - - // Normalize children - if (node.nodes) { - transform = node.nodes.reduce((t, child) => { - const prevChild = prevNode ? prevNode.getChild(child.key) : null - return normalizeNode(transform, schema, child, prevChild) - }, transform) - } - - // Normalize the node itself - let failure - if (failure = schema.__validate(node)) { - const { value, rule } = failure - transform = rule.normalize(transform, node, value) - } - - return transform -} - -/** - * Normalize the state with a schema. + * Normalize a node using a schema. * * @param {Transform} transform - * @param {Schema} schema + * @param {Node} node + * @param {Node} prevNode * @return {Transform} */ -export function normalizeWith(transform, schema) { - const { state } = transform - const { document } = state - return normalizeNode(transform, schema, document, null) +export function normalizeWith(transform, schema, node, prevNode) { + let { state } = transform + + // If no node specific, normalize the whole document + node = node || state.document + + if (node === prevNode) { + return transform + } + + const failure = schema.__validate(node) + + if (failure) { + const { value, rule } = failure + + // Normalize and get the new state + transform = rule.normalize(transform, node, value) + const newState = transform.state + + // Search for the updated node in the new state + node = newState.document.getDescendant(node.key) + + // Node no longer exist, exit + if (!node) { + return transform + } + + return transform.normalizeWith(schema, node, prevNode) + } + + // No child, stop here + if (!node.nodes) { + return transform + } + + return node.nodes.reduce((t, child) => { + const prevChild = prevNode ? prevNode.getChild(child.key) : null + return t.normalizeWith(schema, child, prevChild) + }, transform) } /** @@ -58,7 +60,7 @@ export function normalizeWith(transform, schema) { */ export function normalize(transform) { - return transform.normalizeWith(defaultSchema) + return transform.normalizeWith(defaultSchema) } /**