dd/node_modules/prosemirror-model/src/replace.ts
2026-06-14 15:46:45 +08:00

217 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {Fragment} from "./fragment"
import {Schema} from "./schema"
import {Node, TextNode} from "./node"
import {ResolvedPos} from "./resolvedpos"
/// Error type raised by [`Node.replace`](#model.Node.replace) when
/// given an invalid replacement.
export class ReplaceError extends Error {}
/// A slice represents a piece cut out of a larger document. It
/// stores not only a fragment, but also the depth up to which nodes on
/// both side are open (cut through).
export class Slice {
/// Create a slice. When specifying a non-zero open depth, you must
/// make sure that there are nodes of at least that depth at the
/// appropriate side of the fragment—i.e. if the fragment is an
/// empty paragraph node, `openStart` and `openEnd` can't be greater
/// than 1.
///
/// It is not necessary for the content of open nodes to conform to
/// the schema's content constraints, though it should be a valid
/// start/end/middle for such a node, depending on which sides are
/// open.
constructor(
/// The slice's content.
readonly content: Fragment,
/// The open depth at the start of the fragment.
readonly openStart: number,
/// The open depth at the end.
readonly openEnd: number
) {}
/// The size this slice would add when inserted into a document.
get size(): number {
return this.content.size - this.openStart - this.openEnd
}
/// @internal
insertAt(pos: number, fragment: Fragment) {
let content = insertInto(this.content, pos + this.openStart, fragment, this.openStart + 1, this.openEnd + 1)
return content && new Slice(content, this.openStart, this.openEnd)
}
/// @internal
removeBetween(from: number, to: number) {
return new Slice(removeRange(this.content, from + this.openStart, to + this.openStart), this.openStart, this.openEnd)
}
/// Tests whether this slice is equal to another slice.
eq(other: Slice): boolean {
return this.content.eq(other.content) && this.openStart == other.openStart && this.openEnd == other.openEnd
}
/// @internal
toString() {
return this.content + "(" + this.openStart + "," + this.openEnd + ")"
}
/// Convert a slice to a JSON-serializable representation.
toJSON(): any {
if (!this.content.size) return null
let json: any = {content: this.content.toJSON()}
if (this.openStart > 0) json.openStart = this.openStart
if (this.openEnd > 0) json.openEnd = this.openEnd
return json
}
/// Deserialize a slice from its JSON representation.
static fromJSON(schema: Schema, json: any): Slice {
if (!json) return Slice.empty
let openStart = json.openStart || 0, openEnd = json.openEnd || 0
if (typeof openStart != "number" || typeof openEnd != "number")
throw new RangeError("Invalid input for Slice.fromJSON")
return new Slice(Fragment.fromJSON(schema, json.content), openStart, openEnd)
}
/// Create a slice from a fragment by taking the maximum possible
/// open value on both side of the fragment.
static maxOpen(fragment: Fragment, openIsolating = true) {
let openStart = 0, openEnd = 0
for (let n = fragment.firstChild; n && !n.isLeaf && (openIsolating || !n.type.spec.isolating); n = n.firstChild) openStart++
for (let n = fragment.lastChild; n && !n.isLeaf && (openIsolating || !n.type.spec.isolating); n = n.lastChild) openEnd++
return new Slice(fragment, openStart, openEnd)
}
/// The empty slice.
static empty = new Slice(Fragment.empty, 0, 0)
}
function removeRange(content: Fragment, from: number, to: number): Fragment {
let {index, offset} = content.findIndex(from), child = content.maybeChild(index)
let {index: indexTo, offset: offsetTo} = content.findIndex(to)
if (offset == from || child!.isText) {
if (offsetTo != to && !content.child(indexTo).isText) throw new RangeError("Removing non-flat range")
return content.cut(0, from).append(content.cut(to))
}
if (index != indexTo) throw new RangeError("Removing non-flat range")
return content.replaceChild(index, child!.copy(removeRange(child!.content, from - offset - 1, to - offset - 1)))
}
function insertInto(content: Fragment, dist: number, insert: Fragment, openStart: number, openEnd: number,
parent?: Node | null): Fragment | null {
let {index, offset} = content.findIndex(dist), child = content.maybeChild(index)
if (offset == dist || child!.isText) {
if (parent && openStart <= 0 && openEnd <= 0 && !parent.canReplace(index, index, insert)) return null
return content.cut(0, dist).append(insert).append(content.cut(dist))
}
let inner = insertInto(child!.content, dist - offset - 1, insert, index == 0 ? openStart - 1 : 0,
index == content.childCount - 1 ? openEnd - 1 : 0, child)
return inner && content.replaceChild(index, child!.copy(inner))
}
export function replace($from: ResolvedPos, $to: ResolvedPos, slice: Slice) {
if (slice.openStart > $from.depth)
throw new ReplaceError("Inserted content deeper than insertion position")
if ($from.depth - slice.openStart != $to.depth - slice.openEnd)
throw new ReplaceError("Inconsistent open depths")
return replaceOuter($from, $to, slice, 0)
}
function replaceOuter($from: ResolvedPos, $to: ResolvedPos, slice: Slice, depth: number): Node {
let index = $from.index(depth), node = $from.node(depth)
if (index == $to.index(depth) && depth < $from.depth - slice.openStart) {
let inner = replaceOuter($from, $to, slice, depth + 1)
return node.copy(node.content.replaceChild(index, inner))
} else if (!slice.content.size) {
return close(node, replaceTwoWay($from, $to, depth))
} else if (!slice.openStart && !slice.openEnd && $from.depth == depth && $to.depth == depth) { // Simple, flat case
let parent = $from.parent, content = parent.content
return close(parent, content.cut(0, $from.parentOffset).append(slice.content).append(content.cut($to.parentOffset)))
} else {
let {start, end} = prepareSliceForReplace(slice, $from)
return close(node, replaceThreeWay($from, start, end, $to, depth))
}
}
function checkJoin(main: Node, sub: Node) {
if (!sub.type.compatibleContent(main.type))
throw new ReplaceError("Cannot join " + sub.type.name + " onto " + main.type.name)
}
function joinable($before: ResolvedPos, $after: ResolvedPos, depth: number) {
let node = $before.node(depth)
checkJoin(node, $after.node(depth))
return node
}
function addNode(child: Node, target: Node[]) {
let last = target.length - 1
if (last >= 0 && child.isText && child.sameMarkup(target[last]))
target[last] = (child as TextNode).withText(target[last].text! + child.text!)
else
target.push(child)
}
function addRange($start: ResolvedPos | null, $end: ResolvedPos | null, depth: number, target: Node[]) {
let node = ($end || $start)!.node(depth)
let startIndex = 0, endIndex = $end ? $end.index(depth) : node.childCount
if ($start) {
startIndex = $start.index(depth)
if ($start.depth > depth) {
startIndex++
} else if ($start.textOffset) {
addNode($start.nodeAfter!, target)
startIndex++
}
}
for (let i = startIndex; i < endIndex; i++) addNode(node.child(i), target)
if ($end && $end.depth == depth && $end.textOffset)
addNode($end.nodeBefore!, target)
}
function close(node: Node, content: Fragment) {
node.type.checkContent(content)
return node.copy(content)
}
function replaceThreeWay($from: ResolvedPos, $start: ResolvedPos, $end: ResolvedPos, $to: ResolvedPos, depth: number) {
let openStart = $from.depth > depth && joinable($from, $start, depth + 1)
let openEnd = $to.depth > depth && joinable($end, $to, depth + 1)
let content: Node[] = []
addRange(null, $from, depth, content)
if (openStart && openEnd && $start.index(depth) == $end.index(depth)) {
checkJoin(openStart, openEnd)
addNode(close(openStart, replaceThreeWay($from, $start, $end, $to, depth + 1)), content)
} else {
if (openStart)
addNode(close(openStart, replaceTwoWay($from, $start, depth + 1)), content)
addRange($start, $end, depth, content)
if (openEnd)
addNode(close(openEnd, replaceTwoWay($end, $to, depth + 1)), content)
}
addRange($to, null, depth, content)
return new Fragment(content)
}
function replaceTwoWay($from: ResolvedPos, $to: ResolvedPos, depth: number) {
let content: Node[] = []
addRange(null, $from, depth, content)
if ($from.depth > depth) {
let type = joinable($from, $to, depth + 1)
addNode(close(type, replaceTwoWay($from, $to, depth + 1)), content)
}
addRange($to, null, depth, content)
return new Fragment(content)
}
function prepareSliceForReplace(slice: Slice, $along: ResolvedPos) {
let extra = $along.depth - slice.openStart, parent = $along.node(extra)
let node = parent.copy(slice.content)
for (let i = extra - 1; i >= 0; i--)
node = $along.node(i).copy(Fragment.from(node))
return {start: node.resolveNoCache(slice.openStart + extra),
end: node.resolveNoCache(node.content.size - slice.openEnd - extra)}
}