1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-31 19:01:54 +02:00

Optimize getMarks(AtRange), getActiveMarksAtRange, getInsertMarksAtRange, remove the dependency of getCharacters (#1808)

* Rewrite getCharacters(AtRange); rewrite getMarks

* Single concat for getMarks

* getActiveMarksAtRange

* getMarksAtRange and getInsertMakrsAtRange

* fix typo

* import getTextsBetweenPositionsAsArray to run getMarks

* Fix eslint error

* Restore getTextsAtRange

* Remove getMarksAtCollapsedRange; Add annotations

* Annotation

* Fix endTexts

* Fix typos

* Fix long line

* Fix getTextsAtRange List; Fix getTextsAtRangeAsArray

* Explain early return for character.marks short cut

* Styling

* Styling

* Styling
This commit is contained in:
Jinxuan Zhu
2018-05-10 22:15:33 -04:00
committed by Ian Storm Taylor
parent 2298d2dda1
commit 49c84cfaec
3 changed files with 255 additions and 193 deletions

View File

@@ -10,7 +10,6 @@ import Inline from './inline'
import Range from './range'
import Text from './text'
import generateKey from '../utils/generate-key'
import isIndexInRange from '../utils/is-index-in-range'
import memoize from '../utils/memoize'
/**
@@ -469,22 +468,7 @@ class Node {
*/
getCharacters() {
const array = this.getCharactersAsArray()
return new List(array)
}
/**
* Get all of the characters for every text node as an array
*
* @return {Array}
*/
getCharactersAsArray() {
return this.nodes.reduce((arr, node) => {
return node.object == 'text'
? arr.concat(node.characters.toArray())
: arr.concat(node.getCharactersAsArray())
}, [])
return this.getTexts().flatMap(t => t.characters)
}
/**
@@ -495,28 +479,23 @@ class Node {
*/
getCharactersAtRange(range) {
const array = this.getCharactersAtRangeAsArray(range)
return new List(array)
}
/**
* Get a list of the characters in a `range` as an array.
*
* @param {Range} range
* @return {Array}
*/
getCharactersAtRangeAsArray(range) {
range = range.normalize(this)
if (range.isUnset) return []
if (range.isUnset) return List()
const { startKey, endKey, startOffset, endOffset } = range
if (startKey === endKey) {
const endText = this.getDescendant(endKey)
return endText.characters.slice(startOffset, endOffset)
}
return this.getTextsAtRange(range).reduce((arr, text) => {
const chars = text.characters
.filter((char, i) => isIndexInRange(i, text, range))
.toArray()
return arr.concat(chars)
}, [])
return this.getTextsAtRange(range).flatMap(t => {
if (t.key === startKey) {
return t.characters.slice(startOffset)
}
if (t.key === endKey) {
return t.characters.slice(0, endOffset)
}
return t.characters
})
}
/**
@@ -1024,9 +1003,13 @@ class Node {
*/
getMarksAsArray() {
return this.nodes.reduce((marks, node) => {
return marks.concat(node.getMarksAsArray())
}, [])
// PERF: use only one concat rather than multiple concat
// becuase one concat is faster
const result = []
this.nodes.forEach(node => {
result.push(node.getMarksAsArray())
})
return Array.prototype.concat.apply([], result)
}
/**
@@ -1037,8 +1020,7 @@ class Node {
*/
getMarksAtRange(range) {
const array = this.getMarksAtRangeAsArray(range)
return new Set(array)
return new Set(this.getOrderedMarksAtRange(range))
}
/**
@@ -1049,8 +1031,18 @@ class Node {
*/
getInsertMarksAtRange(range) {
const array = this.getInsertMarksAtRangeAsArray(range)
return new Set(array)
range = range.normalize(this)
if (range.isUnset) return Set()
if (range.isCollapsed) {
// PERF: range is not cachable, use key and offset as proxies for cache
return this.getMarksAtPosition(range.startKey, range.startOffset)
}
const text = this.getDescendant(range.startKey)
const char = text.characters.get(range.startOffset)
if (!char) return Set()
return char.marks
}
/**
@@ -1061,8 +1053,54 @@ class Node {
*/
getOrderedMarksAtRange(range) {
const array = this.getMarksAtRangeAsArray(range)
return new OrderedSet(array)
range = range.normalize(this)
if (range.isUnset) return OrderedSet()
if (range.isCollapsed) {
// PERF: range is not cachable, use key and offset as proxies for cache
return this.getMarksAtPosition(range.startKey, range.startOffset)
}
const { startKey, startOffset, endKey, endOffset } = range
return this.getOrderedMarksBetweenPositions(
startKey,
startOffset,
endKey,
endOffset
)
}
/**
* Get a set of the marks in a `range`.
* PERF: arguments use key and offset for utilizing cache
*
* @param {string} startKey
* @param {number} startOffset
* @param {string} endKey
* @param {number} endOffset
* @returns {OrderedSet<Mark>}
*/
getOrderedMarksBetweenPositions(startKey, startOffset, endKey, endOffset) {
if (startKey === endKey) {
const startText = this.getDescendant(startKey)
return startText.getMarksBetweenOffsets(startOffset, endOffset)
}
const texts = this.getTextsBetweenPositionsAsArray(startKey, endKey)
return OrderedSet().withMutations(result => {
texts.forEach(text => {
if (text.key === startKey) {
result.union(
text.getMarksBetweenOffsets(startOffset, text.text.length)
)
} else if (text.key === endKey) {
result.union(text.getMarksBetweenOffsets(0, endOffset))
} else {
result.union(text.getMarks())
}
})
})
}
/**
@@ -1073,108 +1111,81 @@ class Node {
*/
getActiveMarksAtRange(range) {
const array = this.getActiveMarksAtRangeAsArray(range)
return new Set(array)
}
/**
* Get a set of the marks in a `range`, by unioning.
*
* @param {Range} range
* @return {Array}
*/
getMarksAtRangeAsArray(range) {
range = range.normalize(this)
if (range.isUnset) return []
if (range.isCollapsed) return this.getMarksAtCollapsedRangeAsArray(range)
return this.getCharactersAtRange(range).reduce((memo, char) => {
if (char) {
char.marks.toArray().forEach(c => memo.push(c))
}
return memo
}, [])
}
/**
* Get a set of the marks in a `range` for insertion behavior.
*
* @param {Range} range
* @return {Array}
*/
getInsertMarksAtRangeAsArray(range) {
range = range.normalize(this)
if (range.isUnset) return []
if (range.isCollapsed) return this.getMarksAtCollapsedRangeAsArray(range)
const text = this.getDescendant(range.startKey)
const char = text.characters.get(range.startOffset)
if (!char) return []
return char.marks.toArray()
}
/**
* Get a set of marks in a `range`, by treating it as collapsed.
*
* @param {Range} range
* @return {Array}
*/
getMarksAtCollapsedRangeAsArray(range) {
if (range.isUnset) return []
const { startKey, startOffset } = range
if (startOffset == 0) {
const previous = this.getPreviousText(startKey)
if (!previous || previous.text.length == 0) return []
if (
this.getClosestBlock(startKey) !== this.getClosestBlock(previous.key)
) {
return []
}
const char = previous.characters.get(previous.text.length - 1)
if (!char) return []
return char.marks.toArray()
if (range.isUnset) return Set()
if (range.isCollapsed) {
const { startKey, startOffset } = range
return this.getMarksAtPosition(startKey, startOffset).toSet()
}
const text = this.getDescendant(startKey)
const char = text.characters.get(startOffset - 1)
if (!char) return []
let { startKey, endKey, startOffset, endOffset } = range
let startText = this.getDescendant(startKey)
return char.marks.toArray()
if (startKey !== endKey) {
while (startKey !== endKey && endOffset === 0) {
const endText = this.getPreviousText(endKey)
endKey = endText.key
endOffset = endText.text.length
}
while (startKey !== endKey && startOffset === startText.text.length) {
startText = this.getNextText(startKey)
startKey = startText.key
startOffset = 0
}
}
if (startKey === endKey) {
return startText.getActiveMarksBetweenOffsets(startOffset, endOffset)
}
const startMarks = startText.getActiveMarksBetweenOffsets(
startOffset,
startText.text.length
)
if (startMarks.size === 0) return Set()
const endText = this.getDescendant(endKey)
const endMarks = endText.getActiveMarksBetweenOffsets(0, endOffset)
let marks = startMarks.intersect(endMarks)
// If marks is already empty, the active marks is empty
if (marks.size === 0) return marks
let text = this.getNextText(startKey)
while (text.key !== endKey) {
if (text.text.length !== 0) {
marks = marks.intersect(text.getActiveMarks())
if (marks.size === 0) return Set()
}
text = this.getNextText(text.key)
}
return marks
}
/**
* Get a set of marks in a `range`, by intersecting.
* Get a set of marks in a `position`, the equivalent of a collapsed range
*
* @param {Range} range
* @return {Array}
* @param {string} key
* @param {number} offset
* @return {OrderedSet}
*/
getActiveMarksAtRangeAsArray(range) {
range = range.normalize(this)
if (range.isUnset) return []
if (range.isCollapsed) return this.getMarksAtCollapsedRangeAsArray(range)
getMarksAtPosition(key, offset) {
if (offset == 0) {
const previous = this.getPreviousText(key)
if (!previous || previous.text.length == 0) return OrderedSet()
if (this.getClosestBlock(key) !== this.getClosestBlock(previous.key)) {
return OrderedSet()
}
// Otherwise, get a set of the marks for each character in the range.
const chars = this.getCharactersAtRange(range)
const first = chars.first()
if (!first) return []
const char = previous.characters.last()
if (!char) return OrderedSet()
return new OrderedSet(char.marks)
}
let memo = first.marks
chars.slice(1).forEach(char => {
const marks = char ? char.marks : []
memo = memo.intersect(marks)
return memo.size != 0
})
return memo.toArray()
const text = this.getDescendant(key)
const char = text.characters.get(offset - 1)
if (!char) return OrderedSet()
return new OrderedSet(char.marks)
}
/**
@@ -1622,8 +1633,33 @@ class Node {
*/
getTextsAtRange(range) {
const array = this.getTextsAtRangeAsArray(range)
return new List(array)
range = range.normalize(this)
if (range.isUnset) return List()
const { startKey, endKey } = range
return new List(this.getTextsBetweenPositionsAsArray(startKey, endKey))
}
/**
* Get all of the text nodes in a `range` as an array.
* PERF: use key in arguments for cache
*
* @param {string} startKey
* @param {string} endKey
* @returns {Array}
*/
getTextsBetweenPositionsAsArray(startKey, endKey) {
const startText = this.getDescendant(startKey)
// PERF: the most common case is when the range is in a single text node,
// where we can avoid a lot of iterating of the tree.
if (startKey == endKey) return [startText]
const endText = this.getDescendant(endKey)
const texts = this.getTextsAsArray()
const start = texts.indexOf(startText)
const end = texts.indexOf(endText, start)
return texts.slice(start, end + 1)
}
/**
@@ -1636,19 +1672,8 @@ class Node {
getTextsAtRangeAsArray(range) {
range = range.normalize(this)
if (range.isUnset) return []
const { startKey, endKey } = range
const startText = this.getDescendant(startKey)
// PERF: the most common case is when the range is in a single text node,
// where we can avoid a lot of iterating of the tree.
if (startKey == endKey) return [startText]
const endText = this.getDescendant(endKey)
const texts = this.getTextsAsArray()
const start = texts.indexOf(startText)
const end = texts.indexOf(endText)
return texts.slice(start, end + 1)
return this.getTextsBetweenPositionsAsArray(startKey, endKey)
}
/**
@@ -2050,13 +2075,10 @@ function assertKey(arg) {
memoize(Node.prototype, [
'areDescendantsSorted',
'getActiveMarksAtRangeAsArray',
'getAncestors',
'getBlocksAsArray',
'getBlocksAtRangeAsArray',
'getBlocksByTypeAsArray',
'getCharactersAtRangeAsArray',
'getCharactersAsArray',
'getChild',
'getClosestBlock',
'getClosestInline',
@@ -2076,8 +2098,9 @@ memoize(Node.prototype, [
'getInlinesAtRangeAsArray',
'getInlinesByTypeAsArray',
'getMarksAsArray',
'getMarksAtRangeAsArray',
'getInsertMarksAtRangeAsArray',
'getMarksAtPosition',
'getOrderedMarksBetweenPositions',
'getInsertMarksAtRange',
'getKeysAsArray',
'getLastText',
'getMarksByTypeAsArray',
@@ -2098,7 +2121,7 @@ memoize(Node.prototype, [
'getTextAtOffset',
'getTextDirection',
'getTextsAsArray',
'getTextsAtRangeAsArray',
'getTextsBetweenPositionsAsArray',
'isLeafBlock',
'isLeafInline',
'validate',

View File

@@ -296,6 +296,66 @@ class Text extends Record(DEFAULTS) {
return leaves
}
/**
* Get all of the active marks on between two offsets
*
* @return {Set<Mark>}
*/
getActiveMarksBetweenOffsets(startOffset, endOffset) {
if (startOffset === 0 && endOffset === this.characters.size) {
return this.getActiveMarks()
}
const startCharacter = this.characters.get(startOffset)
if (!startCharacter) return Set()
const result = startCharacter.marks
if (result.size === 0) return result
return result.withMutations(x => {
for (let index = startOffset + 1; index < endOffset; index++) {
const c = this.characters.get(index)
x.intersect(c.marks)
if (x.size === 0) return
}
})
}
/**
* Get all of the active marks on the text
*
* @return {Set<Mark>}
*/
getActiveMarks() {
if (this.characters.size === 0) return Set()
const result = this.characters.first().marks
if (result.size === 0) return result
return result.withMutations(x => {
this.characters.forEach(c => {
x.intersect(c.marks)
if (x.size === 0) return false
})
})
}
/**
* Get all of the marks on between two offsets
*
* @return {OrderedSet<Mark>}
*/
getMarksBetweenOffsets(startOffset, endOffset) {
if (startOffset === 0 && endOffset === this.characters.size) {
return this.getMarks()
}
return new OrderedSet().withMutations(result => {
for (let index = startOffset; index < endOffset; index++) {
const c = this.characters.get(index)
result.union(c.marks)
}
})
}
/**
* Get all of the marks on the text.
*
@@ -314,9 +374,19 @@ class Text extends Record(DEFAULTS) {
*/
getMarksAsArray() {
return this.characters.reduce((array, char) => {
return array.concat(char.marks.toArray())
}, [])
if (this.characters.size === 0) return []
const first = this.characters.first().marks
let previousMark = first
const result = []
this.characters.forEach(c => {
// If the character marks is the same with the
// previous characters, we do not need to
// add the marks twice
if (c.marks === previousMark) return true
previousMark = c.marks
result.push(previousMark.toArray())
})
return Array.prototype.concat.apply(first.toArray(), result)
}
/**
@@ -519,14 +589,13 @@ Text.prototype[MODEL_TYPES.TEXT] = true
* Memoize read methods.
*/
memoize(Text.prototype, ['getMarks', 'getMarksAsArray'], {
takesArguments: false,
})
memoize(Text.prototype, [
'getDecoratedCharacters',
'getDecorations',
'getLeaves',
'getActiveMarks',
'getMarks',
'getMarksAsArray',
'getMarksAtIndex',
'validate',
])

View File

@@ -1,30 +0,0 @@
/**
* Check if an `index` of a `text` node is in a `range`.
*
* @param {Number} index
* @param {Text} text
* @param {Range} range
* @return {Boolean}
*/
function isIndexInRange(index, text, range) {
const { startKey, startOffset, endKey, endOffset } = range
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
}
}
/**
* Export.
*
* @type {Function}
*/
export default isIndexInRange