wordinweb 0.3.5 → 0.3.7

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.
@@ -2131,6 +2131,22 @@ function serializeXml(el26, declaration = false) {
2131
2131
  write2(el26);
2132
2132
  return out.join("");
2133
2133
  }
2134
+ function isEmptyPlaceholder(e) {
2135
+ return !!e.omitWhenEmpty && Object.keys(e.attrs).length === 0 && e.text.length === 0 && e.children.length === 1 && localName(e.children[0].name) === "t" && e.children[0].children.length === 0 && e.children[0].text.length === 0 && Object.keys(e.children[0].attrs).every((key) => key === "xml:space" && e.children[0].attrs[key] === "preserve");
2136
+ }
2137
+ function sameXml(a, b) {
2138
+ if (a.name !== b.name) return false;
2139
+ const ak = Object.keys(a.attrs);
2140
+ if (ak.length !== Object.keys(b.attrs).length) return false;
2141
+ for (const k of ak) if (a.attrs[k] !== b.attrs[k]) return false;
2142
+ if (a.children.length === 0 && b.children.length === 0) return a.text === b.text;
2143
+ const ac = a.children.filter((c) => !isEmptyPlaceholder(c));
2144
+ const bc = b.children.filter((c) => !isEmptyPlaceholder(c));
2145
+ if (ac.length !== bc.length) return false;
2146
+ if (ac.length === 0) return (a.children.length > 0 ? "" : a.text) === (b.children.length > 0 ? "" : b.text);
2147
+ for (let i = 0; i < ac.length; i++) if (!sameXml(ac[i], bc[i])) return false;
2148
+ return true;
2149
+ }
2134
2150
  function cloneXml(el26) {
2135
2151
  return {
2136
2152
  name: el26.name,
@@ -2176,6 +2192,12 @@ var Package = class _Package {
2176
2192
  static from(data) {
2177
2193
  return new _Package(data instanceof Uint8Array ? data : new Uint8Array(data));
2178
2194
  }
2195
+ /** A package over an already-unzipped entry map (DocxDocument.clone). */
2196
+ static fromFiles(files) {
2197
+ const pkg = Object.create(_Package.prototype);
2198
+ pkg.files = files;
2199
+ return pkg;
2200
+ }
2179
2201
  has(name) {
2180
2202
  return this.normalize(name) in this.files;
2181
2203
  }
@@ -2198,6 +2220,13 @@ var Package = class _Package {
2198
2220
  return name.startsWith("/") ? name.slice(1) : name;
2199
2221
  }
2200
2222
  };
2223
+ function relativePartPath(fromPart, toPart) {
2224
+ const fromDir = fromPart.split("/").slice(0, -1);
2225
+ const to = toPart.split("/");
2226
+ let i = 0;
2227
+ while (i < fromDir.length && i < to.length - 1 && fromDir[i] === to[i]) i++;
2228
+ return [...fromDir.slice(i).map(() => ".."), ...to.slice(i)].join("/");
2229
+ }
2201
2230
  function resolvePartPath(sourcePart, target) {
2202
2231
  if (target.startsWith("/")) return target.slice(1);
2203
2232
  const baseDir = sourcePart.includes("/") ? sourcePart.slice(0, sourcePart.lastIndexOf("/")) : "";
@@ -4432,6 +4461,11 @@ function parseParagraph(p, ctx) {
4432
4461
  }
4433
4462
  }
4434
4463
  ensureCaretAnchor(p, para);
4464
+ if (ctx.revisionView === "markup" && pPr) {
4465
+ const markRPr = child(pPr, "rPr");
4466
+ if (markRPr && child(markRPr, "del")) para.markRevision = "del";
4467
+ else if (markRPr && child(markRPr, "ins")) para.markRevision = "ins";
4468
+ }
4435
4469
  return para;
4436
4470
  }
4437
4471
  function hasVisibleContent(c) {
@@ -8798,6 +8832,10 @@ var _DocxDocument = class _DocxDocument {
8798
8832
  /** The header part roots, in package order. A watermark is authored into
8799
8833
  * every one of them: Word paints it on every page, and a document with a
8800
8834
  * first-page or even-page header has more than one header part. */
8835
+ /** The root of the header or footer part at `target`, if this document has one. */
8836
+ headerFooterRoot(target) {
8837
+ return this.hfParts.find((p) => p.target === target)?.root;
8838
+ }
8801
8839
  headerRoots() {
8802
8840
  return this.hfParts.filter((p) => p.isHeader).map((p) => p.root);
8803
8841
  }
@@ -8961,6 +8999,22 @@ var _DocxDocument = class _DocxDocument {
8961
8999
  this.saveJournal = null;
8962
9000
  }
8963
9001
  }
9002
+ /**
9003
+ * An independent copy: what `DocxDocument.load(doc.save())` gives, without
9004
+ * zipping the package and unzipping it again. compareDocuments starts from a
9005
+ * copy of the revised document, and the round trip through the zip was a
9006
+ * third of its time on a document that had not changed at all.
9007
+ */
9008
+ clone() {
9009
+ const journal = [];
9010
+ this.saveJournal = journal;
9011
+ try {
9012
+ return new _DocxDocument(Package.fromFiles(this.buildPackageFiles()));
9013
+ } finally {
9014
+ for (let i = journal.length - 1; i >= 0; i--) journal[i]();
9015
+ this.saveJournal = null;
9016
+ }
9017
+ }
8964
9018
  saveAsync() {
8965
9019
  const journal = [];
8966
9020
  this.saveJournal = journal;
@@ -9162,8 +9216,91 @@ var _DocxDocument = class _DocxDocument {
9162
9216
  return this.relsRoot;
9163
9217
  }
9164
9218
  /** Register an external hyperlink relationship and return its rId. */
9165
- addHyperlinkRel(url) {
9166
- const rels = this.ensureRelsRoot();
9219
+ /**
9220
+ * Copy the part that `source`'s main-document relationship `relId` points
9221
+ * at into this package, with every part it references in turn (a chart's
9222
+ * embedded workbook, SmartArt's layout, style and colour parts) and their
9223
+ * content types, and add a relationship to it. Returns the new
9224
+ * relationship id, or null when the source has no such internal part.
9225
+ *
9226
+ * compareDocuments needs this for struck content: a removed picture keeps
9227
+ * its w:drawing, and without its image part the result's reject-all
9228
+ * brought the picture back as an empty frame.
9229
+ */
9230
+ adoptRelFrom(source, relId, scope = {}) {
9231
+ const rel = source.relationshipsOf(scope.from).get(relId);
9232
+ if (!rel || rel.external) return null;
9233
+ const copied = /* @__PURE__ */ new Map();
9234
+ const sameBytes = (path2, bytes) => {
9235
+ const have = this.pkg.binary(path2);
9236
+ return !!have && have.length === bytes.length && have.every((b, i) => b === bytes[i]);
9237
+ };
9238
+ const copyPart = (from) => {
9239
+ const known = copied.get(from);
9240
+ if (known) return known;
9241
+ const bytes = source.pkg.binary(from);
9242
+ if (!bytes) return null;
9243
+ let to = from;
9244
+ for (let n = 2; this.pkg.has(to) && !sameBytes(to, bytes); n++) to = from.replace(/(\.[^./]+)?$/, `-${n}$1`);
9245
+ copied.set(from, to);
9246
+ if (this.pkg.has(to)) return to;
9247
+ this.pkg.raw()[to] = bytes;
9248
+ this.adoptContentType(source, from, to);
9249
+ const relsXml = source.pkg.text(relsPathFor(from));
9250
+ if (relsXml) {
9251
+ const root = parseXml(relsXml);
9252
+ for (const r of root.children) {
9253
+ if (r.attrs.TargetMode === "External" || !r.attrs.Target) continue;
9254
+ const sub = copyPart(resolvePartPath(from, r.attrs.Target));
9255
+ if (sub) r.attrs.Target = relativePartPath(to, sub);
9256
+ }
9257
+ this.pkg.raw()[relsPathFor(to)] = strToU82(serializeXml(root, true));
9258
+ }
9259
+ return to;
9260
+ };
9261
+ const part = copyPart(rel.target);
9262
+ if (!part) return null;
9263
+ const owner = this.relsOwnerOf(scope.into);
9264
+ const rels = owner.relsRoot;
9265
+ let maxId = 0;
9266
+ for (const r of rels.children) {
9267
+ const m = /^rId(\d+)$/.exec(r.attrs["Id"] ?? "");
9268
+ if (m) maxId = Math.max(maxId, parseInt(m[1], 10));
9269
+ }
9270
+ const id = `rId${maxId + 1}`;
9271
+ rels.children.push({
9272
+ name: "Relationship",
9273
+ attrs: { Id: id, Type: rel.type, Target: relativePartPath(owner.partPath, part) },
9274
+ children: [],
9275
+ text: ""
9276
+ });
9277
+ owner.relationships.set(id, { id, type: rel.type, target: part, external: false });
9278
+ return id;
9279
+ }
9280
+ /** Give a part copied from `source` the content type it had there. */
9281
+ adoptContentType(source, from, to) {
9282
+ const types = this.contentTypesRoot;
9283
+ const theirs = source.contentTypesRoot;
9284
+ if (!types || !theirs) return;
9285
+ const override = theirs.children.find((c) => localName(c.name) === "Override" && c.attrs.PartName === `/${from}`);
9286
+ const prefixEnd = types.name.indexOf(":") + 1;
9287
+ const prefix = prefixEnd > 0 ? types.name.slice(0, prefixEnd) : "";
9288
+ if (override) {
9289
+ if (!types.children.some((c) => localName(c.name) === "Override" && c.attrs.PartName === `/${to}`)) {
9290
+ types.children.push({ name: `${prefix}Override`, attrs: { PartName: `/${to}`, ContentType: override.attrs.ContentType }, children: [], text: "" });
9291
+ }
9292
+ return;
9293
+ }
9294
+ const ext = from.slice(from.lastIndexOf(".") + 1).toLowerCase();
9295
+ const byExt = (root) => root.children.find((c) => localName(c.name) === "Default" && c.attrs.Extension?.toLowerCase() === ext);
9296
+ const fallback = byExt(theirs);
9297
+ if (fallback && !byExt(types)) {
9298
+ types.children.unshift({ name: `${prefix}Default`, attrs: { Extension: ext, ContentType: fallback.attrs.ContentType }, children: [], text: "" });
9299
+ }
9300
+ }
9301
+ addHyperlinkRel(url, ownerPart) {
9302
+ const owner = this.relsOwnerOf(ownerPart);
9303
+ const rels = owner.relsRoot;
9167
9304
  let maxId = 0;
9168
9305
  for (const r of rels.children) {
9169
9306
  const m = /^rId(\d+)$/.exec(r.attrs["Id"] ?? "");
@@ -9181,7 +9318,7 @@ var _DocxDocument = class _DocxDocument {
9181
9318
  children: [],
9182
9319
  text: ""
9183
9320
  });
9184
- this.documentRels.set(id, { id, type: "hyperlink", target: url, external: true });
9321
+ owner.relationships.set(id, { id, type: "hyperlink", target: url, external: true });
9185
9322
  return id;
9186
9323
  }
9187
9324
  /** Retarget an existing external relationship (hyperlink href edit). */
@@ -9193,6 +9330,23 @@ var _DocxDocument = class _DocxDocument {
9193
9330
  if (el26) el26.attrs["Target"] = url;
9194
9331
  return true;
9195
9332
  }
9333
+ /** The relationships owned by `part` (a header or footer part), or by the
9334
+ * document part when null; a header or footer gets a rels root on demand. */
9335
+ relsOwnerOf(part) {
9336
+ const owner = part ? this.hfParts.find((p) => p.target === part) : void 0;
9337
+ if (!owner) return { relsRoot: this.ensureRelsRoot(), relationships: this.documentRels, partPath: this.docPart };
9338
+ owner.relsRoot ?? (owner.relsRoot = {
9339
+ name: "Relationships",
9340
+ attrs: { xmlns: "http://schemas.openxmlformats.org/package/2006/relationships" },
9341
+ children: [],
9342
+ text: ""
9343
+ });
9344
+ return { relsRoot: owner.relsRoot, relationships: owner.rels, partPath: owner.target };
9345
+ }
9346
+ /** The relationships of `part` (a header or footer), or of the document part. */
9347
+ relationshipsOf(part) {
9348
+ return (part ? this.hfParts.find((p) => p.target === part)?.rels : void 0) ?? this.documentRels;
9349
+ }
9196
9350
  /** The relationships of the part that holds `source`: a header or footer
9197
9351
  * part when the element is inside one, else the document part. */
9198
9352
  imageRelsOwner(source) {
@@ -13702,6 +13856,20 @@ function buildAtoms(doc, para, measurer, fields, fallbackFamily, gridPitch) {
13702
13856
  if (childEl.type === "run") pushRun(childEl);
13703
13857
  else for (const r of childEl.runs) pushRun(r, childEl.href ?? (childEl.anchor ? "#" + childEl.anchor : void 0));
13704
13858
  }
13859
+ if (para.markRevision) {
13860
+ const last = para.children.at(-1);
13861
+ const lastRun = last?.type === "hyperlink" ? last.runs.at(-1) : last;
13862
+ const deleted = para.markRevision === "del";
13863
+ pushRun({
13864
+ type: "run",
13865
+ props: {
13866
+ ...lastRun?.props ?? {},
13867
+ color: deleted ? "#D93025" : "#188038",
13868
+ ...deleted ? { strike: true } : { underline: "single" }
13869
+ },
13870
+ content: [{ kind: "text", text: "\xB6" }]
13871
+ });
13872
+ }
13705
13873
  if (gridPitch) {
13706
13874
  for (let i = 0; i + 1 < atoms.length; i++) {
13707
13875
  const punct = atoms[i];
@@ -19232,15 +19400,20 @@ var Engine = class {
19232
19400
  while (out.length < nCols) out.push(bw(fallback));
19233
19401
  return out;
19234
19402
  };
19403
+ const colTopPads = (r) => {
19404
+ const fallback = this.cellMarginsOf(tbl, false).top ?? 0;
19405
+ return rows[r].cells.flatMap((c) => Array(Math.max(1, c.props.gridSpan)).fill(c.props.margins?.top ?? fallback));
19406
+ };
19235
19407
  const boundary = (k) => {
19236
19408
  if (k === 0) return Math.max(0, ...colWidths(0, "top", tb?.top));
19237
19409
  if (k === rows.length)
19238
19410
  return Math.max(0, ...colWidths(rows.length - 1, "bottom", tb?.bottom));
19239
19411
  const above = colWidths(k - 1, "bottom", tb?.insideH);
19240
19412
  const below = colWidths(k, "top", tb?.insideH);
19413
+ const topPads = colTopPads(k);
19241
19414
  const n = Math.max(above.length, below.length);
19242
19415
  let w = 0;
19243
- for (let i = 0; i < n; i++) w = Math.max(w, Math.max(above[i] ?? 0, below[i] ?? 0));
19416
+ for (let i = 0; i < n; i++) w = Math.max(w, below[i] ?? 0, (above[i] ?? 0) - (topPads[i] ?? 0));
19244
19417
  return w;
19245
19418
  };
19246
19419
  return { top: boundary(ri), bottom: boundary(ri + 1) };
@@ -21446,6 +21619,8 @@ function renderPageShell(page, zoom, options) {
21446
21619
  if (options.pageShadow !== false) {
21447
21620
  el26.style.boxShadow = "var(--dxw-page-shadow, 0 1px 3px rgba(0,0,0,.28), 0 4px 14px rgba(0,0,0,.12))";
21448
21621
  }
21622
+ el26.dataset.pageWidth = String(page.width);
21623
+ el26.dataset.pageHeight = String(page.height);
21449
21624
  el26.dataset.bodyTop = String(page.bodyTop);
21450
21625
  el26.dataset.bodyBottom = String(page.bodyBottom);
21451
21626
  el26.dataset.columnBands = JSON.stringify(page.columnBands);
@@ -21762,9 +21937,21 @@ function trackPaint(work) {
21762
21937
  async function paintsSettled() {
21763
21938
  while (pendingPaints.size > 0) await Promise.allSettled([...pendingPaints]);
21764
21939
  }
21765
- function printStylesheet(pageWidthPx, pageHeightPx) {
21940
+ function printStylesheet(pageWidthPx, pageHeightPx, root) {
21941
+ const inches = (px) => `${Math.round(px / 96 * 1e3) / 1e3}in`;
21942
+ const sizes = /* @__PURE__ */ new Set();
21943
+ for (const el26 of root ? Array.from(root.querySelectorAll(".dxw-page[data-page-width]")) : []) {
21944
+ sizes.add(`${el26.dataset.pageWidth}x${el26.dataset.pageHeight}`);
21945
+ }
21946
+ const named = [...sizes].map((key) => {
21947
+ const [w, h] = key.split("x");
21948
+ const name = `dxw-${key.replace(/\./g, "_")}`;
21949
+ return `@page ${name} { size: ${inches(Number(w))} ${inches(Number(h))}; margin: 0; }
21950
+ .dxw-page[data-page-width="${w}"][data-page-height="${h}"] { page: ${name}; }`;
21951
+ });
21766
21952
  return `
21767
- @page { size: ${pageWidthPx / 96}in ${pageHeightPx / 96}in; margin: 0; }
21953
+ @page { size: ${inches(pageWidthPx)} ${inches(pageHeightPx)}; margin: 0; }
21954
+ ${named.join("\n ")}
21768
21955
  html, body { margin: 0; padding: 0; }
21769
21956
  .dxw-pages { display: block !important; padding: 0 !important; gap: 0 !important; }
21770
21957
  .dxw-pages, .dxw-pages * {
@@ -21775,13 +21962,25 @@ function printStylesheet(pageWidthPx, pageHeightPx) {
21775
21962
  .dxw-comment-card, .dxw-hf-marker { display: none !important; }
21776
21963
  `;
21777
21964
  }
21965
+ function pruneBlankSpans(root) {
21966
+ let removed = 0;
21967
+ for (const el26 of Array.from(root.querySelectorAll(".dxw-page span"))) {
21968
+ if (el26.childElementCount > 0 || el26.textContent?.trim()) continue;
21969
+ if (el26.style.textDecoration.includes("underline")) continue;
21970
+ el26.remove();
21971
+ removed++;
21972
+ }
21973
+ return removed;
21974
+ }
21778
21975
  function buildPrintHtml(root, pageWidthPx, pageHeightPx) {
21779
21976
  const head = [`<base href="${document.location.href}">`];
21780
21977
  for (const node of Array.from(document.head.querySelectorAll("style, link[rel=stylesheet]"))) {
21781
21978
  head.push(node.outerHTML);
21782
21979
  }
21783
- head.push(`<style>${printStylesheet(pageWidthPx, pageHeightPx)}</style>`);
21784
- return `<!doctype html><html><head>${head.join("")}</head><body>${root.outerHTML}</body></html>`;
21980
+ head.push(`<style>${printStylesheet(pageWidthPx, pageHeightPx, root)}</style>`);
21981
+ const pages = root.cloneNode(true);
21982
+ pruneBlankSpans(pages);
21983
+ return `<!doctype html><html><head>${head.join("")}</head><body>${pages.outerHTML}</body></html>`;
21785
21984
  }
21786
21985
  function printPages(root, pageWidthPx, pageHeightPx) {
21787
21986
  const iframe = document.createElement("iframe");
@@ -23828,7 +24027,9 @@ function revisionWrapper(sample, kind, content, meta) {
23828
24027
  }
23829
24028
  function markDeletedText(el26) {
23830
24029
  const walk2 = (e) => {
24030
+ if (localName(e.name) === "txbxContent") return;
23831
24031
  if (localName(e.name) === "t") e.name = e.name.replace(/t$/, "delText");
24032
+ else if (localName(e.name) === "instrText") e.name = e.name.replace(/instrText$/, "delInstrText");
23832
24033
  for (const c of e.children) walk2(c);
23833
24034
  };
23834
24035
  walk2(el26);
@@ -24087,7 +24288,8 @@ function ensurePPrRPr(pEl) {
24087
24288
  let rPr = pPr.children.find((c) => localName(c.name) === "rPr");
24088
24289
  if (!rPr) {
24089
24290
  rPr = el2(`${w}rPr`);
24090
- pPr.children.push(rPr);
24291
+ const at = pPr.children.findIndex((c) => localName(c.name) === "sectPr" || localName(c.name) === "pPrChange");
24292
+ pPr.children.splice(at === -1 ? pPr.children.length : at, 0, rPr);
24091
24293
  }
24092
24294
  return rPr;
24093
24295
  }
@@ -24247,6 +24449,8 @@ function convertDelTextToText(el26) {
24247
24449
  const walk2 = (e) => {
24248
24450
  if (localName(e.name) === "delText") {
24249
24451
  e.name = e.name.replace(/delText$/, "t");
24452
+ } else if (localName(e.name) === "delInstrText") {
24453
+ e.name = e.name.replace(/delInstrText$/, "instrText");
24250
24454
  }
24251
24455
  for (const c of e.children) walk2(c);
24252
24456
  };
@@ -24268,7 +24472,7 @@ function removeEl(doc, el26) {
24268
24472
  parent.children.splice(idx, 1);
24269
24473
  return true;
24270
24474
  }
24271
- var RUN_CONTAINERS = ["hyperlink", "smartTag", "dir", "bdo"];
24475
+ var RUN_CONTAINERS = ["hyperlink", "smartTag", "dir", "bdo", "fldSimple"];
24272
24476
  function removeRevisionAndEmptyHost(doc, el26) {
24273
24477
  const parent = doc.findParentOf(el26);
24274
24478
  if (!removeEl(doc, el26)) return false;
@@ -24277,26 +24481,57 @@ function removeRevisionAndEmptyHost(doc, el26) {
24277
24481
  }
24278
24482
  return true;
24279
24483
  }
24280
- function joinWithNext(doc, pEl) {
24484
+ function joinWithNext(doc, pEl, gone) {
24281
24485
  const parent = doc.findParentOf(pEl);
24282
24486
  if (!parent) return false;
24283
24487
  const idx = parent.children.indexOf(pEl);
24284
- const next = parent.children[idx + 1];
24285
- if (!next || localName(next.name) !== "p") return false;
24488
+ let at = idx + 1;
24489
+ while (at < parent.children.length && MARKERS.includes(localName(parent.children[at].name))) at++;
24490
+ const next = parent.children[at];
24491
+ if (!next || localName(next.name) !== "p") {
24492
+ const goes = gone === "del" ? ["pPr", "del", "moveFrom", "proofErr", ...MOVE_RANGES] : ["pPr", "ins", "moveTo", "proofErr", ...MOVE_RANGES];
24493
+ const stays = MARKERS.filter((m) => m !== "proofErr");
24494
+ const removable = (c) => goes.includes(localName(c.name)) || stays.includes(localName(c.name)) || isEmptyPlaceholder(c) || RUN_CONTAINERS.includes(localName(c.name)) && c.children.every(removable);
24495
+ if (!pEl.children.every(removable)) return false;
24496
+ parent.children.splice(idx, 1, ...pEl.children.filter((c) => stays.includes(localName(c.name))));
24497
+ dropEmptiedControl(doc, parent);
24498
+ return true;
24499
+ }
24500
+ const between = parent.children.slice(idx + 1, at);
24286
24501
  const moved = next.children.filter((c) => localName(c.name) !== "pPr");
24287
- pEl.children.push(...moved);
24288
- parent.children.splice(idx + 1, 1);
24502
+ pEl.children.push(...between, ...moved);
24503
+ parent.children.splice(idx + 1, at - idx);
24504
+ carrySectionBreak(pEl, next);
24289
24505
  return true;
24290
24506
  }
24507
+ var MARKERS = ["bookmarkStart", "bookmarkEnd", "commentRangeStart", "commentRangeEnd", "permStart", "permEnd", "proofErr"];
24508
+ function carrySectionBreak(pEl, next) {
24509
+ const pPrOf = (p) => p.children.find((c) => localName(c.name) === "pPr");
24510
+ const sectOf = (p) => pPrOf(p)?.children.find((c) => localName(c.name) === "sectPr");
24511
+ const mine = sectOf(pEl);
24512
+ const theirs = sectOf(next);
24513
+ if (!mine && !theirs) return;
24514
+ let pPr = pPrOf(pEl);
24515
+ if (!pPr) {
24516
+ pPr = { name: "w:pPr", attrs: {}, children: [], text: "" };
24517
+ pEl.children.unshift(pPr);
24518
+ }
24519
+ pPr.children = pPr.children.filter((c) => c !== mine);
24520
+ if (theirs) {
24521
+ const at = pPr.children.findIndex((c) => localName(c.name) === "pPrChange");
24522
+ pPr.children.splice(at === -1 ? pPr.children.length : at, 0, theirs);
24523
+ }
24524
+ if (pPr.children.length === 0) pEl.children.splice(pEl.children.indexOf(pPr), 1);
24525
+ }
24291
24526
  function collectRevisions(doc) {
24292
24527
  const out = [];
24293
24528
  const walk2 = (el26) => {
24294
24529
  const ln = localName(el26.name);
24295
24530
  if (ln === "pPr") return;
24296
- if (ln === "ins" || ln === "del") {
24531
+ if (ln === "ins" || ln === "del" || ln === "moveTo" || ln === "moveFrom") {
24297
24532
  out.push({
24298
24533
  el: el26,
24299
- kind: ln === "ins" ? "insertion" : "deletion",
24534
+ kind: ln === "ins" || ln === "moveTo" ? "insertion" : "deletion",
24300
24535
  author: attrByLocal(el26, "author")
24301
24536
  });
24302
24537
  }
@@ -24330,10 +24565,10 @@ function collectRevisions(doc) {
24330
24565
  const rPr = pPr?.children.find((c) => localName(c.name) === "rPr");
24331
24566
  for (const c of rPr?.children ?? []) {
24332
24567
  const cn = localName(c.name);
24333
- if (cn === "ins" || cn === "del") {
24568
+ if (cn === "ins" || cn === "del" || cn === "moveTo" || cn === "moveFrom") {
24334
24569
  out.push({
24335
24570
  el: c,
24336
- kind: cn === "ins" ? "markInsertion" : "markDeletion",
24571
+ kind: cn === "ins" || cn === "moveTo" ? "markInsertion" : "markDeletion",
24337
24572
  paragraph: el26,
24338
24573
  author: attrByLocal(c, "author")
24339
24574
  });
@@ -24355,7 +24590,7 @@ function applyAccept(doc, ref) {
24355
24590
  case "markDeletion":
24356
24591
  if (!ref.paragraph) return false;
24357
24592
  removeMarkRevision(ref);
24358
- return joinWithNext(doc, ref.paragraph);
24593
+ return joinWithNext(doc, ref.paragraph, "del");
24359
24594
  case "rowInsertion":
24360
24595
  return removeRowRevision(doc, ref);
24361
24596
  case "rowDeletion":
@@ -24426,7 +24661,7 @@ function applyReject(doc, ref) {
24426
24661
  case "markInsertion":
24427
24662
  if (!ref.paragraph) return false;
24428
24663
  removeMarkRevision(ref);
24429
- return joinWithNext(doc, ref.paragraph);
24664
+ return joinWithNext(doc, ref.paragraph, "ins");
24430
24665
  case "markDeletion":
24431
24666
  return removeMarkRevision(ref);
24432
24667
  case "rowInsertion":
@@ -24477,11 +24712,23 @@ function applyToAll(doc, apply, author) {
24477
24712
  for (const part of held) notes.add(part);
24478
24713
  }
24479
24714
  if (n > 0) {
24715
+ dropSettledMoveRanges(doc);
24480
24716
  doc.markNotePartsChanged([...notes]);
24481
24717
  doc.refresh();
24482
24718
  }
24483
24719
  return n;
24484
24720
  }
24721
+ var MOVE_RANGES = ["moveFromRangeStart", "moveFromRangeEnd", "moveToRangeStart", "moveToRangeEnd"];
24722
+ function dropSettledMoveRanges(doc) {
24723
+ const roots = doc.revisionRoots();
24724
+ const has = (el26) => ["moveFrom", "moveTo"].includes(localName(el26.name)) || el26.children.some(has);
24725
+ if (roots.some(has)) return;
24726
+ const strip = (el26) => {
24727
+ el26.children = el26.children.filter((c) => !MOVE_RANGES.includes(localName(c.name)));
24728
+ for (const c of el26.children) strip(c);
24729
+ };
24730
+ for (const root of roots) strip(root);
24731
+ }
24485
24732
  function removeMarkRevision(ref) {
24486
24733
  const pPr = ref.paragraph?.children.find((c) => localName(c.name) === "pPr");
24487
24734
  const rPr = pPr?.children.find((c) => localName(c.name) === "rPr");
@@ -24513,9 +24760,24 @@ function removeRow(doc, ref) {
24513
24760
  const idx = tbl.children.indexOf(ref.row);
24514
24761
  if (idx === -1) return false;
24515
24762
  tbl.children.splice(idx, 1);
24516
- if (!tbl.children.some((c) => localName(c.name) === "tr")) removeEl(doc, tbl);
24763
+ if (!tbl.children.some((c) => localName(c.name) === "tr")) {
24764
+ const holder = doc.findParentOf(tbl);
24765
+ removeEl(doc, tbl);
24766
+ if (holder) dropEmptiedControl(doc, holder);
24767
+ }
24517
24768
  return true;
24518
24769
  }
24770
+ function dropEmptiedControl(doc, container) {
24771
+ const ln = localName(container.name);
24772
+ const control = ln === "sdtContent" ? doc.findParentOf(container) : ln === "customXml" ? container : void 0;
24773
+ if (!control) return;
24774
+ const left = container.children.filter((c) => localName(c.name) !== "customXmlPr");
24775
+ if (left.some((c) => !MARKERS.includes(localName(c.name)))) return;
24776
+ const holder = doc.findParentOf(control);
24777
+ if (!holder) return;
24778
+ holder.children.splice(holder.children.indexOf(control), 1, ...left.filter((c) => localName(c.name) !== "proofErr"));
24779
+ dropEmptiedControl(doc, holder);
24780
+ }
24519
24781
 
24520
24782
  // ../core/src/edit/blocks.ts
24521
24783
  function el3(name, attrs = {}, children3 = [], text = "") {
@@ -24859,8 +25121,32 @@ function mergeParagraphBackward(doc, pEl) {
24859
25121
  const moved = pEl.children.filter((c) => localName(c.name) !== "pPr" && !isPlaceholderRun(c));
24860
25122
  prev.children.push(...moved);
24861
25123
  parent.children.splice(idx, 1);
25124
+ carryMarkRevision(prev, pEl);
24862
25125
  return finishMerge(prev);
24863
25126
  }
25127
+ function carryMarkRevision(survivor, gone) {
25128
+ const markOf = (p) => p.children.find((c) => localName(c.name) === "pPr")?.children.find((c) => localName(c.name) === "rPr");
25129
+ const isRev = (c) => localName(c.name) === "ins" || localName(c.name) === "del";
25130
+ const theirs = markOf(gone)?.children.filter(isRev) ?? [];
25131
+ let pPr = survivor.children.find((c) => localName(c.name) === "pPr");
25132
+ let rPr = markOf(survivor);
25133
+ if (!rPr && theirs.length === 0) return;
25134
+ if (!rPr) {
25135
+ const w = survivor.name.includes(":") ? survivor.name.slice(0, survivor.name.indexOf(":") + 1) : "";
25136
+ if (!pPr) {
25137
+ pPr = { name: `${w}pPr`, attrs: {}, children: [], text: "" };
25138
+ survivor.children.unshift(pPr);
25139
+ }
25140
+ rPr = { name: `${w}rPr`, attrs: {}, children: [], text: "" };
25141
+ const at = pPr.children.findIndex((c) => localName(c.name) === "sectPr" || localName(c.name) === "pPrChange");
25142
+ pPr.children.splice(at === -1 ? pPr.children.length : at, 0, rPr);
25143
+ }
25144
+ rPr.children = [...theirs, ...rPr.children.filter((c) => !isRev(c))];
25145
+ if (rPr.children.length === 0 && pPr) {
25146
+ pPr.children.splice(pPr.children.indexOf(rPr), 1);
25147
+ if (pPr.children.length === 0) survivor.children.splice(survivor.children.indexOf(pPr), 1);
25148
+ }
25149
+ }
24864
25150
  function firstTextOf(pEl) {
24865
25151
  const walk2 = (el26) => {
24866
25152
  for (const c of el26.children) {
@@ -27815,7 +28101,7 @@ function sortTableRows(doc, tblEl, colIdx, order, compare, hasHeader = false) {
27815
28101
  return tc ? cellText(tc).trim() : "";
27816
28102
  };
27817
28103
  const numeric2 = (s) => parseFloat(s.replace(/[^0-9.eE+-]/g, ""));
27818
- const compareRows = (a, b) => {
28104
+ const compareRows2 = (a, b) => {
27819
28105
  const ka = keyOf2(a);
27820
28106
  const kb = keyOf2(b);
27821
28107
  let d;
@@ -27831,7 +28117,7 @@ function sortTableRows(doc, tblEl, colIdx, order, compare, hasHeader = false) {
27831
28117
  }
27832
28118
  return order === "asc" ? d : -d;
27833
28119
  };
27834
- const sorted = [...body].sort(compareRows);
28120
+ const sorted = [...body].sort(compareRows2);
27835
28121
  if (sorted.every((tr, i) => tr === body[i])) return false;
27836
28122
  const slots = body.map((tr) => tblEl.children.indexOf(tr));
27837
28123
  sorted.forEach((tr, i) => {
@@ -41791,17 +42077,7 @@ var _DocxEditor = class _DocxEditor {
41791
42077
  if (pEl && firstTextOf(pEl) === caret.t) {
41792
42078
  this.host.history?.checkpoint();
41793
42079
  if (this.clearListAtCaret()) return;
41794
- const prev = siblingParagraph(this.host.doc, pEl, -1);
41795
- if (!prev) return;
41796
- const junction = lastTextOf(prev);
41797
- const mergeBlockId = this.host.onIntent && this.host.doc.stableIds ? this.host.doc.stableIds.idOf(pEl) : void 0;
41798
- if (mergeParagraphBackward(this.host.doc, pEl)) {
41799
- if (mergeBlockId !== void 0) this.host.onIntent?.({ kind: "mergeParagraph", blockId: mergeBlockId });
41800
- const target = junction && paragraphOf(this.host.doc, junction) ? junction : caret.t;
41801
- this.caret = { t: target, run: caret.run, offset: target === junction ? target.text.length : 0 };
41802
- invalidateParagraphSignature(paragraphOf(this.host.doc, target) ?? pEl);
41803
- this.commit(false, "local", true);
41804
- }
42080
+ this.mergeIntoPrevious(pEl);
41805
42081
  return;
41806
42082
  }
41807
42083
  if (!this.stepToNeighbor(-1)) return;
@@ -41817,19 +42093,8 @@ var _DocxEditor = class _DocxEditor {
41817
42093
  if (caret.offset >= caret.t.text.length) {
41818
42094
  const pEl = paragraphOf(this.host.doc, caret.t);
41819
42095
  if (pEl && lastTextOf(pEl) === caret.t) {
41820
- const next = siblingParagraph(this.host.doc, pEl, 1);
41821
- if (!next) return;
41822
42096
  this.host.history?.checkpoint();
41823
- const mergeNextId = this.host.onIntent && this.host.doc.stableIds ? this.host.doc.stableIds.idOf(next) : void 0;
41824
- if (mergeParagraphBackward(this.host.doc, next)) {
41825
- if (mergeNextId !== void 0) this.host.onIntent?.({ kind: "mergeParagraph", blockId: mergeNextId });
41826
- if (!paragraphOf(this.host.doc, caret.t)) {
41827
- const t = firstTextOf(next);
41828
- if (t) this.caret = { t, run: caret.run, offset: 0 };
41829
- }
41830
- invalidateParagraphSignature(paragraphOf(this.host.doc, this.caret?.t ?? caret.t) ?? pEl);
41831
- this.commit(false, "local", true);
41832
- }
42097
+ this.mergeNextInto(pEl);
41833
42098
  return;
41834
42099
  }
41835
42100
  if (!this.stepToNeighbor(1)) return;
@@ -41847,6 +42112,44 @@ var _DocxEditor = class _DocxEditor {
41847
42112
  emitDelete(blockId, runId, start, end) {
41848
42113
  if (end > start) this.host.onIntent?.({ kind: "deleteText", blockId, runId, start, end });
41849
42114
  }
42115
+ /** Backspace at the start of `pEl`: merge it into the previous paragraph. */
42116
+ mergeIntoPrevious(pEl) {
42117
+ const caret = this.caret;
42118
+ if (!caret) return;
42119
+ const prev = siblingParagraph(this.host.doc, pEl, -1);
42120
+ if (!prev) return;
42121
+ const junction = lastTextOf(prev);
42122
+ const mergeBlockId = this.host.onIntent && this.host.doc.stableIds ? this.host.doc.stableIds.idOf(pEl) : void 0;
42123
+ if (mergeParagraphBackward(this.host.doc, pEl)) {
42124
+ if (mergeBlockId !== void 0) this.host.onIntent?.({ kind: "mergeParagraph", blockId: mergeBlockId });
42125
+ const target = junction && paragraphOf(this.host.doc, junction) ? junction : caret.t;
42126
+ this.caret = { t: target, run: caret.run, offset: target === junction ? target.text.length : 0 };
42127
+ invalidateParagraphSignature(paragraphOf(this.host.doc, target) ?? pEl);
42128
+ this.commit(false, "local", true);
42129
+ }
42130
+ }
42131
+ /** Delete at the end of `pEl`: merge the next paragraph into it. */
42132
+ mergeNextInto(pEl) {
42133
+ const caret = this.caret;
42134
+ if (!caret) return;
42135
+ const next = siblingParagraph(this.host.doc, pEl, 1);
42136
+ if (!next) return;
42137
+ const mergeNextId = this.host.onIntent && this.host.doc.stableIds ? this.host.doc.stableIds.idOf(next) : void 0;
42138
+ if (mergeParagraphBackward(this.host.doc, next)) {
42139
+ if (mergeNextId !== void 0) this.host.onIntent?.({ kind: "mergeParagraph", blockId: mergeNextId });
42140
+ if (!paragraphOf(this.host.doc, caret.t)) {
42141
+ const t = firstTextOf(next);
42142
+ if (t) this.caret = { t, run: caret.run, offset: 0 };
42143
+ }
42144
+ invalidateParagraphSignature(paragraphOf(this.host.doc, this.caret?.t ?? caret.t) ?? pEl);
42145
+ this.commit(false, "local", true);
42146
+ }
42147
+ }
42148
+ /** True when `pEl`'s paragraph mark is a break this author suggested. */
42149
+ ownSuggestedBreak(pEl) {
42150
+ const mark = paragraphGlyphRevision(pEl, "ins");
42151
+ return !!mark && attr(mark, "author") === this.revisionAuthor;
42152
+ }
41850
42153
  /** Backspace/Delete in suggesting mode: mark one character (or a paragraph
41851
42154
  * mark at a boundary) as a tracked deletion instead of removing it. Skips over
41852
42155
  * text already struck by this author so a repeated press keeps eating real
@@ -41867,6 +42170,10 @@ var _DocxEditor = class _DocxEditor {
41867
42170
  if (this.suggestUnbulletAtCaret()) return;
41868
42171
  const prev = siblingParagraph(doc, pEl, -1);
41869
42172
  if (!prev) return;
42173
+ if (this.ownSuggestedBreak(prev)) {
42174
+ this.mergeIntoPrevious(pEl);
42175
+ return;
42176
+ }
41870
42177
  if (paragraphGlyphRevision(prev, "del")) {
41871
42178
  this.stepToNeighbor(-1);
41872
42179
  this.positionCaret();
@@ -41896,6 +42203,10 @@ var _DocxEditor = class _DocxEditor {
41896
42203
  if (caret.offset >= caret.t.text.length) {
41897
42204
  const pEl = paragraphOf(doc, caret.t);
41898
42205
  if (pEl && lastTextOf(pEl) === caret.t) {
42206
+ if (this.ownSuggestedBreak(pEl)) {
42207
+ this.mergeNextInto(pEl);
42208
+ return;
42209
+ }
41899
42210
  if (paragraphGlyphRevision(pEl, "del")) {
41900
42211
  this.stepToNeighbor(1);
41901
42212
  this.positionCaret();
@@ -43010,6 +43321,15 @@ var COMPARE_TUNING = {
43010
43321
  shortParagraphTokens: 5,
43011
43322
  /** Levenshtein ratio a short paragraph pair must reach instead of minDice. */
43012
43323
  minShortRatio: 0.8,
43324
+ /**
43325
+ * Or the share of words a short pair has in common (Dice over words), which
43326
+ * a one-word edit in a short heading or list item keeps at half or more
43327
+ * ("Bullet one" / "Bullet updated", "Alignment and flow" / "Alignment and
43328
+ * flow as amended") while its character ratio falls under minShortRatio.
43329
+ * Below it, those came out struck and reinserted whole: 99% of the compare
43330
+ * benchmark's whole-paragraph misses were paragraphs under five words.
43331
+ */
43332
+ minShortWordDice: 0.5,
43013
43333
  /**
43014
43334
  * ABSOLUTE floor for the coalescing pass: a matched run shorter than this,
43015
43335
  * sitting between two changes in the inner word diff, is absorbed into the
@@ -43028,15 +43348,19 @@ var COMPARE_TUNING = {
43028
43348
  * next to 1) while a rewritten clause becomes one (3 is small next to 7).
43029
43349
  *
43030
43350
  * Both numbers were settled against test/compare-corpus.test.ts; see the
43031
- * calibration note there for what each fixture pins down.
43351
+ * calibration note there for what each fixture pins down. The ratio was 2
43352
+ * while spaces rode on words: "hardware," and "hardware " differed, so the
43353
+ * rewritten sentence of fixture 3 shared only three words. Matched exactly,
43354
+ * it shares four ("shortage of test hardware") beside seven changed, which
43355
+ * 1.5 absorbs (6 < 7) and 2 does not.
43032
43356
  */
43033
- coalesceRatio: 2
43357
+ coalesceRatio: 1.5
43034
43358
  };
43035
43359
  function normalizeText(s) {
43036
43360
  return s.replace(/\s+/g, " ").trim();
43037
43361
  }
43038
43362
  function wordTokens(s) {
43039
- return s.match(/\S+\s*|\s+/g) ?? [];
43363
+ return s.match(/[\p{L}\p{M}\p{N}]+(?:['\u2019][\p{L}\p{M}\p{N}]+)*|[^\s\p{L}\p{M}\p{N}]|\s+/gu) ?? [];
43040
43364
  }
43041
43365
  function similarityTokens(s) {
43042
43366
  const n = normalizeText(s).toLowerCase();
@@ -43082,14 +43406,16 @@ function similarity(a, b) {
43082
43406
  const nb = normalizeText(b).toLowerCase();
43083
43407
  const longest = Math.max(na.length, nb.length);
43084
43408
  if (longest === 0) return 1;
43085
- return 1 - levenshtein(na, nb) / longest;
43409
+ const ratio = 1 - levenshtein(na, nb) / longest;
43410
+ const words2 = dice(new Set(ta), new Set(tb));
43411
+ return Math.max(words2, ratio >= COMPARE_TUNING.minShortRatio ? ratio : 0);
43086
43412
  }
43087
43413
  return dice(bigrams(ta), bigrams(tb));
43088
43414
  }
43089
43415
  function similarityFloor(a, b) {
43090
43416
  const la = similarityTokens(a).length;
43091
43417
  const lb = similarityTokens(b).length;
43092
- return isShortPair(la, lb) ? COMPARE_TUNING.minShortRatio : COMPARE_TUNING.minDice;
43418
+ return isShortPair(la, lb) ? COMPARE_TUNING.minShortWordDice : COMPARE_TUNING.minDice;
43093
43419
  }
43094
43420
  function isShortPair(la, lb) {
43095
43421
  return Math.min(la, lb) < COMPARE_TUNING.shortParagraphTokens;
@@ -43100,6 +43426,25 @@ var PASSENGERS = /* @__PURE__ */ new Set(["proofErr", "bookmarkStart", "bookmark
43100
43426
  function isPassenger(ln) {
43101
43427
  return PASSENGERS.has(ln);
43102
43428
  }
43429
+ var WORD_CHAR = /[\p{L}\p{M}\p{N}]/u;
43430
+ var CONTAINERS = /* @__PURE__ */ new Set(["hyperlink", "smartTag", "fldSimple", "customXml", "dir", "bdo", "sdt"]);
43431
+ var CONTAINER_PROPS = /* @__PURE__ */ new Set(["pPr", "sdtPr", "sdtEndPr", "smartTagPr", "customXmlPr"]);
43432
+ function atomKey(atom, noteKey) {
43433
+ const ln = localName(atom.name);
43434
+ if (ln === "footnoteReference" || ln === "endnoteReference") {
43435
+ const { "w:id": _id, ...rest } = atom.attrs;
43436
+ return serializeXml({ ...atom, attrs: rest }) + (noteKey ? "\0" + noteKey(atom) : "");
43437
+ }
43438
+ const strip = (e) => localName(e.name) === "txbxContent" ? { ...e, children: [], text: "" } : { ...e, children: e.children.map(strip) };
43439
+ return serializeXml(hasTextBox(atom) ? strip(atom) : atom);
43440
+ }
43441
+ function hasTextBox(el26) {
43442
+ return localName(el26.name) === "txbxContent" || el26.children.some(hasTextBox);
43443
+ }
43444
+ function textBoxesIn(el26) {
43445
+ if (localName(el26.name) === "txbxContent") return [el26];
43446
+ return el26.children.flatMap(textBoxesIn);
43447
+ }
43103
43448
  function readParagraph(pEl, options = {}) {
43104
43449
  const keyOf2 = options.whitespace === false ? (word) => word.trimEnd() : (word) => word;
43105
43450
  const tokens = [];
@@ -43112,31 +43457,80 @@ function readParagraph(pEl, options = {}) {
43112
43457
  }
43113
43458
  tokens.push(token);
43114
43459
  };
43115
- for (const cEl of pEl.children) {
43116
- const ln = localName(cEl.name);
43117
- if (ln === "pPr") continue;
43118
- if (PASSENGERS.has(ln)) {
43119
- pending.push(cEl);
43120
- continue;
43121
- }
43122
- if (ln !== "r") {
43123
- inlineDiffable = false;
43124
- continue;
43125
- }
43126
- for (const rc of cEl.children) {
43127
- const rln = localName(rc.name);
43128
- if (rln === "rPr") continue;
43129
- if (rln === "t") {
43130
- let at = 0;
43131
- for (const word of wordTokens(rc.text)) {
43132
- push({ key: "w\0" + keyOf2(word), text: word, run: cEl, t: rc, start: at, end: at + word.length });
43133
- at += word.length;
43134
- }
43460
+ let segment = [];
43461
+ let segmentPath = [];
43462
+ const flush = () => {
43463
+ if (segment.length === 0) return;
43464
+ const text = segment.map((p) => p.text).join("");
43465
+ const cuts = [];
43466
+ let base = 0;
43467
+ for (let i = 0; i + 1 < segment.length; i++) {
43468
+ base += segment[i].text.length;
43469
+ if (WORD_CHAR.test(text[base - 1] ?? "") && WORD_CHAR.test(text[base] ?? "") && runPropsKey(segment[i].run) !== runPropsKey(segment[i + 1].run)) {
43470
+ cuts.push(base);
43471
+ }
43472
+ }
43473
+ const words2 = [];
43474
+ let from = 0;
43475
+ for (const cut of [...cuts, text.length]) {
43476
+ words2.push(...wordTokens(text.slice(from, cut)));
43477
+ from = cut;
43478
+ }
43479
+ let at = 0;
43480
+ for (const word of words2) {
43481
+ const end = at + word.length;
43482
+ const pieces = [];
43483
+ let base2 = 0;
43484
+ for (const p of segment) {
43485
+ const from2 = Math.max(at, base2);
43486
+ const to = Math.min(end, base2 + p.text.length);
43487
+ if (from2 < to) pieces.push({ run: p.run, t: p.t, start: from2 - base2, end: to - base2, text: text.slice(from2, to) });
43488
+ base2 += p.text.length;
43489
+ }
43490
+ const first = pieces[0];
43491
+ push({ key: "w\0" + keyOf2(word), text: word, run: first.run, t: first.t, start: first.start, end: first.end, pieces, path: segmentPath });
43492
+ at = end;
43493
+ }
43494
+ segment = [];
43495
+ };
43496
+ const walk2 = (kids, path2) => {
43497
+ for (const cEl of kids) {
43498
+ const ln = localName(cEl.name);
43499
+ if (CONTAINER_PROPS.has(ln)) continue;
43500
+ if (PASSENGERS.has(ln)) {
43501
+ flush();
43502
+ pending.push(cEl);
43503
+ continue;
43504
+ }
43505
+ if (CONTAINERS.has(ln)) {
43506
+ const inner = ln === "sdt" ? cEl.children.find((c) => localName(c.name) === "sdtContent") : cEl;
43507
+ if (!inner) continue;
43508
+ flush();
43509
+ walk2(inner.children, [...path2, cEl]);
43510
+ flush();
43135
43511
  continue;
43136
43512
  }
43137
- push({ key: "a\0" + serializeXml(rc), text: "", run: cEl, atom: rc });
43513
+ if (ln !== "r") {
43514
+ flush();
43515
+ inlineDiffable = false;
43516
+ continue;
43517
+ }
43518
+ if (segment.length > 0 && segmentPath !== path2) flush();
43519
+ segmentPath = path2;
43520
+ for (const rc of cEl.children) {
43521
+ const rln = localName(rc.name);
43522
+ if (rln === "rPr") continue;
43523
+ if (rln === "t") {
43524
+ segment.push({ run: cEl, t: rc, text: rc.text });
43525
+ continue;
43526
+ }
43527
+ flush();
43528
+ push({ key: "a\0" + atomKey(rc, options.noteKey), text: "", run: cEl, atom: rc, path: path2 });
43529
+ }
43138
43530
  }
43139
- }
43531
+ };
43532
+ walk2(pEl.children, []);
43533
+ flush();
43140
43534
  return { tokens, tail: pending, text: paragraphText3(pEl), inlineDiffable };
43141
43535
  }
43142
43536
  function paragraphText3(el26) {
@@ -43153,31 +43547,41 @@ function paragraphText3(el26) {
43153
43547
  }
43154
43548
  return out;
43155
43549
  }
43156
- function sliceContent(tokens, from, to, deleted) {
43550
+ function sliceItems(tokens, from, to, deleted) {
43157
43551
  const out = [];
43158
- let i = from;
43159
- while (i < to) {
43160
- const token = tokens[i];
43161
- if (!deleted && token.lead) out.push(...token.lead.map(cloneXml));
43162
- const run = token.run;
43552
+ let open = null;
43553
+ let text = null;
43554
+ const runFor = (run, path2) => {
43555
+ if (open?.run === run) return open.el;
43163
43556
  const rPr = run.children.find((c) => localName(c.name) === "rPr");
43164
- const children3 = rPr ? [cloneXml(rPr)] : [];
43165
- while (i < to && tokens[i].run === run) {
43166
- const cur = tokens[i];
43167
- if (cur.atom) {
43168
- children3.push(cloneXml(cur.atom));
43169
- i++;
43557
+ const el26 = { name: run.name, attrs: { ...run.attrs }, children: rPr ? [cloneXml(rPr)] : [], text: "" };
43558
+ out.push({ el: el26, path: path2 });
43559
+ open = { run, el: el26 };
43560
+ text = null;
43561
+ return el26;
43562
+ };
43563
+ for (let i = from; i < to; i++) {
43564
+ const token = tokens[i];
43565
+ if (!deleted && token.lead) {
43566
+ out.push(...token.lead.map((el26) => ({ el: cloneXml(el26), path: token.path })));
43567
+ open = null;
43568
+ text = null;
43569
+ }
43570
+ if (token.atom) {
43571
+ runFor(token.run, token.path).children.push(cloneXml(token.atom));
43572
+ text = null;
43573
+ continue;
43574
+ }
43575
+ for (const piece of token.pieces ?? []) {
43576
+ const el26 = runFor(piece.run, token.path);
43577
+ if (text && text.t === piece.t) {
43578
+ text.el.text += piece.text;
43170
43579
  continue;
43171
43580
  }
43172
- const t = cur.t;
43173
- let text = "";
43174
- while (i < to && tokens[i].run === run && tokens[i].t === t) {
43175
- text += tokens[i].text;
43176
- i++;
43177
- }
43178
- children3.push(textElement2(t, text, deleted));
43581
+ const te = textElement2(piece.t, piece.text, deleted);
43582
+ el26.children.push(te);
43583
+ text = { t: piece.t, el: te };
43179
43584
  }
43180
- out.push({ name: run.name, attrs: { ...run.attrs }, children: children3, text: "" });
43181
43585
  }
43182
43586
  return out;
43183
43587
  }
@@ -43284,46 +43688,232 @@ function lcsFallback(a, b, aLo, aHi, bLo, bHi, out) {
43284
43688
 
43285
43689
  // ../core/src/edit/compare/index.ts
43286
43690
  function compareDocuments(original, revised, options) {
43287
- const merged = DocxDocument.load(revised.save());
43691
+ const note = options.onNote ?? (() => {
43692
+ });
43693
+ const accepted = (doc, which) => {
43694
+ if (collectRevisions(doc).length === 0) return doc;
43695
+ const copy = doc.clone();
43696
+ acceptAllRevisions(copy);
43697
+ note({ code: "existing-revisions-accepted", detail: `the ${which} document held tracked changes; it was compared as if they were accepted` });
43698
+ return copy;
43699
+ };
43700
+ const merged = accepted(revised, "revised");
43701
+ return compareInto(accepted(original, "original"), merged === revised ? revised.clone() : merged, options);
43702
+ }
43703
+ function compareInto(original, merged, options) {
43704
+ let issued = 0;
43288
43705
  const ctx = {
43289
43706
  doc: merged,
43290
43707
  original,
43291
43708
  meta: {
43292
43709
  author: options.author,
43293
43710
  date: options.date,
43294
- nextId: () => merged.nextRevisionId()
43711
+ nextId: () => {
43712
+ issued++;
43713
+ return merged.nextRevisionId();
43714
+ }
43295
43715
  },
43296
43716
  formatting: options.formatting !== false,
43297
43717
  whitespace: options.whitespace !== false,
43298
43718
  note: options.onNote ?? (() => {
43299
- })
43719
+ }),
43720
+ scope: { from: null, into: null },
43721
+ noteIds: { footnote: /* @__PURE__ */ new Map(), endnote: /* @__PURE__ */ new Map() }
43300
43722
  };
43301
43723
  const originalBody = child(original.docRoot, "body");
43302
43724
  const mergedBody = child(merged.docRoot, "body");
43303
43725
  if (!originalBody || !mergedBody) throw new Error("document.xml has no w:body");
43304
- reportUncomparedParts(original, revised, ctx);
43726
+ reportUncomparedParts(original, merged, ctx);
43727
+ const headerFooterPairs = pairHeadersFooters(original, merged);
43728
+ compareNotes("footnote", original, merged, ctx);
43729
+ compareNotes("endnote", original, merged, ctx);
43305
43730
  const tail = mergedBody.children.filter((c) => localName(c.name) === "sectPr");
43306
43731
  const entries = alignBlocks(blockList(originalBody, "sectPr"), blockList(mergedBody, "sectPr"), ctx);
43307
43732
  applyParagraphMarks(entries, ctx);
43308
43733
  mergedBody.children = [...entries.map((e) => e.el), ...tail];
43309
- merged.refresh();
43734
+ giveStruckReferencesTheirOwnNotes("footnote", merged, ctx);
43735
+ giveStruckReferencesTheirOwnNotes("endnote", merged, ctx);
43736
+ for (const [from, into] of headerFooterPairs) {
43737
+ const oRoot = original.headerFooterRoot(from);
43738
+ const mRoot = merged.headerFooterRoot(into);
43739
+ if (oRoot && mRoot) compareContainer(oRoot, mRoot, { ...ctx, scope: { from, into } });
43740
+ }
43741
+ if (issued > 0) merged.refresh();
43310
43742
  return merged;
43311
43743
  }
43744
+ function compareContainer(oRoot, mRoot, ctx) {
43745
+ const o = blockList(oRoot);
43746
+ const m = blockList(mRoot);
43747
+ if (o.length === m.length && o.every((el26, i) => sameXml(el26, m[i]))) return;
43748
+ const entries = alignBlocks(o, m, ctx);
43749
+ applyParagraphMarks(entries, ctx);
43750
+ const blocks = new Set(m);
43751
+ mRoot.children = [...mRoot.children.filter((c) => !blocks.has(c) && !["p", "tbl", "sdt"].includes(localName(c.name))), ...entries.map((e) => e.el)];
43752
+ }
43753
+ function pairHeadersFooters(original, merged) {
43754
+ const refs = (doc) => {
43755
+ const out = /* @__PURE__ */ new Map();
43756
+ const body = child(doc.docRoot, "body");
43757
+ const sects = (body?.children ?? []).flatMap(
43758
+ (c) => localName(c.name) === "sectPr" ? [c] : localName(c.name) === "p" ? [child(child(c, "pPr"), "sectPr")].filter((x) => !!x) : []
43759
+ );
43760
+ sects.forEach((sp, i) => {
43761
+ for (const ref of sp.children) {
43762
+ const ln = localName(ref.name);
43763
+ if (ln !== "headerReference" && ln !== "footerReference") continue;
43764
+ const target = doc.documentRels.get(ref.attrs["r:id"])?.target;
43765
+ if (target) out.set(`${i}:${ln}:${ref.attrs["w:type"] ?? "default"}`, target);
43766
+ }
43767
+ });
43768
+ return out;
43769
+ };
43770
+ const mine = refs(merged);
43771
+ const pairs = [];
43772
+ const done = /* @__PURE__ */ new Set();
43773
+ for (const [key, from] of refs(original)) {
43774
+ const into = mine.get(key);
43775
+ if (!into || done.has(into)) continue;
43776
+ done.add(into);
43777
+ pairs.push([from, into]);
43778
+ }
43779
+ return pairs;
43780
+ }
43781
+ function compareNotes(kind, original, merged, ctx) {
43782
+ const tree = (doc, create = false) => kind === "footnote" ? doc.footnotesTree(create) : doc.endnotesTree(create);
43783
+ const oRoot = tree(original);
43784
+ const mRoot = tree(merged);
43785
+ if (!oRoot && !mRoot) return;
43786
+ const refName = `${kind}Reference`;
43787
+ const inOrder = (doc, root) => {
43788
+ if (!root) return [];
43789
+ const byId = new Map(
43790
+ root.children.filter((c) => localName(c.name) === kind && !c.attrs["w:type"]).map((c) => [c.attrs["w:id"], c])
43791
+ );
43792
+ const out = [];
43793
+ const seen = /* @__PURE__ */ new Set();
43794
+ const walk2 = (e) => {
43795
+ if (localName(e.name) === refName) {
43796
+ const id = e.attrs["w:id"];
43797
+ const note = byId.get(id);
43798
+ if (note && !seen.has(id)) {
43799
+ seen.add(id);
43800
+ out.push(note);
43801
+ }
43802
+ }
43803
+ for (const c of e.children) walk2(c);
43804
+ };
43805
+ walk2(doc.docRoot);
43806
+ return out;
43807
+ };
43808
+ const oNotes = inOrder(original, oRoot);
43809
+ const rNotes = inOrder(merged, mRoot);
43810
+ const text = (n) => normalizeText(paragraphText3(n));
43811
+ const pairs = [];
43812
+ const waiting = /* @__PURE__ */ new Map();
43813
+ rNotes.forEach((n, j) => {
43814
+ const list = waiting.get(text(n));
43815
+ if (list) list.push(j);
43816
+ else waiting.set(text(n), [j]);
43817
+ });
43818
+ oNotes.forEach((n, i) => {
43819
+ const j = waiting.get(text(n))?.shift();
43820
+ if (j !== void 0) pairs.push([i, j]);
43821
+ });
43822
+ const exactO = new Set(pairs.map((p) => p[0]));
43823
+ const exactR = new Set(pairs.map((p) => p[1]));
43824
+ const oRest = oNotes.map((_, i) => i).filter((i) => !exactO.has(i));
43825
+ const rRest = rNotes.map((_, j) => j).filter((j) => !exactR.has(j));
43826
+ let oAt = 0;
43827
+ let rAt = 0;
43828
+ const rest = histogramMatch(oRest.map((i) => text(oNotes[i])), rRest.map((j) => text(rNotes[j])));
43829
+ for (const a of [...rest, { a: oRest.length, b: rRest.length }]) {
43830
+ if (a.a - oAt === 1 && a.b - rAt === 1) pairs.push([oRest[oAt], rRest[rAt]]);
43831
+ if (a.a < oRest.length && a.b < rRest.length) pairs.push([oRest[a.a], rRest[a.b]]);
43832
+ oAt = a.a + 1;
43833
+ rAt = a.b + 1;
43834
+ }
43835
+ const ids = ctx.noteIds[kind];
43836
+ const pairedO = new Set(pairs.map((p) => p[0]));
43837
+ const pairedR = new Set(pairs.map((p) => p[1]));
43838
+ let changed = false;
43839
+ for (const [i, j] of pairs) {
43840
+ ids.set(oNotes[i].attrs["w:id"], rNotes[j].attrs["w:id"]);
43841
+ const before = ctx.meta.nextId;
43842
+ let touched = false;
43843
+ compareContainer(oNotes[i], rNotes[j], { ...ctx, meta: { ...ctx.meta, nextId: () => (touched = true, before()) } });
43844
+ changed || (changed = touched);
43845
+ }
43846
+ rNotes.forEach((note, j) => {
43847
+ if (pairedR.has(j)) return;
43848
+ for (const block of blockList(note)) if (localName(block.name) === "p") wrapContainerContent(block, "ins", ctx);
43849
+ changed = true;
43850
+ });
43851
+ const removed = oNotes.filter((_, i) => !pairedO.has(i));
43852
+ if (removed.length > 0) {
43853
+ const root = tree(merged, true);
43854
+ if (root) {
43855
+ let next = 1 + Math.max(0, ...root.children.map((c) => Number(c.attrs["w:id"]) || 0));
43856
+ for (const o of removed) {
43857
+ const copy = cloneXml(o);
43858
+ copy.attrs["w:id"] = String(next);
43859
+ ids.set(o.attrs["w:id"], String(next++));
43860
+ for (const block of blockList(copy)) {
43861
+ if (localName(block.name) !== "p") continue;
43862
+ adoptFromOriginal(block, { ...ctx, scope: { from: null, into: null } });
43863
+ wrapContainerContent(block, "del", ctx);
43864
+ }
43865
+ root.children.push(copy);
43866
+ }
43867
+ changed = true;
43868
+ }
43869
+ }
43870
+ if (changed) merged.markNotePartsChanged([kind === "footnote" ? "footnotes" : "endnotes"]);
43871
+ }
43872
+ function giveStruckReferencesTheirOwnNotes(kind, merged, ctx) {
43873
+ const root = kind === "footnote" ? merged.footnotesTree() : merged.endnotesTree();
43874
+ if (!root) return;
43875
+ const refName = `${kind}Reference`;
43876
+ const refs = /* @__PURE__ */ new Map();
43877
+ const walk2 = (e, struck) => {
43878
+ if (localName(e.name) === refName) {
43879
+ const list = refs.get(e.attrs["w:id"]) ?? [];
43880
+ list.push({ ref: e, struck });
43881
+ refs.set(e.attrs["w:id"], list);
43882
+ }
43883
+ for (const c of e.children) walk2(c, struck || localName(c.name) === "del" || rowDeleted(c));
43884
+ };
43885
+ const rowDeleted = (tr) => localName(tr.name) === "tr" && !!child(child(tr, "trPr"), "del");
43886
+ walk2(merged.docRoot, false);
43887
+ let next = 1 + Math.max(0, ...root.children.map((c) => Number(c.attrs["w:id"]) || 0));
43888
+ let changed = false;
43889
+ for (const [id, list] of refs) {
43890
+ if (list.length < 2) continue;
43891
+ const note = root.children.find((c) => localName(c.name) === kind && c.attrs["w:id"] === id);
43892
+ if (!note) continue;
43893
+ const keep = list.find((r) => !r.struck) ?? list[0];
43894
+ for (const r of list) {
43895
+ if (r === keep || !r.struck) continue;
43896
+ const copy = cloneXml(note);
43897
+ copy.attrs["w:id"] = String(next);
43898
+ r.ref.attrs["w:id"] = String(next++);
43899
+ for (const block of blockList(copy)) if (localName(block.name) === "p") wrapContainerContent(block, "del", ctx);
43900
+ root.children.push(copy);
43901
+ changed = true;
43902
+ }
43903
+ }
43904
+ if (changed) merged.markNotePartsChanged([kind === "footnote" ? "footnotes" : "endnotes"]);
43905
+ }
43312
43906
  function blockList(container, ...skip) {
43313
43907
  return container.children.filter((c) => !skip.includes(localName(c.name)));
43314
43908
  }
43315
43909
  function reportUncomparedParts(original, revised, ctx) {
43316
43910
  const parts = [
43317
43911
  ["styles.xml", original.stylesTree()],
43318
- ["numbering.xml", original.numberingTree()],
43319
- ["footnotes.xml", original.footnotesTree()],
43320
- ["endnotes.xml", original.endnotesTree()]
43912
+ ["numbering.xml", original.numberingTree()]
43321
43913
  ];
43322
43914
  const revisedTrees = {
43323
43915
  "styles.xml": revised.stylesTree(),
43324
- "numbering.xml": revised.numberingTree(),
43325
- "footnotes.xml": revised.footnotesTree(),
43326
- "endnotes.xml": revised.endnotesTree()
43916
+ "numbering.xml": revised.numberingTree()
43327
43917
  };
43328
43918
  for (const [name, tree] of parts) {
43329
43919
  const other = revisedTrees[name];
@@ -43333,13 +43923,12 @@ function reportUncomparedParts(original, revised, ctx) {
43333
43923
  ctx.note({ code: "part-not-compared", detail: `${name} differs; the result carries the revised document's` });
43334
43924
  }
43335
43925
  }
43336
- const hf = (doc) => [...doc.headerRoots(), ...doc.footerRoots()].map((root) => serializeXml(root)).join("");
43337
- if (hf(original) !== hf(revised)) {
43338
- ctx.note({ code: "part-not-compared", detail: "headers/footers differ; the result carries the revised document's" });
43339
- }
43340
43926
  }
43341
43927
  function alignBlocks(oBlocks, rBlocks, ctx) {
43342
- const anchors = histogramMatch(oBlocks.map(blockFingerprint), rBlocks.map(blockFingerprint));
43928
+ const anchors = histogramMatch(
43929
+ oBlocks.map((el26) => blockFingerprint(el26, ctx.original)),
43930
+ rBlocks.map((el26) => blockFingerprint(el26, ctx.doc))
43931
+ );
43343
43932
  const entries = [];
43344
43933
  let oAt = 0;
43345
43934
  let rAt = 0;
@@ -43353,19 +43942,23 @@ function alignBlocks(oBlocks, rBlocks, ctx) {
43353
43942
  }
43354
43943
  return entries;
43355
43944
  }
43356
- function blockFingerprint(el26) {
43945
+ function blockFingerprint(el26, doc) {
43357
43946
  const ln = localName(el26.name);
43358
43947
  if (ln === "p") {
43359
43948
  const pPr = child(el26, "pPr");
43360
43949
  const style = child(pPr, "pStyle")?.attrs["w:val"] ?? "";
43361
43950
  const numPr = child(pPr, "numPr");
43362
- return `P\0${normalizeText(paragraphText3(el26))}\0${style}\0${numPr ? serializeXml(numPr) : ""}`;
43951
+ const jc = child(pPr, "jc")?.attrs["w:val"] ?? "";
43952
+ const sect = sectionKey(el26, doc);
43953
+ return `P\0${normalizeText(paragraphText3(el26))}\0${style}\0${numPr ? serializeXml(numPr) : ""}\0${jc}\0${sect}`;
43363
43954
  }
43364
43955
  if (ln === "tbl") return `T\0${normalizeText(paragraphText3(el26))}`;
43365
43956
  return `X\0${serializeXml(el26)}`;
43366
43957
  }
43367
43958
  function emitGap(oBlocks, rBlocks, oLo, oHi, rLo, rHi, entries, ctx) {
43368
- const pairings = resolveGap(oBlocks, rBlocks, oLo, oHi, rLo, rHi, ctx, oLo === 0 && rLo === 0);
43959
+ const before = entries[entries.length - 1];
43960
+ const unanchored = (!before || !isParagraph(before.el)) && (rHi >= rBlocks.length || !isParagraph(rBlocks[rHi]));
43961
+ const pairings = resolveGap(oBlocks, rBlocks, oLo, oHi, rLo, rHi, ctx, oLo === 0 && rLo === 0 || unanchored);
43369
43962
  let oAt = oLo;
43370
43963
  let rAt = rLo;
43371
43964
  for (const pairing of [...pairings, { kind: "pair", o: oHi, r: rHi, other: -1 }]) {
@@ -43397,16 +43990,43 @@ function emitGap(oBlocks, rBlocks, oLo, oHi, rLo, rHi, entries, ctx) {
43397
43990
  }
43398
43991
  }
43399
43992
  }
43993
+ var GAP_PAIR_CAP = 200 * 200;
43994
+ var GAP_WINDOW = 50;
43400
43995
  function resolveGap(oBlocks, rBlocks, oLo, oHi, rLo, rHi, ctx, needsAnchor = false) {
43401
43996
  const out = [];
43402
43997
  const oUsed = /* @__PURE__ */ new Set();
43403
43998
  const rUsed = /* @__PURE__ */ new Set();
43404
- const crosses = (o, r) => out.some((p) => o < p.o !== r < p.r);
43999
+ const placed = [];
44000
+ const crosses = (o, r) => {
44001
+ let lo = 0;
44002
+ let hi = placed.length;
44003
+ while (lo < hi) {
44004
+ const mid = lo + hi >> 1;
44005
+ if (placed[mid].o < o) lo = mid + 1;
44006
+ else hi = mid;
44007
+ }
44008
+ return lo > 0 && placed[lo - 1].r >= r || lo < placed.length && placed[lo].r <= r;
44009
+ };
44010
+ const accept = (p) => {
44011
+ out.push(p);
44012
+ let at = placed.length;
44013
+ while (at > 0 && placed[at - 1].o > p.o) at--;
44014
+ placed.splice(at, 0, p);
44015
+ };
44016
+ const oLen = oHi - oLo;
44017
+ const rLen = rHi - rLo;
44018
+ const windowed = oLen * rLen > GAP_PAIR_CAP;
44019
+ const span = (i) => {
44020
+ if (!windowed) return [rLo, rHi];
44021
+ const centre = rLo + Math.round((i - oLo) * rLen / Math.max(1, oLen));
44022
+ return [Math.max(rLo, centre - GAP_WINDOW), Math.min(rHi, centre + GAP_WINDOW + 1)];
44023
+ };
43405
44024
  for (let i = oLo; i < oHi; i++) {
43406
- for (let j = rLo; j + 1 < rHi; j++) {
44025
+ const [jLo, jHi] = span(i);
44026
+ for (let j = jLo; j + 1 < jHi; j++) {
43407
44027
  if (oUsed.has(i) || rUsed.has(j) || rUsed.has(j + 1) || crosses(i, j)) continue;
43408
44028
  if (concatMatches([oBlocks[i]], [rBlocks[j], rBlocks[j + 1]], ctx)) {
43409
- out.push({ kind: "split", o: i, r: j, other: j + 1 });
44029
+ accept({ kind: "split", o: i, r: j, other: j + 1 });
43410
44030
  oUsed.add(i);
43411
44031
  rUsed.add(j);
43412
44032
  rUsed.add(j + 1);
@@ -43414,10 +44034,11 @@ function resolveGap(oBlocks, rBlocks, oLo, oHi, rLo, rHi, ctx, needsAnchor = fal
43414
44034
  }
43415
44035
  }
43416
44036
  for (let i = oLo; i + 1 < oHi; i++) {
43417
- for (let j = rLo; j < rHi; j++) {
44037
+ const [jLo, jHi] = span(i);
44038
+ for (let j = jLo; j < jHi; j++) {
43418
44039
  if (oUsed.has(i) || oUsed.has(i + 1) || rUsed.has(j) || crosses(i, j)) continue;
43419
44040
  if (concatMatches([oBlocks[i], oBlocks[i + 1]], [rBlocks[j]], ctx)) {
43420
- out.push({ kind: "merge", o: i, r: j, other: i + 1 });
44041
+ accept({ kind: "merge", o: i, r: j, other: i + 1 });
43421
44042
  oUsed.add(i);
43422
44043
  oUsed.add(i + 1);
43423
44044
  rUsed.add(j);
@@ -43427,9 +44048,10 @@ function resolveGap(oBlocks, rBlocks, oLo, oHi, rLo, rHi, ctx, needsAnchor = fal
43427
44048
  const scored = [];
43428
44049
  for (let i = oLo; i < oHi; i++) {
43429
44050
  if (oUsed.has(i)) continue;
43430
- for (let j = rLo; j < rHi; j++) {
44051
+ const [jLo, jHi] = span(i);
44052
+ for (let j = jLo; j < jHi; j++) {
43431
44053
  if (rUsed.has(j)) continue;
43432
- if (localName(oBlocks[i].name) !== localName(rBlocks[j].name)) continue;
44054
+ if (!pairable(oBlocks[i], rBlocks[j], ctx)) continue;
43433
44055
  const a = paragraphText3(oBlocks[i]);
43434
44056
  const b = paragraphText3(rBlocks[j]);
43435
44057
  const score = similarity(a, b);
@@ -43440,30 +44062,74 @@ function resolveGap(oBlocks, rBlocks, oLo, oHi, rLo, rHi, ctx, needsAnchor = fal
43440
44062
  for (const cand of scored) {
43441
44063
  if (oUsed.has(cand.o) || rUsed.has(cand.r)) continue;
43442
44064
  if (crosses(cand.o, cand.r)) continue;
43443
- out.push({ kind: "pair", o: cand.o, r: cand.r, other: -1 });
44065
+ accept({ kind: "pair", o: cand.o, r: cand.r, other: -1 });
43444
44066
  oUsed.add(cand.o);
43445
44067
  rUsed.add(cand.r);
43446
44068
  }
44069
+ const settled = [...out].sort((a, b) => a.o - b.o);
44070
+ let oFrom = oLo;
44071
+ let rFrom = rLo;
44072
+ for (const p of [...settled, { kind: "pair", o: oHi, r: rHi, other: -1 }]) {
44073
+ const os = [];
44074
+ const rs = [];
44075
+ for (let i = oFrom; i < p.o; i++) if (!oUsed.has(i)) os.push(i);
44076
+ for (let j = rFrom; j < p.r; j++) if (!rUsed.has(j)) rs.push(j);
44077
+ if (os.length === 1 && rs.length === 1 && sameShape2(oBlocks[os[0]], rBlocks[rs[0]], ctx)) {
44078
+ accept({ kind: "pair", o: os[0], r: rs[0], other: -1 });
44079
+ oUsed.add(os[0]);
44080
+ rUsed.add(rs[0]);
44081
+ }
44082
+ oFrom = p.kind === "merge" ? p.other + 1 : p.o + 1;
44083
+ rFrom = p.kind === "split" ? p.other + 1 : p.r + 1;
44084
+ }
43447
44085
  if (needsAnchor && out.length === 0) {
43448
44086
  for (let i = oLo; i < oHi && out.length === 0; i++) {
43449
44087
  if (oUsed.has(i)) continue;
43450
44088
  for (let j = rLo; j < rHi; j++) {
43451
- if (rUsed.has(j) || localName(oBlocks[i].name) !== localName(rBlocks[j].name)) continue;
43452
- out.push({ kind: "pair", o: i, r: j, other: -1 });
44089
+ if (rUsed.has(j) || !pairable(oBlocks[i], rBlocks[j], ctx)) continue;
44090
+ accept({ kind: "pair", o: i, r: j, other: -1 });
43453
44091
  break;
43454
44092
  }
43455
44093
  }
43456
44094
  }
43457
44095
  return out.sort((a, b) => a.o - b.o);
43458
44096
  }
44097
+ function pairable(a, b, ctx) {
44098
+ if (localName(a.name) !== localName(b.name)) return false;
44099
+ if (!isParagraph(a)) return true;
44100
+ return sectionKey(a, ctx.original) === sectionKey(b, ctx.doc);
44101
+ }
44102
+ function sectionKey(el26, doc) {
44103
+ const sectPr = child(child(el26, "pPr"), "sectPr");
44104
+ if (!sectPr) return "";
44105
+ return serializeXml(sectPr).replace(/r:id="([^"]*)"/g, (_, id) => `r:id="${doc.documentRels.get(id)?.target ?? id}"`);
44106
+ }
44107
+ function sameShape2(a, b, ctx) {
44108
+ if (isParagraph(a) && isParagraph(b)) return pairable(a, b, ctx);
44109
+ if (localName(a.name) === localName(b.name) && ["tbl", "sdt", "customXml"].includes(localName(a.name))) return true;
44110
+ const cells = (tr) => tr.children.filter((c) => localName(c.name) === "tc").length;
44111
+ return localName(a.name) === "tr" && localName(b.name) === "tr" && cells(a) === cells(b);
44112
+ }
44113
+ function readOptions(ctx, original) {
44114
+ const noteKey = (ref) => {
44115
+ const kind = localName(ref.name) === "footnoteReference" ? "footnote" : "endnote";
44116
+ const id = ref.attrs["w:id"];
44117
+ return original ? `${ctx.noteIds[kind].get(id) ?? `original ${id}`}` : id;
44118
+ };
44119
+ return { whitespace: ctx.whitespace, noteKey };
44120
+ }
43459
44121
  function concatMatches(oEls, rEls, ctx) {
43460
44122
  const key = (els) => {
43461
44123
  const parts = [];
43462
44124
  for (const el26 of els) {
43463
44125
  if (localName(el26.name) !== "p") return null;
43464
- const content = readParagraph(el26, { whitespace: ctx.whitespace });
44126
+ if (child(child(el26, "pPr"), "sectPr")) return null;
44127
+ const content = readParagraph(el26, readOptions(ctx, oEls.includes(el26)));
43465
44128
  if (!content.inlineDiffable) return null;
43466
- for (const token of content.tokens) parts.push(token.key + "" + runPropsKey(token.run));
44129
+ for (const token of content.tokens) {
44130
+ const props = token.pieces ? token.pieces.map((p) => `${p.text.length}:${runPropsKey(p.run)}`).join("|") : runPropsKey(token.run);
44131
+ parts.push(token.key + "" + props + "" + token.path.map((c) => serializeXml({ ...c, children: c.children.filter((k) => /Pr$/.test(localName(k.name))) })).join(">"));
44132
+ }
43467
44133
  }
43468
44134
  return parts.join("");
43469
44135
  };
@@ -43475,14 +44141,24 @@ function emitMatched(oEl, rEl, ctx) {
43475
44141
  const ln = localName(oEl.name);
43476
44142
  if (ln === "p") comparePair(oEl, rEl, ctx);
43477
44143
  else if (ln === "tbl") compareTable(oEl, rEl, ctx);
44144
+ else if (ln === "sdt") {
44145
+ const o = child(oEl, "sdtContent");
44146
+ const r = child(rEl, "sdtContent");
44147
+ if (o && r) compareContainer(o, r, ctx);
44148
+ } else if (ln === "customXml") compareContainer(oEl, rEl, ctx);
43478
44149
  return { el: rEl, cls: "kept" };
43479
44150
  }
43480
44151
  var REL_ATTRS = ["id", "embed", "link", "pict", "dm", "lo", "qs", "cs"];
43481
44152
  function adoptFromOriginal(root, ctx) {
43482
- const mine = ctx.doc.documentRels;
43483
- const theirs = ctx.original.documentRels;
44153
+ const mine = ctx.doc.relationshipsOf(ctx.scope.into);
44154
+ const theirs = ctx.original.relationshipsOf(ctx.scope.from);
43484
44155
  const remapped = /* @__PURE__ */ new Map();
43485
44156
  const walk2 = (el26) => {
44157
+ const ln = localName(el26.name);
44158
+ if (ln === "footnoteReference" || ln === "endnoteReference") {
44159
+ const to = ctx.noteIds[ln === "footnoteReference" ? "footnote" : "endnote"].get(el26.attrs["w:id"]);
44160
+ if (to !== void 0) el26.attrs["w:id"] = to;
44161
+ }
43486
44162
  for (const key of Object.keys(el26.attrs)) {
43487
44163
  if (!key.startsWith("r:") || !REL_ATTRS.includes(localName(key))) continue;
43488
44164
  const id = el26.attrs[key];
@@ -43492,7 +44168,7 @@ function adoptFromOriginal(root, ctx) {
43492
44168
  if (here && here.target === source.target && here.type === source.type) continue;
43493
44169
  let replacement = remapped.get(id);
43494
44170
  if (replacement === void 0) {
43495
- replacement = source.external ? ctx.doc.addHyperlinkRel(source.target) : null;
44171
+ replacement = source.external ? ctx.doc.addHyperlinkRel(source.target, ctx.scope.into) : ctx.doc.adoptRelFrom(ctx.original, id, ctx.scope);
43496
44172
  if (replacement === null) {
43497
44173
  ctx.note({
43498
44174
  code: "unresolved-relationship",
@@ -43511,32 +44187,31 @@ function adoptFromOriginal(root, ctx) {
43511
44187
  function emitDeleted(el26, ctx) {
43512
44188
  const clone = cloneXml(el26);
43513
44189
  adoptFromOriginal(clone, ctx);
43514
- const ln = localName(clone.name);
43515
- if (ln === "p") {
43516
- wrapContainerContent(clone, "del", ctx);
43517
- } else if (ln === "tbl") {
43518
- for (const row of clone.children.filter((c) => localName(c.name) === "tr")) {
43519
- markRowRevision(row, "del", ctx.meta);
43520
- wrapRowContent(row, "del", ctx);
43521
- }
43522
- } else if (!isPassenger(ln)) {
43523
- ctx.note({ code: "untracked-block", detail: `a <${clone.name}> block was removed and is not expressible as a revision` });
43524
- }
44190
+ markWholeBlock(clone, "del", ctx);
43525
44191
  return { el: clone, cls: "deleted" };
43526
44192
  }
43527
44193
  function emitInserted(el26, ctx) {
44194
+ markWholeBlock(el26, "ins", ctx);
44195
+ return { el: el26, cls: "inserted" };
44196
+ }
44197
+ function markWholeBlock(el26, kind, ctx) {
43528
44198
  const ln = localName(el26.name);
43529
44199
  if (ln === "p") {
43530
- wrapContainerContent(el26, "ins", ctx);
44200
+ wrapContainerContent(el26, kind, ctx);
43531
44201
  } else if (ln === "tbl") {
43532
44202
  for (const row of el26.children.filter((c) => localName(c.name) === "tr")) {
43533
- markRowRevision(row, "ins", ctx.meta);
43534
- wrapRowContent(row, "ins", ctx);
44203
+ markRowRevision(row, kind, ctx.meta);
44204
+ wrapRowContent(row, kind, ctx);
43535
44205
  }
43536
- } else if (!isPassenger(ln)) {
43537
- ctx.note({ code: "untracked-block", detail: `a <${el26.name}> block was added and is not expressible as a revision` });
44206
+ } else if (ln === "sdt" || ln === "customXml") {
44207
+ const inner = ln === "sdt" ? child(el26, "sdtContent") : el26;
44208
+ for (const block of inner?.children ?? []) {
44209
+ markWholeBlock(block, kind, ctx);
44210
+ if (isParagraph(block)) markParagraphGlyph(block, kind, ctx.meta);
44211
+ }
44212
+ } else if (!isPassenger(ln) && ln !== "sdtPr" && ln !== "sdtEndPr" && ln !== "customXmlPr") {
44213
+ ctx.note({ code: "untracked-block", detail: `a <${el26.name}> block was ${kind === "ins" ? "added" : "removed"} and is not expressible as a revision` });
43538
44214
  }
43539
- return { el: el26, cls: "inserted" };
43540
44215
  }
43541
44216
  function wrapRowContent(row, kind, ctx) {
43542
44217
  for (const cell of row.children.filter((c) => localName(c.name) === "tc")) {
@@ -43546,7 +44221,8 @@ function wrapRowContent(row, kind, ctx) {
43546
44221
  }
43547
44222
  }
43548
44223
  function wrapContainerContent(container, kind, ctx) {
43549
- const NESTED = ["hyperlink", "smartTag", "dir", "bdo"];
44224
+ const NESTED = ["hyperlink", "smartTag", "dir", "bdo", "fldSimple"];
44225
+ const WRAPPABLE = ["r", "sdt", "customXml"];
43550
44226
  const out = [];
43551
44227
  let group = [];
43552
44228
  const flush = () => {
@@ -43557,7 +44233,7 @@ function wrapContainerContent(container, kind, ctx) {
43557
44233
  };
43558
44234
  for (const c of container.children) {
43559
44235
  const ln = localName(c.name);
43560
- if (ln === "r") {
44236
+ if (WRAPPABLE.includes(ln)) {
43561
44237
  group.push(c);
43562
44238
  continue;
43563
44239
  }
@@ -43575,10 +44251,10 @@ function wrapContainerContent(container, kind, ctx) {
43575
44251
  container.children = out;
43576
44252
  }
43577
44253
  function comparePair(oEl, rEl, ctx) {
43578
- if (serializeXml(oEl) === serializeXml(rEl)) return;
44254
+ if (sameXml(oEl, rEl)) return;
43579
44255
  if (ctx.formatting) alignParagraphProps(rEl, child(rEl, "pPr"), child(oEl, "pPr"), ctx);
43580
- const oc = readParagraph(oEl, { whitespace: ctx.whitespace });
43581
- const rc = readParagraph(rEl, { whitespace: ctx.whitespace });
44256
+ const oc = readParagraph(oEl, readOptions(ctx, true));
44257
+ const rc = readParagraph(rEl, readOptions(ctx, false));
43582
44258
  if (!oc.inlineDiffable || !rc.inlineDiffable) {
43583
44259
  if (oc.text !== rc.text || serializeXml(stripProps(oEl)) !== serializeXml(stripProps(rEl))) {
43584
44260
  const deleted = cloneXml(oEl);
@@ -43593,70 +44269,180 @@ function comparePair(oEl, rEl, ctx) {
43593
44269
  ];
43594
44270
  ctx.note({
43595
44271
  code: "coarse-paragraph",
43596
- detail: "a paragraph holding a hyperlink, content control, smart tag or inline math changed; struck and reinserted whole"
44272
+ detail: "a paragraph holding inline math or other content a word diff cannot rebuild changed; struck and reinserted whole"
43597
44273
  });
43598
44274
  }
43599
44275
  return;
43600
44276
  }
43601
44277
  const segments = wordSegments(oc.tokens, rc.tokens);
43602
44278
  const changed = segments.some((s) => !s.equal);
43603
- if (!changed) {
43604
- if (ctx.formatting) alignRunPropsInPlace(oEl, rEl, ctx);
43605
- return;
43606
- }
44279
+ const flat = oc.tokens.every((t) => t.path.length === 0) && rc.tokens.every((t) => t.path.length === 0);
44280
+ if (!changed && (!ctx.formatting || flat && alignRunPropsInPlace(oEl, rEl, ctx))) return;
43607
44281
  const pPr = child(rEl, "pPr");
43608
- const out = [];
44282
+ const items = [];
44283
+ const counterpart = /* @__PURE__ */ new Map();
43609
44284
  for (const seg of segments) {
43610
44285
  if (seg.equal) {
43611
- out.push(...emitEqualSpan(oc.tokens, rc.tokens, seg, ctx));
44286
+ for (const it of emitEqualSpan(oc.tokens, rc.tokens, seg, ctx, counterpart)) items.push({ ...it, kind: null });
43612
44287
  continue;
43613
44288
  }
43614
44289
  if (seg.aEnd > seg.aStart) {
43615
- const struck = sliceContent(oc.tokens, seg.aStart, seg.aEnd, true);
43616
- for (const run of struck) adoptFromOriginal(run, ctx);
43617
- out.push(revisionWrapper(rEl, "del", struck, ctx.meta));
44290
+ for (const it of sliceItems(oc.tokens, seg.aStart, seg.aEnd, true)) {
44291
+ adoptFromOriginal(it.el, ctx);
44292
+ markDeletedText(it.el);
44293
+ items.push({ ...it, kind: "del" });
44294
+ }
43618
44295
  }
43619
44296
  if (seg.bEnd > seg.bStart) {
43620
- out.push(revisionWrapper(rEl, "ins", sliceContent(rc.tokens, seg.bStart, seg.bEnd, false), ctx.meta));
44297
+ for (const it of sliceItems(rc.tokens, seg.bStart, seg.bEnd, false)) items.push({ ...it, kind: "ins" });
43621
44298
  }
43622
44299
  }
43623
- out.push(...rc.tail.map(cloneXml));
43624
- rEl.children = [...pPr ? [pPr] : [], ...out];
44300
+ for (const it of items) if (it.kind === "del") it.path = it.path.map((c) => counterpart.get(c) ?? c);
44301
+ for (const el26 of rc.tail) items.push({ el: cloneXml(el26), path: [], kind: null });
44302
+ const fromOriginal = new Set(oc.tokens.flatMap((t) => t.path));
44303
+ rEl.children = [...pPr ? [pPr] : [], ...nestItems(items, 0, rEl, ctx, fromOriginal)];
44304
+ }
44305
+ var WRAPPABLE_CONTAINERS = /* @__PURE__ */ new Set(["sdt", "customXml"]);
44306
+ function compareTextBoxes(oAtom, rAtom, ctx) {
44307
+ const theirs = textBoxesIn(oAtom);
44308
+ const mine = textBoxesIn(rAtom);
44309
+ for (let i = 0; i < Math.min(theirs.length, mine.length); i++) {
44310
+ if (sameXml(theirs[i], mine[i])) continue;
44311
+ const entries = alignBlocks(blockList(theirs[i]), blockList(mine[i]), ctx);
44312
+ applyParagraphMarks(entries, ctx);
44313
+ mine[i].children = entries.map((e) => e.el);
44314
+ }
44315
+ }
44316
+ function nestItems(items, depth, rEl, ctx, fromOriginal) {
44317
+ const out = [];
44318
+ let i = 0;
44319
+ while (i < items.length) {
44320
+ const c = items[i].path[depth];
44321
+ let j = i + 1;
44322
+ if (c) {
44323
+ while (j < items.length && items[j].path[depth] === c) j++;
44324
+ const group = items.slice(i, j);
44325
+ const kind = group[0].kind;
44326
+ if (kind && WRAPPABLE_CONTAINERS.has(localName(c.name)) && group.every((it) => it.kind === kind)) {
44327
+ const plain = group.map((it) => ({ ...it, kind: null }));
44328
+ out.push(revisionWrapper(rEl, kind, [containerShell(c, nestItems(plain, depth + 1, rEl, ctx, fromOriginal), fromOriginal.has(c), ctx)], ctx.meta));
44329
+ } else {
44330
+ out.push(containerShell(c, nestItems(group, depth + 1, rEl, ctx, fromOriginal), fromOriginal.has(c), ctx));
44331
+ }
44332
+ } else {
44333
+ const kind = items[i].kind;
44334
+ while (j < items.length && !items[j].path[depth] && items[j].kind === kind) j++;
44335
+ const els = items.slice(i, j).map((it) => it.el);
44336
+ if (kind) out.push(revisionWrapper(rEl, kind, els, ctx.meta));
44337
+ else out.push(...els);
44338
+ }
44339
+ i = j;
44340
+ }
44341
+ return out;
44342
+ }
44343
+ function containerShell(c, content, fromOriginal, ctx) {
44344
+ const isSdt = localName(c.name) === "sdt";
44345
+ const inner = isSdt ? c.children.find((k) => localName(k.name) === "sdtContent") : void 0;
44346
+ const shell = {
44347
+ name: c.name,
44348
+ attrs: { ...c.attrs },
44349
+ // Property children only (sdtPr, smartTagPr, ...): the content is new.
44350
+ children: c.children.filter((k) => /Pr$/.test(localName(k.name))).map(cloneXml),
44351
+ text: ""
44352
+ };
44353
+ if (fromOriginal) adoptFromOriginal(shell, ctx);
44354
+ if (isSdt) shell.children.push({ name: inner?.name ?? c.name.replace(/sdt$/, "sdtContent"), attrs: { ...inner?.attrs ?? {} }, children: content, text: "" });
44355
+ else shell.children.push(...content);
44356
+ return shell;
43625
44357
  }
43626
44358
  function stripProps(el26) {
43627
44359
  return { ...el26, children: el26.children.filter((c) => localName(c.name) !== "pPr") };
43628
44360
  }
43629
- function emitEqualSpan(oTokens, rTokens, seg, ctx) {
44361
+ function emitEqualSpan(oTokens, rTokens, seg, ctx, counterpart) {
44362
+ const units = [];
44363
+ const pieceAt = (pieces, offset) => {
44364
+ let base = 0;
44365
+ for (const p of pieces) {
44366
+ if (offset < base + p.text.length) return p;
44367
+ base += p.text.length;
44368
+ }
44369
+ return pieces[pieces.length - 1];
44370
+ };
44371
+ for (let k = 0; k < seg.bEnd - seg.bStart; k++) {
44372
+ const o = oTokens[seg.aStart + k];
44373
+ const r = rTokens[seg.bStart + k];
44374
+ for (let d = 0; d < Math.min(o.path.length, r.path.length); d++) {
44375
+ if (localName(o.path[d].name) === localName(r.path[d].name)) counterpart.set(o.path[d], r.path[d]);
44376
+ }
44377
+ if (r.atom || !r.pieces || !o.pieces || o.text !== r.text) {
44378
+ if (r.atom) units.push({ token: r, run: r.run, text: "", atom: r.atom, oAtom: o.atom, was: o.run });
44379
+ else for (const p of r.pieces ?? []) units.push({ token: r, run: p.run, t: p.t, text: p.text, was: o.run });
44380
+ continue;
44381
+ }
44382
+ const cuts = /* @__PURE__ */ new Set([0, r.text.length]);
44383
+ for (const pieces of [o.pieces, r.pieces]) {
44384
+ let base = 0;
44385
+ for (const p of pieces) cuts.add(base += p.text.length);
44386
+ }
44387
+ const sorted = [...cuts].sort((x, y) => x - y);
44388
+ for (let c = 0; c + 1 < sorted.length; c++) {
44389
+ const from = sorted[c];
44390
+ const to = sorted[c + 1];
44391
+ if (from === to) continue;
44392
+ const rp = pieceAt(r.pieces, from);
44393
+ units.push({ token: r, run: rp.run, t: rp.t, text: r.text.slice(from, to), was: pieceAt(o.pieces, from).run });
44394
+ }
44395
+ }
43630
44396
  const out = [];
43631
- let at = seg.bStart;
43632
- while (at < seg.bEnd) {
43633
- const run = rTokens[at].run;
43634
- const oKey = runPropsKey(oTokens[seg.aStart + (at - seg.bStart)].run);
43635
- let end = at + 1;
43636
- while (end < seg.bEnd && rTokens[end].run === run && runPropsKey(oTokens[seg.aStart + (end - seg.bStart)].run) === oKey) {
43637
- end++;
44397
+ let open = null;
44398
+ let text = null;
44399
+ const close = () => {
44400
+ if (open && ctx.formatting && open.was !== runPropsKey(open.run)) swapRunProps(open.el, open.prev, ctx);
44401
+ open = null;
44402
+ text = null;
44403
+ };
44404
+ let lastToken = null;
44405
+ for (const u of units) {
44406
+ if (u.token !== lastToken && u.token.lead) {
44407
+ close();
44408
+ out.push(...u.token.lead.map((el26) => ({ el: cloneXml(el26), path: u.token.path })));
44409
+ }
44410
+ lastToken = u.token;
44411
+ const was = runPropsKey(u.was);
44412
+ if (!open || open.run !== u.run || open.was !== was) {
44413
+ close();
44414
+ const rPr = u.run.children.find((c) => localName(c.name) === "rPr");
44415
+ const el26 = { name: u.run.name, attrs: { ...u.run.attrs }, children: rPr ? [cloneXml(rPr)] : [], text: "" };
44416
+ out.push({ el: el26, path: u.token.path });
44417
+ open = { run: u.run, was, el: el26, prev: child(u.was, "rPr") };
44418
+ }
44419
+ if (u.atom) {
44420
+ const atom = cloneXml(u.atom);
44421
+ if (u.oAtom && !sameXml(u.oAtom, u.atom)) compareTextBoxes(u.oAtom, atom, ctx);
44422
+ open.el.children.push(atom);
44423
+ text = null;
44424
+ continue;
43638
44425
  }
43639
- const piece = sliceContent(rTokens, at, end, false);
43640
- if (ctx.formatting && oKey !== runPropsKey(run)) {
43641
- for (const el26 of piece) {
43642
- if (localName(el26.name) === "r") {
43643
- swapRunProps(el26, child(oTokens[seg.aStart + (at - seg.bStart)].run, "rPr"), ctx);
43644
- }
43645
- }
44426
+ if (text && text.t === u.t) {
44427
+ text.el.text += u.text;
44428
+ continue;
43646
44429
  }
43647
- out.push(...piece);
43648
- at = end;
44430
+ const te = { name: u.t.name, attrs: { ...u.t.attrs, "xml:space": "preserve" }, children: [], text: u.text };
44431
+ open.el.children.push(te);
44432
+ text = { t: u.t, el: te };
43649
44433
  }
44434
+ close();
43650
44435
  return out;
43651
44436
  }
43652
44437
  function alignRunPropsInPlace(oEl, rEl, ctx) {
43653
44438
  const oRuns = oEl.children.filter((c) => localName(c.name) === "r");
43654
44439
  const rRuns = rEl.children.filter((c) => localName(c.name) === "r");
43655
- if (oRuns.length !== rRuns.length) return;
44440
+ if (oRuns.length !== rRuns.length) return false;
43656
44441
  for (let i = 0; i < rRuns.length; i++) {
43657
- if (paragraphText3(oRuns[i]) !== paragraphText3(rRuns[i])) return;
44442
+ if (paragraphText3(oRuns[i]) !== paragraphText3(rRuns[i])) return false;
43658
44443
  }
43659
44444
  for (let i = 0; i < rRuns.length; i++) swapRunProps(rRuns[i], child(oRuns[i], "rPr"), ctx);
44445
+ return true;
43660
44446
  }
43661
44447
  function wordSegments(a, b) {
43662
44448
  const raw = [];
@@ -43685,8 +44471,8 @@ function wordSegments(a, b) {
43685
44471
  for (let i = 1; i + 1 < segments.length; i++) {
43686
44472
  const seg = segments[i];
43687
44473
  if (!seg.equal) continue;
43688
- const len = seg.aEnd - seg.aStart;
43689
- const beside = Math.min(changeSize(segments[i - 1]), changeSize(segments[i + 1]));
44474
+ const len = words(a, seg.aStart, seg.aEnd);
44475
+ const beside = Math.min(changeSize(segments[i - 1], a, b), changeSize(segments[i + 1], a, b));
43690
44476
  if (len < COMPARE_TUNING.coalesceWindow || len * COMPARE_TUNING.coalesceRatio < beside) {
43691
44477
  seg.equal = false;
43692
44478
  absorbed = true;
@@ -43710,17 +44496,26 @@ function mergeChanges(segments) {
43710
44496
  }
43711
44497
  return out;
43712
44498
  }
43713
- function changeSize(seg) {
43714
- return Math.max(seg.aEnd - seg.aStart, seg.bEnd - seg.bStart);
44499
+ function changeSize(seg, a, b) {
44500
+ return Math.max(words(a, seg.aStart, seg.aEnd), words(b, seg.bStart, seg.bEnd));
44501
+ }
44502
+ function words(tokens, from, to) {
44503
+ let n = 0;
44504
+ for (let i = from; i < to; i++) if (tokens[i].atom || !/^\s+$/.test(tokens[i].text)) n++;
44505
+ return n;
43715
44506
  }
43716
44507
  function compareTable(oTbl, rTbl, ctx) {
43717
- if (serializeXml(oTbl) === serializeXml(rTbl)) return;
44508
+ if (sameXml(oTbl, rTbl)) return;
43718
44509
  if (ctx.formatting) {
43719
44510
  swapProps(rTbl, "tblPr", recordTableFormatChange, child(oTbl, "tblPr"), ctx);
43720
44511
  alignTableGrid(oTbl, rTbl, ctx);
43721
44512
  }
43722
- const oRows = oTbl.children.filter((c) => localName(c.name) === "tr");
43723
- const rRows = rTbl.children.filter((c) => localName(c.name) === "tr");
44513
+ compareRows(oTbl, rTbl, ctx);
44514
+ }
44515
+ var ROW_LEVEL = ["tr", "sdt", "customXml"];
44516
+ function compareRows(oParent, rParent, ctx) {
44517
+ const oRows = oParent.children.filter((c) => ROW_LEVEL.includes(localName(c.name)));
44518
+ const rRows = rParent.children.filter((c) => ROW_LEVEL.includes(localName(c.name)));
43724
44519
  const anchors = histogramMatch(oRows.map(rowFingerprint), rRows.map(rowFingerprint));
43725
44520
  const rows = [];
43726
44521
  let oAt = 0;
@@ -43745,24 +44540,41 @@ function compareTable(oTbl, rTbl, ctx) {
43745
44540
  oAt = anchor.a + 1;
43746
44541
  rAt = anchor.b + 1;
43747
44542
  }
43748
- rTbl.children = [...rTbl.children.filter((c) => localName(c.name) !== "tr"), ...rows];
44543
+ rParent.children = [...rParent.children.filter((c) => !ROW_LEVEL.includes(localName(c.name))), ...rows];
43749
44544
  }
43750
44545
  function rowFingerprint(tr) {
44546
+ if (localName(tr.name) !== "tr") return `X\0${serializeXml(tr)}`;
43751
44547
  const cells = tr.children.filter((c) => localName(c.name) === "tc");
43752
44548
  return `${cells.length}\0${normalizeText(paragraphText3(tr))}`;
43753
44549
  }
43754
- function deletedRow(tr, ctx) {
43755
- const clone = cloneXml(tr);
43756
- markRowRevision(clone, "del", ctx.meta);
43757
- wrapRowContent(clone, "del", ctx);
44550
+ function rowsIn(el26) {
44551
+ if (localName(el26.name) === "tr") return [el26];
44552
+ const inner = localName(el26.name) === "sdt" ? child(el26, "sdtContent") : el26;
44553
+ return (inner?.children ?? []).filter((c) => ROW_LEVEL.includes(localName(c.name))).flatMap(rowsIn);
44554
+ }
44555
+ function deletedRow(el26, ctx) {
44556
+ const clone = cloneXml(el26);
44557
+ for (const tr of rowsIn(clone)) {
44558
+ markRowRevision(tr, "del", ctx.meta);
44559
+ wrapRowContent(tr, "del", ctx);
44560
+ }
43758
44561
  return clone;
43759
44562
  }
43760
- function insertedRow(tr, ctx) {
43761
- markRowRevision(tr, "ins", ctx.meta);
43762
- wrapRowContent(tr, "ins", ctx);
43763
- return tr;
44563
+ function insertedRow(el26, ctx) {
44564
+ for (const tr of rowsIn(el26)) {
44565
+ markRowRevision(tr, "ins", ctx.meta);
44566
+ wrapRowContent(tr, "ins", ctx);
44567
+ }
44568
+ return el26;
43764
44569
  }
43765
44570
  function compareRow(oTr, rTr, ctx) {
44571
+ if (localName(oTr.name) !== "tr") {
44572
+ const inner = (el26) => localName(el26.name) === "sdt" ? child(el26, "sdtContent") : el26;
44573
+ const o = inner(oTr);
44574
+ const r = inner(rTr);
44575
+ if (o && r) compareRows(o, r, ctx);
44576
+ return [rTr];
44577
+ }
43766
44578
  const oCells = oTr.children.filter((c) => localName(c.name) === "tc");
43767
44579
  const rCells = rTr.children.filter((c) => localName(c.name) === "tc");
43768
44580
  if (oCells.length !== rCells.length) {
@@ -43785,7 +44597,7 @@ function compareRow(oTr, rTr, ctx) {
43785
44597
  function alignTableGrid(oTbl, rTbl, ctx) {
43786
44598
  const oGrid = child(oTbl, "tblGrid");
43787
44599
  const rGrid = child(rTbl, "tblGrid");
43788
- if (!oGrid || !rGrid || serializeXml(oGrid) === serializeXml(rGrid)) return;
44600
+ if (!oGrid || !rGrid || sameXml(oGrid, rGrid)) return;
43789
44601
  const live = rGrid.children.map(cloneXml);
43790
44602
  rGrid.children = oGrid.children.map(cloneXml);
43791
44603
  recordTableGridChange(rTbl, ctx.meta);
@@ -43872,7 +44684,22 @@ function applyParagraphMarks(entries, ctx) {
43872
44684
  }
43873
44685
  }
43874
44686
  function markRun(entries, start, end, ctx) {
43875
- const shifted = start > 0 && isParagraph(entries[start - 1].el);
44687
+ let at = start;
44688
+ while (at <= end) {
44689
+ if (!isParagraph(entries[at].el)) {
44690
+ at++;
44691
+ continue;
44692
+ }
44693
+ let last = at;
44694
+ while (last + 1 <= end && isParagraph(entries[last + 1].el)) last++;
44695
+ markParagraphRun(entries, at, last, ctx);
44696
+ at = last + 1;
44697
+ }
44698
+ }
44699
+ function markParagraphRun(entries, start, end, ctx) {
44700
+ const endsSection = (el26) => isParagraph(el26) && !!child(child(el26, "pPr"), "sectPr");
44701
+ const before = start > 0 ? entries[start - 1].el : void 0;
44702
+ const shifted = !!before && isParagraph(before) && !endsSection(before) && !entries.slice(start, end + 1).some((e) => endsSection(e.el));
43876
44703
  if (shifted) {
43877
44704
  for (let i = start; i <= end; i++) {
43878
44705
  if (!isParagraph(entries[i].el) || !isParagraph(entries[i - 1].el)) continue;
@@ -45359,9 +46186,10 @@ async function renderForPrint(bytes, container) {
45359
46186
  const doc = DocxDocument.load(bytes);
45360
46187
  const layout = layoutDocument(doc);
45361
46188
  renderToDom(doc, layout, container, { zoom: 1, pageGap: 0, pageShadow: false, comments: false });
46189
+ pruneBlankSpans(container);
45362
46190
  const first = layout.pages[0];
45363
46191
  const style = document.createElement("style");
45364
- style.textContent = printStylesheet(first?.width ?? 816, first?.height ?? 1056);
46192
+ style.textContent = printStylesheet(first?.width ?? 816, first?.height ?? 1056, container);
45365
46193
  document.head.append(style);
45366
46194
  await paintsSettled();
45367
46195
  await Promise.all(Array.from(container.querySelectorAll("img"), (img) => img.decode().catch(() => void 0)));
@@ -55815,4 +56643,4 @@ export {
55815
56643
  cfb/cfb.js:
55816
56644
  (*! crc32.js (C) 2014-present SheetJS -- http://sheetjs.com *)
55817
56645
  */
55818
- //# sourceMappingURL=chunk-QU3LB5BC.js.map
56646
+ //# sourceMappingURL=chunk-ULPL55MT.js.map