wordinweb 0.3.5 → 0.3.6

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("/")) : "";
@@ -8798,6 +8827,10 @@ var _DocxDocument = class _DocxDocument {
8798
8827
  /** The header part roots, in package order. A watermark is authored into
8799
8828
  * every one of them: Word paints it on every page, and a document with a
8800
8829
  * first-page or even-page header has more than one header part. */
8830
+ /** The root of the header or footer part at `target`, if this document has one. */
8831
+ headerFooterRoot(target) {
8832
+ return this.hfParts.find((p) => p.target === target)?.root;
8833
+ }
8801
8834
  headerRoots() {
8802
8835
  return this.hfParts.filter((p) => p.isHeader).map((p) => p.root);
8803
8836
  }
@@ -8961,6 +8994,22 @@ var _DocxDocument = class _DocxDocument {
8961
8994
  this.saveJournal = null;
8962
8995
  }
8963
8996
  }
8997
+ /**
8998
+ * An independent copy: what `DocxDocument.load(doc.save())` gives, without
8999
+ * zipping the package and unzipping it again. compareDocuments starts from a
9000
+ * copy of the revised document, and the round trip through the zip was a
9001
+ * third of its time on a document that had not changed at all.
9002
+ */
9003
+ clone() {
9004
+ const journal = [];
9005
+ this.saveJournal = journal;
9006
+ try {
9007
+ return new _DocxDocument(Package.fromFiles(this.buildPackageFiles()));
9008
+ } finally {
9009
+ for (let i = journal.length - 1; i >= 0; i--) journal[i]();
9010
+ this.saveJournal = null;
9011
+ }
9012
+ }
8964
9013
  saveAsync() {
8965
9014
  const journal = [];
8966
9015
  this.saveJournal = journal;
@@ -9162,8 +9211,91 @@ var _DocxDocument = class _DocxDocument {
9162
9211
  return this.relsRoot;
9163
9212
  }
9164
9213
  /** Register an external hyperlink relationship and return its rId. */
9165
- addHyperlinkRel(url) {
9166
- const rels = this.ensureRelsRoot();
9214
+ /**
9215
+ * Copy the part that `source`'s main-document relationship `relId` points
9216
+ * at into this package, with every part it references in turn (a chart's
9217
+ * embedded workbook, SmartArt's layout, style and colour parts) and their
9218
+ * content types, and add a relationship to it. Returns the new
9219
+ * relationship id, or null when the source has no such internal part.
9220
+ *
9221
+ * compareDocuments needs this for struck content: a removed picture keeps
9222
+ * its w:drawing, and without its image part the result's reject-all
9223
+ * brought the picture back as an empty frame.
9224
+ */
9225
+ adoptRelFrom(source, relId, scope = {}) {
9226
+ const rel = source.relationshipsOf(scope.from).get(relId);
9227
+ if (!rel || rel.external) return null;
9228
+ const copied = /* @__PURE__ */ new Map();
9229
+ const sameBytes = (path2, bytes) => {
9230
+ const have = this.pkg.binary(path2);
9231
+ return !!have && have.length === bytes.length && have.every((b, i) => b === bytes[i]);
9232
+ };
9233
+ const copyPart = (from) => {
9234
+ const known = copied.get(from);
9235
+ if (known) return known;
9236
+ const bytes = source.pkg.binary(from);
9237
+ if (!bytes) return null;
9238
+ let to = from;
9239
+ for (let n = 2; this.pkg.has(to) && !sameBytes(to, bytes); n++) to = from.replace(/(\.[^./]+)?$/, `-${n}$1`);
9240
+ copied.set(from, to);
9241
+ if (this.pkg.has(to)) return to;
9242
+ this.pkg.raw()[to] = bytes;
9243
+ this.adoptContentType(source, from, to);
9244
+ const relsXml = source.pkg.text(relsPathFor(from));
9245
+ if (relsXml) {
9246
+ const root = parseXml(relsXml);
9247
+ for (const r of root.children) {
9248
+ if (r.attrs.TargetMode === "External" || !r.attrs.Target) continue;
9249
+ const sub = copyPart(resolvePartPath(from, r.attrs.Target));
9250
+ if (sub) r.attrs.Target = relativePartPath(to, sub);
9251
+ }
9252
+ this.pkg.raw()[relsPathFor(to)] = strToU82(serializeXml(root, true));
9253
+ }
9254
+ return to;
9255
+ };
9256
+ const part = copyPart(rel.target);
9257
+ if (!part) return null;
9258
+ const owner = this.relsOwnerOf(scope.into);
9259
+ const rels = owner.relsRoot;
9260
+ let maxId = 0;
9261
+ for (const r of rels.children) {
9262
+ const m = /^rId(\d+)$/.exec(r.attrs["Id"] ?? "");
9263
+ if (m) maxId = Math.max(maxId, parseInt(m[1], 10));
9264
+ }
9265
+ const id = `rId${maxId + 1}`;
9266
+ rels.children.push({
9267
+ name: "Relationship",
9268
+ attrs: { Id: id, Type: rel.type, Target: relativePartPath(owner.partPath, part) },
9269
+ children: [],
9270
+ text: ""
9271
+ });
9272
+ owner.relationships.set(id, { id, type: rel.type, target: part, external: false });
9273
+ return id;
9274
+ }
9275
+ /** Give a part copied from `source` the content type it had there. */
9276
+ adoptContentType(source, from, to) {
9277
+ const types = this.contentTypesRoot;
9278
+ const theirs = source.contentTypesRoot;
9279
+ if (!types || !theirs) return;
9280
+ const override = theirs.children.find((c) => localName(c.name) === "Override" && c.attrs.PartName === `/${from}`);
9281
+ const prefixEnd = types.name.indexOf(":") + 1;
9282
+ const prefix = prefixEnd > 0 ? types.name.slice(0, prefixEnd) : "";
9283
+ if (override) {
9284
+ if (!types.children.some((c) => localName(c.name) === "Override" && c.attrs.PartName === `/${to}`)) {
9285
+ types.children.push({ name: `${prefix}Override`, attrs: { PartName: `/${to}`, ContentType: override.attrs.ContentType }, children: [], text: "" });
9286
+ }
9287
+ return;
9288
+ }
9289
+ const ext = from.slice(from.lastIndexOf(".") + 1).toLowerCase();
9290
+ const byExt = (root) => root.children.find((c) => localName(c.name) === "Default" && c.attrs.Extension?.toLowerCase() === ext);
9291
+ const fallback = byExt(theirs);
9292
+ if (fallback && !byExt(types)) {
9293
+ types.children.unshift({ name: `${prefix}Default`, attrs: { Extension: ext, ContentType: fallback.attrs.ContentType }, children: [], text: "" });
9294
+ }
9295
+ }
9296
+ addHyperlinkRel(url, ownerPart) {
9297
+ const owner = this.relsOwnerOf(ownerPart);
9298
+ const rels = owner.relsRoot;
9167
9299
  let maxId = 0;
9168
9300
  for (const r of rels.children) {
9169
9301
  const m = /^rId(\d+)$/.exec(r.attrs["Id"] ?? "");
@@ -9181,7 +9313,7 @@ var _DocxDocument = class _DocxDocument {
9181
9313
  children: [],
9182
9314
  text: ""
9183
9315
  });
9184
- this.documentRels.set(id, { id, type: "hyperlink", target: url, external: true });
9316
+ owner.relationships.set(id, { id, type: "hyperlink", target: url, external: true });
9185
9317
  return id;
9186
9318
  }
9187
9319
  /** Retarget an existing external relationship (hyperlink href edit). */
@@ -9193,6 +9325,23 @@ var _DocxDocument = class _DocxDocument {
9193
9325
  if (el26) el26.attrs["Target"] = url;
9194
9326
  return true;
9195
9327
  }
9328
+ /** The relationships owned by `part` (a header or footer part), or by the
9329
+ * document part when null; a header or footer gets a rels root on demand. */
9330
+ relsOwnerOf(part) {
9331
+ const owner = part ? this.hfParts.find((p) => p.target === part) : void 0;
9332
+ if (!owner) return { relsRoot: this.ensureRelsRoot(), relationships: this.documentRels, partPath: this.docPart };
9333
+ owner.relsRoot ?? (owner.relsRoot = {
9334
+ name: "Relationships",
9335
+ attrs: { xmlns: "http://schemas.openxmlformats.org/package/2006/relationships" },
9336
+ children: [],
9337
+ text: ""
9338
+ });
9339
+ return { relsRoot: owner.relsRoot, relationships: owner.rels, partPath: owner.target };
9340
+ }
9341
+ /** The relationships of `part` (a header or footer), or of the document part. */
9342
+ relationshipsOf(part) {
9343
+ return (part ? this.hfParts.find((p) => p.target === part)?.rels : void 0) ?? this.documentRels;
9344
+ }
9196
9345
  /** The relationships of the part that holds `source`: a header or footer
9197
9346
  * part when the element is inside one, else the document part. */
9198
9347
  imageRelsOwner(source) {
@@ -19232,15 +19381,20 @@ var Engine = class {
19232
19381
  while (out.length < nCols) out.push(bw(fallback));
19233
19382
  return out;
19234
19383
  };
19384
+ const colTopPads = (r) => {
19385
+ const fallback = this.cellMarginsOf(tbl, false).top ?? 0;
19386
+ return rows[r].cells.flatMap((c) => Array(Math.max(1, c.props.gridSpan)).fill(c.props.margins?.top ?? fallback));
19387
+ };
19235
19388
  const boundary = (k) => {
19236
19389
  if (k === 0) return Math.max(0, ...colWidths(0, "top", tb?.top));
19237
19390
  if (k === rows.length)
19238
19391
  return Math.max(0, ...colWidths(rows.length - 1, "bottom", tb?.bottom));
19239
19392
  const above = colWidths(k - 1, "bottom", tb?.insideH);
19240
19393
  const below = colWidths(k, "top", tb?.insideH);
19394
+ const topPads = colTopPads(k);
19241
19395
  const n = Math.max(above.length, below.length);
19242
19396
  let w = 0;
19243
- for (let i = 0; i < n; i++) w = Math.max(w, Math.max(above[i] ?? 0, below[i] ?? 0));
19397
+ for (let i = 0; i < n; i++) w = Math.max(w, below[i] ?? 0, (above[i] ?? 0) - (topPads[i] ?? 0));
19244
19398
  return w;
19245
19399
  };
19246
19400
  return { top: boundary(ri), bottom: boundary(ri + 1) };
@@ -21446,6 +21600,8 @@ function renderPageShell(page, zoom, options) {
21446
21600
  if (options.pageShadow !== false) {
21447
21601
  el26.style.boxShadow = "var(--dxw-page-shadow, 0 1px 3px rgba(0,0,0,.28), 0 4px 14px rgba(0,0,0,.12))";
21448
21602
  }
21603
+ el26.dataset.pageWidth = String(page.width);
21604
+ el26.dataset.pageHeight = String(page.height);
21449
21605
  el26.dataset.bodyTop = String(page.bodyTop);
21450
21606
  el26.dataset.bodyBottom = String(page.bodyBottom);
21451
21607
  el26.dataset.columnBands = JSON.stringify(page.columnBands);
@@ -21762,9 +21918,21 @@ function trackPaint(work) {
21762
21918
  async function paintsSettled() {
21763
21919
  while (pendingPaints.size > 0) await Promise.allSettled([...pendingPaints]);
21764
21920
  }
21765
- function printStylesheet(pageWidthPx, pageHeightPx) {
21921
+ function printStylesheet(pageWidthPx, pageHeightPx, root) {
21922
+ const inches = (px) => `${Math.round(px / 96 * 1e3) / 1e3}in`;
21923
+ const sizes = /* @__PURE__ */ new Set();
21924
+ for (const el26 of root ? Array.from(root.querySelectorAll(".dxw-page[data-page-width]")) : []) {
21925
+ sizes.add(`${el26.dataset.pageWidth}x${el26.dataset.pageHeight}`);
21926
+ }
21927
+ const named = [...sizes].map((key) => {
21928
+ const [w, h] = key.split("x");
21929
+ const name = `dxw-${key.replace(/\./g, "_")}`;
21930
+ return `@page ${name} { size: ${inches(Number(w))} ${inches(Number(h))}; margin: 0; }
21931
+ .dxw-page[data-page-width="${w}"][data-page-height="${h}"] { page: ${name}; }`;
21932
+ });
21766
21933
  return `
21767
- @page { size: ${pageWidthPx / 96}in ${pageHeightPx / 96}in; margin: 0; }
21934
+ @page { size: ${inches(pageWidthPx)} ${inches(pageHeightPx)}; margin: 0; }
21935
+ ${named.join("\n ")}
21768
21936
  html, body { margin: 0; padding: 0; }
21769
21937
  .dxw-pages { display: block !important; padding: 0 !important; gap: 0 !important; }
21770
21938
  .dxw-pages, .dxw-pages * {
@@ -21775,13 +21943,25 @@ function printStylesheet(pageWidthPx, pageHeightPx) {
21775
21943
  .dxw-comment-card, .dxw-hf-marker { display: none !important; }
21776
21944
  `;
21777
21945
  }
21946
+ function pruneBlankSpans(root) {
21947
+ let removed = 0;
21948
+ for (const el26 of Array.from(root.querySelectorAll(".dxw-page span"))) {
21949
+ if (el26.childElementCount > 0 || el26.textContent?.trim()) continue;
21950
+ if (el26.style.textDecoration.includes("underline")) continue;
21951
+ el26.remove();
21952
+ removed++;
21953
+ }
21954
+ return removed;
21955
+ }
21778
21956
  function buildPrintHtml(root, pageWidthPx, pageHeightPx) {
21779
21957
  const head = [`<base href="${document.location.href}">`];
21780
21958
  for (const node of Array.from(document.head.querySelectorAll("style, link[rel=stylesheet]"))) {
21781
21959
  head.push(node.outerHTML);
21782
21960
  }
21783
- head.push(`<style>${printStylesheet(pageWidthPx, pageHeightPx)}</style>`);
21784
- return `<!doctype html><html><head>${head.join("")}</head><body>${root.outerHTML}</body></html>`;
21961
+ head.push(`<style>${printStylesheet(pageWidthPx, pageHeightPx, root)}</style>`);
21962
+ const pages = root.cloneNode(true);
21963
+ pruneBlankSpans(pages);
21964
+ return `<!doctype html><html><head>${head.join("")}</head><body>${pages.outerHTML}</body></html>`;
21785
21965
  }
21786
21966
  function printPages(root, pageWidthPx, pageHeightPx) {
21787
21967
  const iframe = document.createElement("iframe");
@@ -23828,7 +24008,9 @@ function revisionWrapper(sample, kind, content, meta) {
23828
24008
  }
23829
24009
  function markDeletedText(el26) {
23830
24010
  const walk2 = (e) => {
24011
+ if (localName(e.name) === "txbxContent") return;
23831
24012
  if (localName(e.name) === "t") e.name = e.name.replace(/t$/, "delText");
24013
+ else if (localName(e.name) === "instrText") e.name = e.name.replace(/instrText$/, "delInstrText");
23832
24014
  for (const c of e.children) walk2(c);
23833
24015
  };
23834
24016
  walk2(el26);
@@ -24087,7 +24269,8 @@ function ensurePPrRPr(pEl) {
24087
24269
  let rPr = pPr.children.find((c) => localName(c.name) === "rPr");
24088
24270
  if (!rPr) {
24089
24271
  rPr = el2(`${w}rPr`);
24090
- pPr.children.push(rPr);
24272
+ const at = pPr.children.findIndex((c) => localName(c.name) === "sectPr" || localName(c.name) === "pPrChange");
24273
+ pPr.children.splice(at === -1 ? pPr.children.length : at, 0, rPr);
24091
24274
  }
24092
24275
  return rPr;
24093
24276
  }
@@ -24247,6 +24430,8 @@ function convertDelTextToText(el26) {
24247
24430
  const walk2 = (e) => {
24248
24431
  if (localName(e.name) === "delText") {
24249
24432
  e.name = e.name.replace(/delText$/, "t");
24433
+ } else if (localName(e.name) === "delInstrText") {
24434
+ e.name = e.name.replace(/delInstrText$/, "instrText");
24250
24435
  }
24251
24436
  for (const c of e.children) walk2(c);
24252
24437
  };
@@ -24268,7 +24453,7 @@ function removeEl(doc, el26) {
24268
24453
  parent.children.splice(idx, 1);
24269
24454
  return true;
24270
24455
  }
24271
- var RUN_CONTAINERS = ["hyperlink", "smartTag", "dir", "bdo"];
24456
+ var RUN_CONTAINERS = ["hyperlink", "smartTag", "dir", "bdo", "fldSimple"];
24272
24457
  function removeRevisionAndEmptyHost(doc, el26) {
24273
24458
  const parent = doc.findParentOf(el26);
24274
24459
  if (!removeEl(doc, el26)) return false;
@@ -24277,26 +24462,57 @@ function removeRevisionAndEmptyHost(doc, el26) {
24277
24462
  }
24278
24463
  return true;
24279
24464
  }
24280
- function joinWithNext(doc, pEl) {
24465
+ function joinWithNext(doc, pEl, gone) {
24281
24466
  const parent = doc.findParentOf(pEl);
24282
24467
  if (!parent) return false;
24283
24468
  const idx = parent.children.indexOf(pEl);
24284
- const next = parent.children[idx + 1];
24285
- if (!next || localName(next.name) !== "p") return false;
24469
+ let at = idx + 1;
24470
+ while (at < parent.children.length && MARKERS.includes(localName(parent.children[at].name))) at++;
24471
+ const next = parent.children[at];
24472
+ if (!next || localName(next.name) !== "p") {
24473
+ const goes = gone === "del" ? ["pPr", "del", "moveFrom", "proofErr", ...MOVE_RANGES] : ["pPr", "ins", "moveTo", "proofErr", ...MOVE_RANGES];
24474
+ const stays = MARKERS.filter((m) => m !== "proofErr");
24475
+ 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);
24476
+ if (!pEl.children.every(removable)) return false;
24477
+ parent.children.splice(idx, 1, ...pEl.children.filter((c) => stays.includes(localName(c.name))));
24478
+ dropEmptiedControl(doc, parent);
24479
+ return true;
24480
+ }
24481
+ const between = parent.children.slice(idx + 1, at);
24286
24482
  const moved = next.children.filter((c) => localName(c.name) !== "pPr");
24287
- pEl.children.push(...moved);
24288
- parent.children.splice(idx + 1, 1);
24483
+ pEl.children.push(...between, ...moved);
24484
+ parent.children.splice(idx + 1, at - idx);
24485
+ carrySectionBreak(pEl, next);
24289
24486
  return true;
24290
24487
  }
24488
+ var MARKERS = ["bookmarkStart", "bookmarkEnd", "commentRangeStart", "commentRangeEnd", "permStart", "permEnd", "proofErr"];
24489
+ function carrySectionBreak(pEl, next) {
24490
+ const pPrOf = (p) => p.children.find((c) => localName(c.name) === "pPr");
24491
+ const sectOf = (p) => pPrOf(p)?.children.find((c) => localName(c.name) === "sectPr");
24492
+ const mine = sectOf(pEl);
24493
+ const theirs = sectOf(next);
24494
+ if (!mine && !theirs) return;
24495
+ let pPr = pPrOf(pEl);
24496
+ if (!pPr) {
24497
+ pPr = { name: "w:pPr", attrs: {}, children: [], text: "" };
24498
+ pEl.children.unshift(pPr);
24499
+ }
24500
+ pPr.children = pPr.children.filter((c) => c !== mine);
24501
+ if (theirs) {
24502
+ const at = pPr.children.findIndex((c) => localName(c.name) === "pPrChange");
24503
+ pPr.children.splice(at === -1 ? pPr.children.length : at, 0, theirs);
24504
+ }
24505
+ if (pPr.children.length === 0) pEl.children.splice(pEl.children.indexOf(pPr), 1);
24506
+ }
24291
24507
  function collectRevisions(doc) {
24292
24508
  const out = [];
24293
24509
  const walk2 = (el26) => {
24294
24510
  const ln = localName(el26.name);
24295
24511
  if (ln === "pPr") return;
24296
- if (ln === "ins" || ln === "del") {
24512
+ if (ln === "ins" || ln === "del" || ln === "moveTo" || ln === "moveFrom") {
24297
24513
  out.push({
24298
24514
  el: el26,
24299
- kind: ln === "ins" ? "insertion" : "deletion",
24515
+ kind: ln === "ins" || ln === "moveTo" ? "insertion" : "deletion",
24300
24516
  author: attrByLocal(el26, "author")
24301
24517
  });
24302
24518
  }
@@ -24330,10 +24546,10 @@ function collectRevisions(doc) {
24330
24546
  const rPr = pPr?.children.find((c) => localName(c.name) === "rPr");
24331
24547
  for (const c of rPr?.children ?? []) {
24332
24548
  const cn = localName(c.name);
24333
- if (cn === "ins" || cn === "del") {
24549
+ if (cn === "ins" || cn === "del" || cn === "moveTo" || cn === "moveFrom") {
24334
24550
  out.push({
24335
24551
  el: c,
24336
- kind: cn === "ins" ? "markInsertion" : "markDeletion",
24552
+ kind: cn === "ins" || cn === "moveTo" ? "markInsertion" : "markDeletion",
24337
24553
  paragraph: el26,
24338
24554
  author: attrByLocal(c, "author")
24339
24555
  });
@@ -24355,7 +24571,7 @@ function applyAccept(doc, ref) {
24355
24571
  case "markDeletion":
24356
24572
  if (!ref.paragraph) return false;
24357
24573
  removeMarkRevision(ref);
24358
- return joinWithNext(doc, ref.paragraph);
24574
+ return joinWithNext(doc, ref.paragraph, "del");
24359
24575
  case "rowInsertion":
24360
24576
  return removeRowRevision(doc, ref);
24361
24577
  case "rowDeletion":
@@ -24426,7 +24642,7 @@ function applyReject(doc, ref) {
24426
24642
  case "markInsertion":
24427
24643
  if (!ref.paragraph) return false;
24428
24644
  removeMarkRevision(ref);
24429
- return joinWithNext(doc, ref.paragraph);
24645
+ return joinWithNext(doc, ref.paragraph, "ins");
24430
24646
  case "markDeletion":
24431
24647
  return removeMarkRevision(ref);
24432
24648
  case "rowInsertion":
@@ -24477,11 +24693,23 @@ function applyToAll(doc, apply, author) {
24477
24693
  for (const part of held) notes.add(part);
24478
24694
  }
24479
24695
  if (n > 0) {
24696
+ dropSettledMoveRanges(doc);
24480
24697
  doc.markNotePartsChanged([...notes]);
24481
24698
  doc.refresh();
24482
24699
  }
24483
24700
  return n;
24484
24701
  }
24702
+ var MOVE_RANGES = ["moveFromRangeStart", "moveFromRangeEnd", "moveToRangeStart", "moveToRangeEnd"];
24703
+ function dropSettledMoveRanges(doc) {
24704
+ const roots = doc.revisionRoots();
24705
+ const has = (el26) => ["moveFrom", "moveTo"].includes(localName(el26.name)) || el26.children.some(has);
24706
+ if (roots.some(has)) return;
24707
+ const strip = (el26) => {
24708
+ el26.children = el26.children.filter((c) => !MOVE_RANGES.includes(localName(c.name)));
24709
+ for (const c of el26.children) strip(c);
24710
+ };
24711
+ for (const root of roots) strip(root);
24712
+ }
24485
24713
  function removeMarkRevision(ref) {
24486
24714
  const pPr = ref.paragraph?.children.find((c) => localName(c.name) === "pPr");
24487
24715
  const rPr = pPr?.children.find((c) => localName(c.name) === "rPr");
@@ -24513,9 +24741,24 @@ function removeRow(doc, ref) {
24513
24741
  const idx = tbl.children.indexOf(ref.row);
24514
24742
  if (idx === -1) return false;
24515
24743
  tbl.children.splice(idx, 1);
24516
- if (!tbl.children.some((c) => localName(c.name) === "tr")) removeEl(doc, tbl);
24744
+ if (!tbl.children.some((c) => localName(c.name) === "tr")) {
24745
+ const holder = doc.findParentOf(tbl);
24746
+ removeEl(doc, tbl);
24747
+ if (holder) dropEmptiedControl(doc, holder);
24748
+ }
24517
24749
  return true;
24518
24750
  }
24751
+ function dropEmptiedControl(doc, container) {
24752
+ const ln = localName(container.name);
24753
+ const control = ln === "sdtContent" ? doc.findParentOf(container) : ln === "customXml" ? container : void 0;
24754
+ if (!control) return;
24755
+ const left = container.children.filter((c) => localName(c.name) !== "customXmlPr");
24756
+ if (left.some((c) => !MARKERS.includes(localName(c.name)))) return;
24757
+ const holder = doc.findParentOf(control);
24758
+ if (!holder) return;
24759
+ holder.children.splice(holder.children.indexOf(control), 1, ...left.filter((c) => localName(c.name) !== "proofErr"));
24760
+ dropEmptiedControl(doc, holder);
24761
+ }
24519
24762
 
24520
24763
  // ../core/src/edit/blocks.ts
24521
24764
  function el3(name, attrs = {}, children3 = [], text = "") {
@@ -27815,7 +28058,7 @@ function sortTableRows(doc, tblEl, colIdx, order, compare, hasHeader = false) {
27815
28058
  return tc ? cellText(tc).trim() : "";
27816
28059
  };
27817
28060
  const numeric2 = (s) => parseFloat(s.replace(/[^0-9.eE+-]/g, ""));
27818
- const compareRows = (a, b) => {
28061
+ const compareRows2 = (a, b) => {
27819
28062
  const ka = keyOf2(a);
27820
28063
  const kb = keyOf2(b);
27821
28064
  let d;
@@ -27831,7 +28074,7 @@ function sortTableRows(doc, tblEl, colIdx, order, compare, hasHeader = false) {
27831
28074
  }
27832
28075
  return order === "asc" ? d : -d;
27833
28076
  };
27834
- const sorted = [...body].sort(compareRows);
28077
+ const sorted = [...body].sort(compareRows2);
27835
28078
  if (sorted.every((tr, i) => tr === body[i])) return false;
27836
28079
  const slots = body.map((tr) => tblEl.children.indexOf(tr));
27837
28080
  sorted.forEach((tr, i) => {
@@ -43010,6 +43253,15 @@ var COMPARE_TUNING = {
43010
43253
  shortParagraphTokens: 5,
43011
43254
  /** Levenshtein ratio a short paragraph pair must reach instead of minDice. */
43012
43255
  minShortRatio: 0.8,
43256
+ /**
43257
+ * Or the share of words a short pair has in common (Dice over words), which
43258
+ * a one-word edit in a short heading or list item keeps at half or more
43259
+ * ("Bullet one" / "Bullet updated", "Alignment and flow" / "Alignment and
43260
+ * flow as amended") while its character ratio falls under minShortRatio.
43261
+ * Below it, those came out struck and reinserted whole: 99% of the compare
43262
+ * benchmark's whole-paragraph misses were paragraphs under five words.
43263
+ */
43264
+ minShortWordDice: 0.5,
43013
43265
  /**
43014
43266
  * ABSOLUTE floor for the coalescing pass: a matched run shorter than this,
43015
43267
  * sitting between two changes in the inner word diff, is absorbed into the
@@ -43028,15 +43280,19 @@ var COMPARE_TUNING = {
43028
43280
  * next to 1) while a rewritten clause becomes one (3 is small next to 7).
43029
43281
  *
43030
43282
  * Both numbers were settled against test/compare-corpus.test.ts; see the
43031
- * calibration note there for what each fixture pins down.
43283
+ * calibration note there for what each fixture pins down. The ratio was 2
43284
+ * while spaces rode on words: "hardware," and "hardware " differed, so the
43285
+ * rewritten sentence of fixture 3 shared only three words. Matched exactly,
43286
+ * it shares four ("shortage of test hardware") beside seven changed, which
43287
+ * 1.5 absorbs (6 < 7) and 2 does not.
43032
43288
  */
43033
- coalesceRatio: 2
43289
+ coalesceRatio: 1.5
43034
43290
  };
43035
43291
  function normalizeText(s) {
43036
43292
  return s.replace(/\s+/g, " ").trim();
43037
43293
  }
43038
43294
  function wordTokens(s) {
43039
- return s.match(/\S+\s*|\s+/g) ?? [];
43295
+ 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
43296
  }
43041
43297
  function similarityTokens(s) {
43042
43298
  const n = normalizeText(s).toLowerCase();
@@ -43082,14 +43338,16 @@ function similarity(a, b) {
43082
43338
  const nb = normalizeText(b).toLowerCase();
43083
43339
  const longest = Math.max(na.length, nb.length);
43084
43340
  if (longest === 0) return 1;
43085
- return 1 - levenshtein(na, nb) / longest;
43341
+ const ratio = 1 - levenshtein(na, nb) / longest;
43342
+ const words2 = dice(new Set(ta), new Set(tb));
43343
+ return Math.max(words2, ratio >= COMPARE_TUNING.minShortRatio ? ratio : 0);
43086
43344
  }
43087
43345
  return dice(bigrams(ta), bigrams(tb));
43088
43346
  }
43089
43347
  function similarityFloor(a, b) {
43090
43348
  const la = similarityTokens(a).length;
43091
43349
  const lb = similarityTokens(b).length;
43092
- return isShortPair(la, lb) ? COMPARE_TUNING.minShortRatio : COMPARE_TUNING.minDice;
43350
+ return isShortPair(la, lb) ? COMPARE_TUNING.minShortWordDice : COMPARE_TUNING.minDice;
43093
43351
  }
43094
43352
  function isShortPair(la, lb) {
43095
43353
  return Math.min(la, lb) < COMPARE_TUNING.shortParagraphTokens;
@@ -43100,6 +43358,25 @@ var PASSENGERS = /* @__PURE__ */ new Set(["proofErr", "bookmarkStart", "bookmark
43100
43358
  function isPassenger(ln) {
43101
43359
  return PASSENGERS.has(ln);
43102
43360
  }
43361
+ var WORD_CHAR = /[\p{L}\p{M}\p{N}]/u;
43362
+ var CONTAINERS = /* @__PURE__ */ new Set(["hyperlink", "smartTag", "fldSimple", "customXml", "dir", "bdo", "sdt"]);
43363
+ var CONTAINER_PROPS = /* @__PURE__ */ new Set(["pPr", "sdtPr", "sdtEndPr", "smartTagPr", "customXmlPr"]);
43364
+ function atomKey(atom, noteKey) {
43365
+ const ln = localName(atom.name);
43366
+ if (ln === "footnoteReference" || ln === "endnoteReference") {
43367
+ const { "w:id": _id, ...rest } = atom.attrs;
43368
+ return serializeXml({ ...atom, attrs: rest }) + (noteKey ? "\0" + noteKey(atom) : "");
43369
+ }
43370
+ const strip = (e) => localName(e.name) === "txbxContent" ? { ...e, children: [], text: "" } : { ...e, children: e.children.map(strip) };
43371
+ return serializeXml(hasTextBox(atom) ? strip(atom) : atom);
43372
+ }
43373
+ function hasTextBox(el26) {
43374
+ return localName(el26.name) === "txbxContent" || el26.children.some(hasTextBox);
43375
+ }
43376
+ function textBoxesIn(el26) {
43377
+ if (localName(el26.name) === "txbxContent") return [el26];
43378
+ return el26.children.flatMap(textBoxesIn);
43379
+ }
43103
43380
  function readParagraph(pEl, options = {}) {
43104
43381
  const keyOf2 = options.whitespace === false ? (word) => word.trimEnd() : (word) => word;
43105
43382
  const tokens = [];
@@ -43112,31 +43389,80 @@ function readParagraph(pEl, options = {}) {
43112
43389
  }
43113
43390
  tokens.push(token);
43114
43391
  };
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
- }
43392
+ let segment = [];
43393
+ let segmentPath = [];
43394
+ const flush = () => {
43395
+ if (segment.length === 0) return;
43396
+ const text = segment.map((p) => p.text).join("");
43397
+ const cuts = [];
43398
+ let base = 0;
43399
+ for (let i = 0; i + 1 < segment.length; i++) {
43400
+ base += segment[i].text.length;
43401
+ if (WORD_CHAR.test(text[base - 1] ?? "") && WORD_CHAR.test(text[base] ?? "") && runPropsKey(segment[i].run) !== runPropsKey(segment[i + 1].run)) {
43402
+ cuts.push(base);
43403
+ }
43404
+ }
43405
+ const words2 = [];
43406
+ let from = 0;
43407
+ for (const cut of [...cuts, text.length]) {
43408
+ words2.push(...wordTokens(text.slice(from, cut)));
43409
+ from = cut;
43410
+ }
43411
+ let at = 0;
43412
+ for (const word of words2) {
43413
+ const end = at + word.length;
43414
+ const pieces = [];
43415
+ let base2 = 0;
43416
+ for (const p of segment) {
43417
+ const from2 = Math.max(at, base2);
43418
+ const to = Math.min(end, base2 + p.text.length);
43419
+ if (from2 < to) pieces.push({ run: p.run, t: p.t, start: from2 - base2, end: to - base2, text: text.slice(from2, to) });
43420
+ base2 += p.text.length;
43421
+ }
43422
+ const first = pieces[0];
43423
+ push({ key: "w\0" + keyOf2(word), text: word, run: first.run, t: first.t, start: first.start, end: first.end, pieces, path: segmentPath });
43424
+ at = end;
43425
+ }
43426
+ segment = [];
43427
+ };
43428
+ const walk2 = (kids, path2) => {
43429
+ for (const cEl of kids) {
43430
+ const ln = localName(cEl.name);
43431
+ if (CONTAINER_PROPS.has(ln)) continue;
43432
+ if (PASSENGERS.has(ln)) {
43433
+ flush();
43434
+ pending.push(cEl);
43435
+ continue;
43436
+ }
43437
+ if (CONTAINERS.has(ln)) {
43438
+ const inner = ln === "sdt" ? cEl.children.find((c) => localName(c.name) === "sdtContent") : cEl;
43439
+ if (!inner) continue;
43440
+ flush();
43441
+ walk2(inner.children, [...path2, cEl]);
43442
+ flush();
43135
43443
  continue;
43136
43444
  }
43137
- push({ key: "a\0" + serializeXml(rc), text: "", run: cEl, atom: rc });
43445
+ if (ln !== "r") {
43446
+ flush();
43447
+ inlineDiffable = false;
43448
+ continue;
43449
+ }
43450
+ if (segment.length > 0 && segmentPath !== path2) flush();
43451
+ segmentPath = path2;
43452
+ for (const rc of cEl.children) {
43453
+ const rln = localName(rc.name);
43454
+ if (rln === "rPr") continue;
43455
+ if (rln === "t") {
43456
+ segment.push({ run: cEl, t: rc, text: rc.text });
43457
+ continue;
43458
+ }
43459
+ flush();
43460
+ push({ key: "a\0" + atomKey(rc, options.noteKey), text: "", run: cEl, atom: rc, path: path2 });
43461
+ }
43138
43462
  }
43139
- }
43463
+ };
43464
+ walk2(pEl.children, []);
43465
+ flush();
43140
43466
  return { tokens, tail: pending, text: paragraphText3(pEl), inlineDiffable };
43141
43467
  }
43142
43468
  function paragraphText3(el26) {
@@ -43153,31 +43479,41 @@ function paragraphText3(el26) {
43153
43479
  }
43154
43480
  return out;
43155
43481
  }
43156
- function sliceContent(tokens, from, to, deleted) {
43482
+ function sliceItems(tokens, from, to, deleted) {
43157
43483
  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;
43484
+ let open = null;
43485
+ let text = null;
43486
+ const runFor = (run, path2) => {
43487
+ if (open?.run === run) return open.el;
43163
43488
  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++;
43489
+ const el26 = { name: run.name, attrs: { ...run.attrs }, children: rPr ? [cloneXml(rPr)] : [], text: "" };
43490
+ out.push({ el: el26, path: path2 });
43491
+ open = { run, el: el26 };
43492
+ text = null;
43493
+ return el26;
43494
+ };
43495
+ for (let i = from; i < to; i++) {
43496
+ const token = tokens[i];
43497
+ if (!deleted && token.lead) {
43498
+ out.push(...token.lead.map((el26) => ({ el: cloneXml(el26), path: token.path })));
43499
+ open = null;
43500
+ text = null;
43501
+ }
43502
+ if (token.atom) {
43503
+ runFor(token.run, token.path).children.push(cloneXml(token.atom));
43504
+ text = null;
43505
+ continue;
43506
+ }
43507
+ for (const piece of token.pieces ?? []) {
43508
+ const el26 = runFor(piece.run, token.path);
43509
+ if (text && text.t === piece.t) {
43510
+ text.el.text += piece.text;
43170
43511
  continue;
43171
43512
  }
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));
43513
+ const te = textElement2(piece.t, piece.text, deleted);
43514
+ el26.children.push(te);
43515
+ text = { t: piece.t, el: te };
43179
43516
  }
43180
- out.push({ name: run.name, attrs: { ...run.attrs }, children: children3, text: "" });
43181
43517
  }
43182
43518
  return out;
43183
43519
  }
@@ -43284,46 +43620,232 @@ function lcsFallback(a, b, aLo, aHi, bLo, bHi, out) {
43284
43620
 
43285
43621
  // ../core/src/edit/compare/index.ts
43286
43622
  function compareDocuments(original, revised, options) {
43287
- const merged = DocxDocument.load(revised.save());
43623
+ const note = options.onNote ?? (() => {
43624
+ });
43625
+ const accepted = (doc, which) => {
43626
+ if (collectRevisions(doc).length === 0) return doc;
43627
+ const copy = doc.clone();
43628
+ acceptAllRevisions(copy);
43629
+ note({ code: "existing-revisions-accepted", detail: `the ${which} document held tracked changes; it was compared as if they were accepted` });
43630
+ return copy;
43631
+ };
43632
+ const merged = accepted(revised, "revised");
43633
+ return compareInto(accepted(original, "original"), merged === revised ? revised.clone() : merged, options);
43634
+ }
43635
+ function compareInto(original, merged, options) {
43636
+ let issued = 0;
43288
43637
  const ctx = {
43289
43638
  doc: merged,
43290
43639
  original,
43291
43640
  meta: {
43292
43641
  author: options.author,
43293
43642
  date: options.date,
43294
- nextId: () => merged.nextRevisionId()
43643
+ nextId: () => {
43644
+ issued++;
43645
+ return merged.nextRevisionId();
43646
+ }
43295
43647
  },
43296
43648
  formatting: options.formatting !== false,
43297
43649
  whitespace: options.whitespace !== false,
43298
43650
  note: options.onNote ?? (() => {
43299
- })
43651
+ }),
43652
+ scope: { from: null, into: null },
43653
+ noteIds: { footnote: /* @__PURE__ */ new Map(), endnote: /* @__PURE__ */ new Map() }
43300
43654
  };
43301
43655
  const originalBody = child(original.docRoot, "body");
43302
43656
  const mergedBody = child(merged.docRoot, "body");
43303
43657
  if (!originalBody || !mergedBody) throw new Error("document.xml has no w:body");
43304
- reportUncomparedParts(original, revised, ctx);
43658
+ reportUncomparedParts(original, merged, ctx);
43659
+ const headerFooterPairs = pairHeadersFooters(original, merged);
43660
+ compareNotes("footnote", original, merged, ctx);
43661
+ compareNotes("endnote", original, merged, ctx);
43305
43662
  const tail = mergedBody.children.filter((c) => localName(c.name) === "sectPr");
43306
43663
  const entries = alignBlocks(blockList(originalBody, "sectPr"), blockList(mergedBody, "sectPr"), ctx);
43307
43664
  applyParagraphMarks(entries, ctx);
43308
43665
  mergedBody.children = [...entries.map((e) => e.el), ...tail];
43309
- merged.refresh();
43666
+ giveStruckReferencesTheirOwnNotes("footnote", merged, ctx);
43667
+ giveStruckReferencesTheirOwnNotes("endnote", merged, ctx);
43668
+ for (const [from, into] of headerFooterPairs) {
43669
+ const oRoot = original.headerFooterRoot(from);
43670
+ const mRoot = merged.headerFooterRoot(into);
43671
+ if (oRoot && mRoot) compareContainer(oRoot, mRoot, { ...ctx, scope: { from, into } });
43672
+ }
43673
+ if (issued > 0) merged.refresh();
43310
43674
  return merged;
43311
43675
  }
43676
+ function compareContainer(oRoot, mRoot, ctx) {
43677
+ const o = blockList(oRoot);
43678
+ const m = blockList(mRoot);
43679
+ if (o.length === m.length && o.every((el26, i) => sameXml(el26, m[i]))) return;
43680
+ const entries = alignBlocks(o, m, ctx);
43681
+ applyParagraphMarks(entries, ctx);
43682
+ const blocks = new Set(m);
43683
+ mRoot.children = [...mRoot.children.filter((c) => !blocks.has(c) && !["p", "tbl", "sdt"].includes(localName(c.name))), ...entries.map((e) => e.el)];
43684
+ }
43685
+ function pairHeadersFooters(original, merged) {
43686
+ const refs = (doc) => {
43687
+ const out = /* @__PURE__ */ new Map();
43688
+ const body = child(doc.docRoot, "body");
43689
+ const sects = (body?.children ?? []).flatMap(
43690
+ (c) => localName(c.name) === "sectPr" ? [c] : localName(c.name) === "p" ? [child(child(c, "pPr"), "sectPr")].filter((x) => !!x) : []
43691
+ );
43692
+ sects.forEach((sp, i) => {
43693
+ for (const ref of sp.children) {
43694
+ const ln = localName(ref.name);
43695
+ if (ln !== "headerReference" && ln !== "footerReference") continue;
43696
+ const target = doc.documentRels.get(ref.attrs["r:id"])?.target;
43697
+ if (target) out.set(`${i}:${ln}:${ref.attrs["w:type"] ?? "default"}`, target);
43698
+ }
43699
+ });
43700
+ return out;
43701
+ };
43702
+ const mine = refs(merged);
43703
+ const pairs = [];
43704
+ const done = /* @__PURE__ */ new Set();
43705
+ for (const [key, from] of refs(original)) {
43706
+ const into = mine.get(key);
43707
+ if (!into || done.has(into)) continue;
43708
+ done.add(into);
43709
+ pairs.push([from, into]);
43710
+ }
43711
+ return pairs;
43712
+ }
43713
+ function compareNotes(kind, original, merged, ctx) {
43714
+ const tree = (doc, create = false) => kind === "footnote" ? doc.footnotesTree(create) : doc.endnotesTree(create);
43715
+ const oRoot = tree(original);
43716
+ const mRoot = tree(merged);
43717
+ if (!oRoot && !mRoot) return;
43718
+ const refName = `${kind}Reference`;
43719
+ const inOrder = (doc, root) => {
43720
+ if (!root) return [];
43721
+ const byId = new Map(
43722
+ root.children.filter((c) => localName(c.name) === kind && !c.attrs["w:type"]).map((c) => [c.attrs["w:id"], c])
43723
+ );
43724
+ const out = [];
43725
+ const seen = /* @__PURE__ */ new Set();
43726
+ const walk2 = (e) => {
43727
+ if (localName(e.name) === refName) {
43728
+ const id = e.attrs["w:id"];
43729
+ const note = byId.get(id);
43730
+ if (note && !seen.has(id)) {
43731
+ seen.add(id);
43732
+ out.push(note);
43733
+ }
43734
+ }
43735
+ for (const c of e.children) walk2(c);
43736
+ };
43737
+ walk2(doc.docRoot);
43738
+ return out;
43739
+ };
43740
+ const oNotes = inOrder(original, oRoot);
43741
+ const rNotes = inOrder(merged, mRoot);
43742
+ const text = (n) => normalizeText(paragraphText3(n));
43743
+ const pairs = [];
43744
+ const waiting = /* @__PURE__ */ new Map();
43745
+ rNotes.forEach((n, j) => {
43746
+ const list = waiting.get(text(n));
43747
+ if (list) list.push(j);
43748
+ else waiting.set(text(n), [j]);
43749
+ });
43750
+ oNotes.forEach((n, i) => {
43751
+ const j = waiting.get(text(n))?.shift();
43752
+ if (j !== void 0) pairs.push([i, j]);
43753
+ });
43754
+ const exactO = new Set(pairs.map((p) => p[0]));
43755
+ const exactR = new Set(pairs.map((p) => p[1]));
43756
+ const oRest = oNotes.map((_, i) => i).filter((i) => !exactO.has(i));
43757
+ const rRest = rNotes.map((_, j) => j).filter((j) => !exactR.has(j));
43758
+ let oAt = 0;
43759
+ let rAt = 0;
43760
+ const rest = histogramMatch(oRest.map((i) => text(oNotes[i])), rRest.map((j) => text(rNotes[j])));
43761
+ for (const a of [...rest, { a: oRest.length, b: rRest.length }]) {
43762
+ if (a.a - oAt === 1 && a.b - rAt === 1) pairs.push([oRest[oAt], rRest[rAt]]);
43763
+ if (a.a < oRest.length && a.b < rRest.length) pairs.push([oRest[a.a], rRest[a.b]]);
43764
+ oAt = a.a + 1;
43765
+ rAt = a.b + 1;
43766
+ }
43767
+ const ids = ctx.noteIds[kind];
43768
+ const pairedO = new Set(pairs.map((p) => p[0]));
43769
+ const pairedR = new Set(pairs.map((p) => p[1]));
43770
+ let changed = false;
43771
+ for (const [i, j] of pairs) {
43772
+ ids.set(oNotes[i].attrs["w:id"], rNotes[j].attrs["w:id"]);
43773
+ const before = ctx.meta.nextId;
43774
+ let touched = false;
43775
+ compareContainer(oNotes[i], rNotes[j], { ...ctx, meta: { ...ctx.meta, nextId: () => (touched = true, before()) } });
43776
+ changed || (changed = touched);
43777
+ }
43778
+ rNotes.forEach((note, j) => {
43779
+ if (pairedR.has(j)) return;
43780
+ for (const block of blockList(note)) if (localName(block.name) === "p") wrapContainerContent(block, "ins", ctx);
43781
+ changed = true;
43782
+ });
43783
+ const removed = oNotes.filter((_, i) => !pairedO.has(i));
43784
+ if (removed.length > 0) {
43785
+ const root = tree(merged, true);
43786
+ if (root) {
43787
+ let next = 1 + Math.max(0, ...root.children.map((c) => Number(c.attrs["w:id"]) || 0));
43788
+ for (const o of removed) {
43789
+ const copy = cloneXml(o);
43790
+ copy.attrs["w:id"] = String(next);
43791
+ ids.set(o.attrs["w:id"], String(next++));
43792
+ for (const block of blockList(copy)) {
43793
+ if (localName(block.name) !== "p") continue;
43794
+ adoptFromOriginal(block, { ...ctx, scope: { from: null, into: null } });
43795
+ wrapContainerContent(block, "del", ctx);
43796
+ }
43797
+ root.children.push(copy);
43798
+ }
43799
+ changed = true;
43800
+ }
43801
+ }
43802
+ if (changed) merged.markNotePartsChanged([kind === "footnote" ? "footnotes" : "endnotes"]);
43803
+ }
43804
+ function giveStruckReferencesTheirOwnNotes(kind, merged, ctx) {
43805
+ const root = kind === "footnote" ? merged.footnotesTree() : merged.endnotesTree();
43806
+ if (!root) return;
43807
+ const refName = `${kind}Reference`;
43808
+ const refs = /* @__PURE__ */ new Map();
43809
+ const walk2 = (e, struck) => {
43810
+ if (localName(e.name) === refName) {
43811
+ const list = refs.get(e.attrs["w:id"]) ?? [];
43812
+ list.push({ ref: e, struck });
43813
+ refs.set(e.attrs["w:id"], list);
43814
+ }
43815
+ for (const c of e.children) walk2(c, struck || localName(c.name) === "del" || rowDeleted(c));
43816
+ };
43817
+ const rowDeleted = (tr) => localName(tr.name) === "tr" && !!child(child(tr, "trPr"), "del");
43818
+ walk2(merged.docRoot, false);
43819
+ let next = 1 + Math.max(0, ...root.children.map((c) => Number(c.attrs["w:id"]) || 0));
43820
+ let changed = false;
43821
+ for (const [id, list] of refs) {
43822
+ if (list.length < 2) continue;
43823
+ const note = root.children.find((c) => localName(c.name) === kind && c.attrs["w:id"] === id);
43824
+ if (!note) continue;
43825
+ const keep = list.find((r) => !r.struck) ?? list[0];
43826
+ for (const r of list) {
43827
+ if (r === keep || !r.struck) continue;
43828
+ const copy = cloneXml(note);
43829
+ copy.attrs["w:id"] = String(next);
43830
+ r.ref.attrs["w:id"] = String(next++);
43831
+ for (const block of blockList(copy)) if (localName(block.name) === "p") wrapContainerContent(block, "del", ctx);
43832
+ root.children.push(copy);
43833
+ changed = true;
43834
+ }
43835
+ }
43836
+ if (changed) merged.markNotePartsChanged([kind === "footnote" ? "footnotes" : "endnotes"]);
43837
+ }
43312
43838
  function blockList(container, ...skip) {
43313
43839
  return container.children.filter((c) => !skip.includes(localName(c.name)));
43314
43840
  }
43315
43841
  function reportUncomparedParts(original, revised, ctx) {
43316
43842
  const parts = [
43317
43843
  ["styles.xml", original.stylesTree()],
43318
- ["numbering.xml", original.numberingTree()],
43319
- ["footnotes.xml", original.footnotesTree()],
43320
- ["endnotes.xml", original.endnotesTree()]
43844
+ ["numbering.xml", original.numberingTree()]
43321
43845
  ];
43322
43846
  const revisedTrees = {
43323
43847
  "styles.xml": revised.stylesTree(),
43324
- "numbering.xml": revised.numberingTree(),
43325
- "footnotes.xml": revised.footnotesTree(),
43326
- "endnotes.xml": revised.endnotesTree()
43848
+ "numbering.xml": revised.numberingTree()
43327
43849
  };
43328
43850
  for (const [name, tree] of parts) {
43329
43851
  const other = revisedTrees[name];
@@ -43333,13 +43855,12 @@ function reportUncomparedParts(original, revised, ctx) {
43333
43855
  ctx.note({ code: "part-not-compared", detail: `${name} differs; the result carries the revised document's` });
43334
43856
  }
43335
43857
  }
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
43858
  }
43341
43859
  function alignBlocks(oBlocks, rBlocks, ctx) {
43342
- const anchors = histogramMatch(oBlocks.map(blockFingerprint), rBlocks.map(blockFingerprint));
43860
+ const anchors = histogramMatch(
43861
+ oBlocks.map((el26) => blockFingerprint(el26, ctx.original)),
43862
+ rBlocks.map((el26) => blockFingerprint(el26, ctx.doc))
43863
+ );
43343
43864
  const entries = [];
43344
43865
  let oAt = 0;
43345
43866
  let rAt = 0;
@@ -43353,19 +43874,23 @@ function alignBlocks(oBlocks, rBlocks, ctx) {
43353
43874
  }
43354
43875
  return entries;
43355
43876
  }
43356
- function blockFingerprint(el26) {
43877
+ function blockFingerprint(el26, doc) {
43357
43878
  const ln = localName(el26.name);
43358
43879
  if (ln === "p") {
43359
43880
  const pPr = child(el26, "pPr");
43360
43881
  const style = child(pPr, "pStyle")?.attrs["w:val"] ?? "";
43361
43882
  const numPr = child(pPr, "numPr");
43362
- return `P\0${normalizeText(paragraphText3(el26))}\0${style}\0${numPr ? serializeXml(numPr) : ""}`;
43883
+ const jc = child(pPr, "jc")?.attrs["w:val"] ?? "";
43884
+ const sect = sectionKey(el26, doc);
43885
+ return `P\0${normalizeText(paragraphText3(el26))}\0${style}\0${numPr ? serializeXml(numPr) : ""}\0${jc}\0${sect}`;
43363
43886
  }
43364
43887
  if (ln === "tbl") return `T\0${normalizeText(paragraphText3(el26))}`;
43365
43888
  return `X\0${serializeXml(el26)}`;
43366
43889
  }
43367
43890
  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);
43891
+ const before = entries[entries.length - 1];
43892
+ const unanchored = (!before || !isParagraph(before.el)) && (rHi >= rBlocks.length || !isParagraph(rBlocks[rHi]));
43893
+ const pairings = resolveGap(oBlocks, rBlocks, oLo, oHi, rLo, rHi, ctx, oLo === 0 && rLo === 0 || unanchored);
43369
43894
  let oAt = oLo;
43370
43895
  let rAt = rLo;
43371
43896
  for (const pairing of [...pairings, { kind: "pair", o: oHi, r: rHi, other: -1 }]) {
@@ -43397,16 +43922,43 @@ function emitGap(oBlocks, rBlocks, oLo, oHi, rLo, rHi, entries, ctx) {
43397
43922
  }
43398
43923
  }
43399
43924
  }
43925
+ var GAP_PAIR_CAP = 200 * 200;
43926
+ var GAP_WINDOW = 50;
43400
43927
  function resolveGap(oBlocks, rBlocks, oLo, oHi, rLo, rHi, ctx, needsAnchor = false) {
43401
43928
  const out = [];
43402
43929
  const oUsed = /* @__PURE__ */ new Set();
43403
43930
  const rUsed = /* @__PURE__ */ new Set();
43404
- const crosses = (o, r) => out.some((p) => o < p.o !== r < p.r);
43931
+ const placed = [];
43932
+ const crosses = (o, r) => {
43933
+ let lo = 0;
43934
+ let hi = placed.length;
43935
+ while (lo < hi) {
43936
+ const mid = lo + hi >> 1;
43937
+ if (placed[mid].o < o) lo = mid + 1;
43938
+ else hi = mid;
43939
+ }
43940
+ return lo > 0 && placed[lo - 1].r >= r || lo < placed.length && placed[lo].r <= r;
43941
+ };
43942
+ const accept = (p) => {
43943
+ out.push(p);
43944
+ let at = placed.length;
43945
+ while (at > 0 && placed[at - 1].o > p.o) at--;
43946
+ placed.splice(at, 0, p);
43947
+ };
43948
+ const oLen = oHi - oLo;
43949
+ const rLen = rHi - rLo;
43950
+ const windowed = oLen * rLen > GAP_PAIR_CAP;
43951
+ const span = (i) => {
43952
+ if (!windowed) return [rLo, rHi];
43953
+ const centre = rLo + Math.round((i - oLo) * rLen / Math.max(1, oLen));
43954
+ return [Math.max(rLo, centre - GAP_WINDOW), Math.min(rHi, centre + GAP_WINDOW + 1)];
43955
+ };
43405
43956
  for (let i = oLo; i < oHi; i++) {
43406
- for (let j = rLo; j + 1 < rHi; j++) {
43957
+ const [jLo, jHi] = span(i);
43958
+ for (let j = jLo; j + 1 < jHi; j++) {
43407
43959
  if (oUsed.has(i) || rUsed.has(j) || rUsed.has(j + 1) || crosses(i, j)) continue;
43408
43960
  if (concatMatches([oBlocks[i]], [rBlocks[j], rBlocks[j + 1]], ctx)) {
43409
- out.push({ kind: "split", o: i, r: j, other: j + 1 });
43961
+ accept({ kind: "split", o: i, r: j, other: j + 1 });
43410
43962
  oUsed.add(i);
43411
43963
  rUsed.add(j);
43412
43964
  rUsed.add(j + 1);
@@ -43414,10 +43966,11 @@ function resolveGap(oBlocks, rBlocks, oLo, oHi, rLo, rHi, ctx, needsAnchor = fal
43414
43966
  }
43415
43967
  }
43416
43968
  for (let i = oLo; i + 1 < oHi; i++) {
43417
- for (let j = rLo; j < rHi; j++) {
43969
+ const [jLo, jHi] = span(i);
43970
+ for (let j = jLo; j < jHi; j++) {
43418
43971
  if (oUsed.has(i) || oUsed.has(i + 1) || rUsed.has(j) || crosses(i, j)) continue;
43419
43972
  if (concatMatches([oBlocks[i], oBlocks[i + 1]], [rBlocks[j]], ctx)) {
43420
- out.push({ kind: "merge", o: i, r: j, other: i + 1 });
43973
+ accept({ kind: "merge", o: i, r: j, other: i + 1 });
43421
43974
  oUsed.add(i);
43422
43975
  oUsed.add(i + 1);
43423
43976
  rUsed.add(j);
@@ -43427,9 +43980,10 @@ function resolveGap(oBlocks, rBlocks, oLo, oHi, rLo, rHi, ctx, needsAnchor = fal
43427
43980
  const scored = [];
43428
43981
  for (let i = oLo; i < oHi; i++) {
43429
43982
  if (oUsed.has(i)) continue;
43430
- for (let j = rLo; j < rHi; j++) {
43983
+ const [jLo, jHi] = span(i);
43984
+ for (let j = jLo; j < jHi; j++) {
43431
43985
  if (rUsed.has(j)) continue;
43432
- if (localName(oBlocks[i].name) !== localName(rBlocks[j].name)) continue;
43986
+ if (!pairable(oBlocks[i], rBlocks[j], ctx)) continue;
43433
43987
  const a = paragraphText3(oBlocks[i]);
43434
43988
  const b = paragraphText3(rBlocks[j]);
43435
43989
  const score = similarity(a, b);
@@ -43440,30 +43994,74 @@ function resolveGap(oBlocks, rBlocks, oLo, oHi, rLo, rHi, ctx, needsAnchor = fal
43440
43994
  for (const cand of scored) {
43441
43995
  if (oUsed.has(cand.o) || rUsed.has(cand.r)) continue;
43442
43996
  if (crosses(cand.o, cand.r)) continue;
43443
- out.push({ kind: "pair", o: cand.o, r: cand.r, other: -1 });
43997
+ accept({ kind: "pair", o: cand.o, r: cand.r, other: -1 });
43444
43998
  oUsed.add(cand.o);
43445
43999
  rUsed.add(cand.r);
43446
44000
  }
44001
+ const settled = [...out].sort((a, b) => a.o - b.o);
44002
+ let oFrom = oLo;
44003
+ let rFrom = rLo;
44004
+ for (const p of [...settled, { kind: "pair", o: oHi, r: rHi, other: -1 }]) {
44005
+ const os = [];
44006
+ const rs = [];
44007
+ for (let i = oFrom; i < p.o; i++) if (!oUsed.has(i)) os.push(i);
44008
+ for (let j = rFrom; j < p.r; j++) if (!rUsed.has(j)) rs.push(j);
44009
+ if (os.length === 1 && rs.length === 1 && sameShape2(oBlocks[os[0]], rBlocks[rs[0]], ctx)) {
44010
+ accept({ kind: "pair", o: os[0], r: rs[0], other: -1 });
44011
+ oUsed.add(os[0]);
44012
+ rUsed.add(rs[0]);
44013
+ }
44014
+ oFrom = p.kind === "merge" ? p.other + 1 : p.o + 1;
44015
+ rFrom = p.kind === "split" ? p.other + 1 : p.r + 1;
44016
+ }
43447
44017
  if (needsAnchor && out.length === 0) {
43448
44018
  for (let i = oLo; i < oHi && out.length === 0; i++) {
43449
44019
  if (oUsed.has(i)) continue;
43450
44020
  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 });
44021
+ if (rUsed.has(j) || !pairable(oBlocks[i], rBlocks[j], ctx)) continue;
44022
+ accept({ kind: "pair", o: i, r: j, other: -1 });
43453
44023
  break;
43454
44024
  }
43455
44025
  }
43456
44026
  }
43457
44027
  return out.sort((a, b) => a.o - b.o);
43458
44028
  }
44029
+ function pairable(a, b, ctx) {
44030
+ if (localName(a.name) !== localName(b.name)) return false;
44031
+ if (!isParagraph(a)) return true;
44032
+ return sectionKey(a, ctx.original) === sectionKey(b, ctx.doc);
44033
+ }
44034
+ function sectionKey(el26, doc) {
44035
+ const sectPr = child(child(el26, "pPr"), "sectPr");
44036
+ if (!sectPr) return "";
44037
+ return serializeXml(sectPr).replace(/r:id="([^"]*)"/g, (_, id) => `r:id="${doc.documentRels.get(id)?.target ?? id}"`);
44038
+ }
44039
+ function sameShape2(a, b, ctx) {
44040
+ if (isParagraph(a) && isParagraph(b)) return pairable(a, b, ctx);
44041
+ if (localName(a.name) === localName(b.name) && ["tbl", "sdt", "customXml"].includes(localName(a.name))) return true;
44042
+ const cells = (tr) => tr.children.filter((c) => localName(c.name) === "tc").length;
44043
+ return localName(a.name) === "tr" && localName(b.name) === "tr" && cells(a) === cells(b);
44044
+ }
44045
+ function readOptions(ctx, original) {
44046
+ const noteKey = (ref) => {
44047
+ const kind = localName(ref.name) === "footnoteReference" ? "footnote" : "endnote";
44048
+ const id = ref.attrs["w:id"];
44049
+ return original ? `${ctx.noteIds[kind].get(id) ?? `original ${id}`}` : id;
44050
+ };
44051
+ return { whitespace: ctx.whitespace, noteKey };
44052
+ }
43459
44053
  function concatMatches(oEls, rEls, ctx) {
43460
44054
  const key = (els) => {
43461
44055
  const parts = [];
43462
44056
  for (const el26 of els) {
43463
44057
  if (localName(el26.name) !== "p") return null;
43464
- const content = readParagraph(el26, { whitespace: ctx.whitespace });
44058
+ if (child(child(el26, "pPr"), "sectPr")) return null;
44059
+ const content = readParagraph(el26, readOptions(ctx, oEls.includes(el26)));
43465
44060
  if (!content.inlineDiffable) return null;
43466
- for (const token of content.tokens) parts.push(token.key + "" + runPropsKey(token.run));
44061
+ for (const token of content.tokens) {
44062
+ const props = token.pieces ? token.pieces.map((p) => `${p.text.length}:${runPropsKey(p.run)}`).join("|") : runPropsKey(token.run);
44063
+ parts.push(token.key + "" + props + "" + token.path.map((c) => serializeXml({ ...c, children: c.children.filter((k) => /Pr$/.test(localName(k.name))) })).join(">"));
44064
+ }
43467
44065
  }
43468
44066
  return parts.join("");
43469
44067
  };
@@ -43475,14 +44073,24 @@ function emitMatched(oEl, rEl, ctx) {
43475
44073
  const ln = localName(oEl.name);
43476
44074
  if (ln === "p") comparePair(oEl, rEl, ctx);
43477
44075
  else if (ln === "tbl") compareTable(oEl, rEl, ctx);
44076
+ else if (ln === "sdt") {
44077
+ const o = child(oEl, "sdtContent");
44078
+ const r = child(rEl, "sdtContent");
44079
+ if (o && r) compareContainer(o, r, ctx);
44080
+ } else if (ln === "customXml") compareContainer(oEl, rEl, ctx);
43478
44081
  return { el: rEl, cls: "kept" };
43479
44082
  }
43480
44083
  var REL_ATTRS = ["id", "embed", "link", "pict", "dm", "lo", "qs", "cs"];
43481
44084
  function adoptFromOriginal(root, ctx) {
43482
- const mine = ctx.doc.documentRels;
43483
- const theirs = ctx.original.documentRels;
44085
+ const mine = ctx.doc.relationshipsOf(ctx.scope.into);
44086
+ const theirs = ctx.original.relationshipsOf(ctx.scope.from);
43484
44087
  const remapped = /* @__PURE__ */ new Map();
43485
44088
  const walk2 = (el26) => {
44089
+ const ln = localName(el26.name);
44090
+ if (ln === "footnoteReference" || ln === "endnoteReference") {
44091
+ const to = ctx.noteIds[ln === "footnoteReference" ? "footnote" : "endnote"].get(el26.attrs["w:id"]);
44092
+ if (to !== void 0) el26.attrs["w:id"] = to;
44093
+ }
43486
44094
  for (const key of Object.keys(el26.attrs)) {
43487
44095
  if (!key.startsWith("r:") || !REL_ATTRS.includes(localName(key))) continue;
43488
44096
  const id = el26.attrs[key];
@@ -43492,7 +44100,7 @@ function adoptFromOriginal(root, ctx) {
43492
44100
  if (here && here.target === source.target && here.type === source.type) continue;
43493
44101
  let replacement = remapped.get(id);
43494
44102
  if (replacement === void 0) {
43495
- replacement = source.external ? ctx.doc.addHyperlinkRel(source.target) : null;
44103
+ replacement = source.external ? ctx.doc.addHyperlinkRel(source.target, ctx.scope.into) : ctx.doc.adoptRelFrom(ctx.original, id, ctx.scope);
43496
44104
  if (replacement === null) {
43497
44105
  ctx.note({
43498
44106
  code: "unresolved-relationship",
@@ -43511,32 +44119,31 @@ function adoptFromOriginal(root, ctx) {
43511
44119
  function emitDeleted(el26, ctx) {
43512
44120
  const clone = cloneXml(el26);
43513
44121
  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
- }
44122
+ markWholeBlock(clone, "del", ctx);
43525
44123
  return { el: clone, cls: "deleted" };
43526
44124
  }
43527
44125
  function emitInserted(el26, ctx) {
44126
+ markWholeBlock(el26, "ins", ctx);
44127
+ return { el: el26, cls: "inserted" };
44128
+ }
44129
+ function markWholeBlock(el26, kind, ctx) {
43528
44130
  const ln = localName(el26.name);
43529
44131
  if (ln === "p") {
43530
- wrapContainerContent(el26, "ins", ctx);
44132
+ wrapContainerContent(el26, kind, ctx);
43531
44133
  } else if (ln === "tbl") {
43532
44134
  for (const row of el26.children.filter((c) => localName(c.name) === "tr")) {
43533
- markRowRevision(row, "ins", ctx.meta);
43534
- wrapRowContent(row, "ins", ctx);
44135
+ markRowRevision(row, kind, ctx.meta);
44136
+ wrapRowContent(row, kind, ctx);
44137
+ }
44138
+ } else if (ln === "sdt" || ln === "customXml") {
44139
+ const inner = ln === "sdt" ? child(el26, "sdtContent") : el26;
44140
+ for (const block of inner?.children ?? []) {
44141
+ markWholeBlock(block, kind, ctx);
44142
+ if (isParagraph(block)) markParagraphGlyph(block, kind, ctx.meta);
43535
44143
  }
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` });
44144
+ } else if (!isPassenger(ln) && ln !== "sdtPr" && ln !== "sdtEndPr" && ln !== "customXmlPr") {
44145
+ ctx.note({ code: "untracked-block", detail: `a <${el26.name}> block was ${kind === "ins" ? "added" : "removed"} and is not expressible as a revision` });
43538
44146
  }
43539
- return { el: el26, cls: "inserted" };
43540
44147
  }
43541
44148
  function wrapRowContent(row, kind, ctx) {
43542
44149
  for (const cell of row.children.filter((c) => localName(c.name) === "tc")) {
@@ -43546,7 +44153,8 @@ function wrapRowContent(row, kind, ctx) {
43546
44153
  }
43547
44154
  }
43548
44155
  function wrapContainerContent(container, kind, ctx) {
43549
- const NESTED = ["hyperlink", "smartTag", "dir", "bdo"];
44156
+ const NESTED = ["hyperlink", "smartTag", "dir", "bdo", "fldSimple"];
44157
+ const WRAPPABLE = ["r", "sdt", "customXml"];
43550
44158
  const out = [];
43551
44159
  let group = [];
43552
44160
  const flush = () => {
@@ -43557,7 +44165,7 @@ function wrapContainerContent(container, kind, ctx) {
43557
44165
  };
43558
44166
  for (const c of container.children) {
43559
44167
  const ln = localName(c.name);
43560
- if (ln === "r") {
44168
+ if (WRAPPABLE.includes(ln)) {
43561
44169
  group.push(c);
43562
44170
  continue;
43563
44171
  }
@@ -43575,10 +44183,10 @@ function wrapContainerContent(container, kind, ctx) {
43575
44183
  container.children = out;
43576
44184
  }
43577
44185
  function comparePair(oEl, rEl, ctx) {
43578
- if (serializeXml(oEl) === serializeXml(rEl)) return;
44186
+ if (sameXml(oEl, rEl)) return;
43579
44187
  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 });
44188
+ const oc = readParagraph(oEl, readOptions(ctx, true));
44189
+ const rc = readParagraph(rEl, readOptions(ctx, false));
43582
44190
  if (!oc.inlineDiffable || !rc.inlineDiffable) {
43583
44191
  if (oc.text !== rc.text || serializeXml(stripProps(oEl)) !== serializeXml(stripProps(rEl))) {
43584
44192
  const deleted = cloneXml(oEl);
@@ -43593,70 +44201,180 @@ function comparePair(oEl, rEl, ctx) {
43593
44201
  ];
43594
44202
  ctx.note({
43595
44203
  code: "coarse-paragraph",
43596
- detail: "a paragraph holding a hyperlink, content control, smart tag or inline math changed; struck and reinserted whole"
44204
+ detail: "a paragraph holding inline math or other content a word diff cannot rebuild changed; struck and reinserted whole"
43597
44205
  });
43598
44206
  }
43599
44207
  return;
43600
44208
  }
43601
44209
  const segments = wordSegments(oc.tokens, rc.tokens);
43602
44210
  const changed = segments.some((s) => !s.equal);
43603
- if (!changed) {
43604
- if (ctx.formatting) alignRunPropsInPlace(oEl, rEl, ctx);
43605
- return;
43606
- }
44211
+ const flat = oc.tokens.every((t) => t.path.length === 0) && rc.tokens.every((t) => t.path.length === 0);
44212
+ if (!changed && (!ctx.formatting || flat && alignRunPropsInPlace(oEl, rEl, ctx))) return;
43607
44213
  const pPr = child(rEl, "pPr");
43608
- const out = [];
44214
+ const items = [];
44215
+ const counterpart = /* @__PURE__ */ new Map();
43609
44216
  for (const seg of segments) {
43610
44217
  if (seg.equal) {
43611
- out.push(...emitEqualSpan(oc.tokens, rc.tokens, seg, ctx));
44218
+ for (const it of emitEqualSpan(oc.tokens, rc.tokens, seg, ctx, counterpart)) items.push({ ...it, kind: null });
43612
44219
  continue;
43613
44220
  }
43614
44221
  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));
44222
+ for (const it of sliceItems(oc.tokens, seg.aStart, seg.aEnd, true)) {
44223
+ adoptFromOriginal(it.el, ctx);
44224
+ markDeletedText(it.el);
44225
+ items.push({ ...it, kind: "del" });
44226
+ }
43618
44227
  }
43619
44228
  if (seg.bEnd > seg.bStart) {
43620
- out.push(revisionWrapper(rEl, "ins", sliceContent(rc.tokens, seg.bStart, seg.bEnd, false), ctx.meta));
44229
+ for (const it of sliceItems(rc.tokens, seg.bStart, seg.bEnd, false)) items.push({ ...it, kind: "ins" });
43621
44230
  }
43622
44231
  }
43623
- out.push(...rc.tail.map(cloneXml));
43624
- rEl.children = [...pPr ? [pPr] : [], ...out];
44232
+ for (const it of items) if (it.kind === "del") it.path = it.path.map((c) => counterpart.get(c) ?? c);
44233
+ for (const el26 of rc.tail) items.push({ el: cloneXml(el26), path: [], kind: null });
44234
+ const fromOriginal = new Set(oc.tokens.flatMap((t) => t.path));
44235
+ rEl.children = [...pPr ? [pPr] : [], ...nestItems(items, 0, rEl, ctx, fromOriginal)];
44236
+ }
44237
+ var WRAPPABLE_CONTAINERS = /* @__PURE__ */ new Set(["sdt", "customXml"]);
44238
+ function compareTextBoxes(oAtom, rAtom, ctx) {
44239
+ const theirs = textBoxesIn(oAtom);
44240
+ const mine = textBoxesIn(rAtom);
44241
+ for (let i = 0; i < Math.min(theirs.length, mine.length); i++) {
44242
+ if (sameXml(theirs[i], mine[i])) continue;
44243
+ const entries = alignBlocks(blockList(theirs[i]), blockList(mine[i]), ctx);
44244
+ applyParagraphMarks(entries, ctx);
44245
+ mine[i].children = entries.map((e) => e.el);
44246
+ }
44247
+ }
44248
+ function nestItems(items, depth, rEl, ctx, fromOriginal) {
44249
+ const out = [];
44250
+ let i = 0;
44251
+ while (i < items.length) {
44252
+ const c = items[i].path[depth];
44253
+ let j = i + 1;
44254
+ if (c) {
44255
+ while (j < items.length && items[j].path[depth] === c) j++;
44256
+ const group = items.slice(i, j);
44257
+ const kind = group[0].kind;
44258
+ if (kind && WRAPPABLE_CONTAINERS.has(localName(c.name)) && group.every((it) => it.kind === kind)) {
44259
+ const plain = group.map((it) => ({ ...it, kind: null }));
44260
+ out.push(revisionWrapper(rEl, kind, [containerShell(c, nestItems(plain, depth + 1, rEl, ctx, fromOriginal), fromOriginal.has(c), ctx)], ctx.meta));
44261
+ } else {
44262
+ out.push(containerShell(c, nestItems(group, depth + 1, rEl, ctx, fromOriginal), fromOriginal.has(c), ctx));
44263
+ }
44264
+ } else {
44265
+ const kind = items[i].kind;
44266
+ while (j < items.length && !items[j].path[depth] && items[j].kind === kind) j++;
44267
+ const els = items.slice(i, j).map((it) => it.el);
44268
+ if (kind) out.push(revisionWrapper(rEl, kind, els, ctx.meta));
44269
+ else out.push(...els);
44270
+ }
44271
+ i = j;
44272
+ }
44273
+ return out;
44274
+ }
44275
+ function containerShell(c, content, fromOriginal, ctx) {
44276
+ const isSdt = localName(c.name) === "sdt";
44277
+ const inner = isSdt ? c.children.find((k) => localName(k.name) === "sdtContent") : void 0;
44278
+ const shell = {
44279
+ name: c.name,
44280
+ attrs: { ...c.attrs },
44281
+ // Property children only (sdtPr, smartTagPr, ...): the content is new.
44282
+ children: c.children.filter((k) => /Pr$/.test(localName(k.name))).map(cloneXml),
44283
+ text: ""
44284
+ };
44285
+ if (fromOriginal) adoptFromOriginal(shell, ctx);
44286
+ if (isSdt) shell.children.push({ name: inner?.name ?? c.name.replace(/sdt$/, "sdtContent"), attrs: { ...inner?.attrs ?? {} }, children: content, text: "" });
44287
+ else shell.children.push(...content);
44288
+ return shell;
43625
44289
  }
43626
44290
  function stripProps(el26) {
43627
44291
  return { ...el26, children: el26.children.filter((c) => localName(c.name) !== "pPr") };
43628
44292
  }
43629
- function emitEqualSpan(oTokens, rTokens, seg, ctx) {
44293
+ function emitEqualSpan(oTokens, rTokens, seg, ctx, counterpart) {
44294
+ const units = [];
44295
+ const pieceAt = (pieces, offset) => {
44296
+ let base = 0;
44297
+ for (const p of pieces) {
44298
+ if (offset < base + p.text.length) return p;
44299
+ base += p.text.length;
44300
+ }
44301
+ return pieces[pieces.length - 1];
44302
+ };
44303
+ for (let k = 0; k < seg.bEnd - seg.bStart; k++) {
44304
+ const o = oTokens[seg.aStart + k];
44305
+ const r = rTokens[seg.bStart + k];
44306
+ for (let d = 0; d < Math.min(o.path.length, r.path.length); d++) {
44307
+ if (localName(o.path[d].name) === localName(r.path[d].name)) counterpart.set(o.path[d], r.path[d]);
44308
+ }
44309
+ if (r.atom || !r.pieces || !o.pieces || o.text !== r.text) {
44310
+ if (r.atom) units.push({ token: r, run: r.run, text: "", atom: r.atom, oAtom: o.atom, was: o.run });
44311
+ else for (const p of r.pieces ?? []) units.push({ token: r, run: p.run, t: p.t, text: p.text, was: o.run });
44312
+ continue;
44313
+ }
44314
+ const cuts = /* @__PURE__ */ new Set([0, r.text.length]);
44315
+ for (const pieces of [o.pieces, r.pieces]) {
44316
+ let base = 0;
44317
+ for (const p of pieces) cuts.add(base += p.text.length);
44318
+ }
44319
+ const sorted = [...cuts].sort((x, y) => x - y);
44320
+ for (let c = 0; c + 1 < sorted.length; c++) {
44321
+ const from = sorted[c];
44322
+ const to = sorted[c + 1];
44323
+ if (from === to) continue;
44324
+ const rp = pieceAt(r.pieces, from);
44325
+ units.push({ token: r, run: rp.run, t: rp.t, text: r.text.slice(from, to), was: pieceAt(o.pieces, from).run });
44326
+ }
44327
+ }
43630
44328
  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++;
44329
+ let open = null;
44330
+ let text = null;
44331
+ const close = () => {
44332
+ if (open && ctx.formatting && open.was !== runPropsKey(open.run)) swapRunProps(open.el, open.prev, ctx);
44333
+ open = null;
44334
+ text = null;
44335
+ };
44336
+ let lastToken = null;
44337
+ for (const u of units) {
44338
+ if (u.token !== lastToken && u.token.lead) {
44339
+ close();
44340
+ out.push(...u.token.lead.map((el26) => ({ el: cloneXml(el26), path: u.token.path })));
44341
+ }
44342
+ lastToken = u.token;
44343
+ const was = runPropsKey(u.was);
44344
+ if (!open || open.run !== u.run || open.was !== was) {
44345
+ close();
44346
+ const rPr = u.run.children.find((c) => localName(c.name) === "rPr");
44347
+ const el26 = { name: u.run.name, attrs: { ...u.run.attrs }, children: rPr ? [cloneXml(rPr)] : [], text: "" };
44348
+ out.push({ el: el26, path: u.token.path });
44349
+ open = { run: u.run, was, el: el26, prev: child(u.was, "rPr") };
44350
+ }
44351
+ if (u.atom) {
44352
+ const atom = cloneXml(u.atom);
44353
+ if (u.oAtom && !sameXml(u.oAtom, u.atom)) compareTextBoxes(u.oAtom, atom, ctx);
44354
+ open.el.children.push(atom);
44355
+ text = null;
44356
+ continue;
43638
44357
  }
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
- }
44358
+ if (text && text.t === u.t) {
44359
+ text.el.text += u.text;
44360
+ continue;
43646
44361
  }
43647
- out.push(...piece);
43648
- at = end;
44362
+ const te = { name: u.t.name, attrs: { ...u.t.attrs, "xml:space": "preserve" }, children: [], text: u.text };
44363
+ open.el.children.push(te);
44364
+ text = { t: u.t, el: te };
43649
44365
  }
44366
+ close();
43650
44367
  return out;
43651
44368
  }
43652
44369
  function alignRunPropsInPlace(oEl, rEl, ctx) {
43653
44370
  const oRuns = oEl.children.filter((c) => localName(c.name) === "r");
43654
44371
  const rRuns = rEl.children.filter((c) => localName(c.name) === "r");
43655
- if (oRuns.length !== rRuns.length) return;
44372
+ if (oRuns.length !== rRuns.length) return false;
43656
44373
  for (let i = 0; i < rRuns.length; i++) {
43657
- if (paragraphText3(oRuns[i]) !== paragraphText3(rRuns[i])) return;
44374
+ if (paragraphText3(oRuns[i]) !== paragraphText3(rRuns[i])) return false;
43658
44375
  }
43659
44376
  for (let i = 0; i < rRuns.length; i++) swapRunProps(rRuns[i], child(oRuns[i], "rPr"), ctx);
44377
+ return true;
43660
44378
  }
43661
44379
  function wordSegments(a, b) {
43662
44380
  const raw = [];
@@ -43685,8 +44403,8 @@ function wordSegments(a, b) {
43685
44403
  for (let i = 1; i + 1 < segments.length; i++) {
43686
44404
  const seg = segments[i];
43687
44405
  if (!seg.equal) continue;
43688
- const len = seg.aEnd - seg.aStart;
43689
- const beside = Math.min(changeSize(segments[i - 1]), changeSize(segments[i + 1]));
44406
+ const len = words(a, seg.aStart, seg.aEnd);
44407
+ const beside = Math.min(changeSize(segments[i - 1], a, b), changeSize(segments[i + 1], a, b));
43690
44408
  if (len < COMPARE_TUNING.coalesceWindow || len * COMPARE_TUNING.coalesceRatio < beside) {
43691
44409
  seg.equal = false;
43692
44410
  absorbed = true;
@@ -43710,17 +44428,26 @@ function mergeChanges(segments) {
43710
44428
  }
43711
44429
  return out;
43712
44430
  }
43713
- function changeSize(seg) {
43714
- return Math.max(seg.aEnd - seg.aStart, seg.bEnd - seg.bStart);
44431
+ function changeSize(seg, a, b) {
44432
+ return Math.max(words(a, seg.aStart, seg.aEnd), words(b, seg.bStart, seg.bEnd));
44433
+ }
44434
+ function words(tokens, from, to) {
44435
+ let n = 0;
44436
+ for (let i = from; i < to; i++) if (tokens[i].atom || !/^\s+$/.test(tokens[i].text)) n++;
44437
+ return n;
43715
44438
  }
43716
44439
  function compareTable(oTbl, rTbl, ctx) {
43717
- if (serializeXml(oTbl) === serializeXml(rTbl)) return;
44440
+ if (sameXml(oTbl, rTbl)) return;
43718
44441
  if (ctx.formatting) {
43719
44442
  swapProps(rTbl, "tblPr", recordTableFormatChange, child(oTbl, "tblPr"), ctx);
43720
44443
  alignTableGrid(oTbl, rTbl, ctx);
43721
44444
  }
43722
- const oRows = oTbl.children.filter((c) => localName(c.name) === "tr");
43723
- const rRows = rTbl.children.filter((c) => localName(c.name) === "tr");
44445
+ compareRows(oTbl, rTbl, ctx);
44446
+ }
44447
+ var ROW_LEVEL = ["tr", "sdt", "customXml"];
44448
+ function compareRows(oParent, rParent, ctx) {
44449
+ const oRows = oParent.children.filter((c) => ROW_LEVEL.includes(localName(c.name)));
44450
+ const rRows = rParent.children.filter((c) => ROW_LEVEL.includes(localName(c.name)));
43724
44451
  const anchors = histogramMatch(oRows.map(rowFingerprint), rRows.map(rowFingerprint));
43725
44452
  const rows = [];
43726
44453
  let oAt = 0;
@@ -43745,24 +44472,41 @@ function compareTable(oTbl, rTbl, ctx) {
43745
44472
  oAt = anchor.a + 1;
43746
44473
  rAt = anchor.b + 1;
43747
44474
  }
43748
- rTbl.children = [...rTbl.children.filter((c) => localName(c.name) !== "tr"), ...rows];
44475
+ rParent.children = [...rParent.children.filter((c) => !ROW_LEVEL.includes(localName(c.name))), ...rows];
43749
44476
  }
43750
44477
  function rowFingerprint(tr) {
44478
+ if (localName(tr.name) !== "tr") return `X\0${serializeXml(tr)}`;
43751
44479
  const cells = tr.children.filter((c) => localName(c.name) === "tc");
43752
44480
  return `${cells.length}\0${normalizeText(paragraphText3(tr))}`;
43753
44481
  }
43754
- function deletedRow(tr, ctx) {
43755
- const clone = cloneXml(tr);
43756
- markRowRevision(clone, "del", ctx.meta);
43757
- wrapRowContent(clone, "del", ctx);
44482
+ function rowsIn(el26) {
44483
+ if (localName(el26.name) === "tr") return [el26];
44484
+ const inner = localName(el26.name) === "sdt" ? child(el26, "sdtContent") : el26;
44485
+ return (inner?.children ?? []).filter((c) => ROW_LEVEL.includes(localName(c.name))).flatMap(rowsIn);
44486
+ }
44487
+ function deletedRow(el26, ctx) {
44488
+ const clone = cloneXml(el26);
44489
+ for (const tr of rowsIn(clone)) {
44490
+ markRowRevision(tr, "del", ctx.meta);
44491
+ wrapRowContent(tr, "del", ctx);
44492
+ }
43758
44493
  return clone;
43759
44494
  }
43760
- function insertedRow(tr, ctx) {
43761
- markRowRevision(tr, "ins", ctx.meta);
43762
- wrapRowContent(tr, "ins", ctx);
43763
- return tr;
44495
+ function insertedRow(el26, ctx) {
44496
+ for (const tr of rowsIn(el26)) {
44497
+ markRowRevision(tr, "ins", ctx.meta);
44498
+ wrapRowContent(tr, "ins", ctx);
44499
+ }
44500
+ return el26;
43764
44501
  }
43765
44502
  function compareRow(oTr, rTr, ctx) {
44503
+ if (localName(oTr.name) !== "tr") {
44504
+ const inner = (el26) => localName(el26.name) === "sdt" ? child(el26, "sdtContent") : el26;
44505
+ const o = inner(oTr);
44506
+ const r = inner(rTr);
44507
+ if (o && r) compareRows(o, r, ctx);
44508
+ return [rTr];
44509
+ }
43766
44510
  const oCells = oTr.children.filter((c) => localName(c.name) === "tc");
43767
44511
  const rCells = rTr.children.filter((c) => localName(c.name) === "tc");
43768
44512
  if (oCells.length !== rCells.length) {
@@ -43785,7 +44529,7 @@ function compareRow(oTr, rTr, ctx) {
43785
44529
  function alignTableGrid(oTbl, rTbl, ctx) {
43786
44530
  const oGrid = child(oTbl, "tblGrid");
43787
44531
  const rGrid = child(rTbl, "tblGrid");
43788
- if (!oGrid || !rGrid || serializeXml(oGrid) === serializeXml(rGrid)) return;
44532
+ if (!oGrid || !rGrid || sameXml(oGrid, rGrid)) return;
43789
44533
  const live = rGrid.children.map(cloneXml);
43790
44534
  rGrid.children = oGrid.children.map(cloneXml);
43791
44535
  recordTableGridChange(rTbl, ctx.meta);
@@ -43872,7 +44616,22 @@ function applyParagraphMarks(entries, ctx) {
43872
44616
  }
43873
44617
  }
43874
44618
  function markRun(entries, start, end, ctx) {
43875
- const shifted = start > 0 && isParagraph(entries[start - 1].el);
44619
+ let at = start;
44620
+ while (at <= end) {
44621
+ if (!isParagraph(entries[at].el)) {
44622
+ at++;
44623
+ continue;
44624
+ }
44625
+ let last = at;
44626
+ while (last + 1 <= end && isParagraph(entries[last + 1].el)) last++;
44627
+ markParagraphRun(entries, at, last, ctx);
44628
+ at = last + 1;
44629
+ }
44630
+ }
44631
+ function markParagraphRun(entries, start, end, ctx) {
44632
+ const endsSection = (el26) => isParagraph(el26) && !!child(child(el26, "pPr"), "sectPr");
44633
+ const before = start > 0 ? entries[start - 1].el : void 0;
44634
+ const shifted = !!before && isParagraph(before) && !endsSection(before) && !entries.slice(start, end + 1).some((e) => endsSection(e.el));
43876
44635
  if (shifted) {
43877
44636
  for (let i = start; i <= end; i++) {
43878
44637
  if (!isParagraph(entries[i].el) || !isParagraph(entries[i - 1].el)) continue;
@@ -45359,9 +46118,10 @@ async function renderForPrint(bytes, container) {
45359
46118
  const doc = DocxDocument.load(bytes);
45360
46119
  const layout = layoutDocument(doc);
45361
46120
  renderToDom(doc, layout, container, { zoom: 1, pageGap: 0, pageShadow: false, comments: false });
46121
+ pruneBlankSpans(container);
45362
46122
  const first = layout.pages[0];
45363
46123
  const style = document.createElement("style");
45364
- style.textContent = printStylesheet(first?.width ?? 816, first?.height ?? 1056);
46124
+ style.textContent = printStylesheet(first?.width ?? 816, first?.height ?? 1056, container);
45365
46125
  document.head.append(style);
45366
46126
  await paintsSettled();
45367
46127
  await Promise.all(Array.from(container.querySelectorAll("img"), (img) => img.decode().catch(() => void 0)));
@@ -55815,4 +56575,4 @@ export {
55815
56575
  cfb/cfb.js:
55816
56576
  (*! crc32.js (C) 2014-present SheetJS -- http://sheetjs.com *)
55817
56577
  */
55818
- //# sourceMappingURL=chunk-QU3LB5BC.js.map
56578
+ //# sourceMappingURL=chunk-WPQ6GPG4.js.map