You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
240 lines
8.9 KiB
240 lines
8.9 KiB
import {Node, NodeType, Mark, MarkType, ContentMatch, Slice, Fragment, NodeRange, Attrs} from "prosemirror-model"
|
|
|
|
import {Mapping} from "./map"
|
|
import {Step} from "./step"
|
|
import {addMark, removeMark, clearIncompatible} from "./mark"
|
|
import {replaceStep, replaceRange, replaceRangeWith, deleteRange} from "./replace"
|
|
import {lift, wrap, setBlockType, setNodeMarkup, split, join} from "./structure"
|
|
import {AttrStep} from "./attr_step"
|
|
import {AddNodeMarkStep, RemoveNodeMarkStep} from "./mark_step"
|
|
|
|
/// @internal
|
|
export let TransformError = class extends Error {}
|
|
|
|
TransformError = function TransformError(this: any, message: string) {
|
|
let err = Error.call(this, message)
|
|
;(err as any).__proto__ = TransformError.prototype
|
|
return err
|
|
} as any
|
|
|
|
TransformError.prototype = Object.create(Error.prototype)
|
|
TransformError.prototype.constructor = TransformError
|
|
TransformError.prototype.name = "TransformError"
|
|
|
|
/// Abstraction to build up and track an array of
|
|
/// [steps](#transform.Step) representing a document transformation.
|
|
///
|
|
/// Most transforming methods return the `Transform` object itself, so
|
|
/// that they can be chained.
|
|
export class Transform {
|
|
/// The steps in this transform.
|
|
readonly steps: Step[] = []
|
|
/// The documents before each of the steps.
|
|
readonly docs: Node[] = []
|
|
/// A mapping with the maps for each of the steps in this transform.
|
|
readonly mapping: Mapping = new Mapping
|
|
|
|
/// Create a transform that starts with the given document.
|
|
constructor(
|
|
/// The current document (the result of applying the steps in the
|
|
/// transform).
|
|
public doc: Node
|
|
) {}
|
|
|
|
/// The starting document.
|
|
get before() { return this.docs.length ? this.docs[0] : this.doc }
|
|
|
|
/// Apply a new step in this transform, saving the result. Throws an
|
|
/// error when the step fails.
|
|
step(step: Step) {
|
|
let result = this.maybeStep(step)
|
|
if (result.failed) throw new TransformError(result.failed)
|
|
return this
|
|
}
|
|
|
|
/// Try to apply a step in this transformation, ignoring it if it
|
|
/// fails. Returns the step result.
|
|
maybeStep(step: Step) {
|
|
let result = step.apply(this.doc)
|
|
if (!result.failed) this.addStep(step, result.doc!)
|
|
return result
|
|
}
|
|
|
|
/// True when the document has been changed (when there are any
|
|
/// steps).
|
|
get docChanged() {
|
|
return this.steps.length > 0
|
|
}
|
|
|
|
/// @internal
|
|
addStep(step: Step, doc: Node) {
|
|
this.docs.push(this.doc)
|
|
this.steps.push(step)
|
|
this.mapping.appendMap(step.getMap())
|
|
this.doc = doc
|
|
}
|
|
|
|
/// Replace the part of the document between `from` and `to` with the
|
|
/// given `slice`.
|
|
replace(from: number, to = from, slice = Slice.empty): this {
|
|
let step = replaceStep(this.doc, from, to, slice)
|
|
if (step) this.step(step)
|
|
return this
|
|
}
|
|
|
|
/// Replace the given range with the given content, which may be a
|
|
/// fragment, node, or array of nodes.
|
|
replaceWith(from: number, to: number, content: Fragment | Node | readonly Node[]): this {
|
|
return this.replace(from, to, new Slice(Fragment.from(content), 0, 0))
|
|
}
|
|
|
|
/// Delete the content between the given positions.
|
|
delete(from: number, to: number): this {
|
|
return this.replace(from, to, Slice.empty)
|
|
}
|
|
|
|
/// Insert the given content at the given position.
|
|
insert(pos: number, content: Fragment | Node | readonly Node[]): this {
|
|
return this.replaceWith(pos, pos, content)
|
|
}
|
|
|
|
/// Replace a range of the document with a given slice, using
|
|
/// `from`, `to`, and the slice's
|
|
/// [`openStart`](#model.Slice.openStart) property as hints, rather
|
|
/// than fixed start and end points. This method may grow the
|
|
/// replaced area or close open nodes in the slice in order to get a
|
|
/// fit that is more in line with WYSIWYG expectations, by dropping
|
|
/// fully covered parent nodes of the replaced region when they are
|
|
/// marked [non-defining as
|
|
/// context](#model.NodeSpec.definingAsContext), or including an
|
|
/// open parent node from the slice that _is_ marked as [defining
|
|
/// its content](#model.NodeSpec.definingForContent).
|
|
///
|
|
/// This is the method, for example, to handle paste. The similar
|
|
/// [`replace`](#transform.Transform.replace) method is a more
|
|
/// primitive tool which will _not_ move the start and end of its given
|
|
/// range, and is useful in situations where you need more precise
|
|
/// control over what happens.
|
|
replaceRange(from: number, to: number, slice: Slice): this {
|
|
replaceRange(this, from, to, slice)
|
|
return this
|
|
}
|
|
|
|
/// Replace the given range with a node, but use `from` and `to` as
|
|
/// hints, rather than precise positions. When from and to are the same
|
|
/// and are at the start or end of a parent node in which the given
|
|
/// node doesn't fit, this method may _move_ them out towards a parent
|
|
/// that does allow the given node to be placed. When the given range
|
|
/// completely covers a parent node, this method may completely replace
|
|
/// that parent node.
|
|
replaceRangeWith(from: number, to: number, node: Node): this {
|
|
replaceRangeWith(this, from, to, node)
|
|
return this
|
|
}
|
|
|
|
/// Delete the given range, expanding it to cover fully covered
|
|
/// parent nodes until a valid replace is found.
|
|
deleteRange(from: number, to: number): this {
|
|
deleteRange(this, from, to)
|
|
return this
|
|
}
|
|
|
|
/// Split the content in the given range off from its parent, if there
|
|
/// is sibling content before or after it, and move it up the tree to
|
|
/// the depth specified by `target`. You'll probably want to use
|
|
/// [`liftTarget`](#transform.liftTarget) to compute `target`, to make
|
|
/// sure the lift is valid.
|
|
lift(range: NodeRange, target: number): this {
|
|
lift(this, range, target)
|
|
return this
|
|
}
|
|
|
|
/// Join the blocks around the given position. If depth is 2, their
|
|
/// last and first siblings are also joined, and so on.
|
|
join(pos: number, depth: number = 1): this {
|
|
join(this, pos, depth)
|
|
return this
|
|
}
|
|
|
|
/// Wrap the given [range](#model.NodeRange) in the given set of wrappers.
|
|
/// The wrappers are assumed to be valid in this position, and should
|
|
/// probably be computed with [`findWrapping`](#transform.findWrapping).
|
|
wrap(range: NodeRange, wrappers: readonly {type: NodeType, attrs?: Attrs | null}[]): this {
|
|
wrap(this, range, wrappers)
|
|
return this
|
|
}
|
|
|
|
/// Set the type of all textblocks (partly) between `from` and `to` to
|
|
/// the given node type with the given attributes.
|
|
setBlockType(from: number, to = from, type: NodeType, attrs: Attrs | null = null): this {
|
|
setBlockType(this, from, to, type, attrs)
|
|
return this
|
|
}
|
|
|
|
/// Change the type, attributes, and/or marks of the node at `pos`.
|
|
/// When `type` isn't given, the existing node type is preserved,
|
|
setNodeMarkup(pos: number, type?: NodeType | null, attrs: Attrs | null = null, marks: readonly Mark[] = []): this {
|
|
setNodeMarkup(this, pos, type, attrs, marks)
|
|
return this
|
|
}
|
|
|
|
/// Set a single attribute on a given node to a new value.
|
|
setNodeAttribute(pos: number, attr: string, value: any): this {
|
|
this.step(new AttrStep(pos, attr, value))
|
|
return this
|
|
}
|
|
|
|
/// Add a mark to the node at position `pos`.
|
|
addNodeMark(pos: number, mark: Mark): this {
|
|
this.step(new AddNodeMarkStep(pos, mark))
|
|
return this
|
|
}
|
|
|
|
/// Remove a mark (or a mark of the given type) from the node at
|
|
/// position `pos`.
|
|
removeNodeMark(pos: number, mark: Mark | MarkType): this {
|
|
if (!(mark instanceof Mark)) {
|
|
let node = this.doc.nodeAt(pos)
|
|
if (!node) throw new RangeError("No node at position " + pos)
|
|
mark = mark.isInSet(node.marks)!
|
|
if (!mark) return this
|
|
}
|
|
this.step(new RemoveNodeMarkStep(pos, mark))
|
|
return this
|
|
}
|
|
|
|
/// Split the node at the given position, and optionally, if `depth` is
|
|
/// greater than one, any number of nodes above that. By default, the
|
|
/// parts split off will inherit the node type of the original node.
|
|
/// This can be changed by passing an array of types and attributes to
|
|
/// use after the split.
|
|
split(pos: number, depth = 1, typesAfter?: (null | {type: NodeType, attrs?: Attrs | null})[]) {
|
|
split(this, pos, depth, typesAfter)
|
|
return this
|
|
}
|
|
|
|
/// Add the given mark to the inline content between `from` and `to`.
|
|
addMark(from: number, to: number, mark: Mark): this {
|
|
addMark(this, from, to, mark)
|
|
return this
|
|
}
|
|
|
|
/// Remove marks from inline nodes between `from` and `to`. When
|
|
/// `mark` is a single mark, remove precisely that mark. When it is
|
|
/// a mark type, remove all marks of that type. When it is null,
|
|
/// remove all marks of any type.
|
|
removeMark(from: number, to: number, mark?: Mark | MarkType | null) {
|
|
removeMark(this, from, to, mark)
|
|
return this
|
|
}
|
|
|
|
/// Removes all marks and nodes from the content of the node at
|
|
/// `pos` that don't match the given new parent node type. Accepts
|
|
/// an optional starting [content match](#model.ContentMatch) as
|
|
/// third argument.
|
|
clearIncompatible(pos: number, parentType: NodeType, match?: ContentMatch) {
|
|
clearIncompatible(this, pos, parentType, match)
|
|
return this
|
|
}
|
|
}
|