1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-09-08 22:20:41 +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 Range from './range'
import Text from './text' import Text from './text'
import generateKey from '../utils/generate-key' import generateKey from '../utils/generate-key'
import isIndexInRange from '../utils/is-index-in-range'
import memoize from '../utils/memoize' import memoize from '../utils/memoize'
/** /**
@@ -469,22 +468,7 @@ class Node {
*/ */
getCharacters() { getCharacters() {
const array = this.getCharactersAsArray() return this.getTexts().flatMap(t => t.characters)
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())
}, [])
} }
/** /**
@@ -495,28 +479,23 @@ class Node {
*/ */
getCharactersAtRange(range) { 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) 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) => { return this.getTextsAtRange(range).flatMap(t => {
const chars = text.characters if (t.key === startKey) {
.filter((char, i) => isIndexInRange(i, text, range)) return t.characters.slice(startOffset)
.toArray() }
if (t.key === endKey) {
return arr.concat(chars) return t.characters.slice(0, endOffset)
}, []) }
return t.characters
})
} }
/** /**
@@ -1024,9 +1003,13 @@ class Node {
*/ */
getMarksAsArray() { getMarksAsArray() {
return this.nodes.reduce((marks, node) => { // PERF: use only one concat rather than multiple concat
return marks.concat(node.getMarksAsArray()) // 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) { getMarksAtRange(range) {
const array = this.getMarksAtRangeAsArray(range) return new Set(this.getOrderedMarksAtRange(range))
return new Set(array)
} }
/** /**
@@ -1049,8 +1031,18 @@ class Node {
*/ */
getInsertMarksAtRange(range) { getInsertMarksAtRange(range) {
const array = this.getInsertMarksAtRangeAsArray(range) range = range.normalize(this)
return new Set(array) 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) { getOrderedMarksAtRange(range) {
const array = this.getMarksAtRangeAsArray(range) range = range.normalize(this)
return new OrderedSet(array) 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) { 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) range = range.normalize(this)
if (range.isUnset) return [] if (range.isUnset) return Set()
if (range.isCollapsed) return this.getMarksAtCollapsedRangeAsArray(range) if (range.isCollapsed) {
const { startKey, startOffset } = range
return this.getCharactersAtRange(range).reduce((memo, char) => { return this.getMarksAtPosition(startKey, startOffset).toSet()
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()
} }
const text = this.getDescendant(startKey) let { startKey, endKey, startOffset, endOffset } = range
const char = text.characters.get(startOffset - 1) let startText = this.getDescendant(startKey)
if (!char) return []
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 * @param {string} key
* @return {Array} * @param {number} offset
* @return {OrderedSet}
*/ */
getActiveMarksAtRangeAsArray(range) { getMarksAtPosition(key, offset) {
range = range.normalize(this) if (offset == 0) {
if (range.isUnset) return [] const previous = this.getPreviousText(key)
if (range.isCollapsed) return this.getMarksAtCollapsedRangeAsArray(range) 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 char = previous.characters.last()
const chars = this.getCharactersAtRange(range) if (!char) return OrderedSet()
const first = chars.first() return new OrderedSet(char.marks)
if (!first) return [] }
let memo = first.marks const text = this.getDescendant(key)
const char = text.characters.get(offset - 1)
chars.slice(1).forEach(char => { if (!char) return OrderedSet()
const marks = char ? char.marks : [] return new OrderedSet(char.marks)
memo = memo.intersect(marks)
return memo.size != 0
})
return memo.toArray()
} }
/** /**
@@ -1622,8 +1633,33 @@ class Node {
*/ */
getTextsAtRange(range) { getTextsAtRange(range) {
const array = this.getTextsAtRangeAsArray(range) range = range.normalize(this)
return new List(array) 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) { getTextsAtRangeAsArray(range) {
range = range.normalize(this) range = range.normalize(this)
if (range.isUnset) return [] if (range.isUnset) return []
const { startKey, endKey } = range const { startKey, endKey } = range
const startText = this.getDescendant(startKey) return this.getTextsBetweenPositionsAsArray(startKey, endKey)
// 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)
} }
/** /**
@@ -2050,13 +2075,10 @@ function assertKey(arg) {
memoize(Node.prototype, [ memoize(Node.prototype, [
'areDescendantsSorted', 'areDescendantsSorted',
'getActiveMarksAtRangeAsArray',
'getAncestors', 'getAncestors',
'getBlocksAsArray', 'getBlocksAsArray',
'getBlocksAtRangeAsArray', 'getBlocksAtRangeAsArray',
'getBlocksByTypeAsArray', 'getBlocksByTypeAsArray',
'getCharactersAtRangeAsArray',
'getCharactersAsArray',
'getChild', 'getChild',
'getClosestBlock', 'getClosestBlock',
'getClosestInline', 'getClosestInline',
@@ -2076,8 +2098,9 @@ memoize(Node.prototype, [
'getInlinesAtRangeAsArray', 'getInlinesAtRangeAsArray',
'getInlinesByTypeAsArray', 'getInlinesByTypeAsArray',
'getMarksAsArray', 'getMarksAsArray',
'getMarksAtRangeAsArray', 'getMarksAtPosition',
'getInsertMarksAtRangeAsArray', 'getOrderedMarksBetweenPositions',
'getInsertMarksAtRange',
'getKeysAsArray', 'getKeysAsArray',
'getLastText', 'getLastText',
'getMarksByTypeAsArray', 'getMarksByTypeAsArray',
@@ -2098,7 +2121,7 @@ memoize(Node.prototype, [
'getTextAtOffset', 'getTextAtOffset',
'getTextDirection', 'getTextDirection',
'getTextsAsArray', 'getTextsAsArray',
'getTextsAtRangeAsArray', 'getTextsBetweenPositionsAsArray',
'isLeafBlock', 'isLeafBlock',
'isLeafInline', 'isLeafInline',
'validate', 'validate',

View File

@@ -296,6 +296,66 @@ class Text extends Record(DEFAULTS) {
return leaves 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. * Get all of the marks on the text.
* *
@@ -314,9 +374,19 @@ class Text extends Record(DEFAULTS) {
*/ */
getMarksAsArray() { getMarksAsArray() {
return this.characters.reduce((array, char) => { if (this.characters.size === 0) return []
return array.concat(char.marks.toArray()) 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 read methods.
*/ */
memoize(Text.prototype, ['getMarks', 'getMarksAsArray'], {
takesArguments: false,
})
memoize(Text.prototype, [ memoize(Text.prototype, [
'getDecoratedCharacters', 'getDecoratedCharacters',
'getDecorations', 'getDecorations',
'getLeaves', 'getLeaves',
'getActiveMarks',
'getMarks',
'getMarksAsArray',
'getMarksAtIndex', 'getMarksAtIndex',
'validate', '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