resolvedpos.ts (11616B)
1 import {Mark} from "./mark" 2 import {Node} from "./node" 3 4 /// You can [_resolve_](#model.Node.resolve) a position to get more 5 /// information about it. Objects of this class represent such a 6 /// resolved position, providing various pieces of context 7 /// information, and some helper methods. 8 /// 9 /// Throughout this interface, methods that take an optional `depth` 10 /// parameter will interpret undefined as `this.depth` and negative 11 /// numbers as `this.depth + value`. 12 export class ResolvedPos { 13 /// The number of levels the parent node is from the root. If this 14 /// position points directly into the root node, it is 0. If it 15 /// points into a top-level paragraph, 1, and so on. 16 depth: number 17 18 /// @internal 19 constructor( 20 /// The position that was resolved. 21 readonly pos: number, 22 /// @internal 23 readonly path: any[], 24 /// The offset this position has into its parent node. 25 readonly parentOffset: number 26 ) { 27 this.depth = path.length / 3 - 1 28 } 29 30 /// @internal 31 resolveDepth(val: number | undefined | null) { 32 if (val == null) return this.depth 33 if (val < 0) return this.depth + val 34 return val 35 } 36 37 /// The parent node that the position points into. Note that even if 38 /// a position points into a text node, that node is not considered 39 /// the parent—text nodes are ‘flat’ in this model, and have no content. 40 get parent() { return this.node(this.depth) } 41 42 /// The root node in which the position was resolved. 43 get doc() { return this.node(0) } 44 45 /// The ancestor node at the given level. `p.node(p.depth)` is the 46 /// same as `p.parent`. 47 node(depth?: number | null): Node { return this.path[this.resolveDepth(depth) * 3] } 48 49 /// The index into the ancestor at the given level. If this points 50 /// at the 3rd node in the 2nd paragraph on the top level, for 51 /// example, `p.index(0)` is 1 and `p.index(1)` is 2. 52 index(depth?: number | null): number { return this.path[this.resolveDepth(depth) * 3 + 1] } 53 54 /// The index pointing after this position into the ancestor at the 55 /// given level. 56 indexAfter(depth?: number | null): number { 57 depth = this.resolveDepth(depth) 58 return this.index(depth) + (depth == this.depth && !this.textOffset ? 0 : 1) 59 } 60 61 /// The (absolute) position at the start of the node at the given 62 /// level. 63 start(depth?: number | null): number { 64 depth = this.resolveDepth(depth) 65 return depth == 0 ? 0 : this.path[depth * 3 - 1] + 1 66 } 67 68 /// The (absolute) position at the end of the node at the given 69 /// level. 70 end(depth?: number | null): number { 71 depth = this.resolveDepth(depth) 72 return this.start(depth) + this.node(depth).content.size 73 } 74 75 /// The (absolute) position directly before the wrapping node at the 76 /// given level, or, when `depth` is `this.depth + 1`, the original 77 /// position. 78 before(depth?: number | null): number { 79 depth = this.resolveDepth(depth) 80 if (!depth) throw new RangeError("There is no position before the top-level node") 81 return depth == this.depth + 1 ? this.pos : this.path[depth * 3 - 1] 82 } 83 84 /// The (absolute) position directly after the wrapping node at the 85 /// given level, or the original position when `depth` is `this.depth + 1`. 86 after(depth?: number | null): number { 87 depth = this.resolveDepth(depth) 88 if (!depth) throw new RangeError("There is no position after the top-level node") 89 return depth == this.depth + 1 ? this.pos : this.path[depth * 3 - 1] + this.path[depth * 3].nodeSize 90 } 91 92 /// When this position points into a text node, this returns the 93 /// distance between the position and the start of the text node. 94 /// Will be zero for positions that point between nodes. 95 get textOffset(): number { return this.pos - this.path[this.path.length - 1] } 96 97 /// Get the node directly after the position, if any. If the position 98 /// points into a text node, only the part of that node after the 99 /// position is returned. 100 get nodeAfter(): Node | null { 101 let parent = this.parent, index = this.index(this.depth) 102 if (index == parent.childCount) return null 103 let dOff = this.pos - this.path[this.path.length - 1], child = parent.child(index) 104 return dOff ? parent.child(index).cut(dOff) : child 105 } 106 107 /// Get the node directly before the position, if any. If the 108 /// position points into a text node, only the part of that node 109 /// before the position is returned. 110 get nodeBefore(): Node | null { 111 let index = this.index(this.depth) 112 let dOff = this.pos - this.path[this.path.length - 1] 113 if (dOff) return this.parent.child(index).cut(0, dOff) 114 return index == 0 ? null : this.parent.child(index - 1) 115 } 116 117 /// Get the position at the given index in the parent node at the 118 /// given depth (which defaults to `this.depth`). 119 posAtIndex(index: number, depth?: number | null): number { 120 depth = this.resolveDepth(depth) 121 let node = this.path[depth * 3], pos = depth == 0 ? 0 : this.path[depth * 3 - 1] + 1 122 for (let i = 0; i < index; i++) pos += node.child(i).nodeSize 123 return pos 124 } 125 126 /// Get the marks at this position, factoring in the surrounding 127 /// marks' [`inclusive`](#model.MarkSpec.inclusive) property. If the 128 /// position is at the start of a non-empty node, the marks of the 129 /// node after it (if any) are returned. 130 marks(): readonly Mark[] { 131 let parent = this.parent, index = this.index() 132 133 // In an empty parent, return the empty array 134 if (parent.content.size == 0) return Mark.none 135 136 // When inside a text node, just return the text node's marks 137 if (this.textOffset) return parent.child(index).marks 138 139 let main = parent.maybeChild(index - 1), other = parent.maybeChild(index) 140 // If the `after` flag is true of there is no node before, make 141 // the node after this position the main reference. 142 if (!main) { let tmp = main; main = other; other = tmp } 143 144 // Use all marks in the main node, except those that have 145 // `inclusive` set to false and are not present in the other node. 146 let marks = main!.marks 147 for (var i = 0; i < marks.length; i++) 148 if (marks[i].type.spec.inclusive === false && (!other || !marks[i].isInSet(other.marks))) 149 marks = marks[i--].removeFromSet(marks) 150 151 return marks 152 } 153 154 /// Get the marks after the current position, if any, except those 155 /// that are non-inclusive and not present at position `$end`. This 156 /// is mostly useful for getting the set of marks to preserve after a 157 /// deletion. Will return `null` if this position is at the end of 158 /// its parent node or its parent node isn't a textblock (in which 159 /// case no marks should be preserved). 160 marksAcross($end: ResolvedPos): readonly Mark[] | null { 161 let after = this.parent.maybeChild(this.index()) 162 if (!after || !after.isInline) return null 163 164 let marks = after.marks, next = $end.parent.maybeChild($end.index()) 165 for (var i = 0; i < marks.length; i++) 166 if (marks[i].type.spec.inclusive === false && (!next || !marks[i].isInSet(next.marks))) 167 marks = marks[i--].removeFromSet(marks) 168 return marks 169 } 170 171 /// The depth up to which this position and the given (non-resolved) 172 /// position share the same parent nodes. 173 sharedDepth(pos: number): number { 174 for (let depth = this.depth; depth > 0; depth--) 175 if (this.start(depth) <= pos && this.end(depth) >= pos) return depth 176 return 0 177 } 178 179 /// Returns a range based on the place where this position and the 180 /// given position diverge around block content. If both point into 181 /// the same textblock, for example, a range around that textblock 182 /// will be returned. If they point into different blocks, the range 183 /// around those blocks in their shared ancestor is returned. You can 184 /// pass in an optional predicate that will be called with a parent 185 /// node to see if a range into that parent is acceptable. 186 blockRange(other: ResolvedPos = this, pred?: (node: Node) => boolean): NodeRange | null { 187 if (other.pos < this.pos) return other.blockRange(this) 188 for (let d = this.depth - (this.parent.inlineContent || this.pos == other.pos ? 1 : 0); d >= 0; d--) 189 if (other.pos <= this.end(d) && (!pred || pred(this.node(d)))) 190 return new NodeRange(this, other, d) 191 return null 192 } 193 194 /// Query whether the given position shares the same parent node. 195 sameParent(other: ResolvedPos): boolean { 196 return this.pos - this.parentOffset == other.pos - other.parentOffset 197 } 198 199 /// Return the greater of this and the given position. 200 max(other: ResolvedPos): ResolvedPos { 201 return other.pos > this.pos ? other : this 202 } 203 204 /// Return the smaller of this and the given position. 205 min(other: ResolvedPos): ResolvedPos { 206 return other.pos < this.pos ? other : this 207 } 208 209 /// @internal 210 toString() { 211 let str = "" 212 for (let i = 1; i <= this.depth; i++) 213 str += (str ? "/" : "") + this.node(i).type.name + "_" + this.index(i - 1) 214 return str + ":" + this.parentOffset 215 } 216 217 /// @internal 218 static resolve(doc: Node, pos: number): ResolvedPos { 219 if (!(pos >= 0 && pos <= doc.content.size)) throw new RangeError("Position " + pos + " out of range") 220 let path: Array<Node | number> = [] 221 let start = 0, parentOffset = pos 222 for (let node = doc;;) { 223 let {index, offset} = node.content.findIndex(parentOffset) 224 let rem = parentOffset - offset 225 path.push(node, index, start + offset) 226 if (!rem) break 227 node = node.child(index) 228 if (node.isText) break 229 parentOffset = rem - 1 230 start += offset + 1 231 } 232 return new ResolvedPos(pos, path, parentOffset) 233 } 234 235 /// @internal 236 static resolveCached(doc: Node, pos: number): ResolvedPos { 237 let cache = resolveCache.get(doc) 238 if (cache) { 239 for (let i = 0; i < cache.elts.length; i++) { 240 let elt = cache.elts[i] 241 if (elt.pos == pos) return elt 242 } 243 } else { 244 resolveCache.set(doc, cache = new ResolveCache) 245 } 246 let result = cache.elts[cache.i] = ResolvedPos.resolve(doc, pos) 247 cache.i = (cache.i + 1) % resolveCacheSize 248 return result 249 } 250 } 251 252 class ResolveCache { 253 elts: ResolvedPos[] = [] 254 i = 0 255 } 256 257 const resolveCacheSize = 12, resolveCache = new WeakMap<Node, ResolveCache>() 258 259 /// Represents a flat range of content, i.e. one that starts and 260 /// ends in the same node. 261 export class NodeRange { 262 /// Construct a node range. `$from` and `$to` should point into the 263 /// same node until at least the given `depth`, since a node range 264 /// denotes an adjacent set of nodes in a single parent node. 265 constructor( 266 /// A resolved position along the start of the content. May have a 267 /// `depth` greater than this object's `depth` property, since 268 /// these are the positions that were used to compute the range, 269 /// not re-resolved positions directly at its boundaries. 270 readonly $from: ResolvedPos, 271 /// A position along the end of the content. See 272 /// caveat for [`$from`](#model.NodeRange.$from). 273 readonly $to: ResolvedPos, 274 /// The depth of the node that this range points into. 275 readonly depth: number 276 ) {} 277 278 /// The position at the start of the range. 279 get start() { return this.$from.before(this.depth + 1) } 280 /// The position at the end of the range. 281 get end() { return this.$to.after(this.depth + 1) } 282 283 /// The parent node that the range points into. 284 get parent() { return this.$from.node(this.depth) } 285 /// The start index of the range in the parent node. 286 get startIndex() { return this.$from.index(this.depth) } 287 /// The end index of the range in the parent node. 288 get endIndex() { return this.$to.indexAfter(this.depth) } 289 }