diff --git a/packages/slate-hyperscript/src/index.js b/packages/slate-hyperscript/src/index.js
index 6f4098c5e..29b9a3a7f 100644
--- a/packages/slate-hyperscript/src/index.js
+++ b/packages/slate-hyperscript/src/index.js
@@ -273,7 +273,7 @@ function createChildren(children, options = {}) {
const firstNodeOrText = children.find(c => typeof c !== 'string')
const firstText = Text.isText(firstNodeOrText) ? firstNodeOrText : null
const key = options.key ? options.key : firstText ? firstText.key : undefined
- let node = Text.create({ key })
+ let node = Text.create({ key, leaves: [{ text: '', marks: options.marks }] })
// Create a helper to update the current node while preserving any stored
// anchor or focus information.
@@ -290,10 +290,18 @@ function createChildren(children, options = {}) {
// If the child is a non-text node, push the current node and the new child
// onto the array, then creating a new node for future selection tracking.
if (Node.isNode(child) && !Text.isText(child)) {
- if (node.text.length || node.__anchor != null || node.__focus != null)
+ if (
+ node.text.length ||
+ node.__anchor != null ||
+ node.__focus != null ||
+ node.getMarksAtIndex(0).size
+ ) {
array.push(node)
+ }
array.push(child)
- node = isLast ? null : Text.create()
+ node = isLast
+ ? null
+ : Text.create({ leaves: [{ text: '', marks: options.marks }] })
length = 0
}
diff --git a/packages/slate-hyperscript/test/default/empty-marked-text.js b/packages/slate-hyperscript/test/default/empty-marked-text.js
new file mode 100644
index 000000000..b0deba76d
--- /dev/null
+++ b/packages/slate-hyperscript/test/default/empty-marked-text.js
@@ -0,0 +1,42 @@
+/** @jsx h */
+
+import h from '../..'
+
+export const input = (
+
+
+
+
+
+)
+
+export const output = {
+ object: 'document',
+ data: {},
+ nodes: [
+ {
+ object: 'block',
+ type: 'paragraph',
+ isVoid: false,
+ data: {},
+ nodes: [
+ {
+ object: 'text',
+ leaves: [
+ {
+ object: 'leaf',
+ text: '',
+ marks: [
+ {
+ type: 'bold',
+ object: 'mark',
+ data: {},
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+}
diff --git a/packages/slate/src/models/leaf.js b/packages/slate/src/models/leaf.js
index f7832e7cd..40ee50f11 100644
--- a/packages/slate/src/models/leaf.js
+++ b/packages/slate/src/models/leaf.js
@@ -13,7 +13,7 @@ import Mark from './mark'
*/
const DEFAULTS = {
- marks: new Set(),
+ marks: Set(),
text: '',
}
@@ -49,6 +49,115 @@ class Leaf extends Record(DEFAULTS) {
)
}
+ /**
+ * Create a valid List of `Leaf` from `leaves`
+ *
+ * @param {List} leaves
+ * @return {List}
+ */
+
+ static createLeaves(leaves) {
+ if (leaves.size <= 1) return leaves
+
+ let invalid = false
+
+ // TODO: we can make this faster with [List] and then flatten
+ const result = List().withMutations(cache => {
+ // Search from the leaves left end to find invalid node;
+ leaves.findLast((leaf, index) => {
+ const firstLeaf = cache.first()
+
+ // If the first leaf of cache exist, check whether the first leaf is connectable with the current leaf
+ if (firstLeaf) {
+ // If marks equals, then the two leaves can be connected
+ if (firstLeaf.marks.equals(leaf.marks)) {
+ invalid = true
+ cache.set(0, firstLeaf.set('text', `${leaf.text}${firstLeaf.text}`))
+ return
+ }
+
+ // If the cached leaf is empty, drop the empty leaf with the upcoming leaf
+ if (firstLeaf.text === '') {
+ invalid = true
+ cache.set(0, leaf)
+ return
+ }
+
+ // If the current leaf is empty, drop the leaf
+ if (leaf.text === '') {
+ invalid = true
+ return
+ }
+ }
+
+ cache.unshift(leaf)
+ })
+ })
+
+ if (!invalid) return leaves
+ return result
+ }
+
+ /**
+ * Split a list of leaves to two lists; if the leaves are valid leaves, the returned leaves are also valid
+ * Corner Cases:
+ * 1. if offset is smaller than 0, then return [List(), leaves]
+ * 2. if offset is bigger than the text length, then return [leaves, List()]
+ *
+ * @param {List leaves
+ * @return {Array>}
+ */
+
+ static splitLeaves(leaves, offset) {
+ if (offset < 0) return [List(), leaves]
+
+ if (leaves.size === 0) {
+ return [List(), List()]
+ }
+
+ let endOffset = 0
+ let index = -1
+ let left, right
+
+ leaves.find(leaf => {
+ index++
+ const startOffset = endOffset
+ const { text } = leaf
+ endOffset += text.length
+
+ if (endOffset < offset) return false
+ if (startOffset > offset) return false
+
+ const length = offset - startOffset
+ left = leaf.set('text', text.slice(0, length))
+ right = leaf.set('text', text.slice(length))
+ return true
+ })
+
+ if (!left) return [leaves, List()]
+
+ if (left.text === '') {
+ if (index === 0) {
+ return [List.of(left), leaves]
+ }
+
+ return [leaves.take(index), leaves.skip(index)]
+ }
+
+ if (right.text === '') {
+ if (index === leaves.size - 1) {
+ return [leaves, List.of(right)]
+ }
+
+ return [leaves.take(index + 1), leaves.skip(index + 1)]
+ }
+
+ return [
+ leaves.take(index).push(left),
+ leaves.skip(index + 1).unshift(right),
+ ]
+ }
+
/**
* Create a `Leaf` list from `attrs`.
*
@@ -79,7 +188,7 @@ class Leaf extends Record(DEFAULTS) {
const leaf = new Leaf({
text,
- marks: new Set(marks.map(Mark.fromJSON)),
+ marks: Set(marks.map(Mark.fromJSON)),
})
return leaf
@@ -136,6 +245,10 @@ class Leaf extends Record(DEFAULTS) {
*/
getCharacters() {
+ logger.deprecate(
+ 'slate@0.34.0',
+ 'The `characters` property of Slate objects is deprecated'
+ )
const { marks } = this
const characters = Character.createList(
this.text.split('').map(char => {
@@ -149,6 +262,48 @@ class Leaf extends Record(DEFAULTS) {
return characters
}
+ /**
+ * Update a `mark` at leaf, replace with newMark
+ *
+ * @param {Mark} mark
+ * @param {Mark} newMark
+ * @returns {Leaf}
+ */
+
+ updateMark(mark, newMark) {
+ const { marks } = this
+ if (newMark.equals(mark)) return this
+ if (!marks.has(mark)) return this
+ const newMarks = marks.withMutations(collection => {
+ collection.remove(mark).add(newMark)
+ })
+ return this.set('marks', newMarks)
+ }
+
+ /**
+ * Add a `set` of marks at `index` and `length`.
+ *
+ * @param {Set} set
+ * @returns {Text}
+ */
+
+ addMarks(set) {
+ const { marks } = this
+ return this.set('marks', marks.union(set))
+ }
+
+ /**
+ * Remove a `mark` at `index` and `length`.
+ *
+ * @param {Mark} mark
+ * @returns {Text}
+ */
+
+ removeMark(mark) {
+ const { marks } = this
+ return this.set('marks', marks.remove(mark))
+ }
+
/**
* Return a JSON representation of the leaf.
*
diff --git a/packages/slate/src/models/mark.js b/packages/slate/src/models/mark.js
index 2fb160e96..167ea5329 100644
--- a/packages/slate/src/models/mark.js
+++ b/packages/slate/src/models/mark.js
@@ -63,7 +63,7 @@ class Mark extends Record(DEFAULTS) {
}
if (elements == null) {
- return new Set()
+ return Set()
}
throw new Error(
diff --git a/packages/slate/src/models/node.js b/packages/slate/src/models/node.js
index 45a4e9488..bbfc29ac7 100644
--- a/packages/slate/src/models/node.js
+++ b/packages/slate/src/models/node.js
@@ -1036,11 +1036,9 @@ class Node {
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
+ const { startKey, startOffset } = range
+ const text = this.getDescendant(startKey)
+ return text.getMarksAtIndex(startOffset + 1)
}
/**
@@ -1164,26 +1162,28 @@ class Node {
*
* @param {string} key
* @param {number} offset
- * @return {OrderedSet}
+ * @return {Set}
*/
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()
- }
+ const text = this.getDescendant(key)
+ const currentMarks = text.getMarksAtIndex(offset)
+ if (offset !== 0) return currentMarks
+ const closestBlock = this.getClosestBlock(key)
- const char = previous.characters.last()
- if (!char) return OrderedSet()
- return new OrderedSet(char.marks)
+ if (closestBlock.text === '') {
+ // insert mark for empty block; the empty block are often created by split node or add marks in a range including empty blocks
+ return currentMarks
}
- const text = this.getDescendant(key)
- const char = text.characters.get(offset - 1)
- if (!char) return OrderedSet()
- return new OrderedSet(char.marks)
+ const previous = this.getPreviousText(key)
+ if (!previous) return Set()
+
+ if (closestBlock.hasDescendant(previous.key)) {
+ return previous.getMarksAtIndex(previous.text.length)
+ }
+
+ return currentMarks
}
/**
@@ -1857,10 +1857,9 @@ class Node {
)
}
- // If the nodes are text nodes, concatenate their characters together.
+ // If the nodes are text nodes, concatenate their leaves together
if (one.object == 'text') {
- const characters = one.characters.concat(two.characters)
- one = one.set('characters', characters)
+ one = one.mergeText(two)
} else {
// Otherwise, concatenate their child nodes together.
const nodes = one.nodes.concat(two.nodes)
@@ -1942,7 +1941,7 @@ class Node {
throw new Error(`Could not find a descendant node with key "${key}".`)
const index = parent.nodes.findIndex(n => n.key === key)
- const nodes = parent.nodes.splice(index, 1)
+ const nodes = parent.nodes.delete(index)
parent = parent.set('nodes', nodes)
node = node.updateNode(parent)
@@ -1957,7 +1956,7 @@ class Node {
*/
removeNode(index) {
- const nodes = this.nodes.splice(index, 1)
+ const nodes = this.nodes.delete(index)
return this.set('nodes', nodes)
}
@@ -1978,10 +1977,7 @@ class Node {
// If the child is a text node, the `position` refers to the text offset at
// which to split it.
if (child.object == 'text') {
- const befores = child.characters.take(position)
- const afters = child.characters.skip(position)
- one = child.set('characters', befores)
- two = child.set('characters', afters).regenerateKey()
+ ;[one, two] = child.splitText(position)
} else {
// Otherwise, if the child is not a text node, the `position` refers to the
// index at which to split its children.
diff --git a/packages/slate/src/models/text.js b/packages/slate/src/models/text.js
index fbed967d6..c42efda2c 100644
--- a/packages/slate/src/models/text.js
+++ b/packages/slate/src/models/text.js
@@ -1,9 +1,7 @@
import isPlainObject from 'is-plain-object'
import logger from 'slate-dev-logger'
-import { List, OrderedSet, Record, Set, is } from 'immutable'
+import { List, OrderedSet, Record, Set } from 'immutable'
-import Character from './character'
-import Mark from './mark'
import Leaf from './leaf'
import MODEL_TYPES, { isType } from '../constants/model-types'
import generateKey from '../utils/generate-key'
@@ -16,7 +14,7 @@ import memoize from '../utils/memoize'
*/
const DEFAULTS = {
- characters: new List(),
+ leaves: List(),
key: undefined,
}
@@ -87,14 +85,19 @@ class Text extends Record(DEFAULTS) {
return object
}
- const { leaves = [], key = generateKey() } = object
+ const { key = generateKey() } = object
+ let { leaves = List() } = object
- const characters = leaves
- .map(Leaf.fromJSON)
- .reduce((l, r) => l.concat(r.getCharacters()), new List())
+ if (Array.isArray(leaves)) {
+ leaves = List(leaves.map(x => Leaf.create(x)))
+ } else if (List.isList(leaves)) {
+ leaves = leaves.map(x => Leaf.create(x))
+ } else {
+ throw new Error('leaves must be either Array or Immutable.List')
+ }
const node = new Text({
- characters,
+ leaves: Leaf.createLeaves(leaves),
key,
})
@@ -162,7 +165,61 @@ class Text extends Record(DEFAULTS) {
*/
get text() {
- return this.characters.reduce((string, char) => string + char.text, '')
+ return this.getString()
+ }
+
+ /**
+ * Get the concatenated text of the node, cached for text getter
+ *
+ * @returns {String}
+ */
+
+ getString() {
+ return this.leaves.reduce((string, leaf) => string + leaf.text, '')
+ }
+
+ /**
+ * Get the concatenated characters of the node;
+ *
+ * @returns {String}
+ */
+
+ get characters() {
+ return this.leaves.flatMap(x => x.getCharacters())
+ }
+
+ /**
+ * Find the 'first' leaf at offset; By 'first' the alorighthm prefers `endOffset === offset` than `startOffset === offset`
+ * Corner Cases:
+ * 1. if offset is negative, return the first leaf;
+ * 2. if offset is larger than text length, the leaf is null, startOffset, endOffset and index is of the last leaf
+ *
+ * @param {number}
+ * @returns {Object}
+ * @property {number} startOffset
+ * @property {number} endOffset
+ * @property {number} index
+ * @property {Leaf} leaf
+ */
+
+ searchLeafAtOffset(offset) {
+ let endOffset = 0
+ let startOffset = 0
+ let index = -1
+
+ const leaf = this.leaves.find(l => {
+ index++
+ startOffset = endOffset
+ endOffset = startOffset + l.text.length
+ return endOffset >= offset
+ })
+
+ return {
+ leaf,
+ endOffset,
+ index,
+ startOffset,
+ }
}
/**
@@ -175,12 +232,14 @@ class Text extends Record(DEFAULTS) {
*/
addMark(index, length, mark) {
- const marks = new Set([mark])
+ const marks = Set.of(mark)
return this.addMarks(index, length, marks)
}
/**
* Add a `set` of marks at `index` and `length`.
+ * Corner Cases:
+ * 1. If empty text, and if length === 0 and index === 0
*
* @param {Number} index
* @param {Number} length
@@ -189,42 +248,30 @@ class Text extends Record(DEFAULTS) {
*/
addMarks(index, length, set) {
- const characters = this.characters.map((char, i) => {
- if (i < index) return char
- if (i >= index + length) return char
- let { marks } = char
- marks = marks.union(set)
- char = char.set('marks', marks)
- return char
- })
+ if (this.text === '' && length === 0 && index === 0) {
+ const { leaves } = this
+ const first = leaves.first()
- return this.set('characters', characters)
- }
+ if (!first) {
+ return this.set(
+ 'leaves',
+ List.of(Leaf.fromJSON({ text: '', marks: set }))
+ )
+ }
- /**
- * Derive a set of decorated characters with `decorations`.
- *
- * @param {List} decorations
- * @return {List}
- */
+ const newFirst = first.addMarks(set)
+ if (newFirst === first) return this
+ return this.set('leaves', List.of(newFirst))
+ }
- getDecoratedCharacters(decorations) {
- let node = this
- const { key, characters } = node
+ if (this.text === '') return this
+ if (length === 0) return this
+ if (index >= this.text.length) return this
- // PERF: Exit early if there are no characters to be decorated.
- if (characters.size == 0) return characters
-
- decorations.forEach(range => {
- const { startKey, endKey, startOffset, endOffset, marks } = range
- const hasStart = startKey == key
- const hasEnd = endKey == key
- const index = hasStart ? startOffset : 0
- const length = hasEnd ? endOffset - index : characters.size
- node = node.addMarks(index, length, marks)
- })
-
- return node.characters
+ const [before, bundle] = Leaf.splitLeaves(this.leaves, index)
+ const [middle, after] = Leaf.splitLeaves(bundle, length)
+ const leaves = before.concat(middle.map(x => x.addMarks(set)), after)
+ return this.setLeaves(leaves)
}
/**
@@ -239,82 +286,85 @@ class Text extends Record(DEFAULTS) {
}
/**
- * Derive the leaves for a list of `characters`.
+ * Derive the leaves for a list of `decorations`.
*
* @param {Array|Void} decorations (optional)
* @return {List}
*/
getLeaves(decorations = []) {
- const characters = this.getDecoratedCharacters(decorations)
- let leaves = []
+ let { leaves } = this
+ if (leaves.size === 0) return List.of(Leaf.create({}))
+ if (!decorations || decorations.length === 0) return leaves
+ if (this.text.length === 0) return leaves
+ const { key } = this
- // PERF: cache previous values for faster lookup.
- let prevChar
- let prevLeaf
+ decorations.forEach(range => {
+ const { startKey, endKey, startOffset, endOffset, marks } = range
+ const hasStart = startKey == key
+ const hasEnd = endKey == key
- // If there are no characters, return one empty range.
- if (characters.size == 0) {
- leaves.push({})
- } else {
- // Otherwise, loop the characters and build the leaves...
- characters.forEach((char, i) => {
- const { marks, text } = char
+ if (hasStart && hasEnd) {
+ const index = hasStart ? startOffset : 0
+ const length = hasEnd ? endOffset - index : this.text.length - index
- // The first one can always just be created.
- if (i == 0) {
- prevChar = char
- prevLeaf = { text, marks }
- leaves.push(prevLeaf)
+ if (length < 1) return
+ if (index >= this.text.length) return
+
+ if (index !== 0 || length < this.text.length) {
+ const [before, bundle] = Leaf.splitLeaves(this.leaves, index)
+ const [middle, after] = Leaf.splitLeaves(bundle, length)
+ leaves = before.concat(middle.map(x => x.addMarks(marks)), after)
return
}
+ }
- // Otherwise, compare the current and previous marks.
- const prevMarks = prevChar.marks
- const isSame = is(marks, prevMarks)
+ leaves = leaves.map(x => x.addMarks(marks))
+ })
- // If the marks are the same, add the text to the previous range.
- if (isSame) {
- prevChar = char
- prevLeaf.text += text
- return
- }
-
- // Otherwise, create a new range.
- prevChar = char
- prevLeaf = { text, marks }
- leaves.push(prevLeaf)
- }, [])
- }
-
- // PERF: convert the leaves to immutable objects after iterating.
- leaves = new List(leaves.map(object => new Leaf(object)))
-
- // Return the leaves.
- return leaves
+ if (leaves === this.leaves) return leaves
+ return Leaf.createLeaves(leaves)
}
/**
* Get all of the active marks on between two offsets
+ * Corner Cases:
+ * 1. if startOffset is equal or bigger than endOffset, then return Set();
+ * 2. If no text is selected between start and end, then return Set()
*
* @return {Set}
*/
getActiveMarksBetweenOffsets(startOffset, endOffset) {
- if (startOffset === 0 && endOffset === this.characters.size) {
+ if (startOffset <= 0 && endOffset >= this.text.length) {
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
+
+ if (startOffset >= endOffset) return Set()
+ // For empty text in a paragraph, use getActiveMarks;
+ if (this.text === '') return this.getActiveMarks()
+
+ let result = null
+ let leafEnd = 0
+
+ this.leaves.forEach(leaf => {
+ const leafStart = leafEnd
+ leafEnd = leafStart + leaf.text.length
+
+ if (leafEnd <= startOffset) return
+ if (leafStart >= endOffset) return false
+
+ if (!result) {
+ result = leaf.marks
+ return
}
+
+ result = result.intersect(leaf.marks)
+ if (result && result.size === 0) return false
+ return false
})
+
+ return result || Set()
}
/**
@@ -324,12 +374,13 @@ class Text extends Record(DEFAULTS) {
*/
getActiveMarks() {
- if (this.characters.size === 0) return Set()
- const result = this.characters.first().marks
+ if (this.leaves.size === 0) return Set()
+
+ const result = this.leaves.first().marks
if (result.size === 0) return result
return result.withMutations(x => {
- this.characters.forEach(c => {
+ this.leaves.forEach(c => {
x.intersect(c.marks)
if (x.size === 0) return false
})
@@ -338,20 +389,41 @@ class Text extends Record(DEFAULTS) {
/**
* Get all of the marks on between two offsets
+ * Corner Cases:
+ * 1. if startOffset is equal or bigger than endOffset, then return Set();
+ * 2. If no text is selected between start and end, then return Set()
*
* @return {OrderedSet}
*/
getMarksBetweenOffsets(startOffset, endOffset) {
- if (startOffset === 0 && endOffset === this.characters.size) {
+ if (startOffset <= 0 && endOffset >= this.text.length) {
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)
+
+ if (startOffset >= endOffset) return Set()
+ // For empty text in a paragraph, use getActiveMarks;
+ if (this.text === '') return this.getActiveMarks()
+
+ let result = null
+ let leafEnd = 0
+
+ this.leaves.forEach(leaf => {
+ const leafStart = leafEnd
+ leafEnd = leafStart + leaf.text.length
+
+ if (leafEnd <= startOffset) return
+ if (leafStart >= endOffset) return false
+
+ if (!result) {
+ result = leaf.marks
+ return
}
+
+ result = result.union(leaf.marks)
})
+
+ return result || Set()
}
/**
@@ -372,34 +444,35 @@ class Text extends Record(DEFAULTS) {
*/
getMarksAsArray() {
- if (this.characters.size === 0) return []
- const first = this.characters.first().marks
- let previousMark = first
+ if (this.leaves.size === 0) return []
+ const first = this.leaves.first().marks
+ if (this.leaves.size === 1) return first.toArray()
+
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())
+
+ this.leaves.forEach(leaf => {
+ result.push(leaf.marks.toArray())
})
+
return Array.prototype.concat.apply(first.toArray(), result)
}
/**
* Get the marks on the text at `index`.
+ * Corner Cases:
+ * 1. if no text is before the index, and index !== 0, then return Set()
+ * 2. (for insert after split node or mark at range) if index === 0, and text === '', then return the leaf.marks
+ * 3. if index === 0, text !== '', return Set()
+ *
*
* @param {Number} index
* @return {Set}
*/
getMarksAtIndex(index) {
- if (index == 0) return Mark.createSet()
- const { characters } = this
- const char = characters.get(index - 1)
- if (!char) return Mark.createSet()
- return char.marks
+ const { leaf } = this.searchLeafAtOffset(index)
+ if (!leaf) return Set()
+ return leaf.marks
}
/**
@@ -427,24 +500,42 @@ class Text extends Record(DEFAULTS) {
/**
* Insert `text` at `index`.
*
- * @param {Numbder} index
+ * @param {Numbder} offset
* @param {String} text
- * @param {String} marks (optional)
+ * @param {Set} marks (optional)
* @return {Text}
*/
- insertText(index, text, marks) {
- let { characters } = this
- const chars = Character.createList(
- text.split('').map(char => ({ text: char, marks }))
+ insertText(offset, text, marks) {
+ if (this.text === '') {
+ return this.set('leaves', List.of(Leaf.create({ text, marks })))
+ }
+
+ if (text.length === 0) return this
+ if (!marks) marks = Set()
+
+ const { startOffset, leaf, index } = this.searchLeafAtOffset(offset)
+ const delta = offset - startOffset
+ const beforeText = leaf.text.slice(0, delta)
+ const afterText = leaf.text.slice(delta)
+ const { leaves } = this
+
+ if (leaf.marks.equals(marks)) {
+ return this.set(
+ 'leaves',
+ leaves.set(index, leaf.set('text', beforeText + text + afterText))
+ )
+ }
+
+ const nextLeaves = leaves.splice(
+ index,
+ 1,
+ leaf.set('text', beforeText),
+ Leaf.create({ text, marks }),
+ leaf.set('text', afterText)
)
- characters = characters
- .slice(0, index)
- .concat(chars)
- .concat(characters.slice(index))
-
- return this.set('characters', characters)
+ return this.setLeaves(nextLeaves)
}
/**
@@ -468,32 +559,74 @@ class Text extends Record(DEFAULTS) {
*/
removeMark(index, length, mark) {
- const characters = this.characters.map((char, i) => {
- if (i < index) return char
- if (i >= index + length) return char
- let { marks } = char
- marks = marks.remove(mark)
- char = char.set('marks', marks)
- return char
- })
+ if (this.text === '' && index === 0 && length === 0) {
+ const first = this.leaves.first()
+ if (!first) return this
+ const newFirst = first.removeMark(mark)
+ if (newFirst === first) return this
+ return this.set('leaves', List.of(newFirst))
+ }
- return this.set('characters', characters)
+ if (length <= 0) return this
+ if (index >= this.text.length) return this
+ const [before, bundle] = Leaf.splitLeaves(this.leaves, index)
+ const [middle, after] = Leaf.splitLeaves(bundle, length)
+ const leaves = before.concat(middle.map(x => x.removeMark(mark)), after)
+ return this.setLeaves(leaves)
}
/**
- * Remove text from the text node at `index` for `length`.
+ * Remove text from the text node at `start` for `length`.
*
- * @param {Number} index
+ * @param {Number} start
* @param {Number} length
* @return {Text}
*/
- removeText(index, length) {
- let { characters } = this
- const start = index
- const end = index + length
- characters = characters.filterNot((char, i) => start <= i && i < end)
- return this.set('characters', characters)
+ removeText(start, length) {
+ if (length <= 0) return this
+ if (start >= this.text.length) return this
+
+ // PERF: For simple backspace, we can operate directly on the leaf
+ if (length === 1) {
+ const { leaf, index, startOffset } = this.searchLeafAtOffset(start)
+ const offset = start - startOffset
+
+ if (leaf) {
+ if (leaf.text.length === 1) {
+ const leaves = this.leaves.remove(index)
+ return this.setLeaves(leaves)
+ }
+
+ const beforeText = leaf.text.slice(0, offset)
+ const afterText = leaf.text.slice(offset + length)
+ const text = beforeText + afterText
+
+ if (text.length > 0) {
+ return this.set(
+ 'leaves',
+ this.leaves.set(index, leaf.set('text', text))
+ )
+ }
+ }
+ }
+
+ const [before, bundle] = Leaf.splitLeaves(this.leaves, start)
+ const after = Leaf.splitLeaves(bundle, length)[1]
+ const leaves = Leaf.createLeaves(before.concat(after))
+
+ if (leaves.size === 1) {
+ const first = leaves.first()
+
+ if (first.text === '') {
+ return this.set(
+ 'leaves',
+ List.of(first.set('marks', this.getActiveMarks()))
+ )
+ }
+ }
+
+ return this.set('leaves', leaves)
}
/**
@@ -539,18 +672,51 @@ class Text extends Record(DEFAULTS) {
updateMark(index, length, mark, properties) {
const newMark = mark.merge(properties)
- const characters = this.characters.map((char, i) => {
- if (i < index) return char
- if (i >= index + length) return char
- let { marks } = char
- if (!marks.has(mark)) return char
- marks = marks.remove(mark)
- marks = marks.add(newMark)
- char = char.set('marks', marks)
- return char
- })
+ if (this.text === '' && length === 0 && index === 0) {
+ const { leaves } = this
+ const first = leaves.first()
+ if (!first) return this
+ const newFirst = first.updateMark(mark, newMark)
+ if (newFirst === first) return this
+ return this.set('leaves', List.of(newFirst))
+ }
- return this.set('characters', characters)
+ if (length <= 0) return this
+ if (index >= this.text.length) return this
+
+ const [before, bundle] = Leaf.splitLeaves(this.leaves, index)
+ const [middle, after] = Leaf.splitLeaves(bundle, length)
+
+ const leaves = before.concat(
+ middle.map(x => x.updateMark(mark, newMark)),
+ after
+ )
+
+ return this.setLeaves(leaves)
+ }
+
+ /**
+ * Split this text and return two different texts
+ * @param {Number} position
+ * @returns {Array}
+ */
+
+ splitText(offset) {
+ const splitted = Leaf.splitLeaves(this.leaves, offset)
+ const one = this.set('leaves', splitted[0])
+ const two = this.set('leaves', splitted[1]).regenerateKey()
+ return [one, two]
+ }
+
+ /**
+ * merge this text and another text at the end
+ * @param {Text} text
+ * @returns {Text}
+ */
+
+ mergeText(text) {
+ const leaves = this.leaves.concat(text.leaves)
+ return this.setLeaves(leaves)
}
/**
@@ -566,7 +732,7 @@ class Text extends Record(DEFAULTS) {
/**
* Get the first invalid descendant
- * PREF: Do not cache this method; because it can cause cycle reference
+ * PERF: Do not cache this method; because it can cause cycle reference
*
* @param {Schema} schema
* @returns {Text|Null}
@@ -575,6 +741,28 @@ class Text extends Record(DEFAULTS) {
getFirstInvalidDescendant(schema) {
return this.validate(schema) ? this : null
}
+
+ /**
+ * Set leaves with normalized `leaves`
+ *
+ * @param {Schema} schema
+ * @returns {Text|Null}
+ */
+
+ setLeaves(leaves) {
+ const result = Leaf.createLeaves(leaves)
+
+ if (result.size === 1) {
+ const first = result.first()
+ if (!first.marks || first.marks.size === 0) {
+ if (first.text === '') {
+ return this.set('leaves', List())
+ }
+ }
+ }
+
+ return this.set('leaves', Leaf.createLeaves(leaves))
+ }
}
/**
@@ -588,14 +776,12 @@ Text.prototype[MODEL_TYPES.TEXT] = true
*/
memoize(Text.prototype, [
- 'getDecoratedCharacters',
'getDecorations',
- 'getLeaves',
'getActiveMarks',
'getMarks',
'getMarksAsArray',
- 'getMarksAtIndex',
'validate',
+ 'getString',
])
/**
diff --git a/packages/slate/test/changes/at-current-range/add-mark/across-inlines.js b/packages/slate/test/changes/at-current-range/add-mark/across-inlines.js
index 095cbf980..6b89d38ae 100644
--- a/packages/slate/test/changes/at-current-range/add-mark/across-inlines.js
+++ b/packages/slate/test/changes/at-current-range/add-mark/across-inlines.js
@@ -31,8 +31,10 @@ export const output = (
wo
rd
+
+
an
other
diff --git a/packages/slate/test/changes/at-current-range/add-marks/across-inlines.js b/packages/slate/test/changes/at-current-range/add-marks/across-inlines.js
index 04a31271f..76667c7e4 100644
--- a/packages/slate/test/changes/at-current-range/add-marks/across-inlines.js
+++ b/packages/slate/test/changes/at-current-range/add-marks/across-inlines.js
@@ -33,8 +33,14 @@ export const output = (
rd
+
+
+
+
+
+
an
diff --git a/packages/slate/test/changes/at-current-range/insert-text/blocks-with-overlapping-marks.js b/packages/slate/test/changes/at-current-range/insert-text/blocks-with-overlapping-marks.js
new file mode 100644
index 000000000..579226956
--- /dev/null
+++ b/packages/slate/test/changes/at-current-range/insert-text/blocks-with-overlapping-marks.js
@@ -0,0 +1,41 @@
+/** @jsx h */
+
+import h from '../../../helpers/h'
+
+export default function(change) {
+ change.insertText('is ')
+}
+
+export const input = (
+
+
+
+
+ Cat
+
+
+
+
+ Cute
+
+
+
+
+)
+
+export const output = (
+
+
+
+
+ Cat
+
+
+
+
+ is Cute
+
+
+
+
+)
diff --git a/packages/slate/test/changes/at-current-range/insert-text/empty-block-with-mark.js b/packages/slate/test/changes/at-current-range/insert-text/empty-block-with-mark.js
new file mode 100644
index 000000000..4596489d4
--- /dev/null
+++ b/packages/slate/test/changes/at-current-range/insert-text/empty-block-with-mark.js
@@ -0,0 +1,31 @@
+/** @jsx h */
+
+import h from '../../../helpers/h'
+
+export default function(change) {
+ change.insertText('Cat')
+}
+
+export const input = (
+
+
+
+
+
+
+
+
+
+)
+
+export const output = (
+
+
+
+
+ Cat
+
+
+
+
+)
diff --git a/packages/slate/test/changes/at-current-range/toggle-mark/add-across-inlines.js b/packages/slate/test/changes/at-current-range/toggle-mark/add-across-inlines.js
index 236f0e787..e59b0b3a6 100644
--- a/packages/slate/test/changes/at-current-range/toggle-mark/add-across-inlines.js
+++ b/packages/slate/test/changes/at-current-range/toggle-mark/add-across-inlines.js
@@ -31,8 +31,10 @@ export const output = (
wo
rd
+
+
an
other
diff --git a/packages/slate/test/models/index.js b/packages/slate/test/models/index.js
index 47a535c9e..57c8ed47b 100644
--- a/packages/slate/test/models/index.js
+++ b/packages/slate/test/models/index.js
@@ -50,4 +50,6 @@ describe('models', () => {
}
})
})
+
+ require('./text/')
})
diff --git a/packages/slate/test/models/text/delete/across-leaves/connectable-after-remove.js b/packages/slate/test/models/text/delete/across-leaves/connectable-after-remove.js
new file mode 100644
index 000000000..d2a46fd55
--- /dev/null
+++ b/packages/slate/test/models/text/delete/across-leaves/connectable-after-remove.js
@@ -0,0 +1,19 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+
+export const input = (
+
+ Cat isvery very Cute
+
+)[0]
+
+export default function(t) {
+ return t.removeText(6, 9)
+}
+
+export const output = (
+
+ Cat is Cute
+
+)[0]
diff --git a/packages/slate/test/models/text/delete/across-leaves/in-connectable-after-remove.js b/packages/slate/test/models/text/delete/across-leaves/in-connectable-after-remove.js
new file mode 100644
index 000000000..070771c25
--- /dev/null
+++ b/packages/slate/test/models/text/delete/across-leaves/in-connectable-after-remove.js
@@ -0,0 +1,20 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+
+export const input = (
+
+ Cat isvery very Cute
+
+)[0]
+
+export default function(t) {
+ return t.removeText(6, 9)
+}
+
+export const output = (
+
+ Cat is
+ Cute
+
+)[0]
diff --git a/packages/slate/test/models/text/delete/all-text-length/differently-marked-text.js b/packages/slate/test/models/text/delete/all-text-length/differently-marked-text.js
new file mode 100644
index 000000000..380753a17
--- /dev/null
+++ b/packages/slate/test/models/text/delete/all-text-length/differently-marked-text.js
@@ -0,0 +1,15 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+
+export const input = (
+
+ Cat is Cute
+
+)[0]
+
+export default function(t) {
+ return t.removeText(0, t.text.length)
+}
+
+export const output = [0]
diff --git a/packages/slate/test/models/text/delete/all-text-length/marked-text.js b/packages/slate/test/models/text/delete/all-text-length/marked-text.js
new file mode 100644
index 000000000..ba6b9697a
--- /dev/null
+++ b/packages/slate/test/models/text/delete/all-text-length/marked-text.js
@@ -0,0 +1,11 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+
+export const input = Cat is Cute[0]
+
+export default function(t) {
+ return t.removeText(0, t.text.length)
+}
+
+export const output = [0]
diff --git a/packages/slate/test/models/text/delete/all-text-length/partial-marked-text.js b/packages/slate/test/models/text/delete/all-text-length/partial-marked-text.js
new file mode 100644
index 000000000..4b6ca177f
--- /dev/null
+++ b/packages/slate/test/models/text/delete/all-text-length/partial-marked-text.js
@@ -0,0 +1,15 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+
+export const input = (
+
+ Cat is Cute
+
+)[0]
+
+export default function(t) {
+ return t.removeText(0, t.text.length)
+}
+
+export const output = [0]
diff --git a/packages/slate/test/models/text/delete/inside-a-leaf/delete-a-char.js b/packages/slate/test/models/text/delete/inside-a-leaf/delete-a-char.js
new file mode 100644
index 000000000..c4d0fc1d6
--- /dev/null
+++ b/packages/slate/test/models/text/delete/inside-a-leaf/delete-a-char.js
@@ -0,0 +1,19 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+
+export const input = (
+
+ Catt is Cute
+
+)[0]
+
+export default function(t) {
+ return t.removeText(3, 1)
+}
+
+export const output = (
+
+ Cat is Cute
+
+)[0]
diff --git a/packages/slate/test/models/text/index.js b/packages/slate/test/models/text/index.js
new file mode 100644
index 000000000..ab00927c5
--- /dev/null
+++ b/packages/slate/test/models/text/index.js
@@ -0,0 +1,43 @@
+import assert from 'assert'
+import fs from 'fs'
+import { resolve } from 'path'
+
+describe('texts', () => {
+ const dir = resolve(__dirname)
+
+ const categories = fs
+ .readdirSync(dir)
+ .filter(c => c[0] != '.' && c != 'index.js')
+
+ for (const category of categories) {
+ describe(category, () => {
+ const categoryDir = resolve(dir, category)
+ const methods = fs
+ .readdirSync(categoryDir)
+ .filter(c => c[0] != '.' && !c.includes('.js'))
+
+ for (const method of methods) {
+ describe(method, () => {
+ const testDir = resolve(categoryDir, method)
+ const tests = fs
+ .readdirSync(testDir)
+ .filter(t => t[0] != '.' && t.includes('.js'))
+
+ for (const test of tests) {
+ const module = require(resolve(testDir, test))
+ const { input, output, skip } = module
+ const fn = module.default
+ const t = skip ? it.skip : it
+
+ t(test.replace('.js', ''), () => {
+ const actual = fn(input)
+ const opts = { preserveData: true }
+ const expected = output.toJSON(opts)
+ assert.deepEqual(actual.toJSON(opts), expected)
+ })
+ }
+ })
+ }
+ })
+ }
+})
diff --git a/packages/slate/test/models/text/insert/from-end/pure-text-after-marked-text.js b/packages/slate/test/models/text/insert/from-end/pure-text-after-marked-text.js
new file mode 100644
index 000000000..74787561a
--- /dev/null
+++ b/packages/slate/test/models/text/insert/from-end/pure-text-after-marked-text.js
@@ -0,0 +1,20 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+
+export const input = (
+
+ Cat
+ Cute
+
+)[0]
+
+export default function(t) {
+ return t.insertText(3, ' is')
+}
+
+export const output = (
+
+ Cat is Cute
+
+)[0]
diff --git a/packages/slate/test/models/text/insert/from-end/pure-text-after-pure-text.js b/packages/slate/test/models/text/insert/from-end/pure-text-after-pure-text.js
new file mode 100644
index 000000000..86e3e96fd
--- /dev/null
+++ b/packages/slate/test/models/text/insert/from-end/pure-text-after-pure-text.js
@@ -0,0 +1,19 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+
+export const input = (
+
+ Cat Cute
+
+)[0]
+
+export default function(t) {
+ return t.insertText(3, ' is')
+}
+
+export const output = (
+
+ Cat is Cute
+
+)[0]
diff --git a/packages/slate/test/models/text/insert/from-end/pure-text-at-end-of-all-text.js b/packages/slate/test/models/text/insert/from-end/pure-text-at-end-of-all-text.js
new file mode 100644
index 000000000..d3dc04f6c
--- /dev/null
+++ b/packages/slate/test/models/text/insert/from-end/pure-text-at-end-of-all-text.js
@@ -0,0 +1,19 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+
+export const input = (
+
+ Cat is
+
+)[0]
+
+export default function(t) {
+ return t.insertText(6, ' Cute')
+}
+
+export const output = (
+
+ Cat is Cute
+
+)[0]
diff --git a/packages/slate/test/models/text/insert/from-middle/marked-text-in-middle-of-marked-text.js b/packages/slate/test/models/text/insert/from-middle/marked-text-in-middle-of-marked-text.js
new file mode 100644
index 000000000..1db57ac10
--- /dev/null
+++ b/packages/slate/test/models/text/insert/from-middle/marked-text-in-middle-of-marked-text.js
@@ -0,0 +1,17 @@
+/** @jsx h */
+
+import { Set } from 'immutable'
+import h from '../../../../helpers/h'
+import { Mark } from '../../../../..'
+
+export const input = CatCute[0]
+
+export default function(t) {
+ return t.insertText(3, ' is ', Set.of(Mark.create('bold')))
+}
+
+export const output = (
+
+ Cat is Cute
+
+)[0]
diff --git a/packages/slate/test/models/text/insert/from-middle/marked-text-in-middle-of-pure-text.js b/packages/slate/test/models/text/insert/from-middle/marked-text-in-middle-of-pure-text.js
new file mode 100644
index 000000000..0de7879b7
--- /dev/null
+++ b/packages/slate/test/models/text/insert/from-middle/marked-text-in-middle-of-pure-text.js
@@ -0,0 +1,17 @@
+/** @jsx h */
+
+import { Set } from 'immutable'
+import h from '../../../../helpers/h'
+import { Mark } from '../../../../..'
+
+export const input = CatCute[0]
+
+export default function(t) {
+ return t.insertText(3, ' is ', Set.of(Mark.create('bold')))
+}
+
+export const output = (
+
+ Cat is Cute
+
+)[0]
diff --git a/packages/slate/test/models/text/insert/from-middle/pure-text-into-middle-of-marks.js b/packages/slate/test/models/text/insert/from-middle/pure-text-into-middle-of-marks.js
new file mode 100644
index 000000000..d0e17e190
--- /dev/null
+++ b/packages/slate/test/models/text/insert/from-middle/pure-text-into-middle-of-marks.js
@@ -0,0 +1,19 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+
+export const input = (
+
+ CatCute
+
+)[0]
+
+export default function(t) {
+ return t.insertText(3, ' is ')
+}
+
+export const output = (
+
+ Cat is Cute
+
+)[0]
diff --git a/packages/slate/test/models/text/insert/from-middle/pure-text.js b/packages/slate/test/models/text/insert/from-middle/pure-text.js
new file mode 100644
index 000000000..86802684a
--- /dev/null
+++ b/packages/slate/test/models/text/insert/from-middle/pure-text.js
@@ -0,0 +1,11 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+
+export const input = CatCute[0]
+
+export default function(t) {
+ return t.insertText(3, ' is ')
+}
+
+export const output = Cat is Cute[0]
diff --git a/packages/slate/test/models/text/insert/from-start/marked-text-on-null-text.js b/packages/slate/test/models/text/insert/from-start/marked-text-on-null-text.js
new file mode 100644
index 000000000..3962f4cb4
--- /dev/null
+++ b/packages/slate/test/models/text/insert/from-start/marked-text-on-null-text.js
@@ -0,0 +1,17 @@
+/** @jsx h */
+
+import { List } from 'immutable'
+import { Mark } from '../../../../..'
+import h from '../../../../helpers/h'
+
+export const input = [0]
+
+export default function(t) {
+ return t.insertText(0, 'Cat is Cute', List.of(Mark.create({ type: 'bold' })))
+}
+
+export const output = (
+
+ Cat is Cute
+
+)[0]
diff --git a/packages/slate/test/models/text/insert/from-start/pure-text-on-null-text-at-invalid-offset.js b/packages/slate/test/models/text/insert/from-start/pure-text-on-null-text-at-invalid-offset.js
new file mode 100644
index 000000000..c7b264ab1
--- /dev/null
+++ b/packages/slate/test/models/text/insert/from-start/pure-text-on-null-text-at-invalid-offset.js
@@ -0,0 +1,11 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+
+export const input = [0]
+
+export default function(t) {
+ return t.insertText(1, 'Cat is Cute')
+}
+
+export const output = Cat is Cute[0]
diff --git a/packages/slate/test/models/text/insert/from-start/pure-text-on-null-text.js b/packages/slate/test/models/text/insert/from-start/pure-text-on-null-text.js
new file mode 100644
index 000000000..b7b8f77ee
--- /dev/null
+++ b/packages/slate/test/models/text/insert/from-start/pure-text-on-null-text.js
@@ -0,0 +1,11 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+
+export const input = [0]
+
+export default function(t) {
+ return t.insertText(0, 'Cat is Cute')
+}
+
+export const output = Cat is Cute[0]
diff --git a/packages/slate/test/models/text/marks/add-marks/to-affect-nothing.js b/packages/slate/test/models/text/marks/add-marks/to-affect-nothing.js
new file mode 100644
index 000000000..e93fad8ad
--- /dev/null
+++ b/packages/slate/test/models/text/marks/add-marks/to-affect-nothing.js
@@ -0,0 +1,20 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+import { Mark } from '../../../../..'
+
+export const input = (
+
+ Cat is Cute
+
+)[0]
+
+export default function(t) {
+ return t.addMark(3, 4, Mark.create('bold'))
+}
+
+export const output = (
+
+ Cat is Cute
+
+)[0]
diff --git a/packages/slate/test/models/text/marks/add-marks/to-merge-two-leaves.js b/packages/slate/test/models/text/marks/add-marks/to-merge-two-leaves.js
new file mode 100644
index 000000000..d7b003b16
--- /dev/null
+++ b/packages/slate/test/models/text/marks/add-marks/to-merge-two-leaves.js
@@ -0,0 +1,21 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+import { Mark } from '../../../../..'
+
+export const input = (
+
+ Cat is
+ Cute
+
+)[0]
+
+export default function(t) {
+ return t.addMark(3, 3, Mark.create('bold'))
+}
+
+export const output = (
+
+ Cat is Cute
+
+)[0]
diff --git a/packages/slate/test/models/text/marks/add-marks/to-split-leaves.js b/packages/slate/test/models/text/marks/add-marks/to-split-leaves.js
new file mode 100644
index 000000000..721d12871
--- /dev/null
+++ b/packages/slate/test/models/text/marks/add-marks/to-split-leaves.js
@@ -0,0 +1,25 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+import { Mark } from '../../../../..'
+
+export const input = (
+
+ Cat i
+ s Cute
+
+)[0]
+
+export default function(t) {
+ return t.addMark(3, 4, Mark.create('italic'))
+}
+
+export const output = (
+
+ Cat i
+
+ s
+
+ Cute
+
+)[0]
diff --git a/packages/slate/test/models/text/marks/get-active-marks-between/marked-text-at-leaf-end.js b/packages/slate/test/models/text/marks/get-active-marks-between/marked-text-at-leaf-end.js
new file mode 100644
index 000000000..ecf4da7a8
--- /dev/null
+++ b/packages/slate/test/models/text/marks/get-active-marks-between/marked-text-at-leaf-end.js
@@ -0,0 +1,20 @@
+/** @jsx h */
+
+import { Set } from 'immutable'
+import h from '../../../../helpers/h'
+import { Mark } from '../../../../..'
+
+export const input = (
+
+
+ Cat
+ is Cute
+
+
+)[0]
+
+export default function(t) {
+ return t.getActiveMarksBetweenOffsets(0, 6)
+}
+
+export const output = Set.of(Mark.create('bold'))
diff --git a/packages/slate/test/models/text/marks/get-active-marks-between/marked-text-with-totally-different-marks.js b/packages/slate/test/models/text/marks/get-active-marks-between/marked-text-with-totally-different-marks.js
new file mode 100644
index 000000000..4b65d206f
--- /dev/null
+++ b/packages/slate/test/models/text/marks/get-active-marks-between/marked-text-with-totally-different-marks.js
@@ -0,0 +1,17 @@
+/** @jsx h */
+
+import { Set } from 'immutable'
+import h from '../../../../helpers/h'
+
+export const input = (
+
+ Cat
+ is Cute
+
+)[0]
+
+export default function(t) {
+ return t.getActiveMarksBetweenOffsets(0, 6)
+}
+
+export const output = Set()
diff --git a/packages/slate/test/models/text/marks/get-active-marks-between/null-marked-text.js b/packages/slate/test/models/text/marks/get-active-marks-between/null-marked-text.js
new file mode 100644
index 000000000..29f8242bf
--- /dev/null
+++ b/packages/slate/test/models/text/marks/get-active-marks-between/null-marked-text.js
@@ -0,0 +1,13 @@
+/** @jsx h */
+
+import { Set } from 'immutable'
+import h from '../../../../helpers/h'
+import { Mark } from '../../../../..'
+
+export const input = [0]
+
+export default function(t) {
+ return t.getActiveMarksBetweenOffsets(0, 0)
+}
+
+export const output = Set.of(Mark.create('bold'))
diff --git a/packages/slate/test/models/text/marks/get-active-marks/adject-same-marks.js b/packages/slate/test/models/text/marks/get-active-marks/adject-same-marks.js
new file mode 100644
index 000000000..56d615d18
--- /dev/null
+++ b/packages/slate/test/models/text/marks/get-active-marks/adject-same-marks.js
@@ -0,0 +1,20 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+import { Set } from 'immutable'
+import { Mark } from '../../../../..'
+
+export const input = (
+
+
+ Cat is
+ Cute
+
+
+)[0]
+
+export default function(t) {
+ return t.getActiveMarks()
+}
+
+export const output = Set.of(Mark.create('italic'), Mark.create('bold'))
diff --git a/packages/slate/test/models/text/marks/get-active-marks/marked-text.js b/packages/slate/test/models/text/marks/get-active-marks/marked-text.js
new file mode 100644
index 000000000..8ad15621d
--- /dev/null
+++ b/packages/slate/test/models/text/marks/get-active-marks/marked-text.js
@@ -0,0 +1,20 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+import { Set } from 'immutable'
+import { Mark } from '../../../../..'
+
+export const input = (
+
+
+ Cat is
+ Cute
+
+
+)[0]
+
+export default function(t) {
+ return t.getActiveMarks()
+}
+
+export const output = Set.of(Mark.create('bold'))
diff --git a/packages/slate/test/models/text/marks/get-active-marks/partially-marked-text.js b/packages/slate/test/models/text/marks/get-active-marks/partially-marked-text.js
new file mode 100644
index 000000000..49e45bb26
--- /dev/null
+++ b/packages/slate/test/models/text/marks/get-active-marks/partially-marked-text.js
@@ -0,0 +1,18 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+import { Set } from 'immutable'
+
+export const input = (
+
+ Cat
+ is
+ Cute
+
+)[0]
+
+export default function(t) {
+ return t.getActiveMarks()
+}
+
+export const output = Set()
diff --git a/packages/slate/test/models/text/marks/get-marks-at-index/null-marked-text.js b/packages/slate/test/models/text/marks/get-marks-at-index/null-marked-text.js
new file mode 100644
index 000000000..90f86a2a1
--- /dev/null
+++ b/packages/slate/test/models/text/marks/get-marks-at-index/null-marked-text.js
@@ -0,0 +1,13 @@
+/** @jsx h */
+
+import { Set } from 'immutable'
+import h from '../../../../helpers/h'
+import { Mark } from '../../../../..'
+
+export const input = [0]
+
+export default function(t) {
+ return t.getMarksAtIndex(0)
+}
+
+export const output = Set.of(Mark.create('bold'))
diff --git a/packages/slate/test/models/text/marks/get-marks-between/marked-text-with-leaf-end.js b/packages/slate/test/models/text/marks/get-marks-between/marked-text-with-leaf-end.js
new file mode 100644
index 000000000..87963abae
--- /dev/null
+++ b/packages/slate/test/models/text/marks/get-marks-between/marked-text-with-leaf-end.js
@@ -0,0 +1,18 @@
+/** @jsx h */
+
+import { Set } from 'immutable'
+import h from '../../../../helpers/h'
+import { Mark } from '../../../../..'
+
+export const input = (
+
+ Cat
+ is Cute
+
+)[0]
+
+export default function(t) {
+ return t.getMarksBetweenOffsets(0, 6)
+}
+
+export const output = Set.of(Mark.create('bold'), Mark.create('italic'))
diff --git a/packages/slate/test/models/text/marks/get-marks-between/marked-text-with-many-leaves.js b/packages/slate/test/models/text/marks/get-marks-between/marked-text-with-many-leaves.js
new file mode 100644
index 000000000..3c224e3fa
--- /dev/null
+++ b/packages/slate/test/models/text/marks/get-marks-between/marked-text-with-many-leaves.js
@@ -0,0 +1,30 @@
+/** @jsx h */
+
+import { Set } from 'immutable'
+import h from '../../../../helpers/h'
+import { Mark } from '../../../../..'
+
+export const input = (
+
+ Cat
+ is
+ Cat
+ is
+ Cat
+ is
+ Cat
+ is
+ Cat
+
+)[0]
+
+export default function(t) {
+ return t.getMarksBetweenOffsets(0, 12)
+}
+
+export const output = Set.of(
+ Mark.create({ type: 'bold', data: { x: 1 } }),
+ Mark.create({ type: 'italic', data: { x: 1 } }),
+ Mark.create({ type: 'bold', data: { x: 2 } }),
+ Mark.create({ type: 'italic', data: { x: 2 } })
+)
diff --git a/packages/slate/test/models/text/marks/get-marks-between/null-marked-text.js b/packages/slate/test/models/text/marks/get-marks-between/null-marked-text.js
new file mode 100644
index 000000000..6c5d60f18
--- /dev/null
+++ b/packages/slate/test/models/text/marks/get-marks-between/null-marked-text.js
@@ -0,0 +1,13 @@
+/** @jsx h */
+
+import { Set } from 'immutable'
+import h from '../../../../helpers/h'
+import { Mark } from '../../../../..'
+
+export const input = [0]
+
+export default function(t) {
+ return t.getMarksBetweenOffsets(0, 0)
+}
+
+export const output = Set.of(Mark.create('bold'))
diff --git a/packages/slate/test/models/text/marks/get-marks/marked-text.js b/packages/slate/test/models/text/marks/get-marks/marked-text.js
new file mode 100644
index 000000000..18f965a14
--- /dev/null
+++ b/packages/slate/test/models/text/marks/get-marks/marked-text.js
@@ -0,0 +1,19 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+import { Set } from 'immutable'
+import { Mark } from '../../../../../'
+
+export const input = (
+
+ Cat
+ is
+ Cute
+
+)[0]
+
+export default function(t) {
+ return t.getMarks()
+}
+
+export const output = Set.of(Mark.create('italic'), Mark.create('bold'))
diff --git a/packages/slate/test/models/text/marks/get-marks/null-text-with-marks.js b/packages/slate/test/models/text/marks/get-marks/null-text-with-marks.js
new file mode 100644
index 000000000..9e45c24bc
--- /dev/null
+++ b/packages/slate/test/models/text/marks/get-marks/null-text-with-marks.js
@@ -0,0 +1,13 @@
+/** @jsx h */
+
+import { Set } from 'immutable'
+import h from '../../../../helpers/h'
+import { Mark } from '../../../../../'
+
+export const input = [0]
+
+export default function(t) {
+ return t.getMarks()
+}
+
+export const output = Set.of(Mark.create('bold'))
diff --git a/packages/slate/test/models/text/marks/get-marks/null-text.js b/packages/slate/test/models/text/marks/get-marks/null-text.js
new file mode 100644
index 000000000..f2bc27afa
--- /dev/null
+++ b/packages/slate/test/models/text/marks/get-marks/null-text.js
@@ -0,0 +1,12 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+import { Set } from 'immutable'
+
+export const input = [0]
+
+export default function(t) {
+ return t.getMarks()
+}
+
+export const output = Set()
diff --git a/packages/slate/test/models/text/marks/get-marks/partially-marked-text.js b/packages/slate/test/models/text/marks/get-marks/partially-marked-text.js
new file mode 100644
index 000000000..0a3a883a6
--- /dev/null
+++ b/packages/slate/test/models/text/marks/get-marks/partially-marked-text.js
@@ -0,0 +1,19 @@
+/** @jsx h */
+
+import { Set } from 'immutable'
+import h from '../../../../helpers/h'
+import { Mark } from '../../../../..'
+
+export const input = (
+
+ Cat
+ is
+ Cute
+
+)[0]
+
+export default function(t) {
+ return t.getMarks()
+}
+
+export const output = Set.of(Mark.create('italic'))
diff --git a/packages/slate/test/models/text/marks/get-marks/plain-text.js b/packages/slate/test/models/text/marks/get-marks/plain-text.js
new file mode 100644
index 000000000..201662d76
--- /dev/null
+++ b/packages/slate/test/models/text/marks/get-marks/plain-text.js
@@ -0,0 +1,12 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+import { Set } from 'immutable'
+
+export const input = Cat is Cute [0]
+
+export default function(t) {
+ return t.getMarks()
+}
+
+export const output = Set()
diff --git a/packages/slate/test/models/text/marks/remove-mark/remove-mark.js b/packages/slate/test/models/text/marks/remove-mark/remove-mark.js
new file mode 100644
index 000000000..f918fcd4f
--- /dev/null
+++ b/packages/slate/test/models/text/marks/remove-mark/remove-mark.js
@@ -0,0 +1,20 @@
+/** @jsx h */
+
+import { Mark } from '../../../../..'
+import h from '../../../../helpers/h'
+
+export const input = (
+
+ Cat is Cute
+
+)[0]
+
+export default function(t) {
+ return t.removeMark(0, 3, Mark.create('bold'))
+}
+
+export const output = (
+
+ Cat is Cute
+
+)[0]
diff --git a/packages/slate/test/models/text/marks/update-mark/marked-text-with-some-other-makrs.js b/packages/slate/test/models/text/marks/update-mark/marked-text-with-some-other-makrs.js
new file mode 100644
index 000000000..ce92dae9f
--- /dev/null
+++ b/packages/slate/test/models/text/marks/update-mark/marked-text-with-some-other-makrs.js
@@ -0,0 +1,22 @@
+/** @jsx h */
+
+import { Mark } from '../../../../..'
+import h from '../../../../helpers/h'
+
+export const input = (
+
+ Cat
+ is Cute
+
+)[0]
+
+export default function(t) {
+ return t.updateMark(0, 6, Mark.create('bold'), { data: { x: 1 } })
+}
+
+export const output = (
+
+ Cat
+ is Cute
+
+)[0]
diff --git a/packages/slate/test/models/text/marks/update-mark/marked-text.js b/packages/slate/test/models/text/marks/update-mark/marked-text.js
new file mode 100644
index 000000000..71a4d6bdf
--- /dev/null
+++ b/packages/slate/test/models/text/marks/update-mark/marked-text.js
@@ -0,0 +1,17 @@
+/** @jsx h */
+
+import { Mark } from '../../../../..'
+import h from '../../../../helpers/h'
+
+export const input = Cat is Cute[0]
+
+export default function(t) {
+ return t.updateMark(0, 3, Mark.create('bold'), { data: { x: 1 } })
+}
+
+export const output = (
+
+ Cat
+ is Cute
+
+)[0]
diff --git a/packages/slate/test/models/text/marks/update-mark/null-mark-with-invalid-offset.js b/packages/slate/test/models/text/marks/update-mark/null-mark-with-invalid-offset.js
new file mode 100644
index 000000000..63b015dee
--- /dev/null
+++ b/packages/slate/test/models/text/marks/update-mark/null-mark-with-invalid-offset.js
@@ -0,0 +1,12 @@
+/** @jsx h */
+
+import { Mark } from '../../../../..'
+import h from '../../../../helpers/h'
+
+export const input = [0]
+
+export default function(t) {
+ return t.updateMark(0, 1, Mark.create('bold'), { data: { x: 1 } })
+}
+
+export const output = input
diff --git a/packages/slate/test/models/text/marks/update-mark/null-marked-text.js b/packages/slate/test/models/text/marks/update-mark/null-marked-text.js
new file mode 100644
index 000000000..7b577370b
--- /dev/null
+++ b/packages/slate/test/models/text/marks/update-mark/null-marked-text.js
@@ -0,0 +1,12 @@
+/** @jsx h */
+
+import { Mark } from '../../../../..'
+import h from '../../../../helpers/h'
+
+export const input = [0]
+
+export default function(t) {
+ return t.updateMark(0, 0, Mark.create('bold'), { data: { x: 1 } })
+}
+
+export const output = [0]
diff --git a/packages/slate/test/models/text/merge/empty-leaf-as-next/length-text.js b/packages/slate/test/models/text/merge/empty-leaf-as-next/length-text.js
new file mode 100644
index 000000000..2c659cace
--- /dev/null
+++ b/packages/slate/test/models/text/merge/empty-leaf-as-next/length-text.js
@@ -0,0 +1,11 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+
+export const input = [Some[0], [0]]
+
+export default function(texts) {
+ return texts[0].mergeText(texts[1])
+}
+
+export const output = Some[0]
diff --git a/packages/slate/test/models/text/merge/empty-leaf-as-start/another-empty-text.js b/packages/slate/test/models/text/merge/empty-leaf-as-start/another-empty-text.js
new file mode 100644
index 000000000..78f59da23
--- /dev/null
+++ b/packages/slate/test/models/text/merge/empty-leaf-as-start/another-empty-text.js
@@ -0,0 +1,11 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+
+export const input = [[0], [0]]
+
+export default function(texts) {
+ return texts[0].mergeText(texts[1])
+}
+
+export const output = [0]
diff --git a/packages/slate/test/models/text/merge/empty-leaf-as-start/length-text.js b/packages/slate/test/models/text/merge/empty-leaf-as-start/length-text.js
new file mode 100644
index 000000000..4e8a6af14
--- /dev/null
+++ b/packages/slate/test/models/text/merge/empty-leaf-as-start/length-text.js
@@ -0,0 +1,11 @@
+/** @jsx h */
+
+import h from '../../../../helpers/h'
+
+export const input = [[0], Some[0]]
+
+export default function(texts) {
+ return texts[0].mergeText(texts[1])
+}
+
+export const output = Some[0]