fragment.ts (10443B)
1 import {findDiffStart, findDiffEnd} from "./diff" 2 import {Node, TextNode} from "./node" 3 import {Schema} from "./schema" 4 5 /// A fragment represents a node's collection of child nodes. 6 /// 7 /// Like nodes, fragments are persistent data structures, and you 8 /// should not mutate them or their content. Rather, you create new 9 /// instances whenever needed. The API tries to make this easy. 10 export class Fragment { 11 /// The size of the fragment, which is the total of the size of 12 /// its content nodes. 13 readonly size: number 14 15 /// @internal 16 constructor( 17 /// The child nodes in this fragment. 18 readonly content: readonly Node[], 19 size?: number 20 ) { 21 this.size = size || 0 22 if (size == null) for (let i = 0; i < content.length; i++) 23 this.size += content[i].nodeSize 24 } 25 26 /// Invoke a callback for all descendant nodes between the given two 27 /// positions (relative to start of this fragment). Doesn't descend 28 /// into a node when the callback returns `false`. 29 nodesBetween(from: number, to: number, 30 f: (node: Node, start: number, parent: Node | null, index: number) => boolean | void, 31 nodeStart = 0, 32 parent?: Node) { 33 for (let i = 0, pos = 0; pos < to; i++) { 34 let child = this.content[i], end = pos + child.nodeSize 35 if (end > from && f(child, nodeStart + pos, parent || null, i) !== false && child.content.size) { 36 let start = pos + 1 37 child.nodesBetween(Math.max(0, from - start), 38 Math.min(child.content.size, to - start), 39 f, nodeStart + start) 40 } 41 pos = end 42 } 43 } 44 45 /// Call the given callback for every descendant node. `pos` will be 46 /// relative to the start of the fragment. The callback may return 47 /// `false` to prevent traversal of a given node's children. 48 descendants(f: (node: Node, pos: number, parent: Node | null, index: number) => boolean | void) { 49 this.nodesBetween(0, this.size, f) 50 } 51 52 /// Extract the text between `from` and `to`. See the same method on 53 /// [`Node`](#model.Node.textBetween). 54 textBetween(from: number, to: number, blockSeparator?: string | null, leafText?: string | null | ((leafNode: Node) => string)) { 55 let text = "", first = true 56 this.nodesBetween(from, to, (node, pos) => { 57 let nodeText = node.isText ? node.text!.slice(Math.max(from, pos) - pos, to - pos) 58 : !node.isLeaf ? "" 59 : leafText ? (typeof leafText === "function" ? leafText(node) : leafText) 60 : node.type.spec.leafText ? node.type.spec.leafText(node) 61 : "" 62 if (node.isBlock && (node.isLeaf && nodeText || node.isTextblock) && blockSeparator) { 63 if (first) first = false 64 else text += blockSeparator 65 } 66 text += nodeText 67 }, 0) 68 return text 69 } 70 71 /// Create a new fragment containing the combined content of this 72 /// fragment and the other. 73 append(other: Fragment) { 74 if (!other.size) return this 75 if (!this.size) return other 76 let last = this.lastChild!, first = other.firstChild!, content = this.content.slice(), i = 0 77 if (last.isText && last.sameMarkup(first)) { 78 content[content.length - 1] = (last as TextNode).withText(last.text! + first.text!) 79 i = 1 80 } 81 for (; i < other.content.length; i++) content.push(other.content[i]) 82 return new Fragment(content, this.size + other.size) 83 } 84 85 /// Cut out the sub-fragment between the two given positions. 86 cut(from: number, to = this.size) { 87 if (from == 0 && to == this.size) return this 88 let result: Node[] = [], size = 0 89 if (to > from) for (let i = 0, pos = 0; pos < to; i++) { 90 let child = this.content[i], end = pos + child.nodeSize 91 if (end > from) { 92 if (pos < from || end > to) { 93 if (child.isText) 94 child = child.cut(Math.max(0, from - pos), Math.min(child.text!.length, to - pos)) 95 else 96 child = child.cut(Math.max(0, from - pos - 1), Math.min(child.content.size, to - pos - 1)) 97 } 98 result.push(child) 99 size += child.nodeSize 100 } 101 pos = end 102 } 103 return new Fragment(result, size) 104 } 105 106 /// @internal 107 cutByIndex(from: number, to: number) { 108 if (from == to) return Fragment.empty 109 if (from == 0 && to == this.content.length) return this 110 return new Fragment(this.content.slice(from, to)) 111 } 112 113 /// Create a new fragment in which the node at the given index is 114 /// replaced by the given node. 115 replaceChild(index: number, node: Node) { 116 let current = this.content[index] 117 if (current == node) return this 118 let copy = this.content.slice() 119 let size = this.size + node.nodeSize - current.nodeSize 120 copy[index] = node 121 return new Fragment(copy, size) 122 } 123 124 /// Create a new fragment by prepending the given node to this 125 /// fragment. 126 addToStart(node: Node) { 127 return new Fragment([node].concat(this.content), this.size + node.nodeSize) 128 } 129 130 /// Create a new fragment by appending the given node to this 131 /// fragment. 132 addToEnd(node: Node) { 133 return new Fragment(this.content.concat(node), this.size + node.nodeSize) 134 } 135 136 /// Compare this fragment to another one. 137 eq(other: Fragment): boolean { 138 if (this.content.length != other.content.length) return false 139 for (let i = 0; i < this.content.length; i++) 140 if (!this.content[i].eq(other.content[i])) return false 141 return true 142 } 143 144 /// The first child of the fragment, or `null` if it is empty. 145 get firstChild(): Node | null { return this.content.length ? this.content[0] : null } 146 147 /// The last child of the fragment, or `null` if it is empty. 148 get lastChild(): Node | null { return this.content.length ? this.content[this.content.length - 1] : null } 149 150 /// The number of child nodes in this fragment. 151 get childCount() { return this.content.length } 152 153 /// Get the child node at the given index. Raise an error when the 154 /// index is out of range. 155 child(index: number) { 156 let found = this.content[index] 157 if (!found) throw new RangeError("Index " + index + " out of range for " + this) 158 return found 159 } 160 161 /// Get the child node at the given index, if it exists. 162 maybeChild(index: number): Node | null { 163 return this.content[index] || null 164 } 165 166 /// Call `f` for every child node, passing the node, its offset 167 /// into this parent node, and its index. 168 forEach(f: (node: Node, offset: number, index: number) => void) { 169 for (let i = 0, p = 0; i < this.content.length; i++) { 170 let child = this.content[i] 171 f(child, p, i) 172 p += child.nodeSize 173 } 174 } 175 176 /// Find the first position at which this fragment and another 177 /// fragment differ, or `null` if they are the same. 178 findDiffStart(other: Fragment, pos = 0) { 179 return findDiffStart(this, other, pos) 180 } 181 182 /// Find the first position, searching from the end, at which this 183 /// fragment and the given fragment differ, or `null` if they are 184 /// the same. Since this position will not be the same in both 185 /// nodes, an object with two separate positions is returned. 186 findDiffEnd(other: Fragment, pos = this.size, otherPos = other.size) { 187 return findDiffEnd(this, other, pos, otherPos) 188 } 189 190 /// Find the index and inner offset corresponding to a given relative 191 /// position in this fragment. The result object will be reused 192 /// (overwritten) the next time the function is called. @internal 193 findIndex(pos: number): {index: number, offset: number} { 194 if (pos == 0) return retIndex(0, pos) 195 if (pos == this.size) return retIndex(this.content.length, pos) 196 if (pos > this.size || pos < 0) throw new RangeError(`Position ${pos} outside of fragment (${this})`) 197 for (let i = 0, curPos = 0;; i++) { 198 let cur = this.child(i), end = curPos + cur.nodeSize 199 if (end >= pos) { 200 if (end == pos) return retIndex(i + 1, end) 201 return retIndex(i, curPos) 202 } 203 curPos = end 204 } 205 } 206 207 /// Return a debugging string that describes this fragment. 208 toString(): string { return "<" + this.toStringInner() + ">" } 209 210 /// @internal 211 toStringInner() { return this.content.join(", ") } 212 213 /// Create a JSON-serializeable representation of this fragment. 214 toJSON(): any { 215 return this.content.length ? this.content.map(n => n.toJSON()) : null 216 } 217 218 /// Deserialize a fragment from its JSON representation. 219 static fromJSON(schema: Schema, value: any) { 220 if (!value) return Fragment.empty 221 if (!Array.isArray(value)) throw new RangeError("Invalid input for Fragment.fromJSON") 222 return new Fragment(value.map(schema.nodeFromJSON)) 223 } 224 225 /// Build a fragment from an array of nodes. Ensures that adjacent 226 /// text nodes with the same marks are joined together. 227 static fromArray(array: readonly Node[]) { 228 if (!array.length) return Fragment.empty 229 let joined: Node[] | undefined, size = 0 230 for (let i = 0; i < array.length; i++) { 231 let node = array[i] 232 size += node.nodeSize 233 if (i && node.isText && array[i - 1].sameMarkup(node)) { 234 if (!joined) joined = array.slice(0, i) 235 joined[joined.length - 1] = (node as TextNode) 236 .withText((joined[joined.length - 1] as TextNode).text + (node as TextNode).text) 237 } else if (joined) { 238 joined.push(node) 239 } 240 } 241 return new Fragment(joined || array, size) 242 } 243 244 /// Create a fragment from something that can be interpreted as a 245 /// set of nodes. For `null`, it returns the empty fragment. For a 246 /// fragment, the fragment itself. For a node or array of nodes, a 247 /// fragment containing those nodes. 248 static from(nodes?: Fragment | Node | readonly Node[] | null) { 249 if (!nodes) return Fragment.empty 250 if (nodes instanceof Fragment) return nodes 251 if (Array.isArray(nodes)) return this.fromArray(nodes) 252 if ((nodes as Node).attrs) return new Fragment([nodes as Node], (nodes as Node).nodeSize) 253 throw new RangeError("Can not convert " + nodes + " to a Fragment" + 254 ((nodes as any).nodesBetween ? " (looks like multiple versions of prosemirror-model were loaded)" : "")) 255 } 256 257 /// An empty fragment. Intended to be reused whenever a node doesn't 258 /// contain anything (rather than allocating a new empty fragment for 259 /// each leaf node). 260 static empty: Fragment = new Fragment([], 0) 261 } 262 263 const found = {index: 0, offset: 0} 264 function retIndex(index: number, offset: number) { 265 found.index = index 266 found.offset = offset 267 return found 268 }