gdpr audit implemented, email log, vollmachten, pdf delete cancel data privacy and vollmachten, removed message no id card in engergy car, and other contracts that are not telecom contracts, added insert counter for engery

This commit is contained in:
2026-03-21 11:59:53 +01:00
parent 09e87c951b
commit c3edb8ad2e
1491 changed files with 265550 additions and 1292 deletions
+59
View File
@@ -0,0 +1,59 @@
This module defines a way of modifying documents that allows changes
to be recorded, replayed, and reordered. You can read more about
transformations in [the guide](/docs/guide/#transform).
### Steps
Transforming happens in `Step`s, which are atomic, well-defined
modifications to a document. [Applying](#transform.Step.apply) a step
produces a new document.
Each step provides a [change map](#transform.StepMap) that maps
positions in the old document to position in the transformed document.
Steps can be [inverted](#transform.Step.invert) to create a step that
undoes their effect, and chained together in a convenience object
called a [`Transform`](#transform.Transform).
@Step
@StepResult
@ReplaceStep
@ReplaceAroundStep
@AddMarkStep
@RemoveMarkStep
@AddNodeMarkStep
@RemoveNodeMarkStep
@AttrStep
@DocAttrStep
### Position Mapping
Mapping positions from one document to another by running through the
[step maps](#transform.StepMap) produced by steps is an important
operation in ProseMirror. It is used, for example, for updating the
selection when the document changes.
@Mappable
@MapResult
@StepMap
@Mapping
### Document transforms
Because you often need to collect a number of steps together to effect
a composite change, ProseMirror provides an abstraction to make this
easy. [State transactions](#state.Transaction) are a subclass of
transforms.
@Transform
The following helper functions can be useful when creating
transformations or determining whether they are even possible.
@replaceStep
@liftTarget
@findWrapping
@canSplit
@canJoin
@joinPoint
@insertPoint
@dropPoint
+98
View File
@@ -0,0 +1,98 @@
import {Fragment, Slice, Node, Schema} from "prosemirror-model"
import {Step, StepResult} from "./step"
import {StepMap, Mappable} from "./map"
/// Update an attribute in a specific node.
export class AttrStep extends Step {
/// Construct an attribute step.
constructor(
/// The position of the target node.
readonly pos: number,
/// The attribute to set.
readonly attr: string,
// The attribute's new value.
readonly value: any
) {
super()
}
apply(doc: Node) {
let node = doc.nodeAt(this.pos)
if (!node) return StepResult.fail("No node at attribute step's position")
let attrs = Object.create(null)
for (let name in node.attrs) attrs[name] = node.attrs[name]
attrs[this.attr] = this.value
let updated = node.type.create(attrs, null, node.marks)
return StepResult.fromReplace(doc, this.pos, this.pos + 1, new Slice(Fragment.from(updated), 0, node.isLeaf ? 0 : 1))
}
getMap() {
return StepMap.empty
}
invert(doc: Node) {
return new AttrStep(this.pos, this.attr, doc.nodeAt(this.pos)!.attrs[this.attr])
}
map(mapping: Mappable) {
let pos = mapping.mapResult(this.pos, 1)
return pos.deletedAfter ? null : new AttrStep(pos.pos, this.attr, this.value)
}
toJSON(): any {
return {stepType: "attr", pos: this.pos, attr: this.attr, value: this.value}
}
static fromJSON(schema: Schema, json: any) {
if (typeof json.pos != "number" || typeof json.attr != "string")
throw new RangeError("Invalid input for AttrStep.fromJSON")
return new AttrStep(json.pos, json.attr, json.value)
}
}
Step.jsonID("attr", AttrStep)
/// Update an attribute in the doc node.
export class DocAttrStep extends Step {
/// Construct an attribute step.
constructor(
/// The attribute to set.
readonly attr: string,
// The attribute's new value.
readonly value: any
) {
super()
}
apply(doc: Node) {
let attrs = Object.create(null)
for (let name in doc.attrs) attrs[name] = doc.attrs[name]
attrs[this.attr] = this.value
let updated = doc.type.create(attrs, doc.content, doc.marks)
return StepResult.ok(updated)
}
getMap() {
return StepMap.empty
}
invert(doc: Node) {
return new DocAttrStep(this.attr, doc.attrs[this.attr])
}
map(mapping: Mappable) {
return this
}
toJSON(): any {
return {stepType: "docAttr", attr: this.attr, value: this.value}
}
static fromJSON(schema: Schema, json: any) {
if (typeof json.attr != "string")
throw new RangeError("Invalid input for DocAttrStep.fromJSON")
return new DocAttrStep(json.attr, json.value)
}
}
Step.jsonID("docAttr", DocAttrStep)
+11
View File
@@ -0,0 +1,11 @@
export {Transform} from "./transform"
/// @internal
export {TransformError} from "./transform"
export {Step, StepResult} from "./step"
export {joinPoint, canJoin, canSplit, insertPoint, dropPoint, liftTarget, findWrapping} from "./structure"
export {StepMap, MapResult, Mapping, Mappable} from "./map"
export {AddMarkStep, RemoveMarkStep, AddNodeMarkStep, RemoveNodeMarkStep} from "./mark_step"
export {ReplaceStep, ReplaceAroundStep} from "./replace_step"
export {AttrStep, DocAttrStep} from "./attr_step"
import "./mark"
export {replaceStep} from "./replace"
+284
View File
@@ -0,0 +1,284 @@
/// There are several things that positions can be mapped through.
/// Such objects conform to this interface.
export interface Mappable {
/// Map a position through this object. When given, `assoc` (should
/// be -1 or 1, defaults to 1) determines with which side the
/// position is associated, which determines in which direction to
/// move when a chunk of content is inserted at the mapped position.
map: (pos: number, assoc?: number) => number
/// Map a position, and return an object containing additional
/// information about the mapping. The result's `deleted` field tells
/// you whether the position was deleted (completely enclosed in a
/// replaced range) during the mapping. When content on only one side
/// is deleted, the position itself is only considered deleted when
/// `assoc` points in the direction of the deleted content.
mapResult: (pos: number, assoc?: number) => MapResult
}
// Recovery values encode a range index and an offset. They are
// represented as numbers, because tons of them will be created when
// mapping, for example, a large number of decorations. The number's
// lower 16 bits provide the index, the remaining bits the offset.
//
// Note: We intentionally don't use bit shift operators to en- and
// decode these, since those clip to 32 bits, which we might in rare
// cases want to overflow. A 64-bit float can represent 48-bit
// integers precisely.
const lower16 = 0xffff
const factor16 = Math.pow(2, 16)
function makeRecover(index: number, offset: number) { return index + offset * factor16 }
function recoverIndex(value: number) { return value & lower16 }
function recoverOffset(value: number) { return (value - (value & lower16)) / factor16 }
const DEL_BEFORE = 1, DEL_AFTER = 2, DEL_ACROSS = 4, DEL_SIDE = 8
/// An object representing a mapped position with extra
/// information.
export class MapResult {
/// @internal
constructor(
/// The mapped version of the position.
readonly pos: number,
/// @internal
readonly delInfo: number,
/// @internal
readonly recover: number | null
) {}
/// Tells you whether the position was deleted, that is, whether the
/// step removed the token on the side queried (via the `assoc`)
/// argument from the document.
get deleted() { return (this.delInfo & DEL_SIDE) > 0 }
/// Tells you whether the token before the mapped position was deleted.
get deletedBefore() { return (this.delInfo & (DEL_BEFORE | DEL_ACROSS)) > 0 }
/// True when the token after the mapped position was deleted.
get deletedAfter() { return (this.delInfo & (DEL_AFTER | DEL_ACROSS)) > 0 }
/// Tells whether any of the steps mapped through deletes across the
/// position (including both the token before and after the
/// position).
get deletedAcross() { return (this.delInfo & DEL_ACROSS) > 0 }
}
/// A map describing the deletions and insertions made by a step, which
/// can be used to find the correspondence between positions in the
/// pre-step version of a document and the same position in the
/// post-step version.
export class StepMap implements Mappable {
/// Create a position map. The modifications to the document are
/// represented as an array of numbers, in which each group of three
/// represents a modified chunk as `[start, oldSize, newSize]`.
constructor(
/// @internal
readonly ranges: readonly number[],
/// @internal
readonly inverted = false
) {
if (!ranges.length && StepMap.empty) return StepMap.empty
}
/// @internal
recover(value: number) {
let diff = 0, index = recoverIndex(value)
if (!this.inverted) for (let i = 0; i < index; i++)
diff += this.ranges[i * 3 + 2] - this.ranges[i * 3 + 1]
return this.ranges[index * 3] + diff + recoverOffset(value)
}
mapResult(pos: number, assoc = 1): MapResult { return this._map(pos, assoc, false) as MapResult }
map(pos: number, assoc = 1): number { return this._map(pos, assoc, true) as number }
/// @internal
_map(pos: number, assoc: number, simple: boolean) {
let diff = 0, oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2
for (let i = 0; i < this.ranges.length; i += 3) {
let start = this.ranges[i] - (this.inverted ? diff : 0)
if (start > pos) break
let oldSize = this.ranges[i + oldIndex], newSize = this.ranges[i + newIndex], end = start + oldSize
if (pos <= end) {
let side = !oldSize ? assoc : pos == start ? -1 : pos == end ? 1 : assoc
let result = start + diff + (side < 0 ? 0 : newSize)
if (simple) return result
let recover = pos == (assoc < 0 ? start : end) ? null : makeRecover(i / 3, pos - start)
let del = pos == start ? DEL_AFTER : pos == end ? DEL_BEFORE : DEL_ACROSS
if (assoc < 0 ? pos != start : pos != end) del |= DEL_SIDE
return new MapResult(result, del, recover)
}
diff += newSize - oldSize
}
return simple ? pos + diff : new MapResult(pos + diff, 0, null)
}
/// @internal
touches(pos: number, recover: number) {
let diff = 0, index = recoverIndex(recover)
let oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2
for (let i = 0; i < this.ranges.length; i += 3) {
let start = this.ranges[i] - (this.inverted ? diff : 0)
if (start > pos) break
let oldSize = this.ranges[i + oldIndex], end = start + oldSize
if (pos <= end && i == index * 3) return true
diff += this.ranges[i + newIndex] - oldSize
}
return false
}
/// Calls the given function on each of the changed ranges included in
/// this map.
forEach(f: (oldStart: number, oldEnd: number, newStart: number, newEnd: number) => void) {
let oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2
for (let i = 0, diff = 0; i < this.ranges.length; i += 3) {
let start = this.ranges[i], oldStart = start - (this.inverted ? diff : 0), newStart = start + (this.inverted ? 0 : diff)
let oldSize = this.ranges[i + oldIndex], newSize = this.ranges[i + newIndex]
f(oldStart, oldStart + oldSize, newStart, newStart + newSize)
diff += newSize - oldSize
}
}
/// Create an inverted version of this map. The result can be used to
/// map positions in the post-step document to the pre-step document.
invert() {
return new StepMap(this.ranges, !this.inverted)
}
/// @internal
toString() {
return (this.inverted ? "-" : "") + JSON.stringify(this.ranges)
}
/// Create a map that moves all positions by offset `n` (which may be
/// negative). This can be useful when applying steps meant for a
/// sub-document to a larger document, or vice-versa.
static offset(n: number) {
return n == 0 ? StepMap.empty : new StepMap(n < 0 ? [0, -n, 0] : [0, 0, n])
}
/// A StepMap that contains no changed ranges.
static empty = new StepMap([])
}
/// A mapping represents a pipeline of zero or more [step
/// maps](#transform.StepMap). It has special provisions for losslessly
/// handling mapping positions through a series of steps in which some
/// steps are inverted versions of earlier steps. (This comes up when
/// [rebasing](/docs/guide/#transform.rebasing) steps for
/// collaboration or history management.)
export class Mapping implements Mappable {
/// Create a new mapping with the given position maps.
constructor(
maps?: readonly StepMap[],
/// @internal
public mirror?: number[],
/// The starting position in the `maps` array, used when `map` or
/// `mapResult` is called.
public from = 0,
/// The end position in the `maps` array.
public to = maps ? maps.length : 0
) {
this._maps = (maps as StepMap[]) || []
this.ownData = !(maps || mirror)
}
/// The step maps in this mapping.
get maps(): readonly StepMap[] { return this._maps }
private _maps: StepMap[]
// False if maps/mirror are shared arrays that we shouldn't mutate
private ownData: boolean
/// Create a mapping that maps only through a part of this one.
slice(from = 0, to = this.maps.length) {
return new Mapping(this._maps, this.mirror, from, to)
}
/// Add a step map to the end of this mapping. If `mirrors` is
/// given, it should be the index of the step map that is the mirror
/// image of this one.
appendMap(map: StepMap, mirrors?: number) {
if (!this.ownData) {
this._maps = this._maps.slice()
this.mirror = this.mirror && this.mirror.slice()
this.ownData = true
}
this.to = this._maps.push(map)
if (mirrors != null) this.setMirror(this._maps.length - 1, mirrors)
}
/// Add all the step maps in a given mapping to this one (preserving
/// mirroring information).
appendMapping(mapping: Mapping) {
for (let i = 0, startSize = this._maps.length; i < mapping._maps.length; i++) {
let mirr = mapping.getMirror(i)
this.appendMap(mapping._maps[i], mirr != null && mirr < i ? startSize + mirr : undefined)
}
}
/// Finds the offset of the step map that mirrors the map at the
/// given offset, in this mapping (as per the second argument to
/// `appendMap`).
getMirror(n: number): number | undefined {
if (this.mirror) for (let i = 0; i < this.mirror.length; i++)
if (this.mirror[i] == n) return this.mirror[i + (i % 2 ? -1 : 1)]
}
/// @internal
setMirror(n: number, m: number) {
if (!this.mirror) this.mirror = []
this.mirror.push(n, m)
}
/// Append the inverse of the given mapping to this one.
appendMappingInverted(mapping: Mapping) {
for (let i = mapping.maps.length - 1, totalSize = this._maps.length + mapping._maps.length; i >= 0; i--) {
let mirr = mapping.getMirror(i)
this.appendMap(mapping._maps[i].invert(), mirr != null && mirr > i ? totalSize - mirr - 1 : undefined)
}
}
/// Create an inverted version of this mapping.
invert() {
let inverse = new Mapping
inverse.appendMappingInverted(this)
return inverse
}
/// Map a position through this mapping.
map(pos: number, assoc = 1) {
if (this.mirror) return this._map(pos, assoc, true) as number
for (let i = this.from; i < this.to; i++)
pos = this._maps[i].map(pos, assoc)
return pos
}
/// Map a position through this mapping, returning a mapping
/// result.
mapResult(pos: number, assoc = 1) { return this._map(pos, assoc, false) as MapResult }
/// @internal
_map(pos: number, assoc: number, simple: boolean) {
let delInfo = 0
for (let i = this.from; i < this.to; i++) {
let map = this._maps[i], result = map.mapResult(pos, assoc)
if (result.recover != null) {
let corr = this.getMirror(i)
if (corr != null && corr > i && corr < this.to) {
i = corr
pos = this._maps[corr].recover(result.recover)
continue
}
}
delInfo |= result.delInfo
pos = result.pos
}
return simple ? pos : new MapResult(pos, delInfo, null)
}
}
+106
View File
@@ -0,0 +1,106 @@
import {Mark, MarkType, Slice, Fragment, NodeType} from "prosemirror-model"
import {Step} from "./step"
import {Transform} from "./transform"
import {AddMarkStep, RemoveMarkStep} from "./mark_step"
import {ReplaceStep} from "./replace_step"
export function addMark(tr: Transform, from: number, to: number, mark: Mark) {
let removed: Step[] = [], added: Step[] = []
let removing: RemoveMarkStep | undefined, adding: AddMarkStep | undefined
tr.doc.nodesBetween(from, to, (node, pos, parent) => {
if (!node.isInline) return
let marks = node.marks
if (!mark.isInSet(marks) && parent!.type.allowsMarkType(mark.type)) {
let start = Math.max(pos, from), end = Math.min(pos + node.nodeSize, to)
let newSet = mark.addToSet(marks)
for (let i = 0; i < marks.length; i++) {
if (!marks[i].isInSet(newSet)) {
if (removing && removing.to == start && removing.mark.eq(marks[i]))
(removing as any).to = end
else
removed.push(removing = new RemoveMarkStep(start, end, marks[i]))
}
}
if (adding && adding.to == start)
(adding as any).to = end
else
added.push(adding = new AddMarkStep(start, end, mark))
}
})
removed.forEach(s => tr.step(s))
added.forEach(s => tr.step(s))
}
export function removeMark(tr: Transform, from: number, to: number, mark?: Mark | MarkType | null) {
let matched: {style: Mark, from: number, to: number, step: number}[] = [], step = 0
tr.doc.nodesBetween(from, to, (node, pos) => {
if (!node.isInline) return
step++
let toRemove = null
if (mark instanceof MarkType) {
let set = node.marks, found
while (found = mark.isInSet(set)) {
;(toRemove || (toRemove = [])).push(found)
set = found.removeFromSet(set)
}
} else if (mark) {
if (mark.isInSet(node.marks)) toRemove = [mark]
} else {
toRemove = node.marks
}
if (toRemove && toRemove.length) {
let end = Math.min(pos + node.nodeSize, to)
for (let i = 0; i < toRemove.length; i++) {
let style = toRemove[i], found
for (let j = 0; j < matched.length; j++) {
let m = matched[j]
if (m.step == step - 1 && style.eq(matched[j].style)) found = m
}
if (found) {
found.to = end
found.step = step
} else {
matched.push({style, from: Math.max(pos, from), to: end, step})
}
}
}
})
matched.forEach(m => tr.step(new RemoveMarkStep(m.from, m.to, m.style)))
}
export function clearIncompatible(tr: Transform, pos: number, parentType: NodeType,
match = parentType.contentMatch,
clearNewlines = true) {
let node = tr.doc.nodeAt(pos)!
let replSteps: Step[] = [], cur = pos + 1
for (let i = 0; i < node.childCount; i++) {
let child = node.child(i), end = cur + child.nodeSize
let allowed = match.matchType(child.type)
if (!allowed) {
replSteps.push(new ReplaceStep(cur, end, Slice.empty))
} else {
match = allowed
for (let j = 0; j < child.marks.length; j++) if (!parentType.allowsMarkType(child.marks[j].type))
tr.step(new RemoveMarkStep(cur, end, child.marks[j]))
if (clearNewlines && child.isText && parentType.whitespace != "pre") {
let m, newline = /\r?\n|\r/g, slice
while (m = newline.exec(child.text!)) {
if (!slice) slice = new Slice(Fragment.from(parentType.schema.text(" ", parentType.allowedMarks(child.marks))),
0, 0)
replSteps.push(new ReplaceStep(cur + m.index, cur + m.index + m[0].length, slice))
}
}
}
cur = end
}
if (!match.validEnd) {
let fill = match.fillBefore(Fragment.empty, true)
tr.replace(cur, cur, new Slice(fill!, 0, 0))
}
for (let i = replSteps.length - 1; i >= 0; i--) tr.step(replSteps[i])
}
+224
View File
@@ -0,0 +1,224 @@
import {Fragment, Slice, Node, Mark, Schema} from "prosemirror-model"
import {Step, StepResult} from "./step"
import {Mappable} from "./map"
function mapFragment(fragment: Fragment, f: (child: Node, parent: Node, i: number) => Node, parent: Node): Fragment {
let mapped = []
for (let i = 0; i < fragment.childCount; i++) {
let child = fragment.child(i)
if (child.content.size) child = child.copy(mapFragment(child.content, f, child))
if (child.isInline) child = f(child, parent, i)
mapped.push(child)
}
return Fragment.fromArray(mapped)
}
/// Add a mark to all inline content between two positions.
export class AddMarkStep extends Step {
/// Create a mark step.
constructor(
/// The start of the marked range.
readonly from: number,
/// The end of the marked range.
readonly to: number,
/// The mark to add.
readonly mark: Mark
) {
super()
}
apply(doc: Node) {
let oldSlice = doc.slice(this.from, this.to), $from = doc.resolve(this.from)
let parent = $from.node($from.sharedDepth(this.to))
let slice = new Slice(mapFragment(oldSlice.content, (node, parent) => {
if (!node.isAtom || !parent.type.allowsMarkType(this.mark.type)) return node
return node.mark(this.mark.addToSet(node.marks))
}, parent), oldSlice.openStart, oldSlice.openEnd)
return StepResult.fromReplace(doc, this.from, this.to, slice)
}
invert(): Step {
return new RemoveMarkStep(this.from, this.to, this.mark)
}
map(mapping: Mappable): Step | null {
let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1)
if (from.deleted && to.deleted || from.pos >= to.pos) return null
return new AddMarkStep(from.pos, to.pos, this.mark)
}
merge(other: Step): Step | null {
if (other instanceof AddMarkStep &&
other.mark.eq(this.mark) &&
this.from <= other.to && this.to >= other.from)
return new AddMarkStep(Math.min(this.from, other.from),
Math.max(this.to, other.to), this.mark)
return null
}
toJSON(): any {
return {stepType: "addMark", mark: this.mark.toJSON(),
from: this.from, to: this.to}
}
/// @internal
static fromJSON(schema: Schema, json: any) {
if (typeof json.from != "number" || typeof json.to != "number")
throw new RangeError("Invalid input for AddMarkStep.fromJSON")
return new AddMarkStep(json.from, json.to, schema.markFromJSON(json.mark))
}
}
Step.jsonID("addMark", AddMarkStep)
/// Remove a mark from all inline content between two positions.
export class RemoveMarkStep extends Step {
/// Create a mark-removing step.
constructor(
/// The start of the unmarked range.
readonly from: number,
/// The end of the unmarked range.
readonly to: number,
/// The mark to remove.
readonly mark: Mark
) {
super()
}
apply(doc: Node) {
let oldSlice = doc.slice(this.from, this.to)
let slice = new Slice(mapFragment(oldSlice.content, node => {
return node.mark(this.mark.removeFromSet(node.marks))
}, doc), oldSlice.openStart, oldSlice.openEnd)
return StepResult.fromReplace(doc, this.from, this.to, slice)
}
invert(): Step {
return new AddMarkStep(this.from, this.to, this.mark)
}
map(mapping: Mappable): Step | null {
let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1)
if (from.deleted && to.deleted || from.pos >= to.pos) return null
return new RemoveMarkStep(from.pos, to.pos, this.mark)
}
merge(other: Step): Step | null {
if (other instanceof RemoveMarkStep &&
other.mark.eq(this.mark) &&
this.from <= other.to && this.to >= other.from)
return new RemoveMarkStep(Math.min(this.from, other.from),
Math.max(this.to, other.to), this.mark)
return null
}
toJSON(): any {
return {stepType: "removeMark", mark: this.mark.toJSON(),
from: this.from, to: this.to}
}
/// @internal
static fromJSON(schema: Schema, json: any) {
if (typeof json.from != "number" || typeof json.to != "number")
throw new RangeError("Invalid input for RemoveMarkStep.fromJSON")
return new RemoveMarkStep(json.from, json.to, schema.markFromJSON(json.mark))
}
}
Step.jsonID("removeMark", RemoveMarkStep)
/// Add a mark to a specific node.
export class AddNodeMarkStep extends Step {
/// Create a node mark step.
constructor(
/// The position of the target node.
readonly pos: number,
/// The mark to add.
readonly mark: Mark
) {
super()
}
apply(doc: Node) {
let node = doc.nodeAt(this.pos)
if (!node) return StepResult.fail("No node at mark step's position")
let updated = node.type.create(node.attrs, null, this.mark.addToSet(node.marks))
return StepResult.fromReplace(doc, this.pos, this.pos + 1, new Slice(Fragment.from(updated), 0, node.isLeaf ? 0 : 1))
}
invert(doc: Node): Step {
let node = doc.nodeAt(this.pos)
if (node) {
let newSet = this.mark.addToSet(node.marks)
if (newSet.length == node.marks.length) {
for (let i = 0; i < node.marks.length; i++)
if (!node.marks[i].isInSet(newSet))
return new AddNodeMarkStep(this.pos, node.marks[i])
return new AddNodeMarkStep(this.pos, this.mark)
}
}
return new RemoveNodeMarkStep(this.pos, this.mark)
}
map(mapping: Mappable): Step | null {
let pos = mapping.mapResult(this.pos, 1)
return pos.deletedAfter ? null : new AddNodeMarkStep(pos.pos, this.mark)
}
toJSON(): any {
return {stepType: "addNodeMark", pos: this.pos, mark: this.mark.toJSON()}
}
/// @internal
static fromJSON(schema: Schema, json: any) {
if (typeof json.pos != "number")
throw new RangeError("Invalid input for AddNodeMarkStep.fromJSON")
return new AddNodeMarkStep(json.pos, schema.markFromJSON(json.mark))
}
}
Step.jsonID("addNodeMark", AddNodeMarkStep)
/// Remove a mark from a specific node.
export class RemoveNodeMarkStep extends Step {
/// Create a mark-removing step.
constructor(
/// The position of the target node.
readonly pos: number,
/// The mark to remove.
readonly mark: Mark
) {
super()
}
apply(doc: Node) {
let node = doc.nodeAt(this.pos)
if (!node) return StepResult.fail("No node at mark step's position")
let updated = node.type.create(node.attrs, null, this.mark.removeFromSet(node.marks))
return StepResult.fromReplace(doc, this.pos, this.pos + 1, new Slice(Fragment.from(updated), 0, node.isLeaf ? 0 : 1))
}
invert(doc: Node): Step {
let node = doc.nodeAt(this.pos)
if (!node || !this.mark.isInSet(node.marks)) return this
return new AddNodeMarkStep(this.pos, this.mark)
}
map(mapping: Mappable): Step | null {
let pos = mapping.mapResult(this.pos, 1)
return pos.deletedAfter ? null : new RemoveNodeMarkStep(pos.pos, this.mark)
}
toJSON(): any {
return {stepType: "removeNodeMark", pos: this.pos, mark: this.mark.toJSON()}
}
/// @internal
static fromJSON(schema: Schema, json: any) {
if (typeof json.pos != "number")
throw new RangeError("Invalid input for RemoveNodeMarkStep.fromJSON")
return new RemoveNodeMarkStep(json.pos, schema.markFromJSON(json.mark))
}
}
Step.jsonID("removeNodeMark", RemoveNodeMarkStep)
+460
View File
@@ -0,0 +1,460 @@
import {Fragment, Slice, Node, ResolvedPos, NodeType, ContentMatch, Attrs} from "prosemirror-model"
import {Step} from "./step"
import {ReplaceStep, ReplaceAroundStep} from "./replace_step"
import {Transform} from "./transform"
import {insertPoint} from "./structure"
/// Fit a slice into a given position in the document, producing a
/// [step](#transform.Step) that inserts it. Will return null if
/// there's no meaningful way to insert the slice here, or inserting it
/// would be a no-op (an empty slice over an empty range).
export function replaceStep(doc: Node, from: number, to = from, slice = Slice.empty): Step | null {
if (from == to && !slice.size) return null
let $from = doc.resolve(from), $to = doc.resolve(to)
// Optimization -- avoid work if it's obvious that it's not needed.
if (fitsTrivially($from, $to, slice)) return new ReplaceStep(from, to, slice)
return new Fitter($from, $to, slice).fit()
}
function fitsTrivially($from: ResolvedPos, $to: ResolvedPos, slice: Slice) {
return !slice.openStart && !slice.openEnd && $from.start() == $to.start() &&
$from.parent.canReplace($from.index(), $to.index(), slice.content)
}
interface Fittable {
sliceDepth: number
frontierDepth: number
parent: Node | null
inject?: Fragment | null
wrap?: readonly NodeType[]
}
// Algorithm for 'placing' the elements of a slice into a gap:
//
// We consider the content of each node that is open to the left to be
// independently placeable. I.e. in <p("foo"), p("bar")>, when the
// paragraph on the left is open, "foo" can be placed (somewhere on
// the left side of the replacement gap) independently from p("bar").
//
// This class tracks the state of the placement progress in the
// following properties:
//
// - `frontier` holds a stack of `{type, match}` objects that
// represent the open side of the replacement. It starts at
// `$from`, then moves forward as content is placed, and is finally
// reconciled with `$to`.
//
// - `unplaced` is a slice that represents the content that hasn't
// been placed yet.
//
// - `placed` is a fragment of placed content. Its open-start value
// is implicit in `$from`, and its open-end value in `frontier`.
class Fitter {
frontier: {type: NodeType, match: ContentMatch}[] = []
placed: Fragment = Fragment.empty
constructor(
readonly $from: ResolvedPos,
readonly $to: ResolvedPos,
public unplaced: Slice
) {
for (let i = 0; i <= $from.depth; i++) {
let node = $from.node(i)
this.frontier.push({
type: node.type,
match: node.contentMatchAt($from.indexAfter(i))
})
}
for (let i = $from.depth; i > 0; i--)
this.placed = Fragment.from($from.node(i).copy(this.placed))
}
get depth() { return this.frontier.length - 1 }
fit() {
// As long as there's unplaced content, try to place some of it.
// If that fails, either increase the open score of the unplaced
// slice, or drop nodes from it, and then try again.
while (this.unplaced.size) {
let fit = this.findFittable()
if (fit) this.placeNodes(fit)
else this.openMore() || this.dropNode()
}
// When there's inline content directly after the frontier _and_
// directly after `this.$to`, we must generate a `ReplaceAround`
// step that pulls that content into the node after the frontier.
// That means the fitting must be done to the end of the textblock
// node after `this.$to`, not `this.$to` itself.
let moveInline = this.mustMoveInline(), placedSize = this.placed.size - this.depth - this.$from.depth
let $from = this.$from, $to = this.close(moveInline < 0 ? this.$to : $from.doc.resolve(moveInline))
if (!$to) return null
// If closing to `$to` succeeded, create a step
let content = this.placed, openStart = $from.depth, openEnd = $to.depth
while (openStart && openEnd && content.childCount == 1) { // Normalize by dropping open parent nodes
content = content.firstChild!.content
openStart--; openEnd--
}
let slice = new Slice(content, openStart, openEnd)
if (moveInline > -1)
return new ReplaceAroundStep($from.pos, moveInline, this.$to.pos, this.$to.end(), slice, placedSize)
if (slice.size || $from.pos != this.$to.pos) // Don't generate no-op steps
return new ReplaceStep($from.pos, $to.pos, slice)
return null
}
// Find a position on the start spine of `this.unplaced` that has
// content that can be moved somewhere on the frontier. Returns two
// depths, one for the slice and one for the frontier.
findFittable(): Fittable | undefined {
let startDepth = this.unplaced.openStart
for (let cur = this.unplaced.content, d = 0, openEnd = this.unplaced.openEnd; d < startDepth; d++) {
let node = cur.firstChild!
if (cur.childCount > 1) openEnd = 0
if (node.type.spec.isolating && openEnd <= d) {
startDepth = d
break
}
cur = node.content
}
// Only try wrapping nodes (pass 2) after finding a place without
// wrapping failed.
for (let pass = 1; pass <= 2; pass++) {
for (let sliceDepth = pass == 1 ? startDepth : this.unplaced.openStart; sliceDepth >= 0; sliceDepth--) {
let fragment, parent = null
if (sliceDepth) {
parent = contentAt(this.unplaced.content, sliceDepth - 1).firstChild
fragment = parent!.content
} else {
fragment = this.unplaced.content
}
let first = fragment.firstChild
for (let frontierDepth = this.depth; frontierDepth >= 0; frontierDepth--) {
let {type, match} = this.frontier[frontierDepth], wrap, inject: Fragment | null = null
// In pass 1, if the next node matches, or there is no next
// node but the parents look compatible, we've found a
// place.
if (pass == 1 && (first ? match.matchType(first.type) || (inject = match.fillBefore(Fragment.from(first), false))
: parent && type.compatibleContent(parent.type)))
return {sliceDepth, frontierDepth, parent, inject}
// In pass 2, look for a set of wrapping nodes that make
// `first` fit here.
else if (pass == 2 && first && (wrap = match.findWrapping(first.type)))
return {sliceDepth, frontierDepth, parent, wrap}
// Don't continue looking further up if the parent node
// would fit here.
if (parent && match.matchType(parent.type)) break
}
}
}
}
openMore() {
let {content, openStart, openEnd} = this.unplaced
let inner = contentAt(content, openStart)
if (!inner.childCount || inner.firstChild!.isLeaf) return false
this.unplaced = new Slice(content, openStart + 1,
Math.max(openEnd, inner.size + openStart >= content.size - openEnd ? openStart + 1 : 0))
return true
}
dropNode() {
let {content, openStart, openEnd} = this.unplaced
let inner = contentAt(content, openStart)
if (inner.childCount <= 1 && openStart > 0) {
let openAtEnd = content.size - openStart <= openStart + inner.size
this.unplaced = new Slice(dropFromFragment(content, openStart - 1, 1), openStart - 1,
openAtEnd ? openStart - 1 : openEnd)
} else {
this.unplaced = new Slice(dropFromFragment(content, openStart, 1), openStart, openEnd)
}
}
// Move content from the unplaced slice at `sliceDepth` to the
// frontier node at `frontierDepth`. Close that frontier node when
// applicable.
placeNodes({sliceDepth, frontierDepth, parent, inject, wrap}: Fittable) {
while (this.depth > frontierDepth) this.closeFrontierNode()
if (wrap) for (let i = 0; i < wrap.length; i++) this.openFrontierNode(wrap[i])
let slice = this.unplaced, fragment = parent ? parent.content : slice.content
let openStart = slice.openStart - sliceDepth
let taken = 0, add = []
let {match, type} = this.frontier[frontierDepth]
if (inject) {
for (let i = 0; i < inject.childCount; i++) add.push(inject.child(i))
match = match.matchFragment(inject)!
}
// Computes the amount of (end) open nodes at the end of the
// fragment. When 0, the parent is open, but no more. When
// negative, nothing is open.
let openEndCount = (fragment.size + sliceDepth) - (slice.content.size - slice.openEnd)
// Scan over the fragment, fitting as many child nodes as
// possible.
while (taken < fragment.childCount) {
let next = fragment.child(taken), matches = match.matchType(next.type)
if (!matches) break
taken++
if (taken > 1 || openStart == 0 || next.content.size) { // Drop empty open nodes
match = matches
add.push(closeNodeStart(next.mark(type.allowedMarks(next.marks)), taken == 1 ? openStart : 0,
taken == fragment.childCount ? openEndCount : -1))
}
}
let toEnd = taken == fragment.childCount
if (!toEnd) openEndCount = -1
this.placed = addToFragment(this.placed, frontierDepth, Fragment.from(add))
this.frontier[frontierDepth].match = match
// If the parent types match, and the entire node was moved, and
// it's not open, close this frontier node right away.
if (toEnd && openEndCount < 0 && parent && parent.type == this.frontier[this.depth].type && this.frontier.length > 1)
this.closeFrontierNode()
// Add new frontier nodes for any open nodes at the end.
for (let i = 0, cur = fragment; i < openEndCount; i++) {
let node = cur.lastChild!
this.frontier.push({type: node.type, match: node.contentMatchAt(node.childCount)})
cur = node.content
}
// Update `this.unplaced`. Drop the entire node from which we
// placed it we got to its end, otherwise just drop the placed
// nodes.
this.unplaced = !toEnd ? new Slice(dropFromFragment(slice.content, sliceDepth, taken), slice.openStart, slice.openEnd)
: sliceDepth == 0 ? Slice.empty
: new Slice(dropFromFragment(slice.content, sliceDepth - 1, 1),
sliceDepth - 1, openEndCount < 0 ? slice.openEnd : sliceDepth - 1)
}
mustMoveInline() {
if (!this.$to.parent.isTextblock) return -1
let top = this.frontier[this.depth], level
if (!top.type.isTextblock || !contentAfterFits(this.$to, this.$to.depth, top.type, top.match, false) ||
(this.$to.depth == this.depth && (level = this.findCloseLevel(this.$to)) && level.depth == this.depth)) return -1
let {depth} = this.$to, after = this.$to.after(depth)
while (depth > 1 && after == this.$to.end(--depth)) ++after
return after
}
findCloseLevel($to: ResolvedPos) {
scan: for (let i = Math.min(this.depth, $to.depth); i >= 0; i--) {
let {match, type} = this.frontier[i]
let dropInner = i < $to.depth && $to.end(i + 1) == $to.pos + ($to.depth - (i + 1))
let fit = contentAfterFits($to, i, type, match, dropInner)
if (!fit) continue
for (let d = i - 1; d >= 0; d--) {
let {match, type} = this.frontier[d]
let matches = contentAfterFits($to, d, type, match, true)
if (!matches || matches.childCount) continue scan
}
return {depth: i, fit, move: dropInner ? $to.doc.resolve($to.after(i + 1)) : $to}
}
}
close($to: ResolvedPos) {
let close = this.findCloseLevel($to)
if (!close) return null
while (this.depth > close.depth) this.closeFrontierNode()
if (close.fit.childCount) this.placed = addToFragment(this.placed, close.depth, close.fit)
$to = close.move
for (let d = close.depth + 1; d <= $to.depth; d++) {
let node = $to.node(d), add = node.type.contentMatch.fillBefore(node.content, true, $to.index(d))!
this.openFrontierNode(node.type, node.attrs, add)
}
return $to
}
openFrontierNode(type: NodeType, attrs: Attrs | null = null, content?: Fragment) {
let top = this.frontier[this.depth]
top.match = top.match.matchType(type)!
this.placed = addToFragment(this.placed, this.depth, Fragment.from(type.create(attrs, content)))
this.frontier.push({type, match: type.contentMatch})
}
closeFrontierNode() {
let open = this.frontier.pop()!
let add = open.match.fillBefore(Fragment.empty, true)!
if (add.childCount) this.placed = addToFragment(this.placed, this.frontier.length, add)
}
}
function dropFromFragment(fragment: Fragment, depth: number, count: number): Fragment {
if (depth == 0) return fragment.cutByIndex(count, fragment.childCount)
return fragment.replaceChild(0, fragment.firstChild!.copy(dropFromFragment(fragment.firstChild!.content, depth - 1, count)))
}
function addToFragment(fragment: Fragment, depth: number, content: Fragment): Fragment {
if (depth == 0) return fragment.append(content)
return fragment.replaceChild(fragment.childCount - 1,
fragment.lastChild!.copy(addToFragment(fragment.lastChild!.content, depth - 1, content)))
}
function contentAt(fragment: Fragment, depth: number) {
for (let i = 0; i < depth; i++) fragment = fragment.firstChild!.content
return fragment
}
function closeNodeStart(node: Node, openStart: number, openEnd: number) {
if (openStart <= 0) return node
let frag = node.content
if (openStart > 1)
frag = frag.replaceChild(0, closeNodeStart(frag.firstChild!, openStart - 1, frag.childCount == 1 ? openEnd - 1 : 0))
if (openStart > 0) {
frag = node.type.contentMatch.fillBefore(frag)!.append(frag)
if (openEnd <= 0) frag = frag.append(node.type.contentMatch.matchFragment(frag)!.fillBefore(Fragment.empty, true)!)
}
return node.copy(frag)
}
function contentAfterFits($to: ResolvedPos, depth: number, type: NodeType, match: ContentMatch, open: boolean) {
let node = $to.node(depth), index = open ? $to.indexAfter(depth) : $to.index(depth)
if (index == node.childCount && !type.compatibleContent(node.type)) return null
let fit = match.fillBefore(node.content, true, index)
return fit && !invalidMarks(type, node.content, index) ? fit : null
}
function invalidMarks(type: NodeType, fragment: Fragment, start: number) {
for (let i = start; i < fragment.childCount; i++)
if (!type.allowsMarks(fragment.child(i).marks)) return true
return false
}
function definesContent(type: NodeType) {
return type.spec.defining || type.spec.definingForContent
}
export function replaceRange(tr: Transform, from: number, to: number, slice: Slice) {
if (!slice.size) return tr.deleteRange(from, to)
let $from = tr.doc.resolve(from), $to = tr.doc.resolve(to)
if (fitsTrivially($from, $to, slice))
return tr.step(new ReplaceStep(from, to, slice))
let targetDepths = coveredDepths($from, $to)
// Can't replace the whole document, so remove 0 if it's present
if (targetDepths[targetDepths.length - 1] == 0) targetDepths.pop()
// Negative numbers represent not expansion over the whole node at
// that depth, but replacing from $from.before(-D) to $to.pos.
let preferredTarget = -($from.depth + 1)
targetDepths.unshift(preferredTarget)
// This loop picks a preferred target depth, if one of the covering
// depths is not outside of a defining node, and adds negative
// depths for any depth that has $from at its start and does not
// cross a defining node.
for (let d = $from.depth, pos = $from.pos - 1; d > 0; d--, pos--) {
let spec = $from.node(d).type.spec
if (spec.defining || spec.definingAsContext || spec.isolating) break
if (targetDepths.indexOf(d) > -1) preferredTarget = d
else if ($from.before(d) == pos) targetDepths.splice(1, 0, -d)
}
// Try to fit each possible depth of the slice into each possible
// target depth, starting with the preferred depths.
let preferredTargetIndex = targetDepths.indexOf(preferredTarget)
let leftNodes: Node[] = [], preferredDepth = slice.openStart
for (let content = slice.content, i = 0;; i++) {
let node = content.firstChild!
leftNodes.push(node)
if (i == slice.openStart) break
content = node.content
}
// Back up preferredDepth to cover defining textblocks directly
// above it, possibly skipping a non-defining textblock.
for (let d = preferredDepth - 1; d >= 0; d--) {
let leftNode = leftNodes[d], def = definesContent(leftNode.type)
if (def && !leftNode.sameMarkup($from.node(Math.abs(preferredTarget) - 1))) preferredDepth = d
else if (def || !leftNode.type.isTextblock) break
}
for (let j = slice.openStart; j >= 0; j--) {
let openDepth = (j + preferredDepth + 1) % (slice.openStart + 1)
let insert = leftNodes[openDepth]
if (!insert) continue
for (let i = 0; i < targetDepths.length; i++) {
// Loop over possible expansion levels, starting with the
// preferred one
let targetDepth = targetDepths[(i + preferredTargetIndex) % targetDepths.length], expand = true
if (targetDepth < 0) { expand = false; targetDepth = -targetDepth }
let parent = $from.node(targetDepth - 1), index = $from.index(targetDepth - 1)
if (parent.canReplaceWith(index, index, insert.type, insert.marks))
return tr.replace($from.before(targetDepth), expand ? $to.after(targetDepth) : to,
new Slice(closeFragment(slice.content, 0, slice.openStart, openDepth),
openDepth, slice.openEnd))
}
}
let startSteps = tr.steps.length
for (let i = targetDepths.length - 1; i >= 0; i--) {
tr.replace(from, to, slice)
if (tr.steps.length > startSteps) break
let depth = targetDepths[i]
if (depth < 0) continue
from = $from.before(depth); to = $to.after(depth)
}
}
function closeFragment(fragment: Fragment, depth: number, oldOpen: number, newOpen: number, parent?: Node) {
if (depth < oldOpen) {
let first = fragment.firstChild!
fragment = fragment.replaceChild(0, first.copy(closeFragment(first.content, depth + 1, oldOpen, newOpen, first)))
}
if (depth > newOpen) {
let match = parent!.contentMatchAt(0)!
let start = match.fillBefore(fragment)!.append(fragment)
fragment = start.append(match.matchFragment(start)!.fillBefore(Fragment.empty, true)!)
}
return fragment
}
export function replaceRangeWith(tr: Transform, from: number, to: number, node: Node) {
if (!node.isInline && from == to && tr.doc.resolve(from).parent.content.size) {
let point = insertPoint(tr.doc, from, node.type)
if (point != null) from = to = point
}
tr.replaceRange(from, to, new Slice(Fragment.from(node), 0, 0))
}
export function deleteRange(tr: Transform, from: number, to: number) {
let $from = tr.doc.resolve(from), $to = tr.doc.resolve(to)
let covered = coveredDepths($from, $to)
for (let i = 0; i < covered.length; i++) {
let depth = covered[i], last = i == covered.length - 1
if ((last && depth == 0) || $from.node(depth).type.contentMatch.validEnd)
return tr.delete($from.start(depth), $to.end(depth))
if (depth > 0 && (last || $from.node(depth - 1).canReplace($from.index(depth - 1), $to.indexAfter(depth - 1))))
return tr.delete($from.before(depth), $to.after(depth))
}
for (let d = 1; d <= $from.depth && d <= $to.depth; d++) {
if (from - $from.start(d) == $from.depth - d && to > $from.end(d) && $to.end(d) - to != $to.depth - d &&
$from.start(d - 1) == $to.start(d - 1) && $from.node(d - 1).canReplace($from.index(d - 1), $to.index(d - 1)))
return tr.delete($from.before(d), to)
}
tr.delete(from, to)
}
// Returns an array of all depths for which $from - $to spans the
// whole content of the nodes at that depth.
function coveredDepths($from: ResolvedPos, $to: ResolvedPos) {
let result: number[] = [], minDepth = Math.min($from.depth, $to.depth)
for (let d = minDepth; d >= 0; d--) {
let start = $from.start(d)
if (start < $from.pos - ($from.depth - d) ||
$to.end(d) > $to.pos + ($to.depth - d) ||
$from.node(d).type.spec.isolating ||
$to.node(d).type.spec.isolating) break
if (start == $to.start(d) ||
(d == $from.depth && d == $to.depth && $from.parent.inlineContent && $to.parent.inlineContent &&
d && $to.start(d - 1) == start - 1))
result.push(d)
}
return result
}
+178
View File
@@ -0,0 +1,178 @@
import {Slice, Node, Schema} from "prosemirror-model"
import {Step, StepResult} from "./step"
import {StepMap, Mappable} from "./map"
/// Replace a part of the document with a slice of new content.
export class ReplaceStep extends Step {
/// The given `slice` should fit the 'gap' between `from` and
/// `to`—the depths must line up, and the surrounding nodes must be
/// able to be joined with the open sides of the slice. When
/// `structure` is true, the step will fail if the content between
/// from and to is not just a sequence of closing and then opening
/// tokens (this is to guard against rebased replace steps
/// overwriting something they weren't supposed to).
constructor(
/// The start position of the replaced range.
readonly from: number,
/// The end position of the replaced range.
readonly to: number,
/// The slice to insert.
readonly slice: Slice,
/// @internal
readonly structure = false
) {
super()
}
apply(doc: Node) {
if (this.structure && contentBetween(doc, this.from, this.to))
return StepResult.fail("Structure replace would overwrite content")
return StepResult.fromReplace(doc, this.from, this.to, this.slice)
}
getMap() {
return new StepMap([this.from, this.to - this.from, this.slice.size])
}
invert(doc: Node) {
return new ReplaceStep(this.from, this.from + this.slice.size, doc.slice(this.from, this.to))
}
map(mapping: Mappable) {
let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1)
if (from.deletedAcross && to.deletedAcross) return null
return new ReplaceStep(from.pos, Math.max(from.pos, to.pos), this.slice, this.structure)
}
merge(other: Step) {
if (!(other instanceof ReplaceStep) || other.structure || this.structure) return null
if (this.from + this.slice.size == other.from && !this.slice.openEnd && !other.slice.openStart) {
let slice = this.slice.size + other.slice.size == 0 ? Slice.empty
: new Slice(this.slice.content.append(other.slice.content), this.slice.openStart, other.slice.openEnd)
return new ReplaceStep(this.from, this.to + (other.to - other.from), slice, this.structure)
} else if (other.to == this.from && !this.slice.openStart && !other.slice.openEnd) {
let slice = this.slice.size + other.slice.size == 0 ? Slice.empty
: new Slice(other.slice.content.append(this.slice.content), other.slice.openStart, this.slice.openEnd)
return new ReplaceStep(other.from, this.to, slice, this.structure)
} else {
return null
}
}
toJSON(): any {
let json: any = {stepType: "replace", from: this.from, to: this.to}
if (this.slice.size) json.slice = this.slice.toJSON()
if (this.structure) json.structure = true
return json
}
/// @internal
static fromJSON(schema: Schema, json: any) {
if (typeof json.from != "number" || typeof json.to != "number")
throw new RangeError("Invalid input for ReplaceStep.fromJSON")
return new ReplaceStep(json.from, json.to, Slice.fromJSON(schema, json.slice), !!json.structure)
}
}
Step.jsonID("replace", ReplaceStep)
/// Replace a part of the document with a slice of content, but
/// preserve a range of the replaced content by moving it into the
/// slice.
export class ReplaceAroundStep extends Step {
/// Create a replace-around step with the given range and gap.
/// `insert` should be the point in the slice into which the content
/// of the gap should be moved. `structure` has the same meaning as
/// it has in the [`ReplaceStep`](#transform.ReplaceStep) class.
constructor(
/// The start position of the replaced range.
readonly from: number,
/// The end position of the replaced range.
readonly to: number,
/// The start of preserved range.
readonly gapFrom: number,
/// The end of preserved range.
readonly gapTo: number,
/// The slice to insert.
readonly slice: Slice,
/// The position in the slice where the preserved range should be
/// inserted.
readonly insert: number,
/// @internal
readonly structure = false
) {
super()
}
apply(doc: Node) {
if (this.structure && (contentBetween(doc, this.from, this.gapFrom) ||
contentBetween(doc, this.gapTo, this.to)))
return StepResult.fail("Structure gap-replace would overwrite content")
let gap = doc.slice(this.gapFrom, this.gapTo)
if (gap.openStart || gap.openEnd)
return StepResult.fail("Gap is not a flat range")
let inserted = this.slice.insertAt(this.insert, gap.content)
if (!inserted) return StepResult.fail("Content does not fit in gap")
return StepResult.fromReplace(doc, this.from, this.to, inserted)
}
getMap() {
return new StepMap([this.from, this.gapFrom - this.from, this.insert,
this.gapTo, this.to - this.gapTo, this.slice.size - this.insert])
}
invert(doc: Node) {
let gap = this.gapTo - this.gapFrom
return new ReplaceAroundStep(this.from, this.from + this.slice.size + gap,
this.from + this.insert, this.from + this.insert + gap,
doc.slice(this.from, this.to).removeBetween(this.gapFrom - this.from, this.gapTo - this.from),
this.gapFrom - this.from, this.structure)
}
map(mapping: Mappable) {
let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1)
let gapFrom = this.from == this.gapFrom ? from.pos : mapping.map(this.gapFrom, -1)
let gapTo = this.to == this.gapTo ? to.pos : mapping.map(this.gapTo, 1)
if ((from.deletedAcross && to.deletedAcross) || gapFrom < from.pos || gapTo > to.pos) return null
return new ReplaceAroundStep(from.pos, to.pos, gapFrom, gapTo, this.slice, this.insert, this.structure)
}
toJSON(): any {
let json: any = {stepType: "replaceAround", from: this.from, to: this.to,
gapFrom: this.gapFrom, gapTo: this.gapTo, insert: this.insert}
if (this.slice.size) json.slice = this.slice.toJSON()
if (this.structure) json.structure = true
return json
}
/// @internal
static fromJSON(schema: Schema, json: any) {
if (typeof json.from != "number" || typeof json.to != "number" ||
typeof json.gapFrom != "number" || typeof json.gapTo != "number" || typeof json.insert != "number")
throw new RangeError("Invalid input for ReplaceAroundStep.fromJSON")
return new ReplaceAroundStep(json.from, json.to, json.gapFrom, json.gapTo,
Slice.fromJSON(schema, json.slice), json.insert, !!json.structure)
}
}
Step.jsonID("replaceAround", ReplaceAroundStep)
function contentBetween(doc: Node, from: number, to: number) {
let $from = doc.resolve(from), dist = to - from, depth = $from.depth
while (dist > 0 && depth > 0 && $from.indexAfter(depth) == $from.node(depth).childCount) {
depth--
dist--
}
if (dist > 0) {
let next = $from.node(depth).maybeChild($from.indexAfter(depth))
while (dist > 0) {
if (!next || next.isLeaf) return true
next = next.firstChild
dist--
}
}
return false
}
+97
View File
@@ -0,0 +1,97 @@
import {ReplaceError, Schema, Slice, Node} from "prosemirror-model"
import {StepMap, Mappable} from "./map"
const stepsByID: {[id: string]: {fromJSON(schema: Schema, json: any): Step}} = Object.create(null)
/// A step object represents an atomic change. It generally applies
/// only to the document it was created for, since the positions
/// stored in it will only make sense for that document.
///
/// New steps are defined by creating classes that extend `Step`,
/// overriding the `apply`, `invert`, `map`, `getMap` and `fromJSON`
/// methods, and registering your class with a unique
/// JSON-serialization identifier using
/// [`Step.jsonID`](#transform.Step^jsonID).
export abstract class Step {
/// Applies this step to the given document, returning a result
/// object that either indicates failure, if the step can not be
/// applied to this document, or indicates success by containing a
/// transformed document.
abstract apply(doc: Node): StepResult
/// Get the step map that represents the changes made by this step,
/// and which can be used to transform between positions in the old
/// and the new document.
getMap(): StepMap { return StepMap.empty }
/// Create an inverted version of this step. Needs the document as it
/// was before the step as argument.
abstract invert(doc: Node): Step
/// Map this step through a mappable thing, returning either a
/// version of that step with its positions adjusted, or `null` if
/// the step was entirely deleted by the mapping.
abstract map(mapping: Mappable): Step | null
/// Try to merge this step with another one, to be applied directly
/// after it. Returns the merged step when possible, null if the
/// steps can't be merged.
merge(other: Step): Step | null { return null }
/// Create a JSON-serializeable representation of this step. When
/// defining this for a custom subclass, make sure the result object
/// includes the step type's [JSON id](#transform.Step^jsonID) under
/// the `stepType` property.
abstract toJSON(): any
/// Deserialize a step from its JSON representation. Will call
/// through to the step class' own implementation of this method.
static fromJSON(schema: Schema, json: any): Step {
if (!json || !json.stepType) throw new RangeError("Invalid input for Step.fromJSON")
let type = stepsByID[json.stepType]
if (!type) throw new RangeError(`No step type ${json.stepType} defined`)
return type.fromJSON(schema, json)
}
/// To be able to serialize steps to JSON, each step needs a string
/// ID to attach to its JSON representation. Use this method to
/// register an ID for your step classes. Try to pick something
/// that's unlikely to clash with steps from other modules.
static jsonID(id: string, stepClass: {fromJSON(schema: Schema, json: any): Step}) {
if (id in stepsByID) throw new RangeError("Duplicate use of step JSON ID " + id)
stepsByID[id] = stepClass
;(stepClass as any).prototype.jsonID = id
return stepClass
}
}
/// The result of [applying](#transform.Step.apply) a step. Contains either a
/// new document or a failure value.
export class StepResult {
/// @internal
constructor(
/// The transformed document, if successful.
readonly doc: Node | null,
/// The failure message, if unsuccessful.
readonly failed: string | null
) {}
/// Create a successful step result.
static ok(doc: Node) { return new StepResult(doc, null) }
/// Create a failed step result.
static fail(message: string) { return new StepResult(null, message) }
/// Call [`Node.replace`](#model.Node.replace) with the given
/// arguments. Create a successful result if it succeeds, and a
/// failed one if it throws a `ReplaceError`.
static fromReplace(doc: Node, from: number, to: number, slice: Slice) {
try {
return StepResult.ok(doc.replace(from, to, slice))
} catch (e) {
if (e instanceof ReplaceError) return StepResult.fail(e.message)
throw e
}
}
}
+349
View File
@@ -0,0 +1,349 @@
import {Slice, Fragment, NodeRange, NodeType, Node, Mark, Attrs, ContentMatch} from "prosemirror-model"
import {Transform} from "./transform"
import {ReplaceStep, ReplaceAroundStep} from "./replace_step"
import {clearIncompatible} from "./mark"
function canCut(node: Node, start: number, end: number) {
return (start == 0 || node.canReplace(start, node.childCount)) &&
(end == node.childCount || node.canReplace(0, end))
}
/// Try to find a target depth to which the content in the given range
/// can be lifted. Will not go across
/// [isolating](#model.NodeSpec.isolating) parent nodes.
export function liftTarget(range: NodeRange): number | null {
let parent = range.parent
let content = parent.content.cutByIndex(range.startIndex, range.endIndex)
for (let depth = range.depth, contentBefore = 0, contentAfter = 0;; --depth) {
let node = range.$from.node(depth)
let index = range.$from.index(depth) + contentBefore, endIndex = range.$to.indexAfter(depth) - contentAfter
if (depth < range.depth && node.canReplace(index, endIndex, content))
return depth
if (depth == 0 || node.type.spec.isolating || !canCut(node, index, endIndex)) break
if (index) contentBefore = 1
if (endIndex < node.childCount) contentAfter = 1
}
return null
}
export function lift(tr: Transform, range: NodeRange, target: number) {
let {$from, $to, depth} = range
let gapStart = $from.before(depth + 1), gapEnd = $to.after(depth + 1)
let start = gapStart, end = gapEnd
let before = Fragment.empty, openStart = 0
for (let d = depth, splitting = false; d > target; d--)
if (splitting || $from.index(d) > 0) {
splitting = true
before = Fragment.from($from.node(d).copy(before))
openStart++
} else {
start--
}
let after = Fragment.empty, openEnd = 0
for (let d = depth, splitting = false; d > target; d--)
if (splitting || $to.after(d + 1) < $to.end(d)) {
splitting = true
after = Fragment.from($to.node(d).copy(after))
openEnd++
} else {
end++
}
tr.step(new ReplaceAroundStep(start, end, gapStart, gapEnd,
new Slice(before.append(after), openStart, openEnd),
before.size - openStart, true))
}
/// Try to find a valid way to wrap the content in the given range in a
/// node of the given type. May introduce extra nodes around and inside
/// the wrapper node, if necessary. Returns null if no valid wrapping
/// could be found. When `innerRange` is given, that range's content is
/// used as the content to fit into the wrapping, instead of the
/// content of `range`.
export function findWrapping(
range: NodeRange,
nodeType: NodeType,
attrs: Attrs | null = null,
innerRange = range
): {type: NodeType, attrs: Attrs | null}[] | null {
let around = findWrappingOutside(range, nodeType)
let inner = around && findWrappingInside(innerRange, nodeType)
if (!inner) return null
return (around!.map(withAttrs) as {type: NodeType, attrs: Attrs | null}[])
.concat({type: nodeType, attrs}).concat(inner.map(withAttrs))
}
function withAttrs(type: NodeType) { return {type, attrs: null} }
function findWrappingOutside(range: NodeRange, type: NodeType) {
let {parent, startIndex, endIndex} = range
let around = parent.contentMatchAt(startIndex).findWrapping(type)
if (!around) return null
let outer = around.length ? around[0] : type
return parent.canReplaceWith(startIndex, endIndex, outer) ? around : null
}
function findWrappingInside(range: NodeRange, type: NodeType) {
let {parent, startIndex, endIndex} = range
let inner = parent.child(startIndex)
let inside = type.contentMatch.findWrapping(inner.type)
if (!inside) return null
let lastType = inside.length ? inside[inside.length - 1] : type
let innerMatch: ContentMatch | null = lastType.contentMatch
for (let i = startIndex; innerMatch && i < endIndex; i++)
innerMatch = innerMatch.matchType(parent.child(i).type)
if (!innerMatch || !innerMatch.validEnd) return null
return inside
}
export function wrap(tr: Transform, range: NodeRange, wrappers: readonly {type: NodeType, attrs?: Attrs | null}[]) {
let content = Fragment.empty
for (let i = wrappers.length - 1; i >= 0; i--) {
if (content.size) {
let match = wrappers[i].type.contentMatch.matchFragment(content)
if (!match || !match.validEnd)
throw new RangeError("Wrapper type given to Transform.wrap does not form valid content of its parent wrapper")
}
content = Fragment.from(wrappers[i].type.create(wrappers[i].attrs, content))
}
let start = range.start, end = range.end
tr.step(new ReplaceAroundStep(start, end, start, end, new Slice(content, 0, 0), wrappers.length, true))
}
export function setBlockType(tr: Transform, from: number, to: number,
type: NodeType, attrs: Attrs | null | ((oldNode: Node) => Attrs)) {
if (!type.isTextblock) throw new RangeError("Type given to setBlockType should be a textblock")
let mapFrom = tr.steps.length
tr.doc.nodesBetween(from, to, (node, pos) => {
let attrsHere = typeof attrs == "function" ? attrs(node) : attrs
if (node.isTextblock && !node.hasMarkup(type, attrsHere) &&
canChangeType(tr.doc, tr.mapping.slice(mapFrom).map(pos), type)) {
let convertNewlines = null
if (type.schema.linebreakReplacement) {
let pre = type.whitespace == "pre", supportLinebreak = !!type.contentMatch.matchType(type.schema.linebreakReplacement)
if (pre && !supportLinebreak) convertNewlines = false
else if (!pre && supportLinebreak) convertNewlines = true
}
// Ensure all markup that isn't allowed in the new node type is cleared
if (convertNewlines === false) replaceLinebreaks(tr, node, pos, mapFrom)
clearIncompatible(tr, tr.mapping.slice(mapFrom).map(pos, 1), type, undefined, convertNewlines === null)
let mapping = tr.mapping.slice(mapFrom)
let startM = mapping.map(pos, 1), endM = mapping.map(pos + node.nodeSize, 1)
tr.step(new ReplaceAroundStep(startM, endM, startM + 1, endM - 1,
new Slice(Fragment.from(type.create(attrsHere, null, node.marks)), 0, 0), 1, true))
if (convertNewlines === true) replaceNewlines(tr, node, pos, mapFrom)
return false
}
})
}
function replaceNewlines(tr: Transform, node: Node, pos: number, mapFrom: number) {
node.forEach((child, offset) => {
if (child.isText) {
let m, newline = /\r?\n|\r/g
while (m = newline.exec(child.text!)) {
let start = tr.mapping.slice(mapFrom).map(pos + 1 + offset + m.index)
tr.replaceWith(start, start + 1, node.type.schema.linebreakReplacement!.create())
}
}
})
}
function replaceLinebreaks(tr: Transform, node: Node, pos: number, mapFrom: number) {
node.forEach((child, offset) => {
if (child.type == child.type.schema.linebreakReplacement) {
let start = tr.mapping.slice(mapFrom).map(pos + 1 + offset)
tr.replaceWith(start, start + 1, node.type.schema.text("\n"))
}
})
}
function canChangeType(doc: Node, pos: number, type: NodeType) {
let $pos = doc.resolve(pos), index = $pos.index()
return $pos.parent.canReplaceWith(index, index + 1, type)
}
/// Change the type, attributes, and/or marks of the node at `pos`.
/// When `type` isn't given, the existing node type is preserved,
export function setNodeMarkup(tr: Transform, pos: number, type: NodeType | undefined | null,
attrs: Attrs | null, marks: readonly Mark[] | undefined) {
let node = tr.doc.nodeAt(pos)
if (!node) throw new RangeError("No node at given position")
if (!type) type = node.type
let newNode = type.create(attrs, null, marks || node.marks)
if (node.isLeaf)
return tr.replaceWith(pos, pos + node.nodeSize, newNode)
if (!type.validContent(node.content))
throw new RangeError("Invalid content for node type " + type.name)
tr.step(new ReplaceAroundStep(pos, pos + node.nodeSize, pos + 1, pos + node.nodeSize - 1,
new Slice(Fragment.from(newNode), 0, 0), 1, true))
}
/// Check whether splitting at the given position is allowed.
export function canSplit(doc: Node, pos: number, depth = 1,
typesAfter?: (null | {type: NodeType, attrs?: Attrs | null})[]): boolean {
let $pos = doc.resolve(pos), base = $pos.depth - depth
let innerType = (typesAfter && typesAfter[typesAfter.length - 1]) || $pos.parent
if (base < 0 || $pos.parent.type.spec.isolating ||
!$pos.parent.canReplace($pos.index(), $pos.parent.childCount) ||
!innerType.type.validContent($pos.parent.content.cutByIndex($pos.index(), $pos.parent.childCount)))
return false
for (let d = $pos.depth - 1, i = depth - 2; d > base; d--, i--) {
let node = $pos.node(d), index = $pos.index(d)
if (node.type.spec.isolating) return false
let rest = node.content.cutByIndex(index, node.childCount)
let overrideChild = typesAfter && typesAfter[i + 1]
if (overrideChild)
rest = rest.replaceChild(0, overrideChild.type.create(overrideChild.attrs))
let after = (typesAfter && typesAfter[i]) || node
if (!node.canReplace(index + 1, node.childCount) || !after.type.validContent(rest))
return false
}
let index = $pos.indexAfter(base)
let baseType = typesAfter && typesAfter[0]
return $pos.node(base).canReplaceWith(index, index, baseType ? baseType.type : $pos.node(base + 1).type)
}
export function split(tr: Transform, pos: number, depth = 1, typesAfter?: (null | {type: NodeType, attrs?: Attrs | null})[]) {
let $pos = tr.doc.resolve(pos), before = Fragment.empty, after = Fragment.empty
for (let d = $pos.depth, e = $pos.depth - depth, i = depth - 1; d > e; d--, i--) {
before = Fragment.from($pos.node(d).copy(before))
let typeAfter = typesAfter && typesAfter[i]
after = Fragment.from(typeAfter ? typeAfter.type.create(typeAfter.attrs, after) : $pos.node(d).copy(after))
}
tr.step(new ReplaceStep(pos, pos, new Slice(before.append(after), depth, depth), true))
}
/// Test whether the blocks before and after a given position can be
/// joined.
export function canJoin(doc: Node, pos: number): boolean {
let $pos = doc.resolve(pos), index = $pos.index()
return joinable($pos.nodeBefore, $pos.nodeAfter) &&
$pos.parent.canReplace(index, index + 1)
}
function canAppendWithSubstitutedLinebreaks(a: Node, b: Node) {
if (!b.content.size) a.type.compatibleContent(b.type)
let match: ContentMatch | null = a.contentMatchAt(a.childCount)
let {linebreakReplacement} = a.type.schema
for (let i = 0; i < b.childCount; i++) {
let child = b.child(i)
let type = child.type == linebreakReplacement ? a.type.schema.nodes.text : child.type
match = match.matchType(type)
if (!match) return false
if (!a.type.allowsMarks(child.marks)) return false
}
return match.validEnd
}
function joinable(a: Node | null, b: Node | null) {
return !!(a && b && !a.isLeaf && canAppendWithSubstitutedLinebreaks(a, b))
}
/// Find an ancestor of the given position that can be joined to the
/// block before (or after if `dir` is positive). Returns the joinable
/// point, if any.
export function joinPoint(doc: Node, pos: number, dir = -1) {
let $pos = doc.resolve(pos)
for (let d = $pos.depth;; d--) {
let before, after, index = $pos.index(d)
if (d == $pos.depth) {
before = $pos.nodeBefore
after = $pos.nodeAfter
} else if (dir > 0) {
before = $pos.node(d + 1)
index++
after = $pos.node(d).maybeChild(index)
} else {
before = $pos.node(d).maybeChild(index - 1)
after = $pos.node(d + 1)
}
if (before && !before.isTextblock && joinable(before, after) &&
$pos.node(d).canReplace(index, index + 1)) return pos
if (d == 0) break
pos = dir < 0 ? $pos.before(d) : $pos.after(d)
}
}
export function join(tr: Transform, pos: number, depth: number) {
let convertNewlines = null
let {linebreakReplacement} = tr.doc.type.schema
let $before = tr.doc.resolve(pos - depth), beforeType = $before.node().type
if (linebreakReplacement && beforeType.inlineContent) {
let pre = beforeType.whitespace == "pre"
let supportLinebreak = !!beforeType.contentMatch.matchType(linebreakReplacement)
if (pre && !supportLinebreak) convertNewlines = false
else if (!pre && supportLinebreak) convertNewlines = true
}
let mapFrom = tr.steps.length
if (convertNewlines === false) {
let $after = tr.doc.resolve(pos + depth)
replaceLinebreaks(tr, $after.node(), $after.before(), mapFrom)
}
if (beforeType.inlineContent)
clearIncompatible(tr, pos + depth - 1, beforeType,
$before.node().contentMatchAt($before.index()), convertNewlines == null)
let mapping = tr.mapping.slice(mapFrom), start = mapping.map(pos - depth)
tr.step(new ReplaceStep(start, mapping.map(pos + depth, - 1), Slice.empty, true))
if (convertNewlines === true) {
let $full = tr.doc.resolve(start)
replaceNewlines(tr, $full.node(), $full.before(), tr.steps.length)
}
return tr
}
/// Try to find a point where a node of the given type can be inserted
/// near `pos`, by searching up the node hierarchy when `pos` itself
/// isn't a valid place but is at the start or end of a node. Return
/// null if no position was found.
export function insertPoint(doc: Node, pos: number, nodeType: NodeType): number | null {
let $pos = doc.resolve(pos)
if ($pos.parent.canReplaceWith($pos.index(), $pos.index(), nodeType)) return pos
if ($pos.parentOffset == 0)
for (let d = $pos.depth - 1; d >= 0; d--) {
let index = $pos.index(d)
if ($pos.node(d).canReplaceWith(index, index, nodeType)) return $pos.before(d + 1)
if (index > 0) return null
}
if ($pos.parentOffset == $pos.parent.content.size)
for (let d = $pos.depth - 1; d >= 0; d--) {
let index = $pos.indexAfter(d)
if ($pos.node(d).canReplaceWith(index, index, nodeType)) return $pos.after(d + 1)
if (index < $pos.node(d).childCount) return null
}
return null
}
/// Finds a position at or around the given position where the given
/// slice can be inserted. Will look at parent nodes' nearest boundary
/// and try there, even if the original position wasn't directly at the
/// start or end of that node. Returns null when no position was found.
export function dropPoint(doc: Node, pos: number, slice: Slice): number | null {
let $pos = doc.resolve(pos)
if (!slice.content.size) return pos
let content = slice.content
for (let i = 0; i < slice.openStart; i++) content = content.firstChild!.content
for (let pass = 1; pass <= (slice.openStart == 0 && slice.size ? 2 : 1); pass++) {
for (let d = $pos.depth; d >= 0; d--) {
let bias = d == $pos.depth ? 0 : $pos.pos <= ($pos.start(d + 1) + $pos.end(d + 1)) / 2 ? -1 : 1
let insertPos = $pos.index(d) + (bias > 0 ? 1 : 0)
let parent = $pos.node(d), fits: boolean | null = false
if (pass == 1) {
fits = parent.canReplace(insertPos, insertPos, content)
} else {
let wrapping = parent.contentMatchAt(insertPos).findWrapping(content.firstChild!.type)
fits = wrapping && parent.canReplaceWith(insertPos, insertPos, wrapping[0])
}
if (fits)
return bias == 0 ? $pos.pos : bias < 0 ? $pos.before(d + 1) : $pos.after(d + 1)
}
}
return null
}
+271
View File
@@ -0,0 +1,271 @@
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, DocAttrStep} 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
}
/// Return a single range, in post-transform document positions,
/// that covers all content changed by this transform. Returns null
/// if no replacements are made. Note that this will ignore changes
/// that add/remove marks without replacing the underlying content.
changedRange() {
let from = 1e9, to = -1e9
for (let i = 0; i < this.mapping.maps.length; i++) {
let map = this.mapping.maps[i]
if (i) {
from = map.map(from, 1)
to = map.map(to, -1)
}
map.forEach((_f, _t, fromB, toB) => {
from = Math.min(from, fromB)
to = Math.max(to, toB)
})
}
return from == 1e9 ? null : {from, to}
}
/// @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 | ((oldNode: Node) => Attrs) = 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.
/// The `pos` addresses the document content. Use `setDocAttribute`
/// to set attributes on the document itself.
setNodeAttribute(pos: number, attr: string, value: any): this {
this.step(new AttrStep(pos, attr, value))
return this
}
/// Set a single attribute on the document to a new value.
setDocAttribute(attr: string, value: any): this {
this.step(new DocAttrStep(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 all marks of the given type) from the node at
/// position `pos`.
removeNodeMark(pos: number, mark: Mark | MarkType): this {
let node = this.doc.nodeAt(pos)
if (!node) throw new RangeError("No node at position " + pos)
if (mark instanceof Mark) {
if (mark.isInSet(node.marks)) this.step(new RemoveNodeMarkStep(pos, mark))
} else {
let set = node.marks, found: Mark | undefined, steps: Step[] = []
while (found = mark.isInSet(set)) {
steps.push(new RemoveNodeMarkStep(pos, found))
set = found.removeFromSet(set)
}
for (let i = steps.length - 1; i >= 0; i--) this.step(steps[i])
}
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 (with the outermost nodes coming first).
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
}
}