transform.ts (9733B)
1 import {Node, NodeType, Mark, MarkType, ContentMatch, Slice, Fragment, NodeRange, Attrs} from "prosemirror-model" 2 3 import {Mapping} from "./map" 4 import {Step} from "./step" 5 import {addMark, removeMark, clearIncompatible} from "./mark" 6 import {replaceStep, replaceRange, replaceRangeWith, deleteRange} from "./replace" 7 import {lift, wrap, setBlockType, setNodeMarkup, split, join} from "./structure" 8 import {AttrStep, DocAttrStep} from "./attr_step" 9 import {AddNodeMarkStep, RemoveNodeMarkStep} from "./mark_step" 10 11 /// @internal 12 export let TransformError = class extends Error {} 13 14 TransformError = function TransformError(this: any, message: string) { 15 let err = Error.call(this, message) 16 ;(err as any).__proto__ = TransformError.prototype 17 return err 18 } as any 19 20 TransformError.prototype = Object.create(Error.prototype) 21 TransformError.prototype.constructor = TransformError 22 TransformError.prototype.name = "TransformError" 23 24 /// Abstraction to build up and track an array of 25 /// [steps](#transform.Step) representing a document transformation. 26 /// 27 /// Most transforming methods return the `Transform` object itself, so 28 /// that they can be chained. 29 export class Transform { 30 /// The steps in this transform. 31 readonly steps: Step[] = [] 32 /// The documents before each of the steps. 33 readonly docs: Node[] = [] 34 /// A mapping with the maps for each of the steps in this transform. 35 readonly mapping: Mapping = new Mapping 36 37 /// Create a transform that starts with the given document. 38 constructor( 39 /// The current document (the result of applying the steps in the 40 /// transform). 41 public doc: Node 42 ) {} 43 44 /// The starting document. 45 get before() { return this.docs.length ? this.docs[0] : this.doc } 46 47 /// Apply a new step in this transform, saving the result. Throws an 48 /// error when the step fails. 49 step(step: Step) { 50 let result = this.maybeStep(step) 51 if (result.failed) throw new TransformError(result.failed) 52 return this 53 } 54 55 /// Try to apply a step in this transformation, ignoring it if it 56 /// fails. Returns the step result. 57 maybeStep(step: Step) { 58 let result = step.apply(this.doc) 59 if (!result.failed) this.addStep(step, result.doc!) 60 return result 61 } 62 63 /// True when the document has been changed (when there are any 64 /// steps). 65 get docChanged() { 66 return this.steps.length > 0 67 } 68 69 /// @internal 70 addStep(step: Step, doc: Node) { 71 this.docs.push(this.doc) 72 this.steps.push(step) 73 this.mapping.appendMap(step.getMap()) 74 this.doc = doc 75 } 76 77 /// Replace the part of the document between `from` and `to` with the 78 /// given `slice`. 79 replace(from: number, to = from, slice = Slice.empty): this { 80 let step = replaceStep(this.doc, from, to, slice) 81 if (step) this.step(step) 82 return this 83 } 84 85 /// Replace the given range with the given content, which may be a 86 /// fragment, node, or array of nodes. 87 replaceWith(from: number, to: number, content: Fragment | Node | readonly Node[]): this { 88 return this.replace(from, to, new Slice(Fragment.from(content), 0, 0)) 89 } 90 91 /// Delete the content between the given positions. 92 delete(from: number, to: number): this { 93 return this.replace(from, to, Slice.empty) 94 } 95 96 /// Insert the given content at the given position. 97 insert(pos: number, content: Fragment | Node | readonly Node[]): this { 98 return this.replaceWith(pos, pos, content) 99 } 100 101 /// Replace a range of the document with a given slice, using 102 /// `from`, `to`, and the slice's 103 /// [`openStart`](#model.Slice.openStart) property as hints, rather 104 /// than fixed start and end points. This method may grow the 105 /// replaced area or close open nodes in the slice in order to get a 106 /// fit that is more in line with WYSIWYG expectations, by dropping 107 /// fully covered parent nodes of the replaced region when they are 108 /// marked [non-defining as 109 /// context](#model.NodeSpec.definingAsContext), or including an 110 /// open parent node from the slice that _is_ marked as [defining 111 /// its content](#model.NodeSpec.definingForContent). 112 /// 113 /// This is the method, for example, to handle paste. The similar 114 /// [`replace`](#transform.Transform.replace) method is a more 115 /// primitive tool which will _not_ move the start and end of its given 116 /// range, and is useful in situations where you need more precise 117 /// control over what happens. 118 replaceRange(from: number, to: number, slice: Slice): this { 119 replaceRange(this, from, to, slice) 120 return this 121 } 122 123 /// Replace the given range with a node, but use `from` and `to` as 124 /// hints, rather than precise positions. When from and to are the same 125 /// and are at the start or end of a parent node in which the given 126 /// node doesn't fit, this method may _move_ them out towards a parent 127 /// that does allow the given node to be placed. When the given range 128 /// completely covers a parent node, this method may completely replace 129 /// that parent node. 130 replaceRangeWith(from: number, to: number, node: Node): this { 131 replaceRangeWith(this, from, to, node) 132 return this 133 } 134 135 /// Delete the given range, expanding it to cover fully covered 136 /// parent nodes until a valid replace is found. 137 deleteRange(from: number, to: number): this { 138 deleteRange(this, from, to) 139 return this 140 } 141 142 /// Split the content in the given range off from its parent, if there 143 /// is sibling content before or after it, and move it up the tree to 144 /// the depth specified by `target`. You'll probably want to use 145 /// [`liftTarget`](#transform.liftTarget) to compute `target`, to make 146 /// sure the lift is valid. 147 lift(range: NodeRange, target: number): this { 148 lift(this, range, target) 149 return this 150 } 151 152 /// Join the blocks around the given position. If depth is 2, their 153 /// last and first siblings are also joined, and so on. 154 join(pos: number, depth: number = 1): this { 155 join(this, pos, depth) 156 return this 157 } 158 159 /// Wrap the given [range](#model.NodeRange) in the given set of wrappers. 160 /// The wrappers are assumed to be valid in this position, and should 161 /// probably be computed with [`findWrapping`](#transform.findWrapping). 162 wrap(range: NodeRange, wrappers: readonly {type: NodeType, attrs?: Attrs | null}[]): this { 163 wrap(this, range, wrappers) 164 return this 165 } 166 167 /// Set the type of all textblocks (partly) between `from` and `to` to 168 /// the given node type with the given attributes. 169 setBlockType(from: number, to = from, type: NodeType, attrs: Attrs | null | ((oldNode: Node) => Attrs) = null): this { 170 setBlockType(this, from, to, type, attrs) 171 return this 172 } 173 174 /// Change the type, attributes, and/or marks of the node at `pos`. 175 /// When `type` isn't given, the existing node type is preserved, 176 setNodeMarkup(pos: number, type?: NodeType | null, attrs: Attrs | null = null, marks?: readonly Mark[]): this { 177 setNodeMarkup(this, pos, type, attrs, marks) 178 return this 179 } 180 181 /// Set a single attribute on a given node to a new value. 182 /// The `pos` addresses the document content. Use `setDocAttribute` 183 /// to set attributes on the document itself. 184 setNodeAttribute(pos: number, attr: string, value: any): this { 185 this.step(new AttrStep(pos, attr, value)) 186 return this 187 } 188 189 /// Set a single attribute on the document to a new value. 190 setDocAttribute(attr: string, value: any): this { 191 this.step(new DocAttrStep(attr, value)) 192 return this 193 } 194 195 /// Add a mark to the node at position `pos`. 196 addNodeMark(pos: number, mark: Mark): this { 197 this.step(new AddNodeMarkStep(pos, mark)) 198 return this 199 } 200 201 /// Remove a mark (or all marks of the given type) from the node at 202 /// position `pos`. 203 removeNodeMark(pos: number, mark: Mark | MarkType): this { 204 let node = this.doc.nodeAt(pos) 205 if (!node) throw new RangeError("No node at position " + pos) 206 if (mark instanceof Mark) { 207 if (mark.isInSet(node.marks)) this.step(new RemoveNodeMarkStep(pos, mark)) 208 } else { 209 let set = node.marks, found, steps: Step[] = [] 210 while (found = mark.isInSet(set)) { 211 steps.push(new RemoveNodeMarkStep(pos, found)) 212 set = found.removeFromSet(set) 213 } 214 for (let i = steps.length - 1; i >= 0; i--) this.step(steps[i]) 215 } 216 return this 217 } 218 219 /// Split the node at the given position, and optionally, if `depth` is 220 /// greater than one, any number of nodes above that. By default, the 221 /// parts split off will inherit the node type of the original node. 222 /// This can be changed by passing an array of types and attributes to 223 /// use after the split (with the outermost nodes coming first). 224 split(pos: number, depth = 1, typesAfter?: (null | {type: NodeType, attrs?: Attrs | null})[]) { 225 split(this, pos, depth, typesAfter) 226 return this 227 } 228 229 /// Add the given mark to the inline content between `from` and `to`. 230 addMark(from: number, to: number, mark: Mark): this { 231 addMark(this, from, to, mark) 232 return this 233 } 234 235 /// Remove marks from inline nodes between `from` and `to`. When 236 /// `mark` is a single mark, remove precisely that mark. When it is 237 /// a mark type, remove all marks of that type. When it is null, 238 /// remove all marks of any type. 239 removeMark(from: number, to: number, mark?: Mark | MarkType | null) { 240 removeMark(this, from, to, mark) 241 return this 242 } 243 244 /// Removes all marks and nodes from the content of the node at 245 /// `pos` that don't match the given new parent node type. Accepts 246 /// an optional starting [content match](#model.ContentMatch) as 247 /// third argument. 248 clearIncompatible(pos: number, parentType: NodeType, match?: ContentMatch) { 249 clearIncompatible(this, pos, parentType, match) 250 return this 251 } 252 }