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.

226 lines
8.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 {}
/*
ReplaceError = function(this: any, message: string) {
let err = Error.call(this, message)
;(err as any).__proto__ = ReplaceError.prototype
return err
} as any
ReplaceError.prototype = Object.create(Error.prototype)
ReplaceError.prototype.constructor = ReplaceError
ReplaceError.prototype.name = "ReplaceError"
*/
/// 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)
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, parent?: Node): Fragment | null {
let {index, offset} = content.findIndex(dist), child = content.maybeChild(index)
if (offset == dist || child!.isText) {
if (parent && !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)
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)}
}