1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-02-25 01:33:37 +01:00
slate/lib/models/node.js

709 lines
16 KiB
JavaScript
Raw Normal View History

2016-06-15 12:07:12 -07:00
import Block from './block'
import Character from './character'
2016-06-22 18:42:49 -07:00
import Data from './data'
import Document from './document'
2016-06-23 23:39:08 -07:00
import Inline from './inline'
import Mark from './mark'
import Selection from './selection'
import Transforms from './transforms'
import Text from './text'
import { List, Map, Set } from 'immutable'
2016-06-15 12:07:12 -07:00
/**
* Node.
*
* And interface that `Document`, `Block` and `Inline` all implement, to make
* working with the recursive node tree easier.
2016-06-15 12:07:12 -07:00
*/
const Node = {
2016-06-20 12:57:31 -07:00
/**
* Assert that the node has a child by `key`.
*
* @param {String or Node} key
*/
2016-06-23 12:34:47 -07:00
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}".`)
}
2016-06-20 12:57:31 -07:00
},
/**
2016-06-23 12:34:47 -07:00
* Recursively find all ancestor nodes by `iterator`.
*
* @param {Function} iterator
* @return {Node} node
*/
2016-06-23 12:34:47 -07:00
findDescendant(iterator) {
return (
this.nodes.find(iterator) ||
this.nodes
.map(node => node.kind == 'text' ? null : node.findDescendant(iterator))
.find(exists => exists)
)
},
2016-06-15 19:46:53 -07:00
/**
2016-06-23 12:34:47 -07:00
* Recursively filter all ancestor nodes with `iterator`.
2016-06-15 19:46:53 -07:00
*
* @param {Function} iterator
2016-06-23 12:34:47 -07:00
* @return {List} nodes
2016-06-15 19:46:53 -07:00
*/
2016-06-23 12:34:47 -07:00
filterDescendants(iterator) {
return this.nodes.reduce((matches, child, i, nodes) => {
if (iterator(child, i, nodes)) matches = matches.push(child)
2016-06-23 12:34:47 -07:00
if (child.kind != 'text') matches = matches.concat(child.filterDescendants(iterator))
2016-06-22 18:42:49 -07:00
return matches
}, Block.createList())
2016-06-22 18:42:49 -07:00
},
/**
* Get the closest block nodes for each text node in a `range`.
*
* @param {Selection} range
2016-06-23 12:34:47 -07:00
* @return {List} nodes
2016-06-22 18:42:49 -07:00
*/
getBlocksAtRange(range) {
range = range.normalize(this)
2016-06-23 12:34:47 -07:00
return this
.getTextsAtRange(range)
.map(text => this.getClosestBlock(text))
},
2016-06-15 19:46:53 -07:00
2016-06-20 12:57:31 -07:00
/**
* Get a list of the characters in a `range`.
*
* @param {Selection} range
* @return {List} characters
*/
getCharactersAtRange(range) {
range = range.normalize(this)
2016-06-23 12:34:47 -07:00
return this
.getTextsAtRange(range)
.reduce((characters, text) => {
const chars = text.characters.filter((char, i) => isInRange(i, text, range))
return characters.concat(chars)
}, Character.createList())
2016-06-20 12:57:31 -07:00
},
2016-06-22 18:42:49 -07:00
/**
2016-06-23 09:15:51 -07:00
* Get closest parent of node by `key` that matches `iterator`.
2016-06-22 18:42:49 -07:00
*
2016-06-23 09:15:51 -07:00
* @param {String or Node} key
2016-06-22 18:42:49 -07:00
* @param {Function} iterator
2016-06-23 12:34:47 -07:00
* @return {Node or Null} node
2016-06-22 18:42:49 -07:00
*/
2016-06-23 09:15:51 -07:00
getClosest(key, iterator) {
2016-06-23 12:34:47 -07:00
let node = this.getDescendant(key)
2016-06-23 09:15:51 -07:00
while (node = this.getParent(node)) {
2016-06-22 18:42:49 -07:00
if (node == this) return null
if (iterator(node)) return node
}
return null
},
2016-06-23 09:15:51 -07:00
/**
* Get the closest block parent of a `node`.
*
* @param {String or Node} key
2016-06-23 12:34:47 -07:00
* @return {Node or Null} node
2016-06-23 09:15:51 -07:00
*/
getClosestBlock(key) {
2016-06-23 12:34:47 -07:00
return this.getClosest(key, parent => parent.kind == 'block')
2016-06-23 09:15:51 -07:00
},
/**
* Get the closest inline parent of a `node`.
*
* @param {String or Node} key
2016-06-23 12:34:47 -07:00
* @return {Node or Null} node
2016-06-23 09:15:51 -07:00
*/
getClosestInline(key) {
2016-06-23 12:34:47 -07:00
return this.getClosest(key, parent => parent.kind == 'inline')
2016-06-23 09:15:51 -07:00
},
/**
2016-06-23 12:34:47 -07:00
* Get a child node by `key`.
2016-06-23 09:15:51 -07:00
*
2016-06-23 15:56:28 -07:00
* @param {String or Node} key
2016-06-23 12:34:47 -07:00
* @return {Node or Null} node
2016-06-23 09:15:51 -07:00
*/
2016-06-23 12:34:47 -07:00
getChild(key) {
2016-06-23 09:15:51 -07:00
key = normalizeKey(key)
2016-06-23 12:34:47 -07:00
return this.nodes.find(node => node.key == key)
2016-06-23 09:15:51 -07:00
},
/**
* Get a fragment of the node at a `range`.
*
* @param {Selection} range
* @return {List} nodes
*/
getFragmentAtRange(range) {
let node = this
let nodes = Block.createList()
// If the range is collapsed, there's nothing to do.
if (range.isCollapsed) return Document.create({ nodes })
// Make sure the children exist.
const { startKey, endKey } = range
node.assertHasDescendant(startKey)
node.assertHasDescendant(endKey)
// Split at the start and end.
const start = range.moveToStart()
const end = range.moveToEnd()
node = node.splitBlockAtRange(start, Infinity)
node = node.splitBlockAtRange(end, Infinity)
// Get the start and end nodes.
const startNode = node.getHighestChild(startKey)
const endNode = node.getHighestChild(endKey)
nodes = node.nodes
.skipUntil(node => node == startNode)
.rest()
.takeUntil(node => node == endNode)
.push(endNode)
return Document.create({ nodes })
},
2016-06-23 15:42:46 -07:00
/**
* Get the highest child ancestor of a node by `key`.
*
* @param {String or Node} key
* @return {Node or Null} node
*/
getHighestChild(key) {
key = normalizeKey(key)
return this.nodes.find(node => {
if (node.key == key) return true
if (node.kind == 'text') return false
return node.hasDescendant(key)
})
},
2016-06-22 18:42:49 -07:00
/**
2016-06-23 12:34:47 -07:00
* Get a descendant node by `key`.
2016-06-22 18:42:49 -07:00
*
* @param {String} key
2016-06-23 12:34:47 -07:00
* @return {Node or Null} node
2016-06-22 18:42:49 -07:00
*/
2016-06-23 12:34:47 -07:00
getDescendant(key) {
2016-06-22 18:42:49 -07:00
key = normalizeKey(key)
2016-06-23 12:34:47 -07:00
return this.findDescendant(node => node.key == key)
2016-06-22 18:42:49 -07:00
},
/**
* Get the depth of a child node by `key`, with optional `startAt`.
*
* @param {String or Node} key
* @param {Number} startAt (optional)
* @return {Number} depth
*/
getDepth(key, startAt = 1) {
key = normalizeKey(key)
2016-06-23 12:34:47 -07:00
this.assertHasDescendant(key)
2016-06-23 15:51:17 -07:00
if (this.hasChild(key)) return startAt
return this
.getHighestChild(key)
.getDepth(key, startAt + 1)
2016-06-22 18:42:49 -07:00
},
2016-06-20 13:21:24 -07:00
/**
2016-06-23 12:34:47 -07:00
* Get the furthest block parent of a node by `key`.
2016-06-20 13:21:24 -07:00
*
2016-06-23 12:34:47 -07:00
* @param {String or Node} key
* @return {Node or Null} node
2016-06-20 13:21:24 -07:00
*/
2016-06-23 12:34:47 -07:00
getFurthestBlock(key) {
let node = this.getDescendant(key)
let furthest = null
while (node = this.getClosestBlock(node)) {
furthest = node
}
return furthest
2016-06-22 18:42:49 -07:00
},
/**
2016-06-23 12:34:47 -07:00
* Get the furthest inline parent of a node by `key`.
2016-06-22 18:42:49 -07:00
*
2016-06-23 12:34:47 -07:00
* @param {String or Node} key
* @return {Node or Null} node
2016-06-22 18:42:49 -07:00
*/
2016-06-23 12:34:47 -07:00
getFurthestInline(key) {
let node = this.getDescendant(key)
let furthest = null
while (node = this.getClosestInline(node)) {
furthest = node
}
2016-06-22 18:42:49 -07:00
2016-06-23 12:34:47 -07:00
return furthest
2016-06-20 13:21:24 -07:00
},
/**
2016-06-23 12:34:47 -07:00
* Get the closest inline nodes for each text node in a `range`.
2016-06-20 13:21:24 -07:00
*
2016-06-23 12:34:47 -07:00
* @param {Selection} range
* @return {List} nodes
2016-06-20 13:21:24 -07:00
*/
2016-06-23 12:34:47 -07:00
getInlinesAtRange(range) {
range = range.normalize(this)
if (range.isUnset) return Inline.createList()
return this
.getTextsAtRange(range)
.map(text => this.getClosestInline(text))
.filter(exists => exists)
2016-06-20 13:21:24 -07:00
},
2016-06-20 12:57:31 -07:00
/**
* Get a set of the marks in a `range`.
*
* @param {Selection} range
* @return {Set} marks
*/
getMarksAtRange(range) {
range = range.normalize(this)
2016-06-23 12:34:47 -07:00
const { startKey, startOffset } = range
const marks = Mark.createSet()
2016-06-20 12:57:31 -07:00
2016-06-23 12:34:47 -07:00
// If the range isn't set, return an empty set.
if (range.isUnset) return marks
2016-06-20 12:57:31 -07:00
2016-06-23 12:34:47 -07:00
// If the range is collapsed at the start of the node, check the previous.
2016-06-20 12:57:31 -07:00
if (range.isCollapsed && startOffset == 0) {
2016-06-23 23:59:22 -07:00
const text = this.getDescendant(startKey)
2016-06-22 18:42:49 -07:00
const previous = this.getPreviousText(startKey)
2016-06-23 12:34:47 -07:00
if (!previous) return marks
2016-06-24 14:07:11 -07:00
const char = previous.characters.get(previous.length - 1)
2016-06-20 12:57:31 -07:00
return char.marks
}
// If the range is collapsed, check the character before the start.
if (range.isCollapsed) {
2016-06-23 12:34:47 -07:00
const text = this.getDescendant(startKey)
2016-06-20 12:57:31 -07:00
const char = text.characters.get(range.startOffset - 1)
return char.marks
}
// Otherwise, get a set of the marks for each character in the range.
2016-06-23 23:59:22 -07:00
return this
2016-06-23 12:34:47 -07:00
.getCharactersAtRange(range)
2016-06-23 23:59:22 -07:00
.reduce((marks, char) => marks.union(char.marks), marks)
2016-06-20 12:57:31 -07:00
},
2016-06-16 16:43:02 -07:00
/**
2016-06-23 12:34:47 -07:00
* Get the node after a descendant by `key`.
2016-06-16 16:43:02 -07:00
*
2016-06-22 18:42:49 -07:00
* @param {String or Node} key
2016-06-23 12:34:47 -07:00
* @return {Node or Null} node
2016-06-16 16:43:02 -07:00
*/
2016-06-22 18:42:49 -07:00
getNextSibling(key) {
2016-06-23 12:34:47 -07:00
const node = this.getDescendant(key)
if (!node) return null
return this
.getParent(node)
.nodes
.skipUntil(child => child == node)
.get(1)
2016-06-22 18:42:49 -07:00
},
/**
2016-06-23 12:34:47 -07:00
* Get the text node after a descendant text node by `key`.
2016-06-22 18:42:49 -07:00
*
* @param {String or Node} key
* @return {Node or Null} node
*/
getNextText(key) {
key = normalizeKey(key)
return this.getTextNodes()
.skipUntil(text => text.key == key)
2016-06-23 12:34:47 -07:00
.get(1)
},
2016-06-16 16:43:02 -07:00
2016-06-20 12:57:31 -07:00
/**
2016-06-23 12:34:47 -07:00
* Get the offset for a descendant text node by `key`.
2016-06-20 12:57:31 -07:00
*
2016-06-20 17:38:56 -07:00
* @param {String or Node} key
* @return {Number} offset
2016-06-20 12:57:31 -07:00
*/
2016-06-22 18:42:49 -07:00
getOffset(key) {
2016-06-23 09:15:51 -07:00
key = normalizeKey(key)
2016-06-23 12:34:47 -07:00
this.assertHasDescendant(key)
2016-06-23 09:15:51 -07:00
2016-06-23 15:51:17 -07:00
// Calculate the offset of the nodes before the highest child.
const child = this.getHighestChild(key)
2016-06-23 15:39:44 -07:00
const offset = this.nodes
.takeUntil(node => node == child)
.reduce((offset, child) => offset + child.length, 0)
// Recurse if need be.
2016-06-23 15:51:17 -07:00
return this.hasChild(key)
2016-06-20 12:57:31 -07:00
? offset
2016-06-22 18:42:49 -07:00
: offset + child.getOffset(key)
2016-06-20 12:57:31 -07:00
},
/**
* Get the offset from a `range`.
*
* @param {Selection} range
* @return {Number} offset
*/
getOffsetAtRange(range) {
if (range.isExpanded) {
throw new Error('The range must be collapsed to calculcate its offset.')
}
const { startKey, startOffset } = range
const startNode = this.getDescendant(startKey)
if (!startNode) {
throw new Error(`Could not find a node by startKey "${startKey}".`)
}
return this.getOffset(startKey) + startOffset
},
2016-06-16 16:43:02 -07:00
/**
2016-06-22 18:42:49 -07:00
* Get the parent of a child node by `key`.
2016-06-16 16:43:02 -07:00
*
2016-06-17 00:09:54 -07:00
* @param {String or Node} key
2016-06-23 12:34:47 -07:00
* @return {Node or Null} node
2016-06-16 16:43:02 -07:00
*/
2016-06-22 18:42:49 -07:00
getParent(key) {
2016-06-20 12:57:31 -07:00
key = normalizeKey(key)
2016-06-23 12:34:47 -07:00
if (this.hasChild(key)) return this
2016-06-16 16:43:02 -07:00
2016-06-22 18:51:30 -07:00
let node = null
2016-06-23 12:34:47 -07:00
2016-06-22 18:42:49 -07:00
this.nodes.forEach((child) => {
if (child.kind == 'text') return
const match = child.getParent(key)
if (match) node = match
})
2016-06-16 16:43:02 -07:00
2016-06-22 18:42:49 -07:00
return node
},
2016-06-16 16:43:02 -07:00
2016-06-17 00:09:54 -07:00
/**
2016-06-23 12:34:47 -07:00
* Get the node before a descendant node by `key`.
2016-06-17 00:09:54 -07:00
*
* @param {String or Node} key
2016-06-23 12:34:47 -07:00
* @return {Node or Null} node
2016-06-17 00:09:54 -07:00
*/
2016-06-22 18:42:49 -07:00
getPreviousSibling(key) {
2016-06-23 12:34:47 -07:00
const node = this.getDescendant(key)
if (!node) return null
return this
.getParent(node)
.nodes
.takeUntil(child => child == node)
.last()
},
2016-06-17 00:09:54 -07:00
2016-06-20 12:57:31 -07:00
/**
2016-06-23 12:34:47 -07:00
* Get the text node before a descendant text node by `key`.
2016-06-17 00:09:54 -07:00
*
* @param {String or Node} key
2016-06-22 18:42:49 -07:00
* @return {Node or Null} node
2016-06-17 00:09:54 -07:00
*/
2016-06-22 18:42:49 -07:00
getPreviousText(key) {
2016-06-20 12:57:31 -07:00
key = normalizeKey(key)
2016-06-22 18:42:49 -07:00
return this.getTextNodes()
.takeUntil(text => text.key == key)
.last()
},
2016-06-17 00:09:54 -07:00
2016-06-16 16:43:02 -07:00
/**
2016-06-23 12:34:47 -07:00
* Get the descendent text node at an `offset`.
2016-06-16 16:43:02 -07:00
*
* @param {String} offset
2016-06-23 12:34:47 -07:00
* @return {Node or Null} node
2016-06-16 16:43:02 -07:00
*/
2016-06-22 18:42:49 -07:00
getTextAtOffset(offset) {
let length = 0
2016-06-23 12:34:47 -07:00
return this
.getTextNodes()
.find((text) => {
length += text.length
return length >= offset
})
},
2016-06-16 16:43:02 -07:00
2016-06-22 18:42:49 -07:00
/**
* Recursively get all of the child text nodes in order of appearance.
*
2016-06-23 12:34:47 -07:00
* @return {List} nodes
2016-06-22 18:42:49 -07:00
*/
getTextNodes() {
return this.nodes.reduce((texts, node) => {
return node.kind == 'text'
? texts.push(node)
2016-06-22 18:42:49 -07:00
: texts.concat(node.getTextNodes())
}, Block.createList())
2016-06-22 18:42:49 -07:00
},
2016-06-20 12:57:31 -07:00
/**
* Get all of the text nodes in a `range`.
*
* @param {Selection} range
2016-06-23 12:34:47 -07:00
* @return {List} nodes
2016-06-20 12:57:31 -07:00
*/
2016-06-22 18:59:19 -07:00
getTextsAtRange(range) {
range = range.normalize(this)
2016-06-20 12:57:31 -07:00
2016-06-23 12:34:47 -07:00
// If the selection is unset, return an empty list.
if (range.isUnset) return Block.createList()
2016-06-20 12:57:31 -07:00
2016-06-23 12:34:47 -07:00
const { startKey, endKey } = range
2016-06-22 18:42:49 -07:00
const texts = this.getTextNodes()
2016-06-23 12:34:47 -07:00
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)
2016-06-20 17:38:56 -07:00
},
2016-06-20 12:57:31 -07:00
2016-06-16 16:43:02 -07:00
/**
2016-06-23 12:34:47 -07:00
* Check if a child node exists by `key`.
2016-06-16 16:43:02 -07:00
*
2016-06-17 00:09:54 -07:00
* @param {String or Node} key
2016-06-23 12:34:47 -07:00
* @return {Boolean} exists
2016-06-16 16:43:02 -07:00
*/
2016-06-23 12:34:47 -07:00
hasChild(key) {
2016-06-20 12:57:31 -07:00
key = normalizeKey(key)
2016-06-23 12:34:47 -07:00
return !! this.nodes.find(node => node.key == key)
},
2016-06-23 09:15:51 -07:00
2016-06-23 12:34:47 -07:00
/**
* Recursively check if a child node exists by `key`.
*
* @param {String or Node} key
* @return {Boolean} exists
*/
hasDescendant(key) {
key = normalizeKey(key)
2016-06-21 17:08:15 -07:00
return !! this.nodes.find((node) => {
return node.kind == 'text'
? node.key == key
2016-06-23 12:34:47 -07:00
: node.key == key || node.hasDescendant(key)
2016-06-21 17:08:15 -07:00
})
},
2016-06-16 16:43:02 -07:00
/**
* Check if the inline nodes are split at a `range`.
*
* @param {Selection} range
* @return {Boolean} isSplit
*/
isInlineSplitAtRange(range) {
range = range.normalize(this)
if (range.isExpanded) throw new Error()
2016-06-20 12:57:31 -07:00
const { startKey } = range
const start = this.getFurthestInline(startKey) || this.getDescendant(startKey)
return range.isAtStartOf(start) || range.isAtEndOf(start)
2016-06-20 12:57:31 -07:00
},
/**
* Normalize the node by joining any two adjacent text child nodes.
*
* @return {Node} node
*/
normalize() {
let node = this
2016-06-24 14:07:11 -07:00
const texts = node.getTextNodes()
// If there are no text nodes, add one.
if (!texts.size) {
const text = Text.create()
const nodes = node.nodes.push(text)
return node.merge({ nodes })
}
// See if there are any adjacent text nodes.
2016-06-23 12:34:47 -07:00
let firstAdjacent = node.findDescendant((child) => {
if (child.kind != 'text') return
2016-06-22 18:42:49 -07:00
const parent = node.getParent(child)
const next = parent.getNextSibling(child)
return next && next.kind == 'text'
})
// If no text nodes are adjacent, abort.
if (!firstAdjacent) return node
// Fix an adjacent text node if one exists.
2016-06-22 18:42:49 -07:00
let parent = node.getParent(firstAdjacent)
const second = parent.getNextSibling(firstAdjacent)
const characters = firstAdjacent.characters.concat(second.characters)
firstAdjacent = firstAdjacent.merge({ characters })
2016-06-23 15:51:17 -07:00
parent = parent.updateDescendant(firstAdjacent)
// Then remove the second node.
2016-06-23 12:34:47 -07:00
parent = parent.removeDescendant(second)
// If the parent isn't this node, it needs to be updated.
if (parent != node) {
2016-06-23 15:51:17 -07:00
node = node.updateDescendant(parent)
} else {
node = parent
}
// Recurse by normalizing again.
return node.normalize()
},
2016-06-17 00:09:54 -07:00
/**
* Remove a `node` from the children node map.
*
* @param {String or Node} key
* @return {Node} node
*/
2016-06-23 12:34:47 -07:00
removeDescendant(key) {
2016-06-20 12:57:31 -07:00
key = normalizeKey(key)
2016-06-23 12:34:47 -07:00
this.assertHasDescendant(key)
2016-06-23 22:30:26 -07:00
const child = this.getChild(key)
if (child) {
const nodes = this.nodes.filterNot(node => node == child)
return this.merge({ nodes })
}
const nodes = this.nodes.map((node) => {
return node.kind == 'text'
? node
: node.removeDescendant(key)
})
2016-06-16 16:43:02 -07:00
return this.merge({ nodes })
},
2016-06-16 16:43:02 -07:00
/**
* Set a new value for a child node by `key`.
*
* @param {Node} node
2016-06-16 16:43:02 -07:00
* @return {Node} node
*/
2016-06-23 15:51:17 -07:00
updateDescendant(node) {
2016-06-23 23:01:45 -07:00
if (this.hasChild(node)) {
const nodes = this.nodes.map(child => child.key == node.key ? node : child)
return this.merge({ nodes })
2016-06-16 16:43:02 -07:00
}
const nodes = this.nodes.map((child) => {
2016-06-23 15:51:17 -07:00
return child.kind == 'text' ? child : child.updateDescendant(node)
2016-06-16 16:43:02 -07:00
})
return this.merge({ nodes })
2016-06-21 19:02:39 -07:00
}
2016-06-15 12:07:12 -07:00
}
2016-06-20 12:57:31 -07:00
/**
* Normalize a `key`, from a key string or a node.
*
* @param {String or Node} key
* @return {String} key
*/
function normalizeKey(key) {
if (typeof key == 'string') return key
return key.key
}
/**
* Check if an `index` of a `text` node is in a `range`.
*
* @param {Number} index
* @param {Text} text
* @param {Selection} range
* @return {Set} characters
*/
function isInRange(index, text, range) {
const { startKey, startOffset, endKey, endOffset } = range
let matcher
if (text.key == startKey && text.key == endKey) {
return startOffset <= index && index < endOffset
} else if (text.key == startKey) {
return startOffset <= index
} else if (text.key == endKey) {
return index < endOffset
} else {
return true
}
}
/**
* Transforms.
*/
for (var key in Transforms) {
Node[key] = Transforms[key]
}
2016-06-15 12:07:12 -07:00
/**
* Export.
*/
export default Node