mark_step.ts (7357B)
1 import {Fragment, Slice, Node, Mark, Schema} from "prosemirror-model" 2 import {Step, StepResult} from "./step" 3 import {Mappable} from "./map" 4 5 function mapFragment(fragment: Fragment, f: (child: Node, parent: Node, i: number) => Node, parent: Node): Fragment { 6 let mapped = [] 7 for (let i = 0; i < fragment.childCount; i++) { 8 let child = fragment.child(i) 9 if (child.content.size) child = child.copy(mapFragment(child.content, f, child)) 10 if (child.isInline) child = f(child, parent, i) 11 mapped.push(child) 12 } 13 return Fragment.fromArray(mapped) 14 } 15 16 /// Add a mark to all inline content between two positions. 17 export class AddMarkStep extends Step { 18 /// Create a mark step. 19 constructor( 20 /// The start of the marked range. 21 readonly from: number, 22 /// The end of the marked range. 23 readonly to: number, 24 /// The mark to add. 25 readonly mark: Mark 26 ) { 27 super() 28 } 29 30 apply(doc: Node) { 31 let oldSlice = doc.slice(this.from, this.to), $from = doc.resolve(this.from) 32 let parent = $from.node($from.sharedDepth(this.to)) 33 let slice = new Slice(mapFragment(oldSlice.content, (node, parent) => { 34 if (!node.isAtom || !parent.type.allowsMarkType(this.mark.type)) return node 35 return node.mark(this.mark.addToSet(node.marks)) 36 }, parent), oldSlice.openStart, oldSlice.openEnd) 37 return StepResult.fromReplace(doc, this.from, this.to, slice) 38 } 39 40 invert(): Step { 41 return new RemoveMarkStep(this.from, this.to, this.mark) 42 } 43 44 map(mapping: Mappable): Step | null { 45 let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1) 46 if (from.deleted && to.deleted || from.pos >= to.pos) return null 47 return new AddMarkStep(from.pos, to.pos, this.mark) 48 } 49 50 merge(other: Step): Step | null { 51 if (other instanceof AddMarkStep && 52 other.mark.eq(this.mark) && 53 this.from <= other.to && this.to >= other.from) 54 return new AddMarkStep(Math.min(this.from, other.from), 55 Math.max(this.to, other.to), this.mark) 56 return null 57 } 58 59 toJSON(): any { 60 return {stepType: "addMark", mark: this.mark.toJSON(), 61 from: this.from, to: this.to} 62 } 63 64 /// @internal 65 static fromJSON(schema: Schema, json: any) { 66 if (typeof json.from != "number" || typeof json.to != "number") 67 throw new RangeError("Invalid input for AddMarkStep.fromJSON") 68 return new AddMarkStep(json.from, json.to, schema.markFromJSON(json.mark)) 69 } 70 } 71 72 Step.jsonID("addMark", AddMarkStep) 73 74 /// Remove a mark from all inline content between two positions. 75 export class RemoveMarkStep extends Step { 76 /// Create a mark-removing step. 77 constructor( 78 /// The start of the unmarked range. 79 readonly from: number, 80 /// The end of the unmarked range. 81 readonly to: number, 82 /// The mark to remove. 83 readonly mark: Mark 84 ) { 85 super() 86 } 87 88 apply(doc: Node) { 89 let oldSlice = doc.slice(this.from, this.to) 90 let slice = new Slice(mapFragment(oldSlice.content, node => { 91 return node.mark(this.mark.removeFromSet(node.marks)) 92 }, doc), oldSlice.openStart, oldSlice.openEnd) 93 return StepResult.fromReplace(doc, this.from, this.to, slice) 94 } 95 96 invert(): Step { 97 return new AddMarkStep(this.from, this.to, this.mark) 98 } 99 100 map(mapping: Mappable): Step | null { 101 let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1) 102 if (from.deleted && to.deleted || from.pos >= to.pos) return null 103 return new RemoveMarkStep(from.pos, to.pos, this.mark) 104 } 105 106 merge(other: Step): Step | null { 107 if (other instanceof RemoveMarkStep && 108 other.mark.eq(this.mark) && 109 this.from <= other.to && this.to >= other.from) 110 return new RemoveMarkStep(Math.min(this.from, other.from), 111 Math.max(this.to, other.to), this.mark) 112 return null 113 } 114 115 toJSON(): any { 116 return {stepType: "removeMark", mark: this.mark.toJSON(), 117 from: this.from, to: this.to} 118 } 119 120 /// @internal 121 static fromJSON(schema: Schema, json: any) { 122 if (typeof json.from != "number" || typeof json.to != "number") 123 throw new RangeError("Invalid input for RemoveMarkStep.fromJSON") 124 return new RemoveMarkStep(json.from, json.to, schema.markFromJSON(json.mark)) 125 } 126 } 127 128 Step.jsonID("removeMark", RemoveMarkStep) 129 130 /// Add a mark to a specific node. 131 export class AddNodeMarkStep extends Step { 132 /// Create a node mark step. 133 constructor( 134 /// The position of the target node. 135 readonly pos: number, 136 /// The mark to add. 137 readonly mark: Mark 138 ) { 139 super() 140 } 141 142 apply(doc: Node) { 143 let node = doc.nodeAt(this.pos) 144 if (!node) return StepResult.fail("No node at mark step's position") 145 let updated = node.type.create(node.attrs, null, this.mark.addToSet(node.marks)) 146 return StepResult.fromReplace(doc, this.pos, this.pos + 1, new Slice(Fragment.from(updated), 0, node.isLeaf ? 0 : 1)) 147 } 148 149 invert(doc: Node): Step { 150 let node = doc.nodeAt(this.pos) 151 if (node) { 152 let newSet = this.mark.addToSet(node.marks) 153 if (newSet.length == node.marks.length) { 154 for (let i = 0; i < node.marks.length; i++) 155 if (!node.marks[i].isInSet(newSet)) 156 return new AddNodeMarkStep(this.pos, node.marks[i]) 157 return new AddNodeMarkStep(this.pos, this.mark) 158 } 159 } 160 return new RemoveNodeMarkStep(this.pos, this.mark) 161 } 162 163 map(mapping: Mappable): Step | null { 164 let pos = mapping.mapResult(this.pos, 1) 165 return pos.deletedAfter ? null : new AddNodeMarkStep(pos.pos, this.mark) 166 } 167 168 toJSON(): any { 169 return {stepType: "addNodeMark", pos: this.pos, mark: this.mark.toJSON()} 170 } 171 172 /// @internal 173 static fromJSON(schema: Schema, json: any) { 174 if (typeof json.pos != "number") 175 throw new RangeError("Invalid input for AddNodeMarkStep.fromJSON") 176 return new AddNodeMarkStep(json.pos, schema.markFromJSON(json.mark)) 177 } 178 } 179 180 Step.jsonID("addNodeMark", AddNodeMarkStep) 181 182 /// Remove a mark from a specific node. 183 export class RemoveNodeMarkStep extends Step { 184 /// Create a mark-removing step. 185 constructor( 186 /// The position of the target node. 187 readonly pos: number, 188 /// The mark to remove. 189 readonly mark: Mark 190 ) { 191 super() 192 } 193 194 apply(doc: Node) { 195 let node = doc.nodeAt(this.pos) 196 if (!node) return StepResult.fail("No node at mark step's position") 197 let updated = node.type.create(node.attrs, null, this.mark.removeFromSet(node.marks)) 198 return StepResult.fromReplace(doc, this.pos, this.pos + 1, new Slice(Fragment.from(updated), 0, node.isLeaf ? 0 : 1)) 199 } 200 201 invert(doc: Node): Step { 202 let node = doc.nodeAt(this.pos) 203 if (!node || !this.mark.isInSet(node.marks)) return this 204 return new AddNodeMarkStep(this.pos, this.mark) 205 } 206 207 map(mapping: Mappable): Step | null { 208 let pos = mapping.mapResult(this.pos, 1) 209 return pos.deletedAfter ? null : new RemoveNodeMarkStep(pos.pos, this.mark) 210 } 211 212 toJSON(): any { 213 return {stepType: "removeNodeMark", pos: this.pos, mark: this.mark.toJSON()} 214 } 215 216 /// @internal 217 static fromJSON(schema: Schema, json: any) { 218 if (typeof json.pos != "number") 219 throw new RangeError("Invalid input for RemoveNodeMarkStep.fromJSON") 220 return new RemoveNodeMarkStep(json.pos, schema.markFromJSON(json.mark)) 221 } 222 } 223 224 Step.jsonID("removeNodeMark", RemoveNodeMarkStep)