diff --git a/Makefile b/Makefile index 8e5dfc6b0..26f6d91b8 100644 --- a/Makefile +++ b/Makefile @@ -24,28 +24,51 @@ clean: # Build the source. dist: $(shell find ./lib) - @ $(babel) --out-dir ./dist ./lib + @ $(babel) \ + --out-dir \ + ./dist \ + ./lib @ touch ./dist # Build the auto-markdown example. example-auto-markdown: - @ $(browserify) --debug --transform babelify --outfile ./examples/auto-markdown/build.js ./examples/auto-markdown/index.js + @ $(browserify) \ + --debug \ + --transform babelify \ + --outfile ./examples/auto-markdown/build.js \ + ./examples/auto-markdown/index.js # Build the links example. example-links: - @ $(browserify) --debug --transform babelify --outfile ./examples/links/build.js ./examples/links/index.js + @ $(browserify) \ + --debug \ + --transform babelify \ + --outfile ./examples/links/build.js \ + ./examples/links/index.js # Build the plain-text example. example-plain-text: - @ $(browserify) --debug --transform babelify --outfile ./examples/plain-text/build.js ./examples/plain-text/index.js + @ $(browserify) \ + --debug \ + --transform babelify \ + --outfile ./examples/plain-text/build.js \ + ./examples/plain-text/index.js # Build the rich-text example. example-rich-text: - @ $(browserify) --debug --transform babelify --outfile ./examples/rich-text/build.js ./examples/rich-text/index.js + @ $(browserify) \ + --debug \ + --transform babelify \ + --outfile ./examples/rich-text/build.js \ + ./examples/rich-text/index.js # Build the table example. example-table: - @ $(browserify) --debug --transform babelify --outfile ./examples/table/build.js ./examples/table/index.js + @ $(browserify) \ + --debug \ + --transform babelify \ + --outfile ./examples/table/build.js \ + ./examples/table/index.js # Install the dependencies. install: @@ -57,14 +80,20 @@ lint: # Build the test source. test/browser/support/build.js: $(shell find ./lib) ./test/browser.js - @ $(browserify) --debug --transform babelify --outfile ./test/support/build.js ./test/browser.js + @ $(browserify) \ + --debug \ + --transform babelify \ + --outfile ./test/support/build.js ./test/browser.js # Run the tests. test: test-browser test-server # Run the browser-side tests. test-browser: ./test/support/build.js - @ $(mocha-phantomjs) --reporter spec --timeout 5000 ./test/support/browser.html + @ $(mocha-phantomjs) \ + --reporter spec \ + --timeout 5000 \ + ./test/support/browser.html # Run the server-side tests. test-server: diff --git a/lib/components/content.js b/lib/components/content.js index 7b81260c5..184494578 100644 --- a/lib/components/content.js +++ b/lib/components/content.js @@ -83,7 +83,7 @@ class Content extends React.Component { const { anchorNode, anchorOffset, focusNode, focusOffset } = native const anchor = OffsetKey.findPoint(anchorNode, anchorOffset) const focus = OffsetKey.findPoint(focusNode, focusOffset) - const edges = document.filterDeep((node) => { + const edges = document.filterDescendants((node) => { return node.key == anchor.key || node.key == focus.key }) diff --git a/lib/models/node.js b/lib/models/node.js index 2ccc7c447..7df5ac09a 100644 --- a/lib/models/node.js +++ b/lib/models/node.js @@ -22,8 +22,24 @@ const Node = { * @param {String or Node} key */ - assertHasDeep(key) { - if (!this.hasDeep(key)) throw new Error('Could not find that child node.') + assertHasChild(key) { + key = normalizeKey(key) + if (!this.hasChild(key)) { + throw new Error(`Could not find a child node with key "${key}".`) + } + }, + + /** + * Assert that the node has a descendant by `key`. + * + * @param {String or Node} key + */ + + assertHasDescendant(key) { + key = normalizeKey(key) + if (!this.hasDescendant(key)) { + throw new Error(`Could not find a descendant node with key "${key}".`) + } }, /** @@ -42,10 +58,10 @@ const Node = { // Make sure the children exist. const { startKey, startOffset, endKey, endOffset } = range - node.assertHasDeep(startKey) - node.assertHasDeep(endKey) + node.assertHasDescendant(startKey) + node.assertHasDescendant(endKey) - let startNode = node.getDeep(startKey) + let startNode = node.getDescendant(startKey) // If the start and end nodes are the same, remove the matching characters. if (startKey == endKey) { @@ -81,11 +97,11 @@ const Node = { let endParent = node.getParent(endKey) const startGrandestParent = node.nodes.find((child) => { - return child == startParent || child.hasDeep(startParent) + return child == startParent || child.hasDescendant(startParent) }) const endGrandestParent = node.nodes.find((child) => { - return child == endParent || child.hasDeep(endParent) + return child == endParent || child.hasDescendant(endParent) }) const nodes = node.nodes @@ -103,9 +119,9 @@ const Node = { // Then remove the end parent. let endGrandparent = node.getParent(endParent) if (endGrandparent == node) { - node = node.removeDeep(endParent) + node = node.removeDescendant(endParent) } else { - endGrandparent = endGrandparent.removeDeep(endParent) + endGrandparent = endGrandparent.removeDescendant(endParent) node = node.updateDeep(endGrandparent) } @@ -133,7 +149,7 @@ const Node = { // When at start of a text node, merge forwards into the next text node. const { startKey } = range - const startNode = node.getDeep(startKey) + const startNode = node.getDescendant(startKey) if (range.isAtStartOf(startNode)) { const previous = node.getPreviousText(startNode) @@ -170,7 +186,7 @@ const Node = { // When at end of a text node, merge forwards into the next text node. const { startKey } = range - const startNode = node.getDeep(startKey) + const startNode = node.getDescendant(startKey) if (range.isAtEndOf(startNode)) { const next = node.getNextText(startNode) @@ -188,33 +204,32 @@ const Node = { }, /** - * Recursively find nodes nodes by `iterator`. + * Recursively find all ancestor nodes by `iterator`. * * @param {Function} iterator * @return {Node} node */ - findDeep(iterator) { - const shallow = this.nodes.find(iterator) - if (shallow != null) return shallow - - return this.nodes - .map(node => node.kind == 'text' ? null : node.findDeep(iterator)) - .filter(node => node) - .first() + findDescendant(iterator) { + return ( + this.nodes.find(iterator) || + this.nodes + .map(node => node.kind == 'text' ? null : node.findDescendant(iterator)) + .find(exists => exists) + ) }, /** - * Recursively filter nodes nodes with `iterator`. + * Recursively filter all ancestor nodes with `iterator`. * * @param {Function} iterator - * @return {OrderedMap} matches + * @return {List} nodes */ - filterDeep(iterator) { + filterDescendants(iterator) { return this.nodes.reduce((matches, child, i, nodes) => { if (iterator(child, i, nodes)) matches = matches.push(child) - if (child.kind != 'text') matches = matches.concat(child.filterDeep(iterator)) + if (child.kind != 'text') matches = matches.concat(child.filterDescendants(iterator)) return matches }, Block.createList()) }, @@ -223,14 +238,14 @@ const Node = { * Get the closest block nodes for each text node in a `range`. * * @param {Selection} range - * @return {OrderedMap} nodes + * @return {List} nodes */ getBlocksAtRange(range) { range = range.normalize(this) - const texts = this.getTextsAtRange(range) - const blocks = texts.map(text => this.getClosestBlock(text)) - return blocks + return this + .getTextsAtRange(range) + .map(text => this.getClosestBlock(text)) }, /** @@ -242,16 +257,12 @@ const Node = { getCharactersAtRange(range) { range = range.normalize(this) - const texts = this.getTextsAtRange(range) - let list = new List() - - texts.forEach((text) => { - let { characters } = text - characters = characters.filter((char, i) => isInRange(i, text, range)) - list = list.concat(characters) - }) - - return list + return this + .getTextsAtRange(range) + .reduce((characters, text) => { + const chars = text.characters.filter((char, i) => isInRange(i, text, range)) + return characters.concat(chars) + }, Character.createList()) }, /** @@ -259,14 +270,11 @@ const Node = { * * @param {String or Node} key * @param {Function} iterator - * @return {Node or Null} parent + * @return {Node or Null} node */ getClosest(key, iterator) { - key = normalizeKey(key) - this.assertHasDeep(key) - - let node = this.getDeep(key) + let node = this.getDescendant(key) while (node = this.getParent(node)) { if (node == this) return null @@ -280,66 +288,46 @@ const Node = { * Get the closest block parent of a `node`. * * @param {String or Node} key - * @return {Node or Null} parent + * @return {Node or Null} node */ getClosestBlock(key) { - key = normalizeKey(key) - this.assertHasDeep(key) - - const match = this.getClosest(key, parent => parent.kind == 'block') - return match + return this.getClosest(key, parent => parent.kind == 'block') }, /** * Get the closest inline parent of a `node`. * * @param {String or Node} key - * @return {Node or Null} parent + * @return {Node or Null} node */ getClosestInline(key) { - key = normalizeKey(key) - this.assertHasDeep(key) - - const match = this.getClosest(key, parent => parent.kind == 'inline') - return match - }, - - /** - * Get the furthest inline parent of a node by `key`. - * - * @param {String or Node} key - * @return {Node or Null} parent - */ - - getFurthestInline(key) { - key = normalizeKey(key) - this.assertHasDeep(key) - - let child = this.getDeep(key) - let furthest = null - let next - - while (next = this.getClosestInline(child)) { - furthest = next - child = next - } - - return furthest + return this.getClosest(key, parent => parent.kind == 'inline') }, /** * Get a child node by `key`. * * @param {String} key - * @return {Node or Null} + * @return {Node or Null} node */ - getDeep(key) { + getChild(key) { key = normalizeKey(key) - const match = this.findDeep(node => node.key == key) - return match || null + return this.nodes.find(node => node.key == key) + }, + + /** + * Get a descendant node by `key`. + * + * @param {String} key + * @return {Node or Null} node + */ + + getDescendant(key) { + key = normalizeKey(key) + return this.findDescendant(node => node.key == key) }, /** @@ -352,7 +340,7 @@ const Node = { getDepth(key, startAt = 1) { key = normalizeKey(key) - this.assertHasDeep(key) + this.assertHasDescendant(key) const shallow = this.nodes.find(node => node.key == key) if (shallow) return startAt @@ -360,7 +348,7 @@ const Node = { const child = this.nodes.find(node => { return node.kind == 'text' ? null - : node.hasDeep(key) + : node.hasDescendant(key) }) return child @@ -369,41 +357,58 @@ const Node = { }, /** - * Get the first text child node. + * Get the furthest block parent of a node by `key`. * - * @return {Text or Null} text + * @param {String or Node} key + * @return {Node or Null} node */ - getFirstText() { - return this.getTextNodes().first() || null + getFurthestBlock(key) { + let node = this.getDescendant(key) + let furthest = null + + while (node = this.getClosestBlock(node)) { + furthest = node + } + + return furthest + }, + + /** + * Get the furthest inline parent of a node by `key`. + * + * @param {String or Node} key + * @return {Node or Null} node + */ + + getFurthestInline(key) { + let node = this.getDescendant(key) + let furthest = null + + while (node = this.getClosestInline(node)) { + furthest = node + } + + return furthest }, /** * Get the closest inline nodes for each text node in a `range`. * * @param {Selection} range - * @return {OrderedMap} nodes + * @return {List} nodes */ getInlinesAtRange(range) { range = range.normalize(this) - const node = this - const texts = node.getTextsAtRange(range) - const inlines = texts - .map(text => node.getClosest(text, p => p.kind == 'inline')) - .filter(inline => inline) - return inlines - }, + // If the range isn't set, return an empty list. + if (range.isUnset) return Inline.createList() - /** - * Get the last text child node. - * - * @return {Text or Null} text - */ - - getLastText() { - return this.getTextNodes().last() || null + return this + .getTextsAtRange(range) + .map(text => this.getClosestInline(text)) + .filter(exists => exists) }, /** @@ -415,65 +420,54 @@ const Node = { getMarksAtRange(range) { range = range.normalize(this) - const { startKey, startOffset, endKey } = range + const { startKey, startOffset } = range + const marks = Mark.createSet() - // If the selection isn't set, return nothing. - if (startKey == null || endKey == null) return new Set() + // If the range isn't set, return an empty set. + if (range.isUnset) return marks - // If the range is collapsed, and at the start of the node, check the - // previous text node. + // If the range is collapsed at the start of the node, check the previous. if (range.isCollapsed && startOffset == 0) { const previous = this.getPreviousText(startKey) - if (!previous) return new Set() + if (!previous) return marks const char = text.characters.get(previous.length - 1) return char.marks } // If the range is collapsed, check the character before the start. if (range.isCollapsed) { - const text = this.getDeep(startKey) + const text = this.getDescendant(startKey) const char = text.characters.get(range.startOffset - 1) return char.marks } // Otherwise, get a set of the marks for each character in the range. - const characters = this.getCharactersAtRange(range) - let set = new Set() - - characters.forEach((char) => { - set = set.union(char.marks) - }) - - return set + this + .getCharactersAtRange(range) + .reduce((marks, char) => { + return marks.union(char.marks) + }, marks) }, /** - * Get the child node after the one by `key`. + * Get the node after a descendant by `key`. * * @param {String or Node} key - * @return {Node or Null} + * @return {Node or Null} node */ getNextSibling(key) { - key = normalizeKey(key) - this.assertHasDeep(key) - - const shallow = this.nodes.find(node => node.key == key) - if (shallow) { - return this.nodes - .skipUntil(node => node.key == key) - .rest() - .first() - } - - return this.nodes - .map(node => node.kind == 'text' ? null : node.getNextSibling(key)) - .filter(node => node) - .first() + const node = this.getDescendant(key) + if (!node) return null + return this + .getParent(node) + .nodes + .skipUntil(child => child == node) + .get(1) }, /** - * Get the text node after a text node by `key`. + * Get the text node after a descendant text node by `key`. * * @param {String or Node} key * @return {Node or Null} node @@ -481,16 +475,13 @@ const Node = { getNextText(key) { key = normalizeKey(key) - this.assertHasDeep(key) - return this.getTextNodes() .skipUntil(text => text.key == key) - .take(2) - .last() + .get(1) }, /** - * Get the offset for a child text node by `key`. + * Get the offset for a descendant text node by `key`. * * @param {String or Node} key * @return {Number} offset @@ -498,16 +489,16 @@ const Node = { getOffset(key) { key = normalizeKey(key) - this.assertHasDeep(key) + this.assertHasDescendant(key) - const match = this.getDeep(key) + const match = this.getDescendant(key) // Find the shallow matching child. const child = this.nodes.find((node) => { if (node == match) return true return node.kind == 'text' ? false - : node.hasDeep(match) + : node.hasDescendant(match) }) // Get all of the nodes that come before the matching child. @@ -529,17 +520,15 @@ const Node = { * Get the parent of a child node by `key`. * * @param {String or Node} key - * @return {Node or Null} + * @return {Node or Null} node */ getParent(key) { key = normalizeKey(key) - // this.assertHasDeep(key) - - const shallow = this.nodes.find(node => node.key == key) - if (shallow) return this + if (this.hasChild(key)) return this let node = null + this.nodes.forEach((child) => { if (child.kind == 'text') return const match = child.getParent(key) @@ -550,31 +539,24 @@ const Node = { }, /** - * Get the child node before the one by `key`. + * Get the node before a descendant node by `key`. * * @param {String or Node} key - * @return {Node or Null} + * @return {Node or Null} node */ getPreviousSibling(key) { - key = normalizeKey(key) - this.assertHasDeep(key) - - const shallow = this.nodes.find(node => node.key == key) - if (shallow) { - return this.nodes - .takeUntil(node => node.key == key) - .last() - } - - return this.nodes - .map(node => node.kind == 'text' ? null : node.getPreviousSibling(key)) - .filter(node => node) - .first() + const node = this.getDescendant(key) + if (!node) return null + return this + .getParent(node) + .nodes + .takeUntil(child => child == node) + .last() }, /** - * Get the text node before a text node by `key`. + * Get the text node before a descendant text node by `key`. * * @param {String or Node} key * @return {Node or Null} node @@ -582,35 +564,32 @@ const Node = { getPreviousText(key) { key = normalizeKey(key) - this.assertHasDeep(key) - return this.getTextNodes() .takeUntil(text => text.key == key) .last() }, /** - * Get the child text node at an `offset`. + * Get the descendent text node at an `offset`. * * @param {String} offset - * @return {Node or Null} + * @return {Node or Null} node */ getTextAtOffset(offset) { let length = 0 - let texts = this.getTextNodes() - let match = texts.find((node) => { - length += node.length - return length >= offset - }) - - return match + return this + .getTextNodes() + .find((text) => { + length += text.length + return length >= offset + }) }, /** * Recursively get all of the child text nodes in order of appearance. * - * @return {OrderedMap} nodes + * @return {List} nodes */ getTextNodes() { @@ -625,43 +604,49 @@ const Node = { * Get all of the text nodes in a `range`. * * @param {Selection} range - * @return {OrderedMap} nodes + * @return {List} nodes */ getTextsAtRange(range) { range = range.normalize(this) + + // If the selection is unset, return an empty list. + if (range.isUnset) return Block.createList() + const { startKey, endKey } = range - - // If the selection is unset, return an empty map. - if (range.isUnset) return new OrderedMap() - - // Assert that the nodes exist before searching. - this.assertHasDeep(startKey) - this.assertHasDeep(endKey) - - // Return the text nodes after the start offset and before the end offset. const texts = this.getTextNodes() - const endNode = this.getDeep(endKey) - const afterStart = texts.skipUntil(node => node.key == startKey) - const upToEnd = afterStart.takeUntil(node => node.key == endKey) - const matches = upToEnd.push(endNode) - return matches + const startText = this.getDescendant(startKey) + const endText = this.getDescendant(endKey) + const start = texts.indexOf(startText) + const end = texts.indexOf(endText) + return texts.slice(start, end + 1) + }, + + /** + * Check if a child node exists by `key`. + * + * @param {String or Node} key + * @return {Boolean} exists + */ + + hasChild(key) { + key = normalizeKey(key) + return !! this.nodes.find(node => node.key == key) }, /** * Recursively check if a child node exists by `key`. * * @param {String or Node} key - * @return {Boolean} true + * @return {Boolean} exists */ - hasDeep(key) { + hasDescendant(key) { key = normalizeKey(key) - return !! this.nodes.find((node) => { return node.kind == 'text' ? node.key == key - : node.key == key || node.hasDeep(key) + : node.key == key || node.hasDescendant(key) }) }, @@ -684,7 +669,7 @@ const Node = { } let { startKey, startOffset } = range - let startNode = node.getDeep(startKey) + let startNode = node.getDescendant(startKey) let { characters } = startNode // Create a list of the new characters, with the marks from the previous @@ -764,7 +749,7 @@ const Node = { let node = this // See if there are any adjacent text nodes. - let firstAdjacent = node.findDeep((child) => { + let firstAdjacent = node.findDescendant((child) => { if (child.kind != 'text') return const parent = node.getParent(child) const next = parent.getNextSibling(child) @@ -782,7 +767,7 @@ const Node = { parent = parent.updateDeep(firstAdjacent) // Then remove the second node. - parent = parent.removeDeep(second) + parent = parent.removeDescendant(second) // If the parent isn't this node, it needs to be updated. if (parent != node) { @@ -795,18 +780,6 @@ const Node = { return node.normalize() }, - /** - * Push a new `node` onto the map of nodes. - * - * @param {Node} node - * @return {Node} node - */ - - pushNode(node) { - const nodes = this.nodes.push(node) - return this.merge({ nodes }) - }, - /** * Remove a `node` from the children node map. * @@ -814,9 +787,9 @@ const Node = { * @return {Node} node */ - removeDeep(key) { + removeDescendant(key) { key = normalizeKey(key) - this.assertHasDeep(key) + this.assertHasDescendant(key) const nodes = this.nodes.filterNot(node => node.key == key) return this.merge({ nodes }) }, @@ -913,14 +886,14 @@ const Node = { // Find the highest inline elements that were split. const { startKey } = range - const firstText = node.getDeep(startKey) + const firstText = node.getDescendant(startKey) const firstChild = node.getFurthestInline(firstText) || firstText const secondText = node.getNextText(startKey) const secondChild = node.getFurthestInline(secondText) || secondText // Remove the second inline child from the first block. let firstBlock = node.getBlocksAtRange(range).first() - firstBlock = firstBlock.removeDeep(secondChild) + firstBlock = firstBlock.removeDescendant(secondChild) // Create a new block with the second inline child in it. const secondBlock = Block.create({ @@ -961,7 +934,7 @@ const Node = { // First split the text nodes. node = node.splitTextAtRange(range) - let firstChild = node.getDeep(range.startKey) + let firstChild = node.getDescendant(range.startKey) let secondChild = node.getNextText(firstChild) let parent @@ -1010,7 +983,7 @@ const Node = { // Split the text node's characters. const { startKey, startOffset } = range - const text = node.getDeep(startKey) + const text = node.getDescendant(startKey) const { characters } = text const firstChars = characters.take(startOffset) const secondChars = characters.skip(startOffset) @@ -1082,7 +1055,7 @@ const Node = { */ updateDeep(node) { - // this.assertHasDeep(key) + // this.assertHasDescendant(key) const shallow = this.nodes.find(child => child.key == node.key) if (shallow) { @@ -1235,7 +1208,7 @@ const Node = { // Determine the new end of the range, and split there. const { startKey, startOffset, endKey, endOffset } = range - const firstNode = node.getDeep(startKey) + const firstNode = node.getDescendant(startKey) const nextNode = node.getNextText(startKey) const end = startKey != endKey ? range.moveToEnd() @@ -1249,7 +1222,7 @@ const Node = { node = node.splitInlineAtRange(end) // Calculate the new range to wrap around. - const endNode = node.getDeep(end.anchorKey) + const endNode = node.getDescendant(end.anchorKey) range = Selection.create({ anchorKey: nextNode.key, anchorOffset: 0, diff --git a/lib/models/selection.js b/lib/models/selection.js index 2728fa291..ae8243515 100644 --- a/lib/models/selection.js +++ b/lib/models/selection.js @@ -114,7 +114,7 @@ class Selection extends Record(DEFAULTS) { isAtStartOf(node) { const { startKey, startOffset } = this - const first = node.kind == 'text' ? node : node.getFirstText() + const first = node.kind == 'text' ? node : node.getTextNodes().first() return startKey == first.key && startOffset == 0 } @@ -127,7 +127,7 @@ class Selection extends Record(DEFAULTS) { isAtEndOf(node) { const { endKey, endOffset } = this - const last = node.kind == 'text' ? node : node.getLastText() + const last = node.kind == 'text' ? node : node.getTextNodes().last() return endKey == last.key && endOffset == last.length } @@ -147,10 +147,10 @@ class Selection extends Record(DEFAULTS) { if (anchorKey == null || focusKey == null) return selection // Asset that the anchor and focus nodes exist in the node tree. - node.assertHasDeep(anchorKey) - node.assertHasDeep(focusKey) - let anchorNode = node.getDeep(anchorKey) - let focusNode = node.getDeep(focusKey) + node.assertHasDescendant(anchorKey) + node.assertHasDescendant(focusKey) + let anchorNode = node.getDescendant(anchorKey) + let focusNode = node.getDescendant(focusKey) // If the anchor node isn't a text node, match it to one. if (anchorNode.kind != 'text') { diff --git a/lib/models/state.js b/lib/models/state.js index 7cbbd9f29..7ddcb8514 100644 --- a/lib/models/state.js +++ b/lib/models/state.js @@ -280,7 +280,7 @@ class State extends Record(DEFAULTS) { // Determine what the selection should be after deleting. const { startKey } = selection - const startNode = document.getDeep(startKey) + const startNode = document.getDescendant(startKey) if (selection.isExpanded) { after = selection.moveToStart() @@ -410,7 +410,7 @@ class State extends Record(DEFAULTS) { // Determine what the selection should be after splitting. const { startKey } = selection - const startNode = document.getDeep(startKey) + const startNode = document.getDescendant(startKey) const parent = document.getParent(startNode) const next = document.getNext(parent) const text = next.nodes.first()