From c0802c57e02789be5cd3eac819bf1ce84607fdc8 Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Sun, 19 Jun 2016 12:12:23 -0700 Subject: [PATCH] move document transforms to node interface --- lib/models/document.js | 303 +-------------------------------------- lib/models/element.js | 5 +- lib/models/node.js | 311 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 314 insertions(+), 305 deletions(-) diff --git a/lib/models/document.js b/lib/models/document.js index 3da4557ab..c9f696321 100644 --- a/lib/models/document.js +++ b/lib/models/document.js @@ -1,9 +1,5 @@ -import Character from './character' -import Element from './element' import Node from './node' -import Selection from './selection' -import Text from './text' import { OrderedMap, Record } from 'immutable' /** @@ -48,7 +44,7 @@ class Document extends Record(DEFAULTS) { */ get text() { - return this + return this.nodes .map(node => node.text) .join('') } @@ -63,303 +59,6 @@ class Document extends Record(DEFAULTS) { return 'document' } - /** - * Delete everything in a `range`. - * - * @param {Selection} range - * @return {Document} document - */ - - deleteAtRange(range) { - let document = this - - // If the range is collapsed, there's nothing to do. - if (range.isCollapsed) return document - - const { startKey, startOffset, endKey, endOffset } = range - let startNode = document.getNode(startKey) - - // If the start and end nodes are the same, remove the matching characters. - if (startKey == endKey) { - let { characters } = startNode - - characters = characters.filterNot((char, i) => { - return startOffset <= i && i < endOffset - }) - - startNode = startNode.merge({ characters }) - document = document.updateNode(startNode) - return document - } - - // Otherwise, remove the text from the first and last nodes... - const startRange = Selection.create({ - anchorKey: startKey, - anchorOffset: startOffset, - focusKey: startKey, - focusOffset: startNode.length - }) - - const endRange = Selection.create({ - anchorKey: endKey, - anchorOffset: 0, - focusKey: endKey, - focusOffset: endOffset - }) - - document = document.deleteAtRange(startRange) - document = document.deleteAtRange(endRange) - - // Then remove any nodes in between the top-most start and end nodes... - let startParent = document.getParentNode(startKey) - let endParent = document.getParentNode(endKey) - - const startGrandestParent = document.nodes.find((node) => { - return node == startParent || node.hasNode(startParent) - }) - - const endGrandestParent = document.nodes.find((node) => { - return node == endParent || node.hasNode(endParent) - }) - - const nodes = document.nodes - .takeUntil(node => node == startGrandestParent) - .set(startGrandestParent.key, startGrandestParent) - .concat(document.nodes.skipUntil(node => node == endGrandestParent)) - - document = document.merge({ nodes }) - - // Then add the end parent's nodes to the start parent node. - const newNodes = startParent.nodes.concat(endParent.nodes) - startParent = startParent.merge({ nodes: newNodes }) - document = document.updateNode(startParent) - - // Then remove the end parent. - let endGrandparent = document.getParentNode(endParent) - if (endGrandparent == document) { - document = document.removeNode(endParent) - } else { - endGrandparent = endGrandparent.removeNode(endParent) - document = document.updateNode(endGrandparent) - } - - // Normalize the document. - return document.normalize() - } - - /** - * Delete backward `n` characters at a `range`. - * - * @param {Selection} range - * @param {Number} n (optional) - * @return {Document} document - */ - - deleteBackwardAtRange(range, n = 1) { - let document = this - - // When collapsed at the end of the document, there's nothing to do. - if (range.isCollapsed && range.isAtEndOf(document)) return document - - // When the range is still expanded, just do a regular delete. - if (range.isExpanded) return document.deleteAtRange(range) - - // When at start of a text node, merge forwards into the next text node. - const { startKey } = range - const startNode = document.getNode(startKey) - - if (range.isAtStartOf(startNode)) { - const parent = document.getParentNode(startNode) - const previous = document.getPreviousNode(parent).nodes.first() - range = range.extendBackwardToEndOf(previous) - document = document.deleteAtRange(range) - return document - } - - // Otherwise, remove `n` characters behind of the cursor. - range = range.extendBackward(n) - document = document.deleteAtRange(range) - - // Normalize the document. - return document.normalize() - } - - /** - * Delete forward `n` characters at a `range`. - * - * @param {Selection} range - * @param {Number} n (optional) - * @return {Document} document - */ - - deleteForwardAtRange(range, n = 1) { - let document = this - - // When collapsed at the end of the document, there's nothing to do. - if (range.isCollapsed && range.isAtEndOf(document)) return document - - // When the range is still expanded, just do a regular delete. - if (range.isExpanded) return document.deleteAtRange(range) - - // When at end of a text node, merge forwards into the next text node. - const { startKey } = range - const startNode = document.getNode(startKey) - - if (range.isAtEndOf(startNode)) { - const parent = document.getParentNode(startNode) - const next = document.getNextNode(parent).nodes.first() - range = range.extendForwardToStartOf(next) - document = document.deleteAtRange(range) - return document - } - - // Otherwise, remove `n` characters ahead of the cursor. - range = range.extendForward(n) - document = document.deleteAtRange(range) - - // Normalize the document. - return document.normalize() - } - - /** - * Insert `text` at a `range`. - * - * @param {Selection} range - * @param {String} text - * @return {Document} document - */ - - insertTextAtRange(range, text) { - let document = this - - // When still expanded, remove the current range first. - if (range.isExpanded) { - document = document.deleteAtRange(range) - range = range.moveToStart() - } - - let { startKey, startOffset } = range - let startNode = document.getNode(startKey) - let { characters } = startNode - - // Create a list of the new characters, with the right marks. - const marks = characters.has(startOffset) - ? characters.get(startOffset).marks - : null - - const newCharacters = text.split('').reduce((list, char) => { - const obj = { text } - if (marks) obj.marks = marks - return list.push(Character.create(obj)) - }, Character.createList()) - - // Splice in the new characters. - const resumeOffset = startOffset + text.length - 1 - characters = characters.slice(0, startOffset) - .concat(newCharacters) - .concat(characters.slice(resumeOffset, Infinity)) - - // Update the existing text node. - startNode = startNode.merge({ characters }) - document = document.updateNode(startNode) - - // Normalize the document. - return document.normalize() - } - - /** - * Normalize the document, joining any two adjacent text nodes. - * - * @return {Document} document - */ - - normalize() { - let document = this - let first = document.findNode((node) => { - if (node.type != 'text') return - const parent = document.getParentNode(node) - const next = parent.getNextNode(node) - return next && next.type == 'text' - }) - - // If no text node was followed by another, do nothing. - if (!first) return document - - // Otherwise, add the text of the second node to the first... - let parent = document.getParentNode(first) - const second = parent.getNextNode(first) - const characters = first.characters.concat(second.characters) - first = first.merge({ characters }) - parent = parent.updateNode(first) - - // Then remove the second node. - parent = parent.removeNode(second) - document = document.updateNode(parent) - - // Finally, recurse by normalizing again. - return document.normalize() - } - - /** - * Split the nodes at a `range`. - * - * @param {Selection} range - * @return {Document} document - */ - - splitAtRange(range) { - let document = this - - // If the range is expanded, remove it first. - if (range.isExpanded) { - document = document.deleteAtRange(range) - range = range.moveToStart() - } - - const { startKey, startOffset } = range - const startNode = document.getNode(startKey) - - // Split the text node's characters. - const { characters, length } = startNode - const firstCharacters = characters.take(startOffset) - const secondCharacters = characters.takeLast(length - startOffset) - - // Create a new first element with only the first set of characters. - const parent = document.getParentNode(startNode) - const firstText = startNode.set('characters', firstCharacters) - const firstElement = parent.updateNode(firstText) - - // Create a brand new second element with the second set of characters. - let secondText = Text.create({}) - let secondElement = Element.create({ - type: firstElement.type, - data: firstElement.data - }) - - secondText = secondText.set('characters', secondCharacters) - secondElement = secondElement.pushNode(secondText) - - // Replace the old parent node in the grandparent with the two new ones. - let grandparent = document.getParentNode(parent) - const befores = grandparent.nodes.takeUntil(node => node.key == parent.key) - const afters = grandparent.nodes.skipUntil(node => node.key == parent.key).rest() - const nodes = befores - .set(firstElement.key, firstElement) - .set(secondElement.key, secondElement) - .concat(afters) - - // If the document is the grandparent, just merge, otherwise deep merge. - if (grandparent == document) { - document = document.merge({ nodes }) - } else { - grandparent = grandparent.merge({ nodes }) - document = document.updateNode(grandparent) - } - - // Normalize the document. - return document.normalize() - } - } /** diff --git a/lib/models/element.js b/lib/models/element.js index c356006b5..bc0370dd6 100644 --- a/lib/models/element.js +++ b/lib/models/element.js @@ -42,8 +42,7 @@ class Element extends Record(DEFAULTS) { static createMap(elements = []) { return elements.reduce((map, element) => { - map = map.set(element.key, element) - return map + return map.set(element.key, element) }, new OrderedMap()) } @@ -64,7 +63,7 @@ class Element extends Record(DEFAULTS) { */ get text() { - return this + return this.nodes .map(node => node.text) .join('') } diff --git a/lib/models/node.js b/lib/models/node.js index c16ee9599..284284e5b 100644 --- a/lib/models/node.js +++ b/lib/models/node.js @@ -1,4 +1,8 @@ +import Character from './character' +import Element from './element' +import Selection from './selection' +import Text from './text' import { OrderedMap } from 'immutable' /** @@ -10,6 +14,168 @@ import { OrderedMap } from 'immutable' const Node = { + /** + * Delete everything in a `range`. + * + * @param {Selection} range + * @return {Node} node + */ + + deleteAtRange(range) { + let node = this + + // If the range is collapsed, there's nothing to do. + if (range.isCollapsed) return node + + // Make sure the children exist. + const { startKey, startOffset, endKey, endOffset } = range + if (!node.hasNode(startKey)) throw new Error('Could not find that start node.') + if (!node.hasNode(endKey)) throw new Error('Could not find that end node.') + + let startNode = node.getNode(startKey) + + // If the start and end nodes are the same, remove the matching characters. + if (startKey == endKey) { + let { characters } = startNode + + characters = characters.filterNot((char, i) => { + return startOffset <= i && i < endOffset + }) + + startNode = startNode.merge({ characters }) + node = node.updateNode(startNode) + return node + } + + // Otherwise, remove the text from the first and last nodes... + const startRange = Selection.create({ + anchorKey: startKey, + anchorOffset: startOffset, + focusKey: startKey, + focusOffset: startNode.length + }) + + const endRange = Selection.create({ + anchorKey: endKey, + anchorOffset: 0, + focusKey: endKey, + focusOffset: endOffset + }) + + node = node.deleteAtRange(startRange) + node = node.deleteAtRange(endRange) + + // Then remove any nodes in between the top-most start and end nodes... + let startParent = node.getParentNode(startKey) + let endParent = node.getParentNode(endKey) + + const startGrandestParent = node.nodes.find((child) => { + return child == startParent || child.hasNode(startParent) + }) + + const endGrandestParent = node.nodes.find((child) => { + return child == endParent || child.hasNode(endParent) + }) + + const nodes = node.nodes + .takeUntil(child => child == startGrandestParent) + .set(startGrandestParent.key, startGrandestParent) + .concat(node.nodes.skipUntil(child => child == endGrandestParent)) + + node = node.merge({ nodes }) + + // Then add the end parent's nodes to the start parent node. + const newNodes = startParent.nodes.concat(endParent.nodes) + startParent = startParent.merge({ nodes: newNodes }) + node = node.updateNode(startParent) + + // Then remove the end parent. + let endGrandparent = node.getParentNode(endParent) + if (endGrandparent == node) { + node = node.removeNode(endParent) + } else { + endGrandparent = endGrandparent.removeNode(endParent) + node = node.updateNode(endGrandparent) + } + + // Normalize the node. + return node.normalize() + }, + + /** + * Delete backward `n` characters at a `range`. + * + * @param {Selection} range + * @param {Number} n (optional) + * @return {Node} node + */ + + deleteBackwardAtRange(range, n = 1) { + let node = this + + // When collapsed at the start of the node, there's nothing to do. + if (range.isCollapsed && range.isAtStartOf(node)) return node + + // When the range is still expanded, just do a regular delete. + if (range.isExpanded) return node.deleteAtRange(range) + + // When at start of a text node, merge forwards into the next text node. + const { startKey } = range + const startNode = node.getNode(startKey) + + if (range.isAtStartOf(startNode)) { + const parent = node.getParentNode(startNode) + const previous = node.getPreviousNode(parent).nodes.first() + range = range.extendBackwardToEndOf(previous) + node = node.deleteAtRange(range) + return node + } + + // Otherwise, remove `n` characters behind of the cursor. + range = range.extendBackward(n) + node = node.deleteAtRange(range) + + // Normalize the node. + return node.normalize() + }, + + /** + * Delete forward `n` characters at a `range`. + * + * @param {Selection} range + * @param {Number} n (optional) + * @return {Node} node + */ + + deleteForwardAtRange(range, n = 1) { + let node = this + + // When collapsed at the end of the node, there's nothing to do. + if (range.isCollapsed && range.isAtEndOf(node)) return node + + // When the range is still expanded, just do a regular delete. + if (range.isExpanded) return node.deleteAtRange(range) + + // When at end of a text node, merge forwards into the next text node. + const { startKey } = range + const startNode = node.getNode(startKey) + + if (range.isAtEndOf(startNode)) { + const parent = node.getParentNode(startNode) + const next = node.getNextNode(parent).nodes.first() + range = range.extendForwardToStartOf(next) + node = node.deleteAtRange(range) + return node + } + + // Otherwise, remove `n` characters ahead of the cursor. + range = range.extendForward(n) + node = node.deleteAtRange(range) + + // Normalize the node. + return node.normalize() + }, + /** * Recursively find nodes nodes by `iterator`. * @@ -175,6 +341,91 @@ const Node = { return false }, + /** + * Insert `text` at a `range`. + * + * @param {Selection} range + * @param {String} text + * @return {Document} node + */ + + insertTextAtRange(range, text) { + let node = this + + // When still expanded, remove the current range first. + if (range.isExpanded) { + node = node.deleteAtRange(range) + range = range.moveToStart() + } + + let { startKey, startOffset } = range + let startNode = node.getNode(startKey) + let { characters } = startNode + + // Create a list of the new characters, with the right marks. + const marks = characters.has(startOffset) + ? characters.get(startOffset).marks + : null + + const newCharacters = text.split('').reduce((list, char) => { + const obj = { text } + if (marks) obj.marks = marks + return list.push(Character.create(obj)) + }, Character.createList()) + + // Splice in the new characters. + const resumeOffset = startOffset + text.length - 1 + characters = characters.slice(0, startOffset) + .concat(newCharacters) + .concat(characters.slice(resumeOffset, Infinity)) + + // Update the existing text node. + startNode = startNode.merge({ characters }) + node = node.updateNode(startNode) + + // Normalize the node. + return node.normalize() + }, + + /** + * Normalize the node, joining any two adjacent text child nodes. + * + * @return {Node} node + */ + + normalize() { + let node = this + let first = node.findNode((child) => { + if (child.type != 'text') return + const parent = node.getParentNode(child) + const next = parent.getNextNode(child) + return next && next.type == 'text' + }) + + // If no text node was followed by another, do nothing. + if (!first) return node + + // Otherwise, add the text of the second node to the first... + let parent = node.getParentNode(first) + const second = parent.getNextNode(first) + const characters = first.characters.concat(second.characters) + first = first.merge({ characters }) + parent = parent.updateNode(first) + + // Then remove the second node. + parent = parent.removeNode(second) + + // If the parent isn't this node, it needs to be updated. + if (parent != node) { + node = node.updateNode(parent) + } else { + node = parent + } + + // Finally, recurse by normalizing again. + return node.normalize() + }, + /** * Push a new `node` onto the map of nodes. * @@ -209,6 +460,66 @@ const Node = { return this.merge({ nodes }) }, + /** + * Split the nodes at a `range`. + * + * @param {Selection} range + * @return {Node} node + */ + + splitAtRange(range) { + let node = this + + // If the range is expanded, remove it first. + if (range.isExpanded) { + node = node.deleteAtRange(range) + range = range.moveToStart() + } + + const { startKey, startOffset } = range + const startNode = node.getNode(startKey) + + // Split the text node's characters. + const { characters, length } = startNode + const firstCharacters = characters.take(startOffset) + const secondCharacters = characters.takeLast(length - startOffset) + + // Create a new first element with only the first set of characters. + const parent = node.getParentNode(startNode) + const firstText = startNode.set('characters', firstCharacters) + const firstElement = parent.updateNode(firstText) + + // Create a brand new second element with the second set of characters. + let secondText = Text.create({}) + let secondElement = Element.create({ + type: firstElement.type, + data: firstElement.data + }) + + secondText = secondText.set('characters', secondCharacters) + secondElement = secondElement.pushNode(secondText) + + // Replace the old parent node in the grandparent with the two new ones. + let grandparent = node.getParentNode(parent) + const befores = grandparent.nodes.takeUntil(child => child.key == parent.key) + const afters = grandparent.nodes.skipUntil(child => child.key == parent.key).rest() + const nodes = befores + .set(firstElement.key, firstElement) + .set(secondElement.key, secondElement) + .concat(afters) + + // If the node is the grandparent, just merge, otherwise deep merge. + if (grandparent == node) { + node = node.merge({ nodes }) + } else { + grandparent = grandparent.merge({ nodes }) + node = node.updateNode(grandparent) + } + + // Normalize the node. + return node.normalize() + }, + /** * Set a new value for a child node by `key`. *