Hi I am using text editor as prosemirror I am trying to track changes like word I am using prosemirror-changset for tracking the changes,I can able to track the text changes only I am trying try style,Marks also, They issue is When try meger the old changes to new changes , I need merge the old set with new one
Any Help Thanks
/* eslint-disable */
import { Node as PMNode } from "prosemirror-model"
import { AddMarkStep, RemoveMarkStep, ReplaceAroundStep, ReplaceStep, Step, StepMap } from "prosemirror-transform"
import { computeDiff } from "./diff"
import { Change, BlockChange, Span, merge } from './change'
import { IChange } from './change/types'
interface ChangeSetConfig {
doc: PMNode
combine: (data1: any, data2: any) => null | any
}
// ::- 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 {
config: ChangeSetConfig
changes: IChange[]
constructor(config: ChangeSetConfig, changes: IChange[]) {
this.config = config
// :: [Change] Replaced regions.
this.changes = changes
}
// :: (Node, [StepMap], union<[any], any>) → ChangeSet
// 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(oldDoc: PMNode, newDoc: PMNode, steps: Step[], data?: any | any[]) {
// 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: IChange[] = []
// Add spans for new steps.
for (let i = 0; i < steps.length; i++) {
const step = steps[i]
let d = Array.isArray(data) ? data[i] : data
let off = 0
if (step instanceof ReplaceStep) {
step.getMap().forEach((fromA: number, toA: number, fromB: number, toB: number) => {
const changedText = oldDoc.content.textBetween(fromA, toA) ?? ''
stepChanges.push(new Change(fromA + off, toA + off, fromB, toB,
fromA == toA ? Span.none : [new Span(toA - fromA, changedText)],
fromB == toB ? Span.none : [new Span(toB - fromB, changedText)],
))
off = (toB - fromB) - (toA - fromA)
})
} else if (step instanceof ReplaceAroundStep) {
let insideReplaceAroundStep = false
let changeId = Math.random().toString()
let node: PMNode | null | undefined
// @ts-ignore
const { slice } = step
const nodeDeleted = slice.size === 0
step.getMap().forEach((fromA: number, toA: number, fromB: number, toB: number) => {
if (!insideReplaceAroundStep) {
node = nodeDeleted ? oldDoc.nodeAt(fromA) : newDoc.nodeAt(fromB)
insideReplaceAroundStep = true
d = { ...d, blockChange: 'start', changeId, nodeType: node?.type.name }
} else {
insideReplaceAroundStep = false
d = { ...d, blockChange: 'end', changeId, nodeType: node?.type.name }
changeId = Math.random().toString()
}
const newtext = oldDoc.content.textBetween(fromA, toA) ?? ''
const inserted = fromA == toA ? Span.none : [new Span(toA - fromA, newtext)]
const deleted = fromB == toB ? Span.none : [new Span(toB - fromB, newtext)]
stepChanges.push(new BlockChange(fromA + off, toA + off, fromB, toB, inserted, deleted, insideReplaceAroundStep))
off = (toB - fromB) - (toA - fromA)
})
} else if (step instanceof AddMarkStep) {
const { from, to, mark } = step
const change = new Change(from, to, from, to, Span.none, [new Span(to - from, mark)])
stepChanges.push(change)
} else if (step instanceof RemoveMarkStep) {
const { from, to, mark } = step
const change = new Change(from, to, from, to, [new Span(to - from, mark)], Span.none)
stepChanges.push(change)
} else {
console.error('Unknown step type! Change not tracked and possibly current changes have become inconsistent', step)
}
}
if (stepChanges.length == 0) return this
let newChanges = mergeAll(stepChanges, this.config.combine)
console.log("changes", newChanges)
let changes = merge(this.changes, newChanges, this.config.combine)
// Minimize changes when possible
for (let i = 0; i < changes.length; i++) {
let change = changes[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)
// 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 (diff.length == 1) {
changes[i] = diff[0]
} else {
changes.splice(i, 1, ...diff)
i += diff.length - 1
}
}
return new ChangeSet(this.config, changes)
}
// :: Node
// The starting document of the change set.
get startDoc() { return this.config.doc }
// :: (f: (range: Change) → any) → ChangeSet
// 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: IChange) => any) {
return new ChangeSet(this.config, this.changes.map(change => {
let result = f(change)
return result === change ? change :
change.create(change.fromA, change.toA, change.fromB, change.toB, change.deleted, change.inserted)
}))
}
// :: (ChangeSet, ?[StepMap]) → ?{from: number, to: number}
// 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?: StepMap[]) {
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
}
// :: (Node, ?(a: any, b: any) → any) → ChangeSet
// 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.
static create(doc: PMNode, combine = (a: any, b: any): any => a === b ? a : null) {
return new ChangeSet({ combine, doc }, [])
}
}
// Exported for testing
// @ts-ignore
ChangeSet.computeDiff = computeDiff
// : ([[Change]], (any, any) → any, number, number) → [Change]
// Divide-and-conquer approach to merging a series of ranges.
function mergeAll(
ranges: IChange[],
combine: (data1: any, data2: any) => null | any,
start = 0,
end = ranges.length
): IChange[] {
if (end == start + 1) return [ranges[start]]
let mid = (start + end) >> 1
return merge(mergeAll(ranges, combine, start, mid),
mergeAll(ranges, combine, mid, end), combine)
}
function endRange(maps: 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: StepMap[]) {
let b = endRange(maps)
if (!b) return null
let a = endRange(maps.map(m => m.invert()).reverse())
if (!a) throw Error('endRange was null!')
return { fromA: a.from, toA: a.to, fromB: b.from, toB: b.to }
}
function sameRanges(a: IChange, b: IChange, 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(a: Span[], b: Span[]) {
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
}
/* eslint-disable */
import { Span } from './span'
import { Change } from './change'
import type { IChange } from './types'
// : ([Change], [Change], (any, any) → any) → [Change]
// 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.
export function merge(x: IChange[], y: IChange[], combine: (a: any, b: any) => any): IChange[] {
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: IChange = x[0], curY: IChange = 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(curX.createNewWithOffset(off, true))
// result.push(off == 0 ? curX :
// new Change(curX.fromA, curX.toA, curX.fromB + off, curX.toB + off,
// curX.deleted, curX.inserted))
// @ts-ignore
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(curY.createNewWithOffset(off, false))
// result.push(off == 0 ? curY :
// new Change(curY.fromA - off, curY.toA - off, curY.fromB, curY.toB,
// curY.deleted, curY.inserted))
// @ts-ignore
curY = iY++ == y.length ? null : y[iY]
} else if (curX.isBlockChange() && curY.isBlockChange() || curX.isBlockChange() && curY.isChange()) {
let off = iY ? y[iY - 1].toB - y[iY - 1].toA : 0
result.push(curX.createNewWithOffset(off, true))
off = iX ? x[iX - 1].toB - x[iX - 1].toA : 0
result.push(curY.createNewWithOffset(off, false))
// @ts-ignore
curX = iX++ == x.length ? null : x[iX]
// @ts-ignore
curY = iY++ == y.length ? null : y[iY]
// debugger
}
// else if (curX.isChange() && curY.isBlockChange()) {
// throw Error('TODO')
// }
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) {
// @ts-ignore
curX = iX++ == x.length ? null : x[iX]
enteredX = false
}
if (inY && next == curY.toA) {
// @ts-ignore
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))
}
}
}