From 8f6a534bc956dbfa9d9170e04d7ad9cf85cd34cf Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Tue, 18 Oct 2016 20:40:36 +0200 Subject: [PATCH] Change signature of Node.normalize to be recursive --- src/models/block.js | 6 +- src/models/document.js | 2 +- src/models/inline.js | 2 +- src/models/node.js | 155 ++++++++---------------------------- src/models/state.js | 13 +-- src/plugins/schema.js | 5 ++ src/transforms/normalize.js | 106 ------------------------ 7 files changed, 44 insertions(+), 245 deletions(-) diff --git a/src/models/block.js b/src/models/block.js index 5cbb914bf..f86cfcbfb 100644 --- a/src/models/block.js +++ b/src/models/block.js @@ -53,11 +53,7 @@ class Block extends new Record(DEFAULTS) { properties.isVoid = !!properties.isVoid properties.nodes = Block.createList(properties.nodes) - // if (properties.nodes.size == 0) { - // properties.nodes = properties.nodes.push(Text.create()) - // } - - return new Block(properties).normalize() + return new Block(properties) } /** diff --git a/src/models/document.js b/src/models/document.js index aea8a3fab..4c9a36535 100644 --- a/src/models/document.js +++ b/src/models/document.js @@ -43,7 +43,7 @@ class Document extends new Record(DEFAULTS) { properties.key = properties.key || uid(4) properties.nodes = Block.createList(properties.nodes) - return new Document(properties).normalize() + return new Document(properties) } /** diff --git a/src/models/inline.js b/src/models/inline.js index dfb433dfe..cc703781a 100644 --- a/src/models/inline.js +++ b/src/models/inline.js @@ -53,7 +53,7 @@ class Inline extends new Record(DEFAULTS) { properties.isVoid = !!properties.isVoid properties.nodes = Inline.createList(properties.nodes) - return new Inline(properties).normalize() + return new Inline(properties) } /** diff --git a/src/models/node.js b/src/models/node.js index 428b70260..ad133c8e6 100644 --- a/src/models/node.js +++ b/src/models/node.js @@ -14,6 +14,8 @@ import includes from 'lodash/includes' import memoize from '../utils/memoize' import uid from '../utils/uid' import { List, Map, OrderedSet, Set } from 'immutable' +import { default as defaultSchema } from '../plugins/schema' + /** * Node. @@ -1039,122 +1041,6 @@ const Node = { return this.merge({ nodes }) }, - /** - * Normalize the node by joining any two adjacent text child nodes. - * - * @return {Node} node - */ - - normalize() { - let node = this - let keys = new Set() - let removals = new Set() - - // Map this node's descendants, ensuring... - node = node.mapDescendants((desc) => { - if (removals.has(desc.key)) return desc - - // ...that there are no duplicate keys. - if (keys.has(desc.key)) desc = desc.set('key', uid()) - keys = keys.add(desc.key) - - // ...that void nodes contain a single space of content. - if (desc.isVoid && desc.text != ' ') { - desc = desc.merge({ - nodes: Text.createList([{ - characters: Character.createList([{ text: ' ' }]) - }]) - }) - } - - // ...that no block or inline has no text node inside it. - if (desc.kind != 'text' && desc.nodes.size == 0) { - const text = Text.create() - const nodes = desc.nodes.push(text) - desc = desc.merge({ nodes }) - } - - // ...that no inline node is empty. - if (desc.kind == 'inline' && desc.text == '') { - removals = removals.add(desc.key) - } - - return desc - }) - - // Remove any nodes marked for removal. - removals.forEach((key) => { - node = node.removeDescendant(key) - }) - - removals = removals.clear() - - // And, ensuring... - node = node.mapDescendants((desc) => { - if (desc.kind == 'text') { - let next = node.getNextSibling(desc) - - // ...that there are no adjacent text nodes. - if (next && next.kind == 'text') { - while (next && next.kind == 'text') { - const characters = desc.characters.concat(next.characters) - desc = desc.merge({ characters }) - removals = removals.add(next.key) - next = node.getNextSibling(next) - } - } - - // ...that there are no extra empty text nodes. - else if (desc.length == 0) { - const parent = node.getParent(desc) - if (!removals.has(parent.key) && parent.nodes.size > 1) { - removals = removals.add(desc.key) - } - } - } - - return desc - }) - - // Remove any nodes marked for removal. - removals.forEach((key) => { - node = node.removeDescendant(key) - }) - - // Ensure that void nodes are surrounded by text nodes - node = node.mapDescendants((desc) => { - if (desc.kind == 'text') { - return desc - } - - const nodes = desc.nodes.reduce((accu, child, i) => { - // We wrap only inline void nodes - if (!child.isVoid || child.kind === 'block') { - return accu.push(child) - } - - const prev = accu.last() - const next = desc.nodes.get(i + 1) - - if (!prev || prev.kind !== 'text') { - accu = accu.push(Text.create()) - } - - accu = accu.push(child) - - if (!next || next.kind !== 'text') { - accu = accu.push(Text.create()) - } - - return accu - }, List()) - - return desc.merge({ nodes }) - }) - - return node - }, - /** * Remove a `node` from the children node map. * @@ -1227,8 +1113,8 @@ const Node = { const { nodes } = child const oneNodes = nodes.takeUntil(n => n.key == one.key).push(one) const twoNodes = nodes.skipUntil(n => n.key == one.key).rest().unshift(two) - one = child.merge({ nodes: oneNodes }).normalize() - two = child.merge({ nodes: twoNodes, key: uid() }).normalize() + one = child.merge({ nodes: oneNodes }) + two = child.merge({ nodes: twoNodes, key: uid() }) } child = base.getParent(child) @@ -1266,7 +1152,6 @@ const Node = { const path = base.getPath(node.key) return this.splitNode(path, offset) - .normalize() }, /** @@ -1290,8 +1175,38 @@ const Node = { validate(schema) { return schema.__validate(this) - } + }, + /** + * Normalize the node using a schema, by pushing operations to a transform. + * "prevNode" can be used to prevent iterating over all children. + * + * @param {Transform} transform + * @param {Schema} schema + * @param {Node} prevNode? + * @return {Transform} + */ + + normalize(transform, schema, prevNode) { + if (prevNode === this) { + return this + } + + // Normalize children + this.nodes.forEach(child => { + const prevChild = prevNode ? prevNode.getChild(child.key) : null + child.normalize(transform, schema, prevChild) + }) + + // Normalize the node itself + let failure + if (failure = this.validate(schema)) { + const { value, rule } = failure + rule.normalize(transform, this, value) + } + + return transform + } } /** diff --git a/src/models/state.js b/src/models/state.js index 56ead2f7e..03a0e90dc 100644 --- a/src/models/state.js +++ b/src/models/state.js @@ -425,19 +425,8 @@ class State extends new Record(DEFAULTS) { const state = this const { document, selection } = this let transform = this.transform() - let failure - document.filterDescendantsDeep((node) => { - if (failure = node.validate(schema)) { - const { value, rule } = failure - rule.normalize(transform, node, value) - } - }) - - if (failure = document.validate(schema)) { - const { value, rule } = failure - rule.normalize(transform, document, value) - } + transform = document.normalize(transform, schema, null) return transform.apply({ save: false }) } diff --git a/src/plugins/schema.js b/src/plugins/schema.js index 822005d74..698f9cb2d 100644 --- a/src/plugins/schema.js +++ b/src/plugins/schema.js @@ -1,5 +1,10 @@ import Schema from '../models/schema' +/* + This module contains the default schema to normalize documents + */ + + /** * A default schema rule to only allow block nodes in documents. * diff --git a/src/transforms/normalize.js b/src/transforms/normalize.js index ede62f481..505a1c78b 100644 --- a/src/transforms/normalize.js +++ b/src/transforms/normalize.js @@ -1,112 +1,6 @@ import Schema from '../models/schema' -/** - * 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 - }, - normalize: (transform, document, invalids) => { - return invalids.reduce((t, n) => t.removeNodeByKey(n.key), transform) - } -} - -/** - * 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 - }, - normalize: (transform, block, invalids) => { - return invalids.reduce((t, n) => t.removeNodeByKey(n.key), transform) - } -} - -/** - * 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 - }, - normalize: (transform, inline, invalids) => { - return invalids.reduce((t, n) => t.removeNodeByKey(n.key), transform) - } -} - -/** - * Join adjacent text nodes. - * - * @type {Object} - */ - -const NO_ADJACENT_TEXT_RULE = { - match: (object) => { - return object.kind == 'block' || object.kind == 'inline' - }, - validate: (node) => { - const { nodes } = node - const invalids = nodes - .filter((n, i) => { - const next = nodes.get(i + 1) - return n.kind == 'text' && next && next.kind == 'text' - }) - .map((n, i) => { - const next = nodes.get(i + 1) - return [n, next] - }) - - return invalids.size ? invalids : null - }, - normalize: (transform, node, pairs) => { - return pairs.reduce((t, pair) => { - const [ first, second ] = pair - return t.joinNodeByKey(first.key, second.key) - }) - } -} - -/** - * The internal normalizing schema. - * - * @type {Schema} - */ - -const SCHEMA = Schema.create({ - rules: [ - DOCUMENT_CHILDREN_RULE, - BLOCK_CHILDREN_RULE, - INLINE_CHILDREN_RULE, - NO_ADJACENT_TEXT_RULE, - ] -}) /** * Normalize the state.