@createiq/htmldiff 1.2.0-beta.8 → 1.2.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/HtmlDiff.cjs CHANGED
@@ -1337,13 +1337,31 @@ function collectInsertionsKeyedByEnd(d) {
1337
1337
  return out;
1338
1338
  }
1339
1339
  /**
1340
- * Emit any insertions at boundary `b`. When both authors inserted at
1341
- * the same boundary AND the inserted token sequences are textually
1342
- * identical, the insertion is treated as agreed and emitted unmarked.
1343
- * Otherwise each side's insertion is emitted with author attribution.
1340
+ * Emit any insertions at boundary `b`. Three cases:
1344
1341
  *
1345
- * The CP-then-Me ordering for disagreement is arbitrary but consistent;
1346
- * callers don't depend on it.
1342
+ * 1. One side inserted, the other didn't emit that side's tokens
1343
+ * with author attribution.
1344
+ * 2. Both sides inserted the EXACT same sequence → settled, emit
1345
+ * unmarked.
1346
+ * 3. Both sides inserted overlapping but different sequences (the
1347
+ * common case: one author accepted the other's insertion and
1348
+ * edited it, so e.g. cp's "X Y Z" overlaps me's "X Y a Z" with
1349
+ * "a" being a one-author-only addition). Run an LCS sub-diff
1350
+ * between the two insertion sequences and emit:
1351
+ * - tokens in BOTH → settled (equal segment)
1352
+ * - tokens only in cp → ins-cp
1353
+ * - tokens only in me → ins-me
1354
+ * The order of emission preserves the natural reading flow of
1355
+ * the merged insertion — common tokens read where they appear,
1356
+ * with author-only deltas inserted in their LCS-determined
1357
+ * positions.
1358
+ *
1359
+ * Without this sub-alignment, real-world flows like "Me added 'add
1360
+ * more things here', CP accepted minus 'things'" would render as two
1361
+ * full redundant insertions (`<ins cp>add more here</ins><ins me>add
1362
+ * more things here</ins>`) rather than the obvious single shared
1363
+ * insertion with a me-only "things" word — confusing to read and a
1364
+ * regression vs Word's track-changes UX.
1347
1365
  */
1348
1366
  function emitBoundary(b, cpInsAt, meInsAt, _cpDiffWords, _meDiffWords, segments) {
1349
1367
  const cpIns = cpInsAt.get(b);
@@ -1351,18 +1369,34 @@ function emitBoundary(b, cpInsAt, meInsAt, _cpDiffWords, _meDiffWords, segments)
1351
1369
  const hasCp = !!cpIns && cpIns.length > 0;
1352
1370
  const hasMe = !!meIns && meIns.length > 0;
1353
1371
  if (!hasCp && !hasMe) return;
1354
- if (hasCp && hasMe && tokenArraysEqual(cpIns, meIns)) {
1372
+ if (!hasCp) {
1373
+ appendSegment(segments, {
1374
+ kind: "ins",
1375
+ author: "me"
1376
+ }, meIns);
1377
+ return;
1378
+ }
1379
+ if (!hasMe) {
1380
+ appendSegment(segments, {
1381
+ kind: "ins",
1382
+ author: "cp"
1383
+ }, cpIns);
1384
+ return;
1385
+ }
1386
+ if (tokenArraysEqual(cpIns, meIns)) {
1355
1387
  appendSegment(segments, { kind: "equal" }, cpIns);
1356
1388
  return;
1357
1389
  }
1358
- if (hasCp) appendSegment(segments, {
1390
+ const alignment = lcsAlign(cpIns, meIns);
1391
+ for (const a of alignment) if (a.oldIdx !== null && a.newIdx !== null) appendSegment(segments, { kind: "equal" }, [cpIns[a.oldIdx]]);
1392
+ else if (a.oldIdx !== null) appendSegment(segments, {
1359
1393
  kind: "ins",
1360
1394
  author: "cp"
1361
- }, cpIns);
1362
- if (hasMe) appendSegment(segments, {
1395
+ }, [cpIns[a.oldIdx]]);
1396
+ else if (a.newIdx !== null) appendSegment(segments, {
1363
1397
  kind: "ins",
1364
1398
  author: "me"
1365
- }, meIns);
1399
+ }, [meIns[a.newIdx]]);
1366
1400
  }
1367
1401
  function tokenArraysEqual(a, b) {
1368
1402
  if (a.length !== b.length) return false;