{"version":3,"sources":["../src/react/history/historyDiff.ts"],"names":["i","j"],"mappings":"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","file":"diff.cjs","sourcesContent":["/**\n * Diff between the current document and a history snapshot — pure, no\n * React, no ProseMirror. Powers the \"hover a history item to see what\n * changed\" preview: `HistoryEntryRow` calls this on hover, then\n * `DocxEditor.previewHistoryDiff()` (see `../../editor.ts`) renders the\n * result as ProseMirror decorations without touching document content.\n *\n * Two levels, both built on the same LCS alignment (`lcsAlign`):\n *   - Block-level (`diffHistoryBlocks`): treats each top-level node of\n *     `doc.content` as one atomic \"line\" — cheap enough for the block\n *     counts a document actually has (at most a few hundred).\n *   - Word-level (`diffWords`): for a block matched as `modified`, diffs its\n *     text word-by-word so the overlay can highlight only the words that\n *     actually changed instead of tinting the whole paragraph — see\n *     `BlockDiffEntry.wordDiff`.\n */\n\nexport type BlockDiffKind = 'added' | 'removed' | 'modified';\n\nexport type WordDiffKind = 'same' | 'added' | 'removed';\n\nexport interface WordDiffEntry {\n  kind: WordDiffKind;\n  /** The token itself — a run of non-whitespace, or a run of whitespace (tokens preserve every character, so join()-ing all `text` fields losslessly reconstructs the original string). */\n  text: string;\n}\n\nexport interface BlockDiffEntry {\n  kind: BlockDiffKind;\n  /** Index into the CURRENT document's top-level blocks (added/modified only). */\n  currentIndex?: number;\n  /** Index into the SNAPSHOT document's top-level blocks (removed/modified only). */\n  snapshotIndex?: number;\n  /**\n   * `removed` only — index into the CURRENT document's top-level blocks that\n   * this removed content should be anchored *before* (`current.length` means\n   * \"at the end of the document\"). Unlike `snapshotIndex`, this stays valid\n   * as a position in the current doc even once it and the snapshot have a\n   * different number of blocks.\n   */\n  anchorIndex?: number;\n  /** Short text preview, mainly for the `removed` widget pill (no node in the current doc to anchor to). */\n  preview?: string;\n  /** `removed` only — full (untruncated) text of the removed block, for rendering the whole removed line. */\n  removedText?: string;\n  /** `modified` only — word-level diff between the snapshot's and current's full text, in order. */\n  wordDiff?: WordDiffEntry[];\n}\n\nexport interface BlockDiffResult {\n  entries: BlockDiffEntry[];\n}\n\nfunction fingerprintBlock(node: any): string {\n  return `${node?.type ?? ''}:${JSON.stringify(node)}`;\n}\n\nfunction extractNodeText(node: any): string {\n  if (!node) return '';\n  if (typeof node.text === 'string') return node.text;\n  if (Array.isArray(node.content)) return node.content.map(extractNodeText).join('');\n  return '';\n}\n\nfunction textOf(node: any): string {\n  return extractNodeText(node).trim().replace(/\\s+/g, ' ');\n}\n\nfunction previewOf(node: any, maxLen = 60): string {\n  const text = textOf(node);\n  return text.length > maxLen ? `${text.slice(0, maxLen)}…` : text;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Shared LCS alignment                                                       */\n/* -------------------------------------------------------------------------- */\n\ntype AlignOp =\n  | { kind: 'match'; aIndex: number; bIndex: number }\n  | { kind: 'onlyA'; aIndex: number }\n  | { kind: 'onlyB'; bIndex: number };\n\n/**\n * Classic LCS diff over two fingerprint arrays (exact match, not fuzzy).\n * `a` is the \"current/new\" side, `b` the \"snapshot/old\" side — used both for\n * block-level diffing (fingerprints = whole-node JSON) and word-level\n * diffing (fingerprints = the token text itself).\n */\nfunction lcsAlign(aFp: string[], bFp: string[]): AlignOp[] {\n  const n = aFp.length;\n  const m = bFp.length;\n  const width = m + 1;\n\n  // Standard LCS DP table, built backwards so we can greedily walk forward.\n  // Flat Int32Array instead of an array of arrays: one contiguous\n  // allocation instead of n+1 separate JS array objects, which matters once\n  // a large document pushes n/m into the thousands (fewer allocations, much\n  // less per-cell overhead, better cache locality for the same O(n*m) work).\n  const dp = new Int32Array((n + 1) * width);\n  for (let i = n - 1; i >= 0; i--) {\n    const row = i * width;\n    const nextRow = row + width;\n    for (let j = m - 1; j >= 0; j--) {\n      dp[row + j] = aFp[i] === bFp[j] ? dp[nextRow + j + 1] + 1 : Math.max(dp[nextRow + j], dp[row + j + 1]);\n    }\n  }\n\n  const ops: AlignOp[] = [];\n  let i = 0;\n  let j = 0;\n  while (i < n && j < m) {\n    if (aFp[i] === bFp[j]) {\n      ops.push({ kind: 'match', aIndex: i, bIndex: j });\n      i++;\n      j++;\n    } else if (dp[(i + 1) * width + j] >= dp[i * width + j + 1]) {\n      ops.push({ kind: 'onlyA', aIndex: i });\n      i++;\n    } else {\n      ops.push({ kind: 'onlyB', bIndex: j });\n      j++;\n    }\n  }\n  while (i < n) ops.push({ kind: 'onlyA', aIndex: i++ });\n  while (j < m) ops.push({ kind: 'onlyB', bIndex: j++ });\n  return ops;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Block-level diff                                                           */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Diff `current.content` against `snapshot.content` (both full TipTap JSON\n * documents). Returns entries in document order (interleaving added/removed/\n * modified as they occur, matched blocks omitted).\n */\nexport function diffHistoryBlocks(currentDoc: any, snapshotDoc: any): BlockDiffResult {\n  const current: any[] = currentDoc?.content ?? [];\n  const snapshot: any[] = snapshotDoc?.content ?? [];\n  const ops = lcsAlign(current.map(fingerprintBlock), snapshot.map(fingerprintBlock));\n\n  // For every op, which CURRENT-doc block index it should anchor *before* —\n  // its own aIndex for match/onlyA, or the nearest following match/onlyA's\n  // aIndex (current.length if none follows) for onlyB. Lets a standalone\n  // `removed` entry point at a real position in the current doc instead of\n  // the snapshot doc's index space (which the current doc's block count no\n  // longer matches once anything has been added/removed).\n  const anchorForOp: number[] = new Array(ops.length);\n  let nextCurrentIndex = current.length;\n  for (let idx = ops.length - 1; idx >= 0; idx--) {\n    const op = ops[idx];\n    anchorForOp[idx] = op.kind === 'onlyB' ? nextCurrentIndex : op.aIndex;\n    if (op.kind !== 'onlyB') nextCurrentIndex = op.aIndex;\n  }\n\n  // Post-pass: an adjacent (onlyA, onlyB) or (onlyB, onlyA) pair of the same\n  // node `type` reads as one edited block (\"modified\"), not a delete+insert.\n  // Heuristic, not content-similarity — a heavily-rewritten paragraph that no\n  // longer lands adjacent to its old position will show as separate\n  // add+remove instead. Acceptable at block-level granularity.\n  const entries: BlockDiffEntry[] = [];\n  let k = 0;\n  while (k < ops.length) {\n    const op = ops[k];\n    if (op.kind === 'match') {\n      k++;\n      continue;\n    }\n    const next = ops[k + 1];\n    const isAddRemovePair =\n      next && ((op.kind === 'onlyA' && next.kind === 'onlyB') || (op.kind === 'onlyB' && next.kind === 'onlyA'));\n    if (isAddRemovePair) {\n      const addOp = (op.kind === 'onlyA' ? op : next) as Extract<AlignOp, { kind: 'onlyA' }>;\n      const removeOp = (op.kind === 'onlyB' ? op : next) as Extract<AlignOp, { kind: 'onlyB' }>;\n      const addNode = current[addOp.aIndex];\n      const removeNode = snapshot[removeOp.bIndex];\n      if (addNode?.type && addNode.type === removeNode?.type) {\n        entries.push({\n          kind: 'modified',\n          currentIndex: addOp.aIndex,\n          snapshotIndex: removeOp.bIndex,\n          preview: previewOf(addNode),\n          wordDiff: diffWords(extractNodeText(addNode), extractNodeText(removeNode)),\n        });\n        k += 2;\n        continue;\n      }\n    }\n    if (op.kind === 'onlyA') {\n      entries.push({ kind: 'added', currentIndex: op.aIndex, preview: previewOf(current[op.aIndex]) });\n    } else {\n      entries.push({\n        kind: 'removed',\n        snapshotIndex: op.bIndex,\n        anchorIndex: anchorForOp[k],\n        preview: previewOf(snapshot[op.bIndex]),\n        removedText: textOf(snapshot[op.bIndex]),\n      });\n    }\n    k++;\n  }\n\n  return { entries };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Word-level diff                                                            */\n/* -------------------------------------------------------------------------- */\n\n// Splits into alternating non-whitespace/whitespace runs, covering every\n// character — join()-ing the matched tokens reconstructs the input exactly,\n// which the overlay extension relies on to map diff offsets back to\n// ProseMirror positions (see history-diff-overlay.ts).\nconst WORD_TOKEN_RE = /\\S+|\\s+/g;\n\nfunction tokenize(text: string): string[] {\n  return text.match(WORD_TOKEN_RE) ?? [];\n}\n\n/**\n * Word-level diff between `currentText` and `snapshotText`. Whitespace runs\n * are tokens too (not diffed away) so the overlay can precisely map every\n * token back to a text offset — see `WordDiffEntry`.\n */\nexport function diffWords(currentText: string, snapshotText: string): WordDiffEntry[] {\n  const currentTokens = tokenize(currentText);\n  const snapshotTokens = tokenize(snapshotText);\n  const ops = lcsAlign(currentTokens, snapshotTokens);\n  return ops.map((op): WordDiffEntry => {\n    if (op.kind === 'match') return { kind: 'same', text: currentTokens[op.aIndex] };\n    if (op.kind === 'onlyA') return { kind: 'added', text: currentTokens[op.aIndex] };\n    return { kind: 'removed', text: snapshotTokens[op.bIndex] };\n  });\n}\n"]}