fix: resolve TypeScript errors in frontend build

This commit is contained in:
Hiro
2026-03-30 23:16:07 +00:00
parent b733306773
commit 24925e1acb
2941 changed files with 418042 additions and 49 deletions

32
node_modules/prosemirror-changeset/src/README.md generated vendored Normal file
View File

@@ -0,0 +1,32 @@
# prosemirror-changeset
This is a helper module that can turn a sequence of document changes
into a set of insertions and deletions, for example to display them in
a change-tracking interface. Such a set can be built up incrementally,
in order to do such change tracking in a halfway performant way during
live editing.
This code is licensed under an [MIT
licence](https://github.com/ProseMirror/prosemirror-changeset/blob/master/LICENSE).
## Programming interface
Insertions and deletions are represented as spans—ranges in the
document. The deleted spans refer to the original document, whereas
the inserted ones point into the current document.
It is possible to associate arbitrary data values with such spans, for
example to track the user that made the change, the timestamp at which
it was made, or the step data necessary to invert it again.
@Change
@Span
@ChangeSet
@simplifyChanges
@TokenEncoder
@ChangeJSON

189
node_modules/prosemirror-changeset/src/change.ts generated vendored Normal file
View File

@@ -0,0 +1,189 @@
/// Stores metadata for a part of a change.
export class Span<Data = any> {
/// @internal
constructor(
/// The length of this span.
readonly length: number,
/// The data associated with this span.
readonly data: Data
) {}
/// @internal
cut(length: number) {
return length == this.length ? this : new Span(length, this.data)
}
/// @internal
static slice(spans: readonly Span[], from: number, to: number) {
if (from == to) return Span.none
if (from == 0 && to == Span.len(spans)) return spans
let result = []
for (let i = 0, off = 0; off < to; i++) {
let span = spans[i], end = off + span.length
let overlap = Math.min(to, end) - Math.max(from, off)
if (overlap > 0) result.push(span.cut(overlap))
off = end
}
return result
}
/// @internal
static join<Data>(a: readonly Span<Data>[], b: readonly Span<Data>[], combine: (dataA: Data, dataB: Data) => Data) {
if (a.length == 0) return b
if (b.length == 0) return a
let combined = combine(a[a.length - 1].data, b[0].data)
if (combined == null) return a.concat(b)
let result = a.slice(0, a.length - 1)
result.push(new Span(a[a.length - 1].length + b[0].length, combined))
for (let i = 1; i < b.length; i++) result.push(b[i])
return result
}
/// @internal
static len(spans: readonly Span[]) {
let len = 0
for (let i = 0; i < spans.length; i++) len += spans[i].length
return len
}
/// @internal
static none: readonly Span[] = []
}
/// A replaced range with metadata associated with it.
export class Change<Data = any> {
/// @internal
constructor(
/// The start of the range deleted/replaced in the old document.
readonly fromA: number,
/// The end of the range in the old document.
readonly toA: number,
/// The start of the range inserted in the new document.
readonly fromB: number,
/// The end of the range in the new document.
readonly toB: number,
/// Data associated with the deleted content. The length of these
/// spans adds up to `this.toA - this.fromA`.
readonly deleted: readonly Span<Data>[],
/// Data associated with the inserted content. Length adds up to
/// `this.toB - this.fromB`.
readonly inserted: readonly Span<Data>[]
) {}
/// @internal
get lenA() { return this.toA - this.fromA }
/// @internal
get lenB() { return this.toB - this.fromB }
/// @internal
slice(startA: number, endA: number, startB: number, endB: number): Change<Data> {
if (startA == 0 && startB == 0 && endA == this.toA - this.fromA &&
endB == this.toB - this.fromB) return this
return new Change(this.fromA + startA, this.fromA + endA,
this.fromB + startB, this.fromB + endB,
Span.slice(this.deleted, startA, endA),
Span.slice(this.inserted, startB, endB))
}
/// This merges two changesets (the end document of x should be the
/// start document of y) into a single one spanning the start of x to
/// the end of y.
static merge<Data>(x: readonly Change<Data>[],
y: readonly Change<Data>[],
combine: (dataA: Data, dataB: Data) => Data): readonly Change<Data>[] {
if (x.length == 0) return y
if (y.length == 0) return x
let result = []
// Iterate over both sets in parallel, using the middle coordinate
// system (B in x, A in y) to synchronize.
for (let iX = 0, iY = 0, curX: Change<Data> | null = x[0], curY: Change<Data> | null = y[0];;) {
if (!curX && !curY) {
return result
} else if (curX && (!curY || curX.toB < curY.fromA)) { // curX entirely in front of curY
let off = iY ? y[iY - 1].toB - y[iY - 1].toA : 0
result.push(off == 0 ? curX :
new Change(curX.fromA, curX.toA, curX.fromB + off, curX.toB + off,
curX.deleted, curX.inserted))
curX = iX++ == x.length ? null : x[iX]
} else if (curY && (!curX || curY.toA < curX.fromB)) { // curY entirely in front of curX
let off = iX ? x[iX - 1].toB - x[iX - 1].toA : 0
result.push(off == 0 ? curY :
new Change(curY.fromA - off, curY.toA - off, curY.fromB, curY.toB,
curY.deleted, curY.inserted))
curY = iY++ == y.length ? null : y[iY]
} else { // Touch, need to merge
// The rules for merging ranges are that deletions from the
// old set and insertions from the new are kept. Areas of the
// middle document covered by a but not by b are insertions
// from a that need to be added, and areas covered by b but
// not a are deletions from b that need to be added.
let pos = Math.min(curX!.fromB, curY!.fromA)
let fromA = Math.min(curX!.fromA, curY!.fromA - (iX ? x[iX - 1].toB - x[iX - 1].toA : 0)), toA = fromA
let fromB = Math.min(curY!.fromB, curX!.fromB + (iY ? y[iY - 1].toB - y[iY - 1].toA : 0)), toB = fromB
let deleted = Span.none, inserted = Span.none
// Used to prevent appending ins/del range for the same Change twice
let enteredX = false, enteredY = false
// Need to have an inner loop since any number of further
// ranges might be touching this group
for (;;) {
let nextX = !curX ? 2e8 : pos >= curX.fromB ? curX.toB : curX.fromB
let nextY = !curY ? 2e8 : pos >= curY.fromA ? curY.toA : curY.fromA
let next = Math.min(nextX, nextY)
let inX = curX && pos >= curX.fromB, inY = curY && pos >= curY.fromA
if (!inX && !inY) break
if (inX && pos == curX!.fromB && !enteredX) {
deleted = Span.join(deleted, curX!.deleted, combine)
toA += curX!.lenA
enteredX = true
}
if (inX && !inY) {
inserted = Span.join(inserted, Span.slice(curX!.inserted, pos - curX!.fromB, next - curX!.fromB), combine)
toB += next - pos
}
if (inY && pos == curY!.fromA && !enteredY) {
inserted = Span.join(inserted, curY!.inserted, combine)
toB += curY!.lenB
enteredY = true
}
if (inY && !inX) {
deleted = Span.join(deleted, Span.slice(curY!.deleted, pos - curY!.fromA, next - curY!.fromA), combine)
toA += next - pos
}
if (inX && next == curX!.toB) {
curX = iX++ == x.length ? null : x[iX]
enteredX = false
}
if (inY && next == curY!.toA) {
curY = iY++ == y.length ? null : y[iY]
enteredY = false
}
pos = next
}
if (fromA < toA || fromB < toB)
result.push(new Change(fromA, toA, fromB, toB, deleted, inserted))
}
}
}
/// Deserialize a change from JSON format.
static fromJSON<Data>(json: ChangeJSON<Data>) {
return new Change(json.fromA, json.toA, json.fromB, json.toB,
json.deleted.map(d => new Span(d.length, d.data)),
json.inserted.map(d => new Span(d.length, d.data)))
}
/// Returns a JSON-serializeable object to represent this change.
toJSON(): ChangeJSON<Data> { return this }
}
/// JSON-serialized form of a change.
export type ChangeJSON<Data> = {
fromA: number, toA: number,
fromB: number, toB: number,
deleted: readonly {length: number, data: Data}[],
inserted: readonly {length: number, data: Data}[]
}

212
node_modules/prosemirror-changeset/src/changeset.ts generated vendored Normal file
View File

@@ -0,0 +1,212 @@
import {Node} from "prosemirror-model"
import {StepMap} from "prosemirror-transform"
import {computeDiff, TokenEncoder, DefaultEncoder} from "./diff"
import {Change, Span, ChangeJSON} from "./change"
export {Change, Span, ChangeJSON}
export {simplifyChanges} from "./simplify"
export {TokenEncoder}
/// A change set tracks the changes to a document from a given point
/// in the past. It condenses a number of step maps down to a flat
/// sequence of replacements, and simplifies replacments that
/// partially undo themselves by comparing their content.
export class ChangeSet<Data = any> {
/// @internal
constructor(
/// @internal
readonly config: {
doc: Node,
combine: (dataA: Data, dataB: Data) => Data,
encoder: TokenEncoder<any>
},
/// Replaced regions.
readonly changes: readonly Change<Data>[]
) {}
/// Computes a new changeset by adding the given step maps and
/// metadata (either as an array, per-map, or as a single value to be
/// associated with all maps) to the current set. Will not mutate the
/// old set.
///
/// Note that due to simplification that happens after each add,
/// incrementally adding steps might create a different final set
/// than adding all those changes at once, since different document
/// tokens might be matched during simplification depending on the
/// boundaries of the current changed ranges.
addSteps(newDoc: Node, maps: readonly StepMap[], data: Data | readonly Data[]): ChangeSet<Data> {
// This works by inspecting the position maps for the changes,
// which indicate what parts of the document were replaced by new
// content, and the size of that new content. It uses these to
// build up Change objects.
//
// These change objects are put in sets and merged together using
// Change.merge, giving us the changes created by the new steps.
// Those changes can then be merged with the existing set of
// changes.
//
// For each change that was touched by the new steps, we recompute
// a diff to try to minimize the change by dropping matching
// pieces of the old and new document from the change.
let stepChanges: Change<Data>[] = []
// Add spans for new steps.
for (let i = 0; i < maps.length; i++) {
let d = Array.isArray(data) ? data[i] : data
let off = 0
maps[i].forEach((fromA, toA, fromB, toB) => {
stepChanges.push(new Change(fromA + off, toA + off, fromB, toB,
fromA == toA ? Span.none : [new Span(toA - fromA, d)],
fromB == toB ? Span.none : [new Span(toB - fromB, d)]))
off = (toB - fromB) - (toA - fromA)
})
}
if (stepChanges.length == 0) return this
let newChanges = mergeAll(stepChanges, this.config.combine)
let changes = Change.merge(this.changes, newChanges, this.config.combine)
let updated: Change<Data>[] = changes as Change<Data>[]
// Minimize changes when possible
for (let i = 0; i < updated.length; i++) {
let change = updated[i]
if (change.fromA == change.toA || change.fromB == change.toB ||
// Only look at changes that touch newly added changed ranges
!newChanges.some(r => r.toB > change.fromB && r.fromB < change.toB)) continue
let diff = computeDiff(this.config.doc.content, newDoc.content, change, this.config.encoder)
// Fast path: If they are completely different, don't do anything
if (diff.length == 1 && diff[0].fromB == 0 && diff[0].toB == change.toB - change.fromB)
continue
if (updated == changes) updated = changes.slice()
if (diff.length == 1) {
updated[i] = diff[0]
} else {
updated.splice(i, 1, ...diff)
i += diff.length - 1
}
}
return new ChangeSet(this.config, updated)
}
/// The starting document of the change set.
get startDoc(): Node { return this.config.doc }
/// Map the span's data values in the given set through a function
/// and construct a new set with the resulting data.
map(f: (range: Span<Data>) => Data): ChangeSet<Data> {
let mapSpan = (span: Span<Data>) => {
let newData = f(span)
return newData === span.data ? span : new Span(span.length, newData)
}
return new ChangeSet(this.config, this.changes.map((ch: Change<Data>) => {
return new Change(ch.fromA, ch.toA, ch.fromB, ch.toB, ch.deleted.map(mapSpan), ch.inserted.map(mapSpan))
}))
}
/// Compare two changesets and return the range in which they are
/// changed, if any. If the document changed between the maps, pass
/// the maps for the steps that changed it as second argument, and
/// make sure the method is called on the old set and passed the new
/// set. The returned positions will be in new document coordinates.
changedRange(b: ChangeSet, maps?: readonly StepMap[]): {from: number, to: number} | null {
if (b == this) return null
let touched = maps && touchedRange(maps)
let moved = touched ? (touched.toB - touched.fromB) - (touched.toA - touched.fromA) : 0
function map(p: number) {
return !touched || p <= touched.fromA ? p : p + moved
}
let from = touched ? touched.fromB : 2e8, to = touched ? touched.toB : -2e8
function add(start: number, end = start) {
from = Math.min(start, from); to = Math.max(end, to)
}
let rA = this.changes, rB = b.changes
for (let iA = 0, iB = 0; iA < rA.length && iB < rB.length;) {
let rangeA = rA[iA], rangeB = rB[iB]
if (rangeA && rangeB && sameRanges(rangeA, rangeB, map)) { iA++; iB++ }
else if (rangeB && (!rangeA || map(rangeA.fromB) >= rangeB.fromB)) { add(rangeB.fromB, rangeB.toB); iB++ }
else { add(map(rangeA.fromB), map(rangeA.toB)); iA++ }
}
return from <= to ? {from, to} : null
}
/// Create a changeset with the given base object and configuration.
///
/// The `combine` function is used to compare and combine metadata—it
/// should return null when metadata isn't compatible, and a combined
/// version for a merged range when it is.
///
/// When given, a token encoder determines how document tokens are
/// serialized and compared when diffing the content produced by
/// changes. The default is to just compare nodes by name and text
/// by character, ignoring marks and attributes.
///
/// To serialize a change set, you can store its document and
/// change array as JSON, and then pass the deserialized (via
/// [`Change.fromJSON`](#changes.Change^fromJSON)) set of changes
/// as fourth argument to `create` to recreate the set.
static create<Data = any>(
doc: Node,
combine: (dataA: Data, dataB: Data) => Data = (a, b) => a === b ? a : null as any,
tokenEncoder: TokenEncoder<any> = DefaultEncoder,
changes: readonly Change<Data>[] = []
) {
return new ChangeSet({combine, doc, encoder: tokenEncoder}, changes)
}
/// Exported for testing @internal
static computeDiff = computeDiff
}
// Divide-and-conquer approach to merging a series of ranges.
function mergeAll<Data>(
ranges: readonly Change<Data>[],
combine: (dA: Data, dB: Data) => Data,
start = 0, end = ranges.length
): readonly Change<Data>[] {
if (end == start + 1) return [ranges[start]]
let mid = (start + end) >> 1
return Change.merge(mergeAll(ranges, combine, start, mid),
mergeAll(ranges, combine, mid, end), combine)
}
function endRange(maps: readonly StepMap[]) {
let from = 2e8, to = -2e8
for (let i = 0; i < maps.length; i++) {
let map = maps[i]
if (from != 2e8) {
from = map.map(from, -1)
to = map.map(to, 1)
}
map.forEach((_s, _e, start, end) => {
from = Math.min(from, start)
to = Math.max(to, end)
})
}
return from == 2e8 ? null : {from, to}
}
function touchedRange(maps: readonly StepMap[]) {
let b = endRange(maps)
if (!b) return null
let a = endRange(maps.map(m => m.invert()).reverse())!
return {fromA: a.from, toA: a.to, fromB: b.from, toB: b.to}
}
function sameRanges<Data>(a: Change<Data>, b: Change<Data>, map: (pos: number) => number) {
return map(a.fromB) == b.fromB && map(a.toB) == b.toB &&
sameSpans(a.deleted, b.deleted) && sameSpans(a.inserted, b.inserted)
}
function sameSpans<Data>(a: readonly Span<Data>[], b: readonly Span<Data>[]) {
if (a.length != b.length) return false
for (let i = 0; i < a.length; i++)
if (a[i].length != b[i].length || a[i].data !== b[i].data) return false
return true
}

151
node_modules/prosemirror-changeset/src/diff.ts generated vendored Normal file
View File

@@ -0,0 +1,151 @@
import {Fragment, Node, NodeType, Mark} from "prosemirror-model"
import {Change} from "./change"
/// A token encoder can be passed when creating a `ChangeSet` in order
/// to influence the way the library runs its diffing algorithm. The
/// encoder determines how document tokens (such as nodes and
/// characters) are encoded and compared.
///
/// Note that both the encoding and the comparison may run a lot, and
/// doing non-trivial work in these functions could impact
/// performance.
export interface TokenEncoder<T> {
/// Encode a given character, with the given marks applied.
encodeCharacter(char: number, marks: readonly Mark[]): T
/// Encode the start of a node or, if this is a leaf node, the
/// entire node.
encodeNodeStart(node: Node): T
/// Encode the end token for the given node. It is valid to encode
/// every end token in the same way.
encodeNodeEnd(node: Node): T
/// Compare the given tokens. Should return true when they count as
/// equal.
compareTokens(a: T, b: T): boolean
}
function typeID(type: NodeType) {
let cache: Record<string, number> = type.schema.cached.changeSetIDs || (type.schema.cached.changeSetIDs = Object.create(null))
let id = cache[type.name]
if (id == null) cache[type.name] = id = Object.keys(type.schema.nodes).indexOf(type.name) + 1
return id
}
// The default token encoder, which encodes node open tokens are
// encoded as strings holding the node name, characters as their
// character code, and node close tokens as negative numbers.
export const DefaultEncoder: TokenEncoder<number | string> = {
encodeCharacter: char => char,
encodeNodeStart: node => node.type.name,
encodeNodeEnd: node => -typeID(node.type),
compareTokens: (a, b) => a === b
}
// Convert the given range of a fragment to tokens.
function tokens<T>(frag: Fragment, encoder: TokenEncoder<T>, start: number, end: number, target: T[]) {
for (let i = 0, off = 0; i < frag.childCount; i++) {
let child = frag.child(i), endOff = off + child.nodeSize
let from = Math.max(off, start), to = Math.min(endOff, end)
if (from < to) {
if (child.isText) {
for (let j = from; j < to; j++) target.push(encoder.encodeCharacter(child.text!.charCodeAt(j - off), child.marks))
} else if (child.isLeaf) {
target.push(encoder.encodeNodeStart(child))
} else {
if (from == off) target.push(encoder.encodeNodeStart(child))
tokens(child.content, encoder, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target)
if (to == endOff) target.push(encoder.encodeNodeEnd(child))
}
}
off = endOff
}
return target
}
// The code below will refuse to compute a diff with more than 5000
// insertions or deletions, which takes about 300ms to reach on my
// machine. This is a safeguard against runaway computations.
const MAX_DIFF_SIZE = 5000
// This obscure mess of constants computes the minimum length of an
// unchanged range (not at the start/end of the compared content). The
// idea is to make it higher in bigger replacements, so that you don't
// get a diff soup of coincidentally identical letters when replacing
// a paragraph.
function minUnchanged(sizeA: number, sizeB: number) {
return Math.min(15, Math.max(2, Math.floor(Math.max(sizeA, sizeB) / 10)))
}
export function computeDiff(fragA: Fragment, fragB: Fragment, range: Change, encoder: TokenEncoder<any> = DefaultEncoder) {
let tokA = tokens(fragA, encoder, range.fromA, range.toA, [])
let tokB = tokens(fragB, encoder, range.fromB, range.toB, [])
// Scan from both sides to cheaply eliminate work
let start = 0, endA = tokA.length, endB = tokB.length
let cmp = encoder.compareTokens
while (start < tokA.length && start < tokB.length && cmp(tokA[start], tokB[start])) start++
if (start == tokA.length && start == tokB.length) return []
while (endA > start && endB > start && cmp(tokA[endA - 1], tokB[endB - 1])) endA--, endB--
// If the result is simple _or_ too big to cheaply compute, return
// the remaining region as the diff
if (endA == start || endB == start || (endA == endB && endA == start + 1))
return [range.slice(start, endA, start, endB)]
// This is an implementation of Myers' diff algorithm
// See https://neil.fraser.name/writing/diff/myers.pdf and
// https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/
let lenA = endA - start, lenB = endB - start
let max = Math.min(MAX_DIFF_SIZE, lenA + lenB), off = max + 1
let history: number[][] = []
let frontier: number[] = []
for (let len = off * 2, i = 0; i < len; i++) frontier[i] = -1
for (let size = 0; size <= max; size++) {
for (let diag = -size; diag <= size; diag += 2) {
let next = frontier[diag + 1 + max], prev = frontier[diag - 1 + max]
let x = next < prev ? prev : next + 1, y = x + diag
while (x < lenA && y < lenB && cmp(tokA[start + x], tokB[start + y])) x++, y++
frontier[diag + max] = x
// Found a match
if (x >= lenA && y >= lenB) {
// Trace back through the history to build up a set of changed ranges.
let diff = [], minSpan = minUnchanged(endA - start, endB - start)
// Used to add steps to a diff one at a time, back to front, merging
// ones that are less than minSpan tokens apart
let fromA = -1, toA = -1, fromB = -1, toB = -1
let add = (fA: number, tA: number, fB: number, tB: number) => {
if (fromA > -1 && fromA < tA + minSpan) {
fromA = fA; fromB = fB
} else {
if (fromA > -1)
diff.push(range.slice(fromA, toA, fromB, toB))
fromA = fA; toA = tA
fromB = fB; toB = tB
}
}
for (let i = size - 1; i >= 0; i--) {
let next = frontier[diag + 1 + max], prev = frontier[diag - 1 + max]
if (next < prev) { // Deletion
diag--
x = prev + start; y = x + diag
add(x, x, y, y + 1)
} else { // Insertion
diag++
x = next + start; y = x + diag
add(x, x + 1, y, y)
}
frontier = history[i >> 1]
}
if (fromA > -1) diff.push(range.slice(fromA, toA, fromB, toB))
return diff.reverse()
}
}
// Since only either odd or even diagonals are read from each
// frontier, we only copy them every other iteration.
if (size % 2 == 0) history.push(frontier.slice())
}
// The loop exited, meaning the maximum amount of work was done.
// Just return a change spanning the entire range.
return [range.slice(start, endA, start, endB)]
}

132
node_modules/prosemirror-changeset/src/simplify.ts generated vendored Normal file
View File

@@ -0,0 +1,132 @@
import {Fragment, Node} from "prosemirror-model"
import {Span, Change} from "./change"
let letter: RegExp | undefined
// If the runtime support unicode properties in regexps, that's a good
// source of info on whether something is a letter.
try { letter = new RegExp("[\\p{Alphabetic}_]", "u") } catch(_) {}
// Otherwise, we see if the character changes when upper/lowercased,
// or if it is part of these common single-case scripts.
const nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/
function isLetter(code: number) {
if (code < 128)
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 79 && code <= 122
let ch = String.fromCharCode(code)
if (letter) return letter.test(ch)
return ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)
}
// Convert a range of document into a string, so that we can easily
// access characters at a given position. Treat non-text tokens as
// spaces so that they aren't considered part of a word.
function getText(frag: Fragment, start: number, end: number) {
let out = ""
function convert(frag: Fragment, start: number, end: number) {
for (let i = 0, off = 0; i < frag.childCount; i++) {
let child = frag.child(i), endOff = off + child.nodeSize
let from = Math.max(off, start), to = Math.min(endOff, end)
if (from < to) {
if (child.isText) {
out += child.text!.slice(Math.max(0, start - off), Math.min(child.text!.length, end - off))
} else if (child.isLeaf) {
out += " "
} else {
if (from == off) out += " "
convert(child.content, Math.max(0, from - off - 1), Math.min(child.content.size, end - off))
if (to == endOff) out += " "
}
}
off = endOff
}
}
convert(frag, start, end)
return out
}
// The distance changes have to be apart for us to not consider them
// candidates for merging.
const MAX_SIMPLIFY_DISTANCE = 30
/// Simplifies a set of changes for presentation. This makes the
/// assumption that having both insertions and deletions within a word
/// is confusing, and, when such changes occur without a word boundary
/// between them, they should be expanded to cover the entire set of
/// words (in the new document) they touch. An exception is made for
/// single-character replacements.
export function simplifyChanges(changes: readonly Change[], doc: Node) {
let result: Change[] = []
for (let i = 0; i < changes.length; i++) {
let end = changes[i].toB, start = i
while (i < changes.length - 1 && changes[i + 1].fromB <= end + MAX_SIMPLIFY_DISTANCE)
end = changes[++i].toB
simplifyAdjacentChanges(changes, start, i + 1, doc, result)
}
return result
}
function simplifyAdjacentChanges(changes: readonly Change[], from: number, to: number, doc: Node, target: Change[]) {
let start = Math.max(0, changes[from].fromB - MAX_SIMPLIFY_DISTANCE)
let end = Math.min(doc.content.size, changes[to - 1].toB + MAX_SIMPLIFY_DISTANCE)
let text = getText(doc.content, start, end)
for (let i = from; i < to; i++) {
let startI = i, last = changes[i], deleted = last.lenA, inserted = last.lenB
while (i < to - 1) {
let next = changes[i + 1], boundary = false
let prevLetter = last.toB == end ? false : isLetter(text.charCodeAt(last.toB - 1 - start))
for (let pos = last.toB; !boundary && pos < next.fromB; pos++) {
let nextLetter = pos == end ? false : isLetter(text.charCodeAt(pos - start))
if ((!prevLetter || !nextLetter) && pos != changes[startI].fromB) boundary = true
prevLetter = nextLetter
}
if (boundary) break
deleted += next.lenA; inserted += next.lenB
last = next
i++
}
if (inserted > 0 && deleted > 0 && !(inserted == 1 && deleted == 1)) {
let from = changes[startI].fromB, to = changes[i].toB
if (from < end && isLetter(text.charCodeAt(from - start)))
while (from > start && isLetter(text.charCodeAt(from - 1 - start))) from--
if (to > start && isLetter(text.charCodeAt(to - 1 - start)))
while (to < end && isLetter(text.charCodeAt(to - start))) to++
let joined = fillChange(changes.slice(startI, i + 1), from, to)
let last = target.length ? target[target.length - 1] : null
if (last && last.toA == joined.fromA)
target[target.length - 1] = new Change(last.fromA, joined.toA, last.fromB, joined.toB,
last.deleted.concat(joined.deleted), last.inserted.concat(joined.inserted))
else
target.push(joined)
} else {
for (let j = startI; j <= i; j++) target.push(changes[j])
}
}
return changes
}
function combine<T>(a: T, b: T): T { return a === b ? a : null as any }
function fillChange(changes: readonly Change[], fromB: number, toB: number) {
let fromA = changes[0].fromA - (changes[0].fromB - fromB)
let last = changes[changes.length - 1]
let toA = last.toA + (toB - last.toB)
let deleted = Span.none, inserted = Span.none
let delData = (changes[0].deleted.length ? changes[0].deleted : changes[0].inserted)[0].data
let insData = (changes[0].inserted.length ? changes[0].inserted : changes[0].deleted)[0].data
for (let posA = fromA, posB = fromB, i = 0;; i++) {
let next = i == changes.length ? null : changes[i]
let endA = next ? next.fromA : toA, endB = next ? next.fromB : toB
if (endA > posA) deleted = Span.join(deleted, [new Span(endA - posA, delData)], combine)
if (endB > posB) inserted = Span.join(inserted, [new Span(endB - posB, insData)], combine)
if (!next) break
deleted = Span.join(deleted, next.deleted, combine)
inserted = Span.join(inserted, next.inserted, combine)
if (deleted.length) delData = deleted[deleted.length - 1].data
if (inserted.length) insData = inserted[inserted.length - 1].data
posA = next.toA; posB = next.toB
}
return new Change(fromA, toA, fromB, toB, deleted, inserted)
}