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]