replace_step.ts (7204B)
1 import {Slice, Node, Schema} from "prosemirror-model" 2 3 import {Step, StepResult} from "./step" 4 import {StepMap, Mappable} from "./map" 5 6 /// Replace a part of the document with a slice of new content. 7 export class ReplaceStep extends Step { 8 /// The given `slice` should fit the 'gap' between `from` and 9 /// `to`—the depths must line up, and the surrounding nodes must be 10 /// able to be joined with the open sides of the slice. When 11 /// `structure` is true, the step will fail if the content between 12 /// from and to is not just a sequence of closing and then opening 13 /// tokens (this is to guard against rebased replace steps 14 /// overwriting something they weren't supposed to). 15 constructor( 16 /// The start position of the replaced range. 17 readonly from: number, 18 /// The end position of the replaced range. 19 readonly to: number, 20 /// The slice to insert. 21 readonly slice: Slice, 22 /// @internal 23 readonly structure = false 24 ) { 25 super() 26 } 27 28 apply(doc: Node) { 29 if (this.structure && contentBetween(doc, this.from, this.to)) 30 return StepResult.fail("Structure replace would overwrite content") 31 return StepResult.fromReplace(doc, this.from, this.to, this.slice) 32 } 33 34 getMap() { 35 return new StepMap([this.from, this.to - this.from, this.slice.size]) 36 } 37 38 invert(doc: Node) { 39 return new ReplaceStep(this.from, this.from + this.slice.size, doc.slice(this.from, this.to)) 40 } 41 42 map(mapping: Mappable) { 43 let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1) 44 if (from.deletedAcross && to.deletedAcross) return null 45 return new ReplaceStep(from.pos, Math.max(from.pos, to.pos), this.slice, this.structure) 46 } 47 48 merge(other: Step) { 49 if (!(other instanceof ReplaceStep) || other.structure || this.structure) return null 50 51 if (this.from + this.slice.size == other.from && !this.slice.openEnd && !other.slice.openStart) { 52 let slice = this.slice.size + other.slice.size == 0 ? Slice.empty 53 : new Slice(this.slice.content.append(other.slice.content), this.slice.openStart, other.slice.openEnd) 54 return new ReplaceStep(this.from, this.to + (other.to - other.from), slice, this.structure) 55 } else if (other.to == this.from && !this.slice.openStart && !other.slice.openEnd) { 56 let slice = this.slice.size + other.slice.size == 0 ? Slice.empty 57 : new Slice(other.slice.content.append(this.slice.content), other.slice.openStart, this.slice.openEnd) 58 return new ReplaceStep(other.from, this.to, slice, this.structure) 59 } else { 60 return null 61 } 62 } 63 64 toJSON(): any { 65 let json: any = {stepType: "replace", from: this.from, to: this.to} 66 if (this.slice.size) json.slice = this.slice.toJSON() 67 if (this.structure) json.structure = true 68 return json 69 } 70 71 /// @internal 72 static fromJSON(schema: Schema, json: any) { 73 if (typeof json.from != "number" || typeof json.to != "number") 74 throw new RangeError("Invalid input for ReplaceStep.fromJSON") 75 return new ReplaceStep(json.from, json.to, Slice.fromJSON(schema, json.slice), !!json.structure) 76 } 77 } 78 79 Step.jsonID("replace", ReplaceStep) 80 81 /// Replace a part of the document with a slice of content, but 82 /// preserve a range of the replaced content by moving it into the 83 /// slice. 84 export class ReplaceAroundStep extends Step { 85 /// Create a replace-around step with the given range and gap. 86 /// `insert` should be the point in the slice into which the content 87 /// of the gap should be moved. `structure` has the same meaning as 88 /// it has in the [`ReplaceStep`](#transform.ReplaceStep) class. 89 constructor( 90 /// The start position of the replaced range. 91 readonly from: number, 92 /// The end position of the replaced range. 93 readonly to: number, 94 /// The start of preserved range. 95 readonly gapFrom: number, 96 /// The end of preserved range. 97 readonly gapTo: number, 98 /// The slice to insert. 99 readonly slice: Slice, 100 /// The position in the slice where the preserved range should be 101 /// inserted. 102 readonly insert: number, 103 /// @internal 104 readonly structure = false 105 ) { 106 super() 107 } 108 109 apply(doc: Node) { 110 if (this.structure && (contentBetween(doc, this.from, this.gapFrom) || 111 contentBetween(doc, this.gapTo, this.to))) 112 return StepResult.fail("Structure gap-replace would overwrite content") 113 114 let gap = doc.slice(this.gapFrom, this.gapTo) 115 if (gap.openStart || gap.openEnd) 116 return StepResult.fail("Gap is not a flat range") 117 let inserted = this.slice.insertAt(this.insert, gap.content) 118 if (!inserted) return StepResult.fail("Content does not fit in gap") 119 return StepResult.fromReplace(doc, this.from, this.to, inserted) 120 } 121 122 getMap() { 123 return new StepMap([this.from, this.gapFrom - this.from, this.insert, 124 this.gapTo, this.to - this.gapTo, this.slice.size - this.insert]) 125 } 126 127 invert(doc: Node) { 128 let gap = this.gapTo - this.gapFrom 129 return new ReplaceAroundStep(this.from, this.from + this.slice.size + gap, 130 this.from + this.insert, this.from + this.insert + gap, 131 doc.slice(this.from, this.to).removeBetween(this.gapFrom - this.from, this.gapTo - this.from), 132 this.gapFrom - this.from, this.structure) 133 } 134 135 map(mapping: Mappable) { 136 let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1) 137 let gapFrom = this.from == this.gapFrom ? from.pos : mapping.map(this.gapFrom, -1) 138 let gapTo = this.to == this.gapTo ? to.pos : mapping.map(this.gapTo, 1) 139 if ((from.deletedAcross && to.deletedAcross) || gapFrom < from.pos || gapTo > to.pos) return null 140 return new ReplaceAroundStep(from.pos, to.pos, gapFrom, gapTo, this.slice, this.insert, this.structure) 141 } 142 143 toJSON(): any { 144 let json: any = {stepType: "replaceAround", from: this.from, to: this.to, 145 gapFrom: this.gapFrom, gapTo: this.gapTo, insert: this.insert} 146 if (this.slice.size) json.slice = this.slice.toJSON() 147 if (this.structure) json.structure = true 148 return json 149 } 150 151 /// @internal 152 static fromJSON(schema: Schema, json: any) { 153 if (typeof json.from != "number" || typeof json.to != "number" || 154 typeof json.gapFrom != "number" || typeof json.gapTo != "number" || typeof json.insert != "number") 155 throw new RangeError("Invalid input for ReplaceAroundStep.fromJSON") 156 return new ReplaceAroundStep(json.from, json.to, json.gapFrom, json.gapTo, 157 Slice.fromJSON(schema, json.slice), json.insert, !!json.structure) 158 } 159 } 160 161 Step.jsonID("replaceAround", ReplaceAroundStep) 162 163 function contentBetween(doc: Node, from: number, to: number) { 164 let $from = doc.resolve(from), dist = to - from, depth = $from.depth 165 while (dist > 0 && depth > 0 && $from.indexAfter(depth) == $from.node(depth).childCount) { 166 depth-- 167 dist-- 168 } 169 if (dist > 0) { 170 let next = $from.node(depth).maybeChild($from.indexAfter(depth)) 171 while (dist > 0) { 172 if (!next || next.isLeaf) return true 173 next = next.firstChild 174 dist-- 175 } 176 } 177 return false 178 }