replace.ts (9145B)
1 import {Fragment} from "./fragment" 2 import {Schema} from "./schema" 3 import {Node, TextNode} from "./node" 4 import {ResolvedPos} from "./resolvedpos" 5 6 /// Error type raised by [`Node.replace`](#model.Node.replace) when 7 /// given an invalid replacement. 8 export class ReplaceError extends Error {} 9 /* 10 ReplaceError = function(this: any, message: string) { 11 let err = Error.call(this, message) 12 ;(err as any).__proto__ = ReplaceError.prototype 13 return err 14 } as any 15 16 ReplaceError.prototype = Object.create(Error.prototype) 17 ReplaceError.prototype.constructor = ReplaceError 18 ReplaceError.prototype.name = "ReplaceError" 19 */ 20 21 /// A slice represents a piece cut out of a larger document. It 22 /// stores not only a fragment, but also the depth up to which nodes on 23 /// both side are ‘open’ (cut through). 24 export class Slice { 25 /// Create a slice. When specifying a non-zero open depth, you must 26 /// make sure that there are nodes of at least that depth at the 27 /// appropriate side of the fragment—i.e. if the fragment is an 28 /// empty paragraph node, `openStart` and `openEnd` can't be greater 29 /// than 1. 30 /// 31 /// It is not necessary for the content of open nodes to conform to 32 /// the schema's content constraints, though it should be a valid 33 /// start/end/middle for such a node, depending on which sides are 34 /// open. 35 constructor( 36 /// The slice's content. 37 readonly content: Fragment, 38 /// The open depth at the start of the fragment. 39 readonly openStart: number, 40 /// The open depth at the end. 41 readonly openEnd: number 42 ) {} 43 44 /// The size this slice would add when inserted into a document. 45 get size(): number { 46 return this.content.size - this.openStart - this.openEnd 47 } 48 49 /// @internal 50 insertAt(pos: number, fragment: Fragment) { 51 let content = insertInto(this.content, pos + this.openStart, fragment) 52 return content && new Slice(content, this.openStart, this.openEnd) 53 } 54 55 /// @internal 56 removeBetween(from: number, to: number) { 57 return new Slice(removeRange(this.content, from + this.openStart, to + this.openStart), this.openStart, this.openEnd) 58 } 59 60 /// Tests whether this slice is equal to another slice. 61 eq(other: Slice): boolean { 62 return this.content.eq(other.content) && this.openStart == other.openStart && this.openEnd == other.openEnd 63 } 64 65 /// @internal 66 toString() { 67 return this.content + "(" + this.openStart + "," + this.openEnd + ")" 68 } 69 70 /// Convert a slice to a JSON-serializable representation. 71 toJSON(): any { 72 if (!this.content.size) return null 73 let json: any = {content: this.content.toJSON()} 74 if (this.openStart > 0) json.openStart = this.openStart 75 if (this.openEnd > 0) json.openEnd = this.openEnd 76 return json 77 } 78 79 /// Deserialize a slice from its JSON representation. 80 static fromJSON(schema: Schema, json: any): Slice { 81 if (!json) return Slice.empty 82 let openStart = json.openStart || 0, openEnd = json.openEnd || 0 83 if (typeof openStart != "number" || typeof openEnd != "number") 84 throw new RangeError("Invalid input for Slice.fromJSON") 85 return new Slice(Fragment.fromJSON(schema, json.content), openStart, openEnd) 86 } 87 88 /// Create a slice from a fragment by taking the maximum possible 89 /// open value on both side of the fragment. 90 static maxOpen(fragment: Fragment, openIsolating = true) { 91 let openStart = 0, openEnd = 0 92 for (let n = fragment.firstChild; n && !n.isLeaf && (openIsolating || !n.type.spec.isolating); n = n.firstChild) openStart++ 93 for (let n = fragment.lastChild; n && !n.isLeaf && (openIsolating || !n.type.spec.isolating); n = n.lastChild) openEnd++ 94 return new Slice(fragment, openStart, openEnd) 95 } 96 97 /// The empty slice. 98 static empty = new Slice(Fragment.empty, 0, 0) 99 } 100 101 function removeRange(content: Fragment, from: number, to: number): Fragment { 102 let {index, offset} = content.findIndex(from), child = content.maybeChild(index) 103 let {index: indexTo, offset: offsetTo} = content.findIndex(to) 104 if (offset == from || child!.isText) { 105 if (offsetTo != to && !content.child(indexTo).isText) throw new RangeError("Removing non-flat range") 106 return content.cut(0, from).append(content.cut(to)) 107 } 108 if (index != indexTo) throw new RangeError("Removing non-flat range") 109 return content.replaceChild(index, child!.copy(removeRange(child!.content, from - offset - 1, to - offset - 1))) 110 } 111 112 function insertInto(content: Fragment, dist: number, insert: Fragment, parent?: Node | null): Fragment | null { 113 let {index, offset} = content.findIndex(dist), child = content.maybeChild(index) 114 if (offset == dist || child!.isText) { 115 if (parent && !parent.canReplace(index, index, insert)) return null 116 return content.cut(0, dist).append(insert).append(content.cut(dist)) 117 } 118 let inner = insertInto(child!.content, dist - offset - 1, insert, child) 119 return inner && content.replaceChild(index, child!.copy(inner)) 120 } 121 122 export function replace($from: ResolvedPos, $to: ResolvedPos, slice: Slice) { 123 if (slice.openStart > $from.depth) 124 throw new ReplaceError("Inserted content deeper than insertion position") 125 if ($from.depth - slice.openStart != $to.depth - slice.openEnd) 126 throw new ReplaceError("Inconsistent open depths") 127 return replaceOuter($from, $to, slice, 0) 128 } 129 130 function replaceOuter($from: ResolvedPos, $to: ResolvedPos, slice: Slice, depth: number): Node { 131 let index = $from.index(depth), node = $from.node(depth) 132 if (index == $to.index(depth) && depth < $from.depth - slice.openStart) { 133 let inner = replaceOuter($from, $to, slice, depth + 1) 134 return node.copy(node.content.replaceChild(index, inner)) 135 } else if (!slice.content.size) { 136 return close(node, replaceTwoWay($from, $to, depth)) 137 } else if (!slice.openStart && !slice.openEnd && $from.depth == depth && $to.depth == depth) { // Simple, flat case 138 let parent = $from.parent, content = parent.content 139 return close(parent, content.cut(0, $from.parentOffset).append(slice.content).append(content.cut($to.parentOffset))) 140 } else { 141 let {start, end} = prepareSliceForReplace(slice, $from) 142 return close(node, replaceThreeWay($from, start, end, $to, depth)) 143 } 144 } 145 146 function checkJoin(main: Node, sub: Node) { 147 if (!sub.type.compatibleContent(main.type)) 148 throw new ReplaceError("Cannot join " + sub.type.name + " onto " + main.type.name) 149 } 150 151 function joinable($before: ResolvedPos, $after: ResolvedPos, depth: number) { 152 let node = $before.node(depth) 153 checkJoin(node, $after.node(depth)) 154 return node 155 } 156 157 function addNode(child: Node, target: Node[]) { 158 let last = target.length - 1 159 if (last >= 0 && child.isText && child.sameMarkup(target[last])) 160 target[last] = (child as TextNode).withText(target[last].text! + child.text!) 161 else 162 target.push(child) 163 } 164 165 function addRange($start: ResolvedPos | null, $end: ResolvedPos | null, depth: number, target: Node[]) { 166 let node = ($end || $start)!.node(depth) 167 let startIndex = 0, endIndex = $end ? $end.index(depth) : node.childCount 168 if ($start) { 169 startIndex = $start.index(depth) 170 if ($start.depth > depth) { 171 startIndex++ 172 } else if ($start.textOffset) { 173 addNode($start.nodeAfter!, target) 174 startIndex++ 175 } 176 } 177 for (let i = startIndex; i < endIndex; i++) addNode(node.child(i), target) 178 if ($end && $end.depth == depth && $end.textOffset) 179 addNode($end.nodeBefore!, target) 180 } 181 182 function close(node: Node, content: Fragment) { 183 node.type.checkContent(content) 184 return node.copy(content) 185 } 186 187 function replaceThreeWay($from: ResolvedPos, $start: ResolvedPos, $end: ResolvedPos, $to: ResolvedPos, depth: number) { 188 let openStart = $from.depth > depth && joinable($from, $start, depth + 1) 189 let openEnd = $to.depth > depth && joinable($end, $to, depth + 1) 190 191 let content: Node[] = [] 192 addRange(null, $from, depth, content) 193 if (openStart && openEnd && $start.index(depth) == $end.index(depth)) { 194 checkJoin(openStart, openEnd) 195 addNode(close(openStart, replaceThreeWay($from, $start, $end, $to, depth + 1)), content) 196 } else { 197 if (openStart) 198 addNode(close(openStart, replaceTwoWay($from, $start, depth + 1)), content) 199 addRange($start, $end, depth, content) 200 if (openEnd) 201 addNode(close(openEnd, replaceTwoWay($end, $to, depth + 1)), content) 202 } 203 addRange($to, null, depth, content) 204 return new Fragment(content) 205 } 206 207 function replaceTwoWay($from: ResolvedPos, $to: ResolvedPos, depth: number) { 208 let content: Node[] = [] 209 addRange(null, $from, depth, content) 210 if ($from.depth > depth) { 211 let type = joinable($from, $to, depth + 1) 212 addNode(close(type, replaceTwoWay($from, $to, depth + 1)), content) 213 } 214 addRange($to, null, depth, content) 215 return new Fragment(content) 216 } 217 218 function prepareSliceForReplace(slice: Slice, $along: ResolvedPos) { 219 let extra = $along.depth - slice.openStart, parent = $along.node(extra) 220 let node = parent.copy(slice.content) 221 for (let i = extra - 1; i >= 0; i--) 222 node = $along.node(i).copy(Fragment.from(node)) 223 return {start: node.resolveNoCache(slice.openStart + extra), 224 end: node.resolveNoCache(node.content.size - slice.openEnd - extra)} 225 }