@ansonlai/docx-redline-js 0.4.0 → 0.5.0

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.
Files changed (100) hide show
  1. package/AGENTS.md +589 -287
  2. package/ARCHITECTURE.md +215 -9
  3. package/CHANGELOG.md +319 -0
  4. package/README.md +604 -360
  5. package/adapters/config.js +45 -43
  6. package/bin/docx-redline.js +3 -0
  7. package/core/list-targeting.js +101 -110
  8. package/core/paragraph-targeting.js +501 -61
  9. package/core/paragraph-text.js +209 -0
  10. package/core/revision-cloning.js +38 -0
  11. package/core/types.js +64 -10
  12. package/core/word-xml.js +43 -15
  13. package/dist/docx-redline-js.esm.js +2849 -466
  14. package/dist/docx-redline-js.esm.js.map +4 -4
  15. package/dist/docx-redline-js.esm.min.js +87 -76
  16. package/dist/docx-redline-js.esm.min.js.map +4 -4
  17. package/docs/TESTING.md +342 -23
  18. package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +1669 -0
  19. package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +669 -0
  20. package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +427 -0
  21. package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +519 -0
  22. package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +69 -0
  23. package/docs/plans/completed/structural-revision-capability-matrix.md +115 -0
  24. package/docs/schemas/document-operations.schema.json +109 -0
  25. package/docs/test-comparison-dashboard.html +4250 -7
  26. package/engine/formatting-removal.js +11 -2
  27. package/engine/oxml-engine.js +491 -336
  28. package/engine/reconstruction-mode.js +15 -14
  29. package/engine/reconstruction-writer.js +247 -142
  30. package/engine/route-selection.js +35 -0
  31. package/engine/rpr-helpers.js +334 -35
  32. package/engine/run-builders.js +239 -196
  33. package/engine/surgical-diff-application.js +222 -37
  34. package/engine/surgical-mode.js +134 -6
  35. package/engine/surgical-spans.js +52 -1
  36. package/engine/table-cell-context.js +3 -6
  37. package/engine/table-mode.js +1 -1
  38. package/index.d.ts +234 -6
  39. package/index.js +24 -1
  40. package/node/cli.js +317 -0
  41. package/node/docx-document.js +302 -0
  42. package/node/index.d.ts +31 -0
  43. package/node/index.js +2 -0
  44. package/node/zip-archive.js +52 -0
  45. package/orchestration/list-markdown.js +10 -16
  46. package/orchestration/list-parsing.js +7 -12
  47. package/orchestration/list-structural-fallback.js +21 -10
  48. package/package.json +24 -3
  49. package/pipeline/content-analysis.js +12 -17
  50. package/pipeline/ingestion-export.js +3 -31
  51. package/pipeline/ingestion-paragraph.js +10 -5
  52. package/pipeline/list-generation.js +150 -55
  53. package/pipeline/list-markers.js +70 -3
  54. package/pipeline/serialization.js +4 -2
  55. package/pipeline/structured-content.js +160 -0
  56. package/scripts/apply_changes.mjs +27 -0
  57. package/scripts/benchmark-operation-session.mjs +137 -0
  58. package/scripts/benchmark-targeting-browser.html +74 -0
  59. package/scripts/benchmark-targeting-hot-paths.mjs +67 -0
  60. package/scripts/benchmark-test-runner.mjs +59 -0
  61. package/scripts/build-test-dashboard.mjs +23 -0
  62. package/scripts/export-lane1-fixtures.mjs +380 -0
  63. package/scripts/export-reredline-stress-fixtures.mjs +317 -0
  64. package/scripts/export-validation-fixtures.mjs +1 -1
  65. package/scripts/extract_text.mjs +7 -0
  66. package/scripts/generate-paragraph-boundary-fixtures.ps1 +215 -0
  67. package/scripts/generate-test-dashboard.mjs +362 -11
  68. package/scripts/lib/word-coverage-catalogue.mjs +6 -2
  69. package/scripts/profile-route-selection.mjs +19 -0
  70. package/scripts/render-agenda-multilevel.mjs +0 -5
  71. package/scripts/render-multilevel-cases.mjs +0 -1
  72. package/scripts/run-tests.mjs +107 -35
  73. package/scripts/word-com-corpus-suite.ps1 +3 -0
  74. package/scripts/word-com-differential.ps1 +64 -4
  75. package/scripts/word-com-suite.ps1 +3 -0
  76. package/services/batch-operation-orchestrator.js +494 -0
  77. package/services/capture-engine.js +226 -0
  78. package/services/comment-builders.js +23 -6
  79. package/services/comment-engine.js +108 -47
  80. package/services/comment-locator.js +187 -82
  81. package/services/comment-replies.js +95 -0
  82. package/services/document-inspection.js +258 -0
  83. package/services/document-operation-applier.js +372 -0
  84. package/services/document-operation-contract.js +323 -0
  85. package/services/document-operation-mutations.js +1733 -0
  86. package/services/document-operation-session.js +258 -0
  87. package/services/numbering-service.js +14 -5
  88. package/services/operation-heuristics.js +173 -0
  89. package/services/operation-preflight.js +366 -0
  90. package/services/receipt-collector.js +288 -0
  91. package/services/revision-comment-management.js +37 -5
  92. package/services/revision-token.js +290 -0
  93. package/services/standalone-docx-plumbing.js +123 -8
  94. package/services/standalone-operation-runner.d.ts +296 -0
  95. package/services/standalone-operation-runner.js +10 -1455
  96. package/services/table-reconciliation.js +15 -6
  97. package/docs/VALIDATION.md +0 -183
  98. package/docs/WORD-MANUAL-REVIEW.md +0 -138
  99. package/docs/plans/2026-09-01-performance-and-complexity-reduction.md +0 -210
  100. /package/docs/plans/{2026-08-30-reliability-testing-improvements.md → completed/2026-08-30-reliability-testing-improvements.md} +0 -0
@@ -1,4 +1,4 @@
1
- // @ansonlai/docx-redline-js v0.4.0 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js v0.5.0 — https://github.com/AnsonLai/docx-redline-js
2
2
  var __create = Object.create;
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -1612,10 +1612,11 @@ function serializeXml(node) {
1612
1612
  }
1613
1613
 
1614
1614
  // adapters/config.js
1615
- var _defaultAuthor = "Author";
1615
+ var fallbackAuthor = () => typeof process !== "undefined" && process.env?.DOCX_REDLINE_AUTHOR || "AI Redliner";
1616
+ var _defaultAuthor = fallbackAuthor();
1616
1617
  var _platform = "Unknown";
1617
1618
  function setDefaultAuthor(author) {
1618
- _defaultAuthor = typeof author === "string" && author.trim() ? author.trim() : "Author";
1619
+ _defaultAuthor = typeof author === "string" && author.trim() ? author.trim() : fallbackAuthor();
1619
1620
  }
1620
1621
  function getDefaultAuthor() {
1621
1622
  return _defaultAuthor;
@@ -1743,7 +1744,7 @@ function decodeHtmlEntities(text) {
1743
1744
  }
1744
1745
 
1745
1746
  // pipeline/list-markers.js
1746
- var LIST_MARKER_CORE = String.raw`(?:\d+(?:\.\d+)*\.?|\((?:\d+|[a-zA-Z]|[ivxlcIVXLC]+)\)|[a-zA-Z]\.|\d+\.|[ivxlcIVXLC]+\.|[-*\u2022])`;
1747
+ var LIST_MARKER_CORE = String.raw`(?:\d+(?:\.\d+)*\.?|\((?:\d+|[a-zA-Z]|[ivxlcIVXLC]+)\)|[a-zA-Z]\.|\d+\.|[ivxlcIVXLC]+\.|[-*+\u2022])`;
1747
1748
  var LINE_REGEX_STRICT = new RegExp(`^(\\s*)((?:${LIST_MARKER_CORE})\\s+)`);
1748
1749
  var LINE_REGEX_LOOSE = new RegExp(`^(\\s*)((?:${LIST_MARKER_CORE})\\s*)`);
1749
1750
  var MULTILINE_REGEX_STRICT = new RegExp(`^(\\s*)((?:${LIST_MARKER_CORE})\\s+)`, "m");
@@ -1766,6 +1767,43 @@ function stripListMarker(line, options = {}) {
1766
1767
  const regex = allowZeroSpaceAfterMarker ? LINE_REGEX_LOOSE : LINE_REGEX_STRICT;
1767
1768
  return line.replace(regex, "");
1768
1769
  }
1770
+ function classifyListMarker(marker) {
1771
+ return /^[-*+\u2022]$/.test(String(marker || "").trim()) ? "bullet" : "numbered";
1772
+ }
1773
+ function inferNumberingStyleFromMarker(marker) {
1774
+ const value = String(marker || "").trim();
1775
+ if (classifyListMarker(value) === "bullet") return "bullet";
1776
+ if (/^\d+(?:\.\d+)*\.?$/.test(value) || /^\(\d+\)$/.test(value)) return "decimal";
1777
+ if (/^[ivxlcdm]+\.$/.test(value)) return "lowerRoman";
1778
+ if (/^[IVXLCDM]{2,}\.$/.test(value)) return "upperRoman";
1779
+ if (/^[a-z]\.$/.test(value)) return "lowerAlpha";
1780
+ if (/^[A-Z]\.$/.test(value)) return "upperAlpha";
1781
+ return "decimal";
1782
+ }
1783
+ function parseOutlineLevelFromMarker(marker) {
1784
+ const value = String(marker || "").trim();
1785
+ if (!/^\d+(?:\.\d+)+\.?$/.test(value)) return null;
1786
+ return Math.max(0, value.replace(/\.$/, "").split(".").length - 1);
1787
+ }
1788
+ function parseListItem(line, options = {}) {
1789
+ const match = matchListMarker(String(line || ""), options);
1790
+ if (!match) return null;
1791
+ const marker = match[2].trim();
1792
+ const indent = (match[1] || "").length;
1793
+ const indentSpaces = Math.max(1, Number(options.indentSpaces) || 2);
1794
+ const markerType = classifyListMarker(marker);
1795
+ return {
1796
+ line: String(line || ""),
1797
+ text: stripListMarker(String(line || ""), options),
1798
+ marker,
1799
+ indent,
1800
+ level: Math.min(8, Math.floor(indent / indentSpaces)),
1801
+ markerType,
1802
+ listType: markerType,
1803
+ numberingStyle: inferNumberingStyleFromMarker(marker),
1804
+ outlineLevel: markerType === "numbered" ? parseOutlineLevelFromMarker(marker) : null
1805
+ };
1806
+ }
1769
1807
 
1770
1808
  // core/types.js
1771
1809
  var NS_W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
@@ -1849,8 +1887,8 @@ var REVISION_ELEMENT_NAMES = /* @__PURE__ */ new Set([
1849
1887
  var revisionAllocatorByDocument = /* @__PURE__ */ new WeakMap();
1850
1888
  function isRevisionIdElement(element) {
1851
1889
  if (!element || element.nodeType !== 1) return false;
1852
- const localName = String(element.localName || element.nodeName || "").replace(/^.*:/, "");
1853
- if (!REVISION_ELEMENT_NAMES.has(localName)) return false;
1890
+ const localName2 = String(element.localName || element.nodeName || "").replace(/^.*:/, "");
1891
+ if (!REVISION_ELEMENT_NAMES.has(localName2)) return false;
1854
1892
  return !element.namespaceURI || element.namespaceURI === NS_W || String(element.nodeName || "").startsWith("w:");
1855
1893
  }
1856
1894
  function readWordId(element) {
@@ -1866,14 +1904,22 @@ var RevisionIdAllocator = class {
1866
1904
  }
1867
1905
  seed(xmlDoc) {
1868
1906
  let maxFound = -1;
1869
- const elements = Array.from(xmlDoc?.getElementsByTagName?.("*") || []);
1870
- if (xmlDoc?.nodeType === 1) elements.unshift(xmlDoc);
1871
- for (const element of elements) {
1872
- if (!isRevisionIdElement(element)) continue;
1873
- const id = readWordId(element);
1874
- if (id == null) continue;
1875
- this.occupiedIds.add(id);
1876
- maxFound = Math.max(maxFound, id);
1907
+ const traversalRoot = xmlDoc?.nodeType === 9 ? xmlDoc.documentElement : xmlDoc;
1908
+ let node = traversalRoot || null;
1909
+ while (node) {
1910
+ if (isRevisionIdElement(node)) {
1911
+ const id = readWordId(node);
1912
+ if (id != null) {
1913
+ this.occupiedIds.add(id);
1914
+ maxFound = Math.max(maxFound, id);
1915
+ }
1916
+ }
1917
+ if (node.firstChild) {
1918
+ node = node.firstChild;
1919
+ continue;
1920
+ }
1921
+ while (node && node !== traversalRoot && !node.nextSibling) node = node.parentNode;
1922
+ node = node && node !== traversalRoot ? node.nextSibling : null;
1877
1923
  }
1878
1924
  const highRiskBoundary = MAX_PRACTICAL_REVISION_ID - REVISION_ID_SAFETY_MARGIN;
1879
1925
  this.nextId = maxFound >= highRiskBoundary ? this.startValue : Math.max(this.nextId, maxFound + 1);
@@ -1920,15 +1966,36 @@ function getNextRevisionId() {
1920
1966
  function getRevisionTimestamp(date = /* @__PURE__ */ new Date()) {
1921
1967
  return date.toISOString();
1922
1968
  }
1923
- function createRevisionMetadata(author, allocatorOrNode = null) {
1969
+ function createRevisionMetadata(author, allocatorOrNode = null, kind = null) {
1924
1970
  const resolvedAuthor = typeof author === "string" && author.trim() ? author.trim() : getDefaultAuthor();
1925
1971
  const allocator = allocatorOrNode instanceof RevisionIdAllocator ? allocatorOrNode : getRevisionIdAllocatorForDocument(allocatorOrNode) || defaultRevisionIdAllocator;
1972
+ const id = allocator.next();
1973
+ if (allocator._receiptCollector) {
1974
+ allocator._receiptCollector.recordRevision(id, kind || "structural");
1975
+ }
1926
1976
  return {
1927
- id: allocator.next(),
1977
+ id,
1928
1978
  author: resolvedAuthor,
1929
1979
  date: getRevisionTimestamp()
1930
1980
  };
1931
1981
  }
1982
+ function createReplacementRevisionEvent(author, allocatorOrNode = null) {
1983
+ const resolvedAuthor = typeof author === "string" && author.trim() ? author.trim() : getDefaultAuthor();
1984
+ const allocator = allocatorOrNode instanceof RevisionIdAllocator ? allocatorOrNode : getRevisionIdAllocatorForDocument(allocatorOrNode) || defaultRevisionIdAllocator;
1985
+ const date = getRevisionTimestamp();
1986
+ const deletionId = allocator.next();
1987
+ const insertionId = allocator.next();
1988
+ if (allocator._receiptCollector) {
1989
+ allocator._receiptCollector.recordRevision(deletionId, "del");
1990
+ allocator._receiptCollector.recordRevision(insertionId, "ins");
1991
+ }
1992
+ return {
1993
+ deletionId,
1994
+ insertionId,
1995
+ author: resolvedAuthor,
1996
+ date
1997
+ };
1998
+ }
1932
1999
  function seedRevisionIdsFromDocument(xmlDoc, allocator = defaultRevisionIdAllocator) {
1933
2000
  const resolvedAllocator = allocator instanceof RevisionIdAllocator ? allocator : defaultRevisionIdAllocator;
1934
2001
  const nextId = resolvedAllocator.seed(xmlDoc);
@@ -1965,22 +2032,22 @@ function getFirstElementByTag(node, tagName) {
1965
2032
  const elements = node.getElementsByTagName(tagName);
1966
2033
  return elements.length > 0 ? elements[0] : null;
1967
2034
  }
1968
- function getElementsByTagNS(node, namespaceUri, localName) {
2035
+ function getElementsByTagNS(node, namespaceUri, localName2) {
1969
2036
  if (!node || typeof node.getElementsByTagNameNS !== "function") return [];
1970
- return Array.from(node.getElementsByTagNameNS(namespaceUri, localName));
2037
+ return Array.from(node.getElementsByTagNameNS(namespaceUri, localName2));
1971
2038
  }
1972
- function getFirstElementByTagNS(node, namespaceUri, localName) {
2039
+ function getFirstElementByTagNS(node, namespaceUri, localName2) {
1973
2040
  if (!node || typeof node.getElementsByTagNameNS !== "function") return null;
1974
- const elements = node.getElementsByTagNameNS(namespaceUri, localName);
2041
+ const elements = node.getElementsByTagNameNS(namespaceUri, localName2);
1975
2042
  return elements.length > 0 ? elements[0] : null;
1976
2043
  }
1977
- function getElementsByTagNSOrTag(node, namespaceUri, localName, fallbackTagName = `w:${localName}`) {
1978
- const namespacedElements = getElementsByTagNS(node, namespaceUri, localName);
2044
+ function getElementsByTagNSOrTag(node, namespaceUri, localName2, fallbackTagName = `w:${localName2}`) {
2045
+ const namespacedElements = getElementsByTagNS(node, namespaceUri, localName2);
1979
2046
  if (namespacedElements.length > 0) return namespacedElements;
1980
2047
  return getElementsByTag(node, fallbackTagName);
1981
2048
  }
1982
- function getFirstElementByTagNSOrTag(node, namespaceUri, localName, fallbackTagName = `w:${localName}`) {
1983
- const namespacedElement = getFirstElementByTagNS(node, namespaceUri, localName);
2049
+ function getFirstElementByTagNSOrTag(node, namespaceUri, localName2, fallbackTagName = `w:${localName2}`) {
2050
+ const namespacedElement = getFirstElementByTagNS(node, namespaceUri, localName2);
1984
2051
  if (namespacedElement) return namespacedElement;
1985
2052
  return getFirstElementByTag(node, fallbackTagName);
1986
2053
  }
@@ -1993,12 +2060,12 @@ function childNodesToArray(node) {
1993
2060
  return Array.from(node?.childNodes || []);
1994
2061
  }
1995
2062
  function serializeAttributes(element) {
1996
- return Array.from(element.attributes).map((attr) => `${attr.name}="${attr.value}"`).join(" ");
2063
+ return Array.from(element.attributes).map((attr4) => `${attr4.name}="${attr4.value}"`).join(" ");
1997
2064
  }
1998
- function isNamespacedNode(node, namespaceUri, localName = "") {
2065
+ function isNamespacedNode(node, namespaceUri, localName2 = "") {
1999
2066
  if (!node || node.namespaceURI !== namespaceUri) return false;
2000
- if (!localName) return true;
2001
- return node.localName === localName;
2067
+ if (!localName2) return true;
2068
+ return node.localName === localName2;
2002
2069
  }
2003
2070
 
2004
2071
  // pipeline/ingestion-paragraph.js
@@ -2048,6 +2115,7 @@ function detectNumberingContext(pElement) {
2048
2115
  const ilvlEl = getFirstElementByTagNS(numPr, NS_W, "ilvl");
2049
2116
  if (!numIdEl) return null;
2050
2117
  const numId = numIdEl.getAttribute("w:val");
2118
+ if (!/^\d+$/.test(numId) || Number.parseInt(numId, 10) === 0) return null;
2051
2119
  const type = numId === "1" ? "bullet" : numId === "2" ? "numbered" : "unknown";
2052
2120
  return {
2053
2121
  numId,
@@ -2246,6 +2314,8 @@ function processRun(runElement, startOffset) {
2246
2314
  text += " ";
2247
2315
  } else if (nodeName.endsWith(":noBreakHyphen") || nodeName === "noBreakHyphen") {
2248
2316
  text += "\u2011";
2317
+ } else if (nodeName.endsWith(":softHyphen") || nodeName === "softHyphen") {
2318
+ text += "\xAD";
2249
2319
  }
2250
2320
  }
2251
2321
  if (!text) return null;
@@ -3155,7 +3225,8 @@ function buildSimpleRun(text, rPrXml) {
3155
3225
  function buildDeletionXml(item, options = {}) {
3156
3226
  const metadata = createRevisionMetadata(
3157
3227
  options.author ?? getDefaultAuthor(),
3158
- options.revisionIdAllocator
3228
+ options.revisionIdAllocator,
3229
+ "del"
3159
3230
  );
3160
3231
  const font = options.font ?? null;
3161
3232
  let rPr = item.rPrXml ? stripNamespaceDeclarations(item.rPrXml) : "";
@@ -3167,7 +3238,8 @@ function buildDeletionXml(item, options = {}) {
3167
3238
  function buildInsertionXml(item, formatHints, options = {}) {
3168
3239
  const metadata = createRevisionMetadata(
3169
3240
  options.author ?? getDefaultAuthor(),
3170
- options.revisionIdAllocator
3241
+ options.revisionIdAllocator,
3242
+ "ins"
3171
3243
  );
3172
3244
  const font = options.font ?? null;
3173
3245
  const applicableHints = getApplicableFormatHints(formatHints, item.startOffset, item.endOffset);
@@ -3255,6 +3327,10 @@ function stripNamespaceDeclarations(xml) {
3255
3327
  }
3256
3328
 
3257
3329
  // services/numbering-service.js
3330
+ function isUsableNumId(numId) {
3331
+ const normalized = String(numId ?? "");
3332
+ return /^\d+$/.test(normalized) && Number.parseInt(normalized, 10) > 0;
3333
+ }
3258
3334
  var NumberingService = class {
3259
3335
  constructor() {
3260
3336
  this.contextMap = /* @__PURE__ */ new Map();
@@ -3268,7 +3344,9 @@ var NumberingService = class {
3268
3344
  * @param {string} numId - Existing numId from Word
3269
3345
  */
3270
3346
  registerExistingNumId(signature, numId) {
3271
- this.contextMap.set(signature, numId);
3347
+ if (isUsableNumId(numId)) {
3348
+ this.contextMap.set(signature, String(numId));
3349
+ }
3272
3350
  }
3273
3351
  /**
3274
3352
  * Resolves the best numId to use for a requested list format.
@@ -3279,13 +3357,15 @@ var NumberingService = class {
3279
3357
  */
3280
3358
  getOrCreateNumId(formatConfig, existingContext = null) {
3281
3359
  const requestedType = formatConfig.type || NumberFormat.BULLET;
3282
- if (existingContext && existingContext.numId) {
3360
+ if (existingContext && isUsableNumId(existingContext.numId)) {
3283
3361
  if (existingContext.type === requestedType || existingContext.type === "unknown") {
3284
3362
  return existingContext.numId;
3285
3363
  }
3286
3364
  }
3287
3365
  if (this.contextMap.has(requestedType)) {
3288
- return this.contextMap.get(requestedType);
3366
+ const cachedNumId = this.contextMap.get(requestedType);
3367
+ if (isUsableNumId(cachedNumId)) return cachedNumId;
3368
+ this.contextMap.delete(requestedType);
3289
3369
  }
3290
3370
  if (requestedType === NumberFormat.OUTLINE) {
3291
3371
  return "3";
@@ -3488,8 +3568,13 @@ var NumberingService = class {
3488
3568
 
3489
3569
  // services/table-reconciliation.js
3490
3570
  function generateTableOoxml(tableData, options = {}) {
3491
- const { generateRedlines = false, author = "AI", revisionIdAllocator = null } = options;
3492
- const tableInsertMeta = generateRedlines ? createRevisionMetadata(author, revisionIdAllocator) : null;
3571
+ const {
3572
+ generateRedlines = false,
3573
+ author = "AI",
3574
+ revisionIdAllocator = null,
3575
+ trackAsBlock = false
3576
+ } = options;
3577
+ const tableInsertMeta = generateRedlines && trackAsBlock ? createRevisionMetadata(author, revisionIdAllocator) : null;
3493
3578
  const numCols = tableData.headers?.length || (tableData.rows?.[0]?.length || 1);
3494
3579
  const tblPr = `
3495
3580
  <w:tblPr>
@@ -3517,7 +3602,7 @@ function generateTableOoxml(tableData, options = {}) {
3517
3602
  const cellText = row[c] || "";
3518
3603
  const { cleanText, formatHints } = preprocessMarkdown(cellText);
3519
3604
  const runModel = [{
3520
- kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
3605
+ kind: generateRedlines && !trackAsBlock ? RunKind.INSERTION : RunKind.TEXT,
3521
3606
  text: cleanText,
3522
3607
  rPrXml: isHeaderRow ? "<w:rPr><w:b/></w:rPr>" : "",
3523
3608
  author,
@@ -3532,7 +3617,7 @@ function generateTableOoxml(tableData, options = {}) {
3532
3617
  const tcPr = '<w:tcPr><w:tcW w:w="0" w:type="auto"/></w:tcPr>';
3533
3618
  cellsXml += `<w:tc>${tcPr}${runsOoxml}</w:tc>`;
3534
3619
  }
3535
- const trPr = "<w:trPr/>";
3620
+ const trPr = isHeaderRow ? "<w:trPr><w:tblHeader/><w:cantSplit/></w:trPr>" : "<w:trPr><w:cantSplit/></w:trPr>";
3536
3621
  rowsXml += `<w:tr>${trPr}${cellsXml}</w:tr>`;
3537
3622
  }
3538
3623
  let tableXml = `<w:tbl>${tblPr}${tblGrid}${rowsXml}</w:tbl>`;
@@ -3750,6 +3835,71 @@ function parseTable(text) {
3750
3835
  };
3751
3836
  }
3752
3837
 
3838
+ // core/word-xml.js
3839
+ function isWordElement(node, localName2) {
3840
+ if (!node || node.nodeType !== 1) return false;
3841
+ if (node.namespaceURI === NS_W && node.localName === localName2) return true;
3842
+ const nodeName = String(node.nodeName || "");
3843
+ return nodeName === `w:${localName2}` || nodeName === localName2;
3844
+ }
3845
+ function createWordElement(xmlDoc, qualifiedName) {
3846
+ return typeof xmlDoc.createElementNS === "function" ? xmlDoc.createElementNS(NS_W, qualifiedName) : xmlDoc.createElement(qualifiedName);
3847
+ }
3848
+ function wordElementsByLocalName(xmlDoc, localName2) {
3849
+ const namespaced = Array.from(xmlDoc?.getElementsByTagNameNS?.(NS_W, localName2) || []);
3850
+ if (namespaced.length > 0) return namespaced;
3851
+ return Array.from(xmlDoc?.getElementsByTagName?.("*") || []).filter((node) => isWordElement(node, localName2));
3852
+ }
3853
+ var TRACKED_CHANGE_NAMES = [
3854
+ "ins",
3855
+ "del",
3856
+ "moveFrom",
3857
+ "moveTo",
3858
+ "moveFromRangeStart",
3859
+ "moveFromRangeEnd",
3860
+ "moveToRangeStart",
3861
+ "moveToRangeEnd",
3862
+ "rPrChange",
3863
+ "pPrChange",
3864
+ "tblPrChange",
3865
+ "trPrChange",
3866
+ "tcPrChange",
3867
+ "cellIns",
3868
+ "cellDel"
3869
+ ];
3870
+ function containsTrackedChanges(xmlDoc) {
3871
+ return TRACKED_CHANGE_NAMES.some((localName2) => wordElementsByLocalName(xmlDoc, localName2).length > 0);
3872
+ }
3873
+ function getTrackedChangeAuthors(xmlDocOrElement) {
3874
+ if (!xmlDocOrElement) return [];
3875
+ const authors = /* @__PURE__ */ new Set();
3876
+ for (const localName2 of TRACKED_CHANGE_NAMES) {
3877
+ for (const node of wordElementsByLocalName(xmlDocOrElement, localName2)) {
3878
+ const author = node.getAttribute?.("w:author") || node.getAttribute?.("author") || (typeof node.getAttributeNS === "function" ? node.getAttributeNS(NS_W, "author") : null);
3879
+ if (author && typeof author === "string" && author.trim()) {
3880
+ authors.add(author.trim());
3881
+ }
3882
+ }
3883
+ }
3884
+ return [...authors].sort();
3885
+ }
3886
+ function classifyOoxmlSourceType(oxml) {
3887
+ const trimmed = String(oxml || "").trim();
3888
+ if (/^<\?xml\b[^>]*>\s*<pkg:package\b/i.test(trimmed) || /^<pkg:package\b/i.test(trimmed)) {
3889
+ return "package";
3890
+ }
3891
+ if (/^<\?xml\b[^>]*>\s*<(?:w:)?document\b/i.test(trimmed) || /^<(?:w:)?document\b/i.test(trimmed)) {
3892
+ return "document";
3893
+ }
3894
+ return "fragment";
3895
+ }
3896
+ function withOoxmlSourceType(result) {
3897
+ if (!result || typeof result !== "object" || result.sourceType || typeof result.oxml !== "string") {
3898
+ return result;
3899
+ }
3900
+ return { ...result, sourceType: classifyOoxmlSourceType(result.oxml) };
3901
+ }
3902
+
3753
3903
  // pipeline/list-generation.js
3754
3904
  async function executeListGeneration(options) {
3755
3905
  const {
@@ -3767,6 +3917,13 @@ async function executeListGeneration(options) {
3767
3917
  const lineMetadata = buildLineMetadata(normalizedListText);
3768
3918
  const rawLines = lineMetadata.map((line) => line.raw);
3769
3919
  const results = [];
3920
+ const sourcePPr = getSourceParagraphProperties(originalRunModel);
3921
+ const inheritedTypographyRPrXml = extractInheritedTypographyRPrXml(originalRunModel, sourcePPr);
3922
+ const inheritedHeadingRPrXml = extractInheritedTypographyRPrXml(
3923
+ originalRunModel,
3924
+ sourcePPr,
3925
+ ["rFonts", "kern", "position", "rtl", "cs", "lang"]
3926
+ );
3770
3927
  let deletionRuns = [];
3771
3928
  if (generateRedlines) {
3772
3929
  if (originalRunModel && originalRunModel.length > 0) {
@@ -3782,6 +3939,15 @@ async function executeListGeneration(options) {
3782
3939
  }];
3783
3940
  }
3784
3941
  }
3942
+ if (generateRedlines && deletionRuns.length > 0) {
3943
+ const deletedPPr = addParagraphMarkRevision(sourcePPr, "del", author, revisionIdAllocator);
3944
+ const deletedParagraph = serializeToOoxml(deletionRuns, deletedPPr, [], {
3945
+ author,
3946
+ generateRedlines,
3947
+ revisionIdAllocator
3948
+ });
3949
+ results.push(deletedParagraph);
3950
+ }
3785
3951
  const indentStep = detectIndentationStep(rawLines);
3786
3952
  log(`[ListGen] Detected indentation step: ${indentStep} spaces/chars`);
3787
3953
  const firstMarker = lineMetadata.find((line) => line.marker)?.marker || "";
@@ -3792,18 +3958,11 @@ async function executeListGeneration(options) {
3792
3958
  if (tableBlock) {
3793
3959
  const tableData = parseTable(tableBlock.tableText);
3794
3960
  if (tableData.headers.length > 0 || tableData.rows.length > 0) {
3795
- if (generateRedlines && results.length === 0 && deletionRuns.length > 0) {
3796
- results.push(serializeToOoxml(deletionRuns, null, [], {
3797
- author,
3798
- generateRedlines,
3799
- font,
3800
- revisionIdAllocator
3801
- }));
3802
- }
3803
3961
  results.push(generateTableOoxml(tableData, {
3804
3962
  generateRedlines,
3805
3963
  author,
3806
- revisionIdAllocator
3964
+ revisionIdAllocator,
3965
+ trackAsBlock: true
3807
3966
  }));
3808
3967
  i = tableBlock.endIndex;
3809
3968
  continue;
@@ -3812,7 +3971,6 @@ async function executeListGeneration(options) {
3812
3971
  const line = lineMetadata[i];
3813
3972
  const entry = buildListEntry(
3814
3973
  line,
3815
- i,
3816
3974
  indentStep,
3817
3975
  numberingContext,
3818
3976
  numberingService,
@@ -3820,18 +3978,17 @@ async function executeListGeneration(options) {
3820
3978
  author,
3821
3979
  font,
3822
3980
  revisionIdAllocator,
3823
- deletionRuns
3981
+ inheritedTypographyRPrXml,
3982
+ inheritedHeadingRPrXml
3824
3983
  );
3825
3984
  results.push(entry.ooxml);
3826
3985
  }
3827
3986
  const numberingXml = numberingService.generateNumberingXml();
3828
3987
  const finalOoxml = results.join("");
3829
- const blankParagraph = "<w:p><w:pPr></w:pPr></w:p>";
3830
- const oxmlWithSpacing = finalOoxml + blankParagraph;
3831
- log(`[ListGen] \u2705 Generated OOXML for ${results.length} list items, total length: ${oxmlWithSpacing.length}`);
3832
- log(`[ListGen] First 200 chars: ${oxmlWithSpacing.substring(0, 200)}...`);
3988
+ log(`[ListGen] \u2705 Generated OOXML for ${results.length} paragraphs, total length: ${finalOoxml.length}`);
3989
+ log(`[ListGen] First 200 chars: ${finalOoxml.substring(0, 200)}...`);
3833
3990
  return {
3834
- ooxml: oxmlWithSpacing,
3991
+ ooxml: finalOoxml,
3835
3992
  isValid: true,
3836
3993
  warnings: ["Paragraph expanded to list fragment"],
3837
3994
  type: "fragment",
@@ -3910,9 +4067,10 @@ function collectMarkdownTableBlock(lineMetadata, index) {
3910
4067
  endIndex: cursor - 1
3911
4068
  };
3912
4069
  }
3913
- function buildListEntry(line, lineIndex, indentStep, numberingContext, numberingService, generateRedlines, author, font, revisionIdAllocator, deletionRuns) {
4070
+ function buildListEntry(line, indentStep, numberingContext, numberingService, generateRedlines, author, font, revisionIdAllocator, inheritedTypographyRPrXml, inheritedHeadingRPrXml) {
3914
4071
  let pPrXml = "";
3915
4072
  let segmentText = "";
4073
+ let insertedRPrXml = inheritedTypographyRPrXml;
3916
4074
  if (line.headerMatch) {
3917
4075
  const level = Math.min(line.headerMatch[1].length, 9);
3918
4076
  const outlineLevel = Math.min(level - 1, 8);
@@ -3920,6 +4078,10 @@ function buildListEntry(line, lineIndex, indentStep, numberingContext, numbering
3920
4078
  const headingSize = headingSizes[level - 1] || headingSizes[headingSizes.length - 1];
3921
4079
  segmentText = line.headerMatch[2].trim();
3922
4080
  pPrXml = `<w:pPr><w:pStyle w:val="Heading${level}"/><w:outlineLvl w:val="${outlineLevel}"/><w:rPr><w:b/><w:sz w:val="${headingSize}"/><w:szCs w:val="${headingSize}"/></w:rPr></w:pPr>`;
4081
+ insertedRPrXml = mergeRunProperties(
4082
+ inheritedHeadingRPrXml,
4083
+ `<w:rPr><w:b/><w:sz w:val="${headingSize}"/><w:szCs w:val="${headingSize}"/></w:rPr>`
4084
+ );
3923
4085
  } else if (line.marker) {
3924
4086
  const lineFormat = numberingService.detectNumberingFormat(line.marker);
3925
4087
  const indentLevel = indentStep > 0 ? Math.floor(line.indentSize / indentStep) : 0;
@@ -3931,14 +4093,20 @@ function buildListEntry(line, lineIndex, indentStep, numberingContext, numbering
3931
4093
  } else {
3932
4094
  segmentText = line.raw;
3933
4095
  }
4096
+ if (generateRedlines) {
4097
+ pPrXml = addParagraphMarkRevision(
4098
+ pPrXml || "<w:pPr/>",
4099
+ "ins",
4100
+ author,
4101
+ revisionIdAllocator
4102
+ );
4103
+ }
3934
4104
  const { cleanText, formatHints } = preprocessMarkdown(segmentText);
3935
4105
  const runModel = [];
3936
- if (lineIndex === 0 && deletionRuns.length > 0) {
3937
- runModel.push(...deletionRuns);
3938
- }
3939
4106
  runModel.push({
3940
4107
  kind: generateRedlines ? "insertion" : "run",
3941
4108
  text: cleanText,
4109
+ rPrXml: insertedRPrXml,
3942
4110
  author,
3943
4111
  startOffset: 0,
3944
4112
  endOffset: cleanText.length
@@ -3952,6 +4120,59 @@ function buildListEntry(line, lineIndex, indentStep, numberingContext, numbering
3952
4120
  })
3953
4121
  };
3954
4122
  }
4123
+ function getSourceParagraphProperties(originalRunModel) {
4124
+ const paragraphStart = (originalRunModel || []).find((run) => run.kind === RunKind.PARAGRAPH_START);
4125
+ return paragraphStart?.pPrElement || paragraphStart?.pPrXml || null;
4126
+ }
4127
+ function extractInheritedTypographyRPrXml(originalRunModel, sourcePPr, tagOrder = ["rFonts", "kern", "position", "sz", "szCs", "rtl", "cs", "lang"]) {
4128
+ const sources = (originalRunModel || []).filter((run) => (run.kind === RunKind.TEXT || run.kind === "text") && run.rPrXml).map((run) => run.rPrXml);
4129
+ if (sourcePPr) {
4130
+ sources.push(typeof sourcePPr === "string" ? sourcePPr : serializeXml(sourcePPr));
4131
+ }
4132
+ const inherited = [];
4133
+ for (const tagName of tagOrder) {
4134
+ let match = null;
4135
+ for (const source of sources) {
4136
+ match = String(source || "").match(new RegExp(`<w:${tagName}\\b[^>]*(?:\\/>|>[\\s\\S]*?<\\/w:${tagName}>)`));
4137
+ if (match) break;
4138
+ }
4139
+ if (match) inherited.push(match[0]);
4140
+ }
4141
+ return inherited.length > 0 ? `<w:rPr>${inherited.join("")}</w:rPr>` : "";
4142
+ }
4143
+ function mergeRunProperties(...sources) {
4144
+ const properties = sources.map((source) => String(source || "").replace(/^\s*<w:rPr[^>]*>|<\/w:rPr>\s*$/g, "")).filter(Boolean).join("");
4145
+ return properties ? `<w:rPr>${properties}</w:rPr>` : "";
4146
+ }
4147
+ function addParagraphMarkRevision(xml, type, author, revisionIdAllocator) {
4148
+ const metadata = createRevisionMetadata(author, revisionIdAllocator);
4149
+ let pPr;
4150
+ let ownerDoc;
4151
+ if (xml?.nodeType === 1) {
4152
+ pPr = xml.cloneNode(true);
4153
+ ownerDoc = pPr.ownerDocument;
4154
+ } else {
4155
+ const pPrXml = typeof xml === "string" && xml.trim() ? xml : "<w:pPr/>";
4156
+ const parsed = parseOoxmlSafe(`<w:root xmlns:w="${NS_W}">${pPrXml}</w:root>`);
4157
+ if (!parsed.doc) {
4158
+ throw new Error(parsed.error?.message || "Could not parse paragraph properties for list revision");
4159
+ }
4160
+ ownerDoc = parsed.doc;
4161
+ pPr = Array.from(ownerDoc.documentElement.childNodes || []).find((node) => node.nodeType === 1 && node.localName === "pPr");
4162
+ }
4163
+ if (!pPr || !ownerDoc) throw new Error("List paragraph properties are unavailable");
4164
+ let rPr = Array.from(pPr.childNodes || []).find((node) => node.nodeType === 1 && node.localName === "rPr");
4165
+ if (!rPr) {
4166
+ rPr = createWordElement(ownerDoc, "w:rPr");
4167
+ pPr.appendChild(rPr);
4168
+ }
4169
+ const marker = createWordElement(ownerDoc, type === "del" ? "w:del" : "w:ins");
4170
+ marker.setAttribute("w:id", String(metadata.id));
4171
+ marker.setAttribute("w:author", metadata.author);
4172
+ marker.setAttribute("w:date", metadata.date);
4173
+ rPr.appendChild(marker);
4174
+ return serializeXml(pPr);
4175
+ }
3955
4176
 
3956
4177
  // pipeline/pipeline.js
3957
4178
  var WEB_PLATFORM_NAMES = /* @__PURE__ */ new Set(["officeonline", "officeweb", "web"]);
@@ -4217,53 +4438,127 @@ var ReconciliationPipeline = class {
4217
4438
  }
4218
4439
  };
4219
4440
 
4220
- // core/word-xml.js
4221
- function isWordElement(node, localName) {
4222
- if (!node || node.nodeType !== 1) return false;
4223
- if (node.namespaceURI === NS_W && node.localName === localName) return true;
4224
- const nodeName = String(node.nodeName || "");
4225
- return nodeName === `w:${localName}` || nodeName === localName;
4226
- }
4227
- function createWordElement(xmlDoc, qualifiedName) {
4228
- return typeof xmlDoc.createElementNS === "function" ? xmlDoc.createElementNS(NS_W, qualifiedName) : xmlDoc.createElement(qualifiedName);
4229
- }
4230
- function wordElementsByLocalName(xmlDoc, localName) {
4231
- const namespaced = Array.from(xmlDoc?.getElementsByTagNameNS?.(NS_W, localName) || []);
4232
- if (namespaced.length > 0) return namespaced;
4233
- return Array.from(xmlDoc?.getElementsByTagName?.("*") || []).filter((node) => isWordElement(node, localName));
4234
- }
4235
- function containsTrackedChanges(xmlDoc) {
4236
- const trackedChangeNames = [
4237
- "ins",
4238
- "del",
4239
- "moveFrom",
4240
- "moveTo",
4241
- "moveFromRangeStart",
4242
- "moveFromRangeEnd",
4243
- "moveToRangeStart",
4244
- "moveToRangeEnd",
4245
- "rPrChange",
4246
- "pPrChange",
4247
- "cellIns",
4248
- "cellDel"
4249
- ];
4250
- return trackedChangeNames.some((localName) => wordElementsByLocalName(xmlDoc, localName).length > 0);
4251
- }
4252
- function classifyOoxmlSourceType(oxml) {
4253
- const trimmed = String(oxml || "").trim();
4254
- if (/^<\?xml\b[^>]*>\s*<pkg:package\b/i.test(trimmed) || /^<pkg:package\b/i.test(trimmed)) {
4255
- return "package";
4256
- }
4257
- if (/^<\?xml\b[^>]*>\s*<(?:w:)?document\b/i.test(trimmed) || /^<(?:w:)?document\b/i.test(trimmed)) {
4258
- return "document";
4441
+ // pipeline/structured-content.js
4442
+ var TABLE_SEPARATOR = /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/;
4443
+ function isBlank(line) {
4444
+ return !line || line.trim().length === 0;
4445
+ }
4446
+ function isTableLine(line) {
4447
+ const trimmed = String(line || "").trim();
4448
+ return trimmed.startsWith("|") && trimmed.endsWith("|");
4449
+ }
4450
+ function tableCells(line) {
4451
+ return String(line || "").trim().split("|").slice(1, -1).map((cell) => cell.trim());
4452
+ }
4453
+ function classifyLine(line) {
4454
+ if (isBlank(line)) return "blank";
4455
+ if (/^\s*#{1,9}\s+\S/.test(line)) return "heading";
4456
+ if (isTableLine(line)) return "table";
4457
+ if (matchListMarker(line)) return "list";
4458
+ return "paragraph";
4459
+ }
4460
+ function analyzeStructuredContent(markdown) {
4461
+ const source = typeof markdown === "string" ? markdown.replace(/\r\n?/g, "\n") : String(markdown ?? "");
4462
+ const lines = source.split("\n");
4463
+ const blocks = [];
4464
+ const issues = [];
4465
+ let index = 0;
4466
+ while (index < lines.length) {
4467
+ if (isBlank(lines[index])) {
4468
+ index++;
4469
+ continue;
4470
+ }
4471
+ const kind = classifyLine(lines[index]);
4472
+ const startLine = index + 1;
4473
+ if (kind === "heading") {
4474
+ const match = lines[index].match(/^\s*(#{1,9})\s+(.+?)\s*$/);
4475
+ blocks.push({ type: "heading", level: match[1].length, text: match[2], markdown: lines[index].trim() });
4476
+ index++;
4477
+ continue;
4478
+ }
4479
+ if (kind === "table") {
4480
+ const tableLines = [];
4481
+ while (index < lines.length && isTableLine(lines[index])) {
4482
+ tableLines.push(lines[index].trim());
4483
+ index++;
4484
+ }
4485
+ const separatorPresent = tableLines.length > 1 && TABLE_SEPARATOR.test(tableLines[1]);
4486
+ const widths = tableLines.filter((line) => !TABLE_SEPARATOR.test(line)).map((line) => tableCells(line).length);
4487
+ if (!separatorPresent) {
4488
+ issues.push({
4489
+ severity: "error",
4490
+ code: "TABLE_SEPARATOR_REQUIRED",
4491
+ line: startLine,
4492
+ message: "Markdown tables require a separator row immediately after the header (for example | --- | --- |)."
4493
+ });
4494
+ }
4495
+ if (widths.length < 2) {
4496
+ issues.push({
4497
+ severity: "error",
4498
+ code: "TABLE_DATA_ROW_REQUIRED",
4499
+ line: startLine,
4500
+ message: "Markdown tables require a header and at least one data row."
4501
+ });
4502
+ } else if (new Set(widths).size > 1) {
4503
+ issues.push({
4504
+ severity: "error",
4505
+ code: "TABLE_COLUMN_COUNT_MISMATCH",
4506
+ line: startLine,
4507
+ message: `Markdown table rows have inconsistent column counts: ${widths.join(", ")}.`
4508
+ });
4509
+ }
4510
+ blocks.push({
4511
+ type: "table",
4512
+ columns: widths[0] || 0,
4513
+ rows: Math.max(0, widths.length - 1),
4514
+ hasHeader: separatorPresent,
4515
+ markdown: tableLines.join("\n")
4516
+ });
4517
+ continue;
4518
+ }
4519
+ if (kind === "list") {
4520
+ const listLines = [];
4521
+ while (index < lines.length && classifyLine(lines[index]) === "list") {
4522
+ listLines.push(lines[index].trimEnd());
4523
+ index++;
4524
+ }
4525
+ blocks.push({ type: "list", items: listLines.length, markdown: listLines.join("\n") });
4526
+ continue;
4527
+ }
4528
+ const paragraphLines = [];
4529
+ while (index < lines.length && classifyLine(lines[index]) === "paragraph") {
4530
+ paragraphLines.push(lines[index].trim());
4531
+ index++;
4532
+ }
4533
+ const text = paragraphLines.join(" ").trim();
4534
+ blocks.push({ type: "paragraph", text, markdown: text });
4259
4535
  }
4260
- return "fragment";
4536
+ const counts = { heading: 0, paragraph: 0, list: 0, table: 0 };
4537
+ for (const block of blocks) counts[block.type] = (counts[block.type] || 0) + 1;
4538
+ const normalizedMarkdown = blocks.map((block) => block.markdown).join("\n\n");
4539
+ return {
4540
+ valid: issues.every((issue) => issue.severity !== "error"),
4541
+ normalizedMarkdown,
4542
+ blocks,
4543
+ issues,
4544
+ counts,
4545
+ requiresStructuredContent: blocks.length > 1 || blocks.some((block) => block.type === "heading" || block.type === "table" || block.type === "list" && block.items > 1)
4546
+ };
4261
4547
  }
4262
- function withOoxmlSourceType(result) {
4263
- if (!result || typeof result !== "object" || result.sourceType || typeof result.oxml !== "string") {
4264
- return result;
4265
- }
4266
- return { ...result, sourceType: classifyOoxmlSourceType(result.oxml) };
4548
+ function planStructuredReplacement(target, markdown, options = {}) {
4549
+ const analysis = analyzeStructuredContent(markdown);
4550
+ return {
4551
+ ...analysis,
4552
+ operation: analysis.valid ? {
4553
+ type: "replace",
4554
+ target,
4555
+ modified: analysis.normalizedMarkdown,
4556
+ structuredContent: true,
4557
+ ...options.author ? { author: options.author } : {},
4558
+ ...typeof options.generateRedlines === "boolean" ? { generateRedlines: options.generateRedlines } : {},
4559
+ ...options.existingRevisions ? { existingRevisions: options.existingRevisions } : {}
4560
+ } : null
4561
+ };
4267
4562
  }
4268
4563
 
4269
4564
  // engine/rpr-helpers.js
@@ -4306,7 +4601,8 @@ var RPR_SCHEMA_ORDER = [
4306
4601
  "w:lang",
4307
4602
  "w:eastAsianLayout",
4308
4603
  "w:specVanish",
4309
- "w:oMath"
4604
+ "w:oMath",
4605
+ "w:rPrChange"
4310
4606
  ];
4311
4607
  function insertRPrChildInOrder(rPr, el) {
4312
4608
  const myIndex = RPR_SCHEMA_ORDER.indexOf(el.nodeName);
@@ -4415,11 +4711,11 @@ function isFormattingElementEnabled(element, isUnderline) {
4415
4711
 
4416
4712
  // engine/format-extraction.js
4417
4713
  var NS_W3 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
4418
- function isWordElement2(node, localName) {
4714
+ function isWordElement2(node, localName2) {
4419
4715
  if (!node || node.nodeType !== 1) return false;
4420
- if (node.namespaceURI === NS_W3 && node.localName === localName) return true;
4716
+ if (node.namespaceURI === NS_W3 && node.localName === localName2) return true;
4421
4717
  const nodeName = String(node.nodeName || "");
4422
- return nodeName === `w:${localName}` || nodeName === localName;
4718
+ return nodeName === `w:${localName2}` || nodeName === localName2;
4423
4719
  }
4424
4720
  function isExcludedRevisionContainer(node) {
4425
4721
  if (!node || node.nodeType !== 1 || node.namespaceURI !== NS_W3) return false;
@@ -4449,8 +4745,8 @@ function getDocumentParagraphs(xmlDoc) {
4449
4745
  return allParagraphs.filter((p) => {
4450
4746
  let node = p.parentNode;
4451
4747
  while (node && node.nodeName) {
4452
- const localName = String(node.localName || "").toLowerCase();
4453
- if (excludedContainers.has(localName)) {
4748
+ const localName2 = String(node.localName || "").toLowerCase();
4749
+ if (excludedContainers.has(localName2)) {
4454
4750
  return false;
4455
4751
  }
4456
4752
  node = node.parentNode;
@@ -4580,9 +4876,9 @@ function extractFormattingFromOoxml(xmlDoc) {
4580
4876
  }
4581
4877
 
4582
4878
  // engine/run-builders.js
4583
- function createTrackChange(xmlDoc, type, run, author) {
4879
+ function createTrackChange(xmlDoc, type, run, author, revisionMetadata = null) {
4584
4880
  const wrapper = createWordElement(xmlDoc, type === "ins" ? "w:ins" : "w:del");
4585
- const metadata = createRevisionMetadata(author, xmlDoc);
4881
+ const metadata = revisionMetadata || createRevisionMetadata(author, xmlDoc, type === "ins" ? "ins" : "del");
4586
4882
  wrapper.setAttribute("w:id", String(metadata.id));
4587
4883
  wrapper.setAttribute("w:author", metadata.author);
4588
4884
  wrapper.setAttribute("w:date", metadata.date);
@@ -4591,8 +4887,8 @@ function createTrackChange(xmlDoc, type, run, author) {
4591
4887
  }
4592
4888
  return wrapper;
4593
4889
  }
4594
- function getDirectWordChild(node, localName) {
4595
- return Array.from(node?.childNodes || []).find((child) => child.nodeType === 1 && (child.localName === localName || child.nodeName === `w:${localName}`)) || null;
4890
+ function getDirectWordChild(node, localName2) {
4891
+ return Array.from(node?.childNodes || []).find((child) => child.nodeType === 1 && (child.localName === localName2 || child.nodeName === `w:${localName2}`)) || null;
4596
4892
  }
4597
4893
  function ensureParagraphProperties(xmlDoc, paragraph) {
4598
4894
  let pPr = getDirectWordChild(paragraph, "pPr");
@@ -4614,7 +4910,7 @@ function ensureParagraphMarkRunProperties(xmlDoc, pPr) {
4614
4910
  }
4615
4911
  return rPr;
4616
4912
  }
4617
- function markParagraphMark(xmlDoc, paragraph, author, type) {
4913
+ function markParagraphMark(xmlDoc, paragraph, author, type, revisionMetadata = null) {
4618
4914
  const pPr = ensureParagraphProperties(xmlDoc, paragraph);
4619
4915
  const rPr = ensureParagraphMarkRunProperties(xmlDoc, pPr);
4620
4916
  for (const child of Array.from(rPr.childNodes || [])) {
@@ -4624,18 +4920,18 @@ function markParagraphMark(xmlDoc, paragraph, author, type) {
4624
4920
  }
4625
4921
  }
4626
4922
  const marker = createWordElement(xmlDoc, type === "ins" ? "w:ins" : "w:del");
4627
- const metadata = createRevisionMetadata(author, xmlDoc);
4923
+ const metadata = revisionMetadata || createRevisionMetadata(author, xmlDoc, type === "ins" ? "ins" : "del");
4628
4924
  marker.setAttribute("w:id", String(metadata.id));
4629
4925
  marker.setAttribute("w:author", metadata.author);
4630
4926
  marker.setAttribute("w:date", metadata.date);
4631
4927
  rPr.appendChild(marker);
4632
4928
  return marker;
4633
4929
  }
4634
- function markParagraphMarkInserted(xmlDoc, paragraph, author) {
4635
- return markParagraphMark(xmlDoc, paragraph, author, "ins");
4930
+ function markParagraphMarkInserted(xmlDoc, paragraph, author, revisionMetadata = null) {
4931
+ return markParagraphMark(xmlDoc, paragraph, author, "ins", revisionMetadata);
4636
4932
  }
4637
- function markParagraphMarkDeleted(xmlDoc, paragraph, author) {
4638
- return markParagraphMark(xmlDoc, paragraph, author, "del");
4933
+ function markParagraphMarkDeleted(xmlDoc, paragraph, author, revisionMetadata = null) {
4934
+ return markParagraphMark(xmlDoc, paragraph, author, "del", revisionMetadata);
4639
4935
  }
4640
4936
  function createTextRun(xmlDoc, text, rPr, isDelete) {
4641
4937
  const run = createWordElement(xmlDoc, "w:r");
@@ -4763,7 +5059,7 @@ function injectFormattingToRPr(xmlDoc, baseRPr, format, author, generateRedlines
4763
5059
  }
4764
5060
  function snapshotAndAttachRPrChange(xmlDoc, rPr, author, dateStr, sourceNode) {
4765
5061
  const rPrChange = createWordElement(xmlDoc, "w:rPrChange");
4766
- const metadata = createRevisionMetadata(author, xmlDoc);
5062
+ const metadata = createRevisionMetadata(author, xmlDoc, "rPrChange");
4767
5063
  rPrChange.setAttribute("w:id", String(metadata.id));
4768
5064
  rPrChange.setAttribute("w:author", metadata.author);
4769
5065
  rPrChange.setAttribute("w:date", dateStr || metadata.date);
@@ -4788,11 +5084,11 @@ function createRPrChange(xmlDoc, rPr, author, previousRPrArg) {
4788
5084
 
4789
5085
  // engine/format-paragraph-targeting.js
4790
5086
  var NS_W4 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
4791
- function isWordElement3(node, localName) {
5087
+ function isWordElement3(node, localName2) {
4792
5088
  if (!node || node.nodeType !== 1) return false;
4793
- if (node.namespaceURI === NS_W4 && node.localName === localName) return true;
5089
+ if (node.namespaceURI === NS_W4 && node.localName === localName2) return true;
4794
5090
  const nodeName = String(node.nodeName || "");
4795
- return nodeName === `w:${localName}` || nodeName === localName;
5091
+ return nodeName === `w:${localName2}` || nodeName === localName2;
4796
5092
  }
4797
5093
  function buildParagraphInfos(xmlDoc, paragraphs, textSpans) {
4798
5094
  void xmlDoc;
@@ -4972,11 +5268,11 @@ function splitSpanAtOffset(xmlDoc, span, absoluteOffset) {
4972
5268
 
4973
5269
  // engine/format-application.js
4974
5270
  var NS_W5 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
4975
- function isWordElement4(node, localName) {
5271
+ function isWordElement4(node, localName2) {
4976
5272
  if (!node || node.nodeType !== 1) return false;
4977
- if (node.namespaceURI === NS_W5 && node.localName === localName) return true;
5273
+ if (node.namespaceURI === NS_W5 && node.localName === localName2) return true;
4978
5274
  const nodeName = String(node.nodeName || "");
4979
- return nodeName === `w:${localName}` || nodeName === localName;
5275
+ return nodeName === `w:${localName2}` || nodeName === localName2;
4980
5276
  }
4981
5277
  function normalizePrecomputedFormatContext(precomputedContext) {
4982
5278
  if (Array.isArray(precomputedContext)) {
@@ -5158,6 +5454,174 @@ function createFormatHintOverlapLookup(formatHints) {
5158
5454
  };
5159
5455
  }
5160
5456
 
5457
+ // core/paragraph-text.js
5458
+ function localName(node) {
5459
+ return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
5460
+ }
5461
+ function isNodeVisibleInRevisionView(node, boundary = null, revisionView = "accepted") {
5462
+ const view = revisionView === "current" ? "accepted" : revisionView;
5463
+ let cursor = node;
5464
+ while (cursor && cursor !== boundary) {
5465
+ const name = localName(cursor);
5466
+ if (view === "accepted" && (name === "del" || name === "moveFrom")) return false;
5467
+ if (view === "rejected" && (name === "ins" || name === "moveTo")) return false;
5468
+ cursor = cursor.parentNode;
5469
+ }
5470
+ return true;
5471
+ }
5472
+ function readCanonicalRunText(run, options = {}) {
5473
+ const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
5474
+ const boundary = options.boundary || null;
5475
+ if (!isNodeVisibleInRevisionView(run, boundary, revisionView)) return "";
5476
+ let text = "";
5477
+ const visit = (node) => {
5478
+ for (const child of Array.from(node?.childNodes || [])) {
5479
+ if (child?.nodeType !== 1 || child.namespaceURI && child.namespaceURI !== NS_W) continue;
5480
+ if (!isNodeVisibleInRevisionView(child, boundary, revisionView)) continue;
5481
+ const name = localName(child);
5482
+ if (name === "t" || name === "delText" && revisionView === "rejected") text += child.textContent || "";
5483
+ else if (name === "tab") text += " ";
5484
+ else if (name === "br" || name === "cr") text += "\n";
5485
+ else if (name === "noBreakHyphen") text += "\u2011";
5486
+ else if (name === "softHyphen") text += "\xAD";
5487
+ else visit(child);
5488
+ }
5489
+ };
5490
+ visit(run);
5491
+ return text;
5492
+ }
5493
+ var attr = (node, name) => node?.getAttribute?.(`w:${name}`) || node?.getAttribute?.(name) || "";
5494
+ function extractParagraphRevisionSegments(paragraph, options = {}) {
5495
+ if (!paragraph) return [];
5496
+ const rawPieces = [];
5497
+ function walk(node, currentRevision, currentCarrier) {
5498
+ for (const child of Array.from(node?.childNodes || [])) {
5499
+ if (child?.nodeType !== 1) continue;
5500
+ if (child.namespaceURI && child.namespaceURI !== NS_W) continue;
5501
+ const name = localName(child);
5502
+ if (name === "pPr" || name === "rPr") continue;
5503
+ let nextRevision = currentRevision;
5504
+ if (name === "ins") {
5505
+ nextRevision = {
5506
+ kind: "insertion",
5507
+ author: attr(child, "author") || void 0,
5508
+ revisionId: attr(child, "id") || void 0
5509
+ };
5510
+ } else if (name === "del") {
5511
+ nextRevision = {
5512
+ kind: "deletion",
5513
+ author: attr(child, "author") || void 0,
5514
+ revisionId: attr(child, "id") || void 0
5515
+ };
5516
+ } else if (name === "moveFrom") {
5517
+ nextRevision = {
5518
+ kind: "move_from",
5519
+ author: attr(child, "author") || void 0,
5520
+ revisionId: attr(child, "id") || void 0
5521
+ };
5522
+ } else if (name === "moveTo") {
5523
+ nextRevision = {
5524
+ kind: "move_to",
5525
+ author: attr(child, "author") || void 0,
5526
+ revisionId: attr(child, "id") || void 0
5527
+ };
5528
+ }
5529
+ let nextCarrier = currentCarrier;
5530
+ if (name === "r") {
5531
+ nextCarrier = child;
5532
+ }
5533
+ let text = null;
5534
+ let kind = nextRevision ? nextRevision.kind : "baseline";
5535
+ let author = nextRevision?.author;
5536
+ let revisionId = nextRevision?.revisionId;
5537
+ if (name === "t") {
5538
+ text = child.textContent || "";
5539
+ } else if (name === "delText") {
5540
+ text = child.textContent || "";
5541
+ if (!nextRevision) {
5542
+ kind = "deletion";
5543
+ }
5544
+ } else if (name === "tab") {
5545
+ text = " ";
5546
+ } else if (name === "br" || name === "cr") {
5547
+ text = "\n";
5548
+ } else if (name === "noBreakHyphen") {
5549
+ text = "\u2011";
5550
+ } else if (name === "softHyphen") {
5551
+ text = "\xAD";
5552
+ }
5553
+ if (text !== null) {
5554
+ if (text.length > 0) {
5555
+ rawPieces.push({
5556
+ text,
5557
+ kind,
5558
+ author,
5559
+ revisionId,
5560
+ carrier: nextCarrier || child,
5561
+ carrierContainer: (nextCarrier || child)?.parentNode || null
5562
+ });
5563
+ }
5564
+ } else {
5565
+ walk(child, nextRevision, nextCarrier);
5566
+ }
5567
+ }
5568
+ }
5569
+ const initialCarrier = localName(paragraph) === "r" ? paragraph : null;
5570
+ walk(paragraph, null, initialCarrier);
5571
+ if (rawPieces.length === 0) return [];
5572
+ const mergeRuns = options.mergeRuns !== false;
5573
+ const merged = [];
5574
+ let current = null;
5575
+ for (const piece of rawPieces) {
5576
+ if (!current) {
5577
+ current = { ...piece };
5578
+ continue;
5579
+ }
5580
+ const sameKind = current.kind === piece.kind;
5581
+ const sameAuthor = current.author === piece.author;
5582
+ const sameRevisionId = current.revisionId === piece.revisionId;
5583
+ const sameCarrier = current.carrier === piece.carrier;
5584
+ const compatibleContainer = mergeRuns && current.carrierContainer === piece.carrierContainer;
5585
+ if (sameKind && sameAuthor && sameRevisionId && (sameCarrier || compatibleContainer)) {
5586
+ current.text += piece.text;
5587
+ } else {
5588
+ merged.push(current);
5589
+ current = { ...piece };
5590
+ }
5591
+ }
5592
+ if (current) merged.push(current);
5593
+ let acceptedCursor = 0;
5594
+ let rejectedCursor = 0;
5595
+ const segments = [];
5596
+ for (const item of merged) {
5597
+ const isAccepted = item.kind !== "deletion" && item.kind !== "move_from";
5598
+ const isRejected = item.kind !== "insertion" && item.kind !== "move_to";
5599
+ const acceptedStart = isAccepted ? acceptedCursor : null;
5600
+ const rejectedStart = isRejected ? rejectedCursor : null;
5601
+ if (isAccepted) acceptedCursor += item.text.length;
5602
+ if (isRejected) rejectedCursor += item.text.length;
5603
+ const segment = {
5604
+ text: item.text,
5605
+ kind: item.kind,
5606
+ acceptedStart,
5607
+ rejectedStart
5608
+ };
5609
+ if (item.author !== void 0) segment.author = item.author;
5610
+ if (item.revisionId !== void 0) segment.revisionId = item.revisionId;
5611
+ segments.push(segment);
5612
+ }
5613
+ return segments;
5614
+ }
5615
+ function extractCanonicalParagraphText(paragraph, options = {}) {
5616
+ if (!paragraph) return "";
5617
+ const view = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
5618
+ const segments = extractParagraphRevisionSegments(paragraph, options);
5619
+ if (view === "rejected") {
5620
+ return segments.filter((s) => s.rejectedStart !== null).map((s) => s.text).join("");
5621
+ }
5622
+ return segments.filter((s) => s.acceptedStart !== null).map((s) => s.text).join("");
5623
+ }
5624
+
5161
5625
  // engine/table-cell-context.js
5162
5626
  var W14_NS = "http://schemas.microsoft.com/office/word/2010/wordml";
5163
5627
  function detectTableCellContext(xmlDoc, originalText, options = {}) {
@@ -5193,11 +5657,7 @@ function detectTableCellContext(xmlDoc, originalText, options = {}) {
5193
5657
  const normalizedTarget = originalText.trim();
5194
5658
  if (!targetParagraph) {
5195
5659
  for (const p of paragraphsInCells) {
5196
- const textNodes = getElementsByTagNSOrTag(p, NS_W, "t");
5197
- let paragraphText = "";
5198
- for (const t of textNodes) {
5199
- paragraphText += t.textContent || "";
5200
- }
5660
+ const paragraphText = extractCanonicalParagraphText(p);
5201
5661
  if (paragraphText.trim() === normalizedTarget) {
5202
5662
  targetParagraph = p;
5203
5663
  log(`[OxmlEngine] Found target paragraph by text match: "${normalizedTarget.substring(0, 30)}..."`);
@@ -5246,10 +5706,11 @@ function getRunChildText(child) {
5246
5706
  if (isWordElement(child, "br") || isWordElement(child, "cr")) return "\n";
5247
5707
  if (isWordElement(child, "tab")) return " ";
5248
5708
  if (isWordElement(child, "noBreakHyphen")) return "\u2011";
5709
+ if (isWordElement(child, "softHyphen")) return "\xAD";
5249
5710
  return "";
5250
5711
  }
5251
5712
  function isTextLikeRunChild(child) {
5252
- return isWordElement(child, "t") || isWordElement(child, "br") || isWordElement(child, "cr") || isWordElement(child, "tab") || isWordElement(child, "noBreakHyphen");
5713
+ return isWordElement(child, "t") || isWordElement(child, "br") || isWordElement(child, "cr") || isWordElement(child, "tab") || isWordElement(child, "noBreakHyphen") || isWordElement(child, "softHyphen");
5253
5714
  }
5254
5715
  function buildSurgicalTextSpans(paragraphs) {
5255
5716
  let fullText = "";
@@ -5265,6 +5726,27 @@ function buildSurgicalTextSpans(paragraphs) {
5265
5726
  fullText += processRunElement(hc, paragraph, container, fullText.length, textSpans).text;
5266
5727
  }
5267
5728
  }
5729
+ } else if (isWordElement(child, "ins")) {
5730
+ for (let ic = child.firstChild; ic; ic = ic.nextSibling) {
5731
+ if (isWordElement(ic, "r")) {
5732
+ fullText += processRunElement(ic, paragraph, container, fullText.length, textSpans).text;
5733
+ }
5734
+ }
5735
+ } else if (isWordElement(child, "sdt")) {
5736
+ const sdtContent = Array.from(child.childNodes || []).find((n) => isWordElement(n, "sdtContent"));
5737
+ if (sdtContent) {
5738
+ for (let sc = sdtContent.firstChild; sc; sc = sc.nextSibling) {
5739
+ if (isWordElement(sc, "r")) {
5740
+ fullText += processRunElement(sc, paragraph, container, fullText.length, textSpans).text;
5741
+ }
5742
+ }
5743
+ }
5744
+ } else if (isWordElement(child, "smartTag")) {
5745
+ for (let st = child.firstChild; st; st = st.nextSibling) {
5746
+ if (isWordElement(st, "r")) {
5747
+ fullText += processRunElement(st, paragraph, container, fullText.length, textSpans).text;
5748
+ }
5749
+ }
5268
5750
  }
5269
5751
  }
5270
5752
  fullText = appendParagraphBoundary(fullText, paragraphIndex, paragraphs.length);
@@ -5374,6 +5856,28 @@ function lowerBound(values, target) {
5374
5856
  }
5375
5857
  return left;
5376
5858
  }
5859
+ function describeInsertionBoundary(spanIndex, pos, fallbackParagraph = null) {
5860
+ if (!spanIndex || !spanIndex.spans || spanIndex.spans.length === 0) {
5861
+ return {
5862
+ leftSpan: null,
5863
+ rightSpan: null,
5864
+ containingSpan: null,
5865
+ isInterior: false,
5866
+ fallbackParagraph
5867
+ };
5868
+ }
5869
+ const containingSpan = findContainingSpan(spanIndex, pos);
5870
+ const isInterior = containingSpan !== null && pos > containingSpan.charStart && pos < containingSpan.charEnd;
5871
+ const leftSpan = findFirstSpanEndingAt(spanIndex, pos) || (pos > 0 ? findLastSpanEndingBeforeOrAt(spanIndex, pos) : null);
5872
+ const rightSpan = spanIndex.spans.find((s) => s.charStart === pos) || (pos === 0 ? spanIndex.spans[0] : null);
5873
+ return {
5874
+ leftSpan,
5875
+ rightSpan,
5876
+ containingSpan,
5877
+ isInterior,
5878
+ fallbackParagraph
5879
+ };
5880
+ }
5377
5881
 
5378
5882
  // engine/surgical-run-splitting.js
5379
5883
  function getRunContentPieces(runElement) {
@@ -5487,7 +5991,7 @@ function reconcileFormattingForTextSpan(xmlDoc, span, start, end, applicableHint
5487
5991
  parent.removeChild(span.runElement);
5488
5992
  return true;
5489
5993
  }
5490
- function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedlines) {
5994
+ function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedlines, revisionMetadata = null) {
5491
5995
  const spans = [];
5492
5996
  forEachOverlappingSpan(spanIndex, startPos, endPos, (span) => {
5493
5997
  spans.push(span);
@@ -5500,6 +6004,7 @@ function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedl
5500
6004
  spansByRun.get(span.runElement).push(span);
5501
6005
  });
5502
6006
  let changed = false;
6007
+ let usedDelMetadata = false;
5503
6008
  spansByRun.forEach((runSpans, runElement) => {
5504
6009
  const parent = runElement.parentNode;
5505
6010
  if (!parent) return;
@@ -5523,7 +6028,9 @@ function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedl
5523
6028
  insertRunPiecesBefore(xmlDoc, parent, runElement, beforePieces, runSpans[0].rPr);
5524
6029
  if (generateRedlines && deletedPieces.length > 0) {
5525
6030
  const delRun = createRunFromPieces(xmlDoc, deletedPieces, runSpans[0].rPr);
5526
- const delWrapper = createTrackChange(xmlDoc, "del", delRun, author);
6031
+ const metadata = revisionMetadata ? usedDelMetadata ? { ...revisionMetadata, id: createRevisionMetadata(author, xmlDoc).id } : revisionMetadata : null;
6032
+ usedDelMetadata = true;
6033
+ const delWrapper = createTrackChange(xmlDoc, "del", delRun, author, metadata);
5527
6034
  parent.insertBefore(delWrapper, runElement);
5528
6035
  }
5529
6036
  insertRunPiecesBefore(xmlDoc, parent, runElement, afterPieces, runSpans[0].rPr);
@@ -5532,50 +6039,200 @@ function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedl
5532
6039
  });
5533
6040
  return changed;
5534
6041
  }
5535
- function processInsert(xmlDoc, spanIndex, pos, text, author, formatHints = [], insertOffset = 0, generateRedlines = true, fallbackParagraph = null) {
5536
- let targetSpan = findContainingSpan(spanIndex, pos);
5537
- if (!targetSpan && pos > 0) {
5538
- targetSpan = findFirstSpanEndingAt(spanIndex, pos);
6042
+ function processInsert(xmlDoc, spanIndex, pos, text, author, formatHints = [], insertOffset = 0, generateRedlines = true, fallbackParagraph = null, revisionMetadata = null, affinity = null) {
6043
+ if (!affinity) {
6044
+ let targetSpan = findContainingSpan(spanIndex, pos);
6045
+ if (!targetSpan && pos > 0) {
6046
+ targetSpan = findFirstSpanEndingAt(spanIndex, pos);
6047
+ }
6048
+ if (!targetSpan && pos > 0) {
6049
+ targetSpan = findLastSpanEndingBeforeOrAt(spanIndex, pos);
6050
+ }
6051
+ if (!targetSpan && spanIndex.spans.length > 0) {
6052
+ targetSpan = spanIndex.spans[spanIndex.spans.length - 1];
6053
+ }
6054
+ if (!targetSpan) {
6055
+ if (!fallbackParagraph) return false;
6056
+ insertTextRuns(xmlDoc, fallbackParagraph, null, text, null, author, formatHints, insertOffset, generateRedlines, revisionMetadata);
6057
+ return true;
6058
+ }
6059
+ const parent2 = targetSpan.runElement.parentNode;
6060
+ if (!parent2) {
6061
+ if (!fallbackParagraph) return false;
6062
+ insertTextRuns(xmlDoc, fallbackParagraph, null, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata);
6063
+ return true;
6064
+ }
6065
+ const pieces = getRunContentPieces(targetSpan.runElement);
6066
+ const targetPiece = pieces.find((piece) => piece.node === targetSpan.textElement);
6067
+ const localInsertPos = targetPiece ? targetPiece.start + Math.max(0, Math.min(pos - targetSpan.charStart, targetSpan.charEnd - targetSpan.charStart)) : pos <= targetSpan.charStart ? 0 : getRunTextLength(pieces);
6068
+ if (localInsertPos > 0 && localInsertPos < getRunTextLength(pieces)) {
6069
+ const beforePieces = sliceRunPieces(xmlDoc, pieces, 0, localInsertPos, false);
6070
+ const afterPieces = sliceRunPieces(xmlDoc, pieces, localInsertPos, getRunTextLength(pieces), false);
6071
+ insertRunPiecesBefore(xmlDoc, parent2, targetSpan.runElement, beforePieces, targetSpan.rPr);
6072
+ insertTextRuns(xmlDoc, parent2, targetSpan.runElement, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata);
6073
+ insertRunPiecesBefore(xmlDoc, parent2, targetSpan.runElement, afterPieces, targetSpan.rPr);
6074
+ parent2.removeChild(targetSpan.runElement);
6075
+ return true;
6076
+ }
6077
+ const referenceNode2 = pos <= targetSpan.charStart ? targetSpan.runElement : targetSpan.runElement.nextSibling;
6078
+ insertTextRuns(xmlDoc, parent2, referenceNode2, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata);
6079
+ return true;
5539
6080
  }
5540
- if (!targetSpan && pos > 0) {
5541
- targetSpan = findLastSpanEndingBeforeOrAt(spanIndex, pos);
6081
+ const boundary = describeInsertionBoundary(spanIndex, pos, fallbackParagraph);
6082
+ const isLeftInHyperlink = boundary.leftSpan && isWordElement(boundary.leftSpan.runElement?.parentNode, "hyperlink");
6083
+ const isRightInHyperlink = boundary.rightSpan && isWordElement(boundary.rightSpan.runElement?.parentNode, "hyperlink");
6084
+ const isContainingInHyperlink = boundary.containingSpan && isWordElement(boundary.containingSpan.runElement?.parentNode, "hyperlink");
6085
+ if (affinity.hyperlink === "outside") {
6086
+ if (boundary.isInterior && isContainingInHyperlink) {
6087
+ return {
6088
+ error: {
6089
+ code: "UNSUPPORTED_INSERTION_AFFINITY",
6090
+ message: "Cannot place insertion outside hyperlink from strictly interior position."
6091
+ }
6092
+ };
6093
+ }
6094
+ } else if (affinity.hyperlink === "inside") {
6095
+ if (!isLeftInHyperlink && !isRightInHyperlink && !isContainingInHyperlink) {
6096
+ return {
6097
+ error: {
6098
+ code: "UNSUPPORTED_INSERTION_AFFINITY",
6099
+ message: "Cannot place insertion inside hyperlink when no hyperlink is present at boundary."
6100
+ }
6101
+ };
6102
+ }
5542
6103
  }
5543
- if (!targetSpan && spanIndex.spans.length > 0) {
5544
- targetSpan = spanIndex.spans[spanIndex.spans.length - 1];
6104
+ let baseRPr = null;
6105
+ if (affinity.formatting === "none") {
6106
+ baseRPr = null;
6107
+ } else if (affinity.formatting === "right") {
6108
+ baseRPr = boundary.rightSpan?.rPr || null;
6109
+ } else if (affinity.formatting === "left") {
6110
+ baseRPr = boundary.leftSpan?.rPr || null;
6111
+ } else {
6112
+ baseRPr = (boundary.containingSpan || boundary.leftSpan || boundary.rightSpan)?.rPr || null;
6113
+ }
6114
+ if (boundary.isInterior) {
6115
+ const targetSpan = boundary.containingSpan;
6116
+ const parent2 = targetSpan.runElement.parentNode || fallbackParagraph;
6117
+ if (!parent2) return false;
6118
+ const pieces = getRunContentPieces(targetSpan.runElement);
6119
+ const targetPiece = pieces.find((piece) => piece.node === targetSpan.textElement);
6120
+ const localInsertPos = targetPiece ? targetPiece.start + Math.max(0, Math.min(pos - targetSpan.charStart, targetSpan.charEnd - targetSpan.charStart)) : pos <= targetSpan.charStart ? 0 : getRunTextLength(pieces);
6121
+ if (localInsertPos > 0 && localInsertPos < getRunTextLength(pieces)) {
6122
+ const beforePieces = sliceRunPieces(xmlDoc, pieces, 0, localInsertPos, false);
6123
+ const afterPieces = sliceRunPieces(xmlDoc, pieces, localInsertPos, getRunTextLength(pieces), false);
6124
+ insertRunPiecesBefore(xmlDoc, parent2, targetSpan.runElement, beforePieces, targetSpan.rPr);
6125
+ insertTextRuns(xmlDoc, parent2, targetSpan.runElement, text, baseRPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata);
6126
+ insertRunPiecesBefore(xmlDoc, parent2, targetSpan.runElement, afterPieces, targetSpan.rPr);
6127
+ parent2.removeChild(targetSpan.runElement);
6128
+ return true;
6129
+ }
5545
6130
  }
5546
- if (!targetSpan) {
5547
- if (!fallbackParagraph) return false;
5548
- insertTextRuns(xmlDoc, fallbackParagraph, null, text, null, author, formatHints, insertOffset, generateRedlines);
5549
- return true;
6131
+ let parent = null;
6132
+ let referenceNode = null;
6133
+ if (affinity.hyperlink === "outside") {
6134
+ if (isRightInHyperlink) {
6135
+ const hyperlinkNode = boundary.rightSpan.runElement.parentNode;
6136
+ parent = hyperlinkNode.parentNode || fallbackParagraph;
6137
+ referenceNode = hyperlinkNode;
6138
+ } else if (isLeftInHyperlink) {
6139
+ const hyperlinkNode = boundary.leftSpan.runElement.parentNode;
6140
+ parent = hyperlinkNode.parentNode || fallbackParagraph;
6141
+ referenceNode = hyperlinkNode.nextSibling;
6142
+ }
6143
+ } else if (affinity.hyperlink === "inside") {
6144
+ if (isRightInHyperlink) {
6145
+ parent = boundary.rightSpan.runElement.parentNode;
6146
+ referenceNode = boundary.rightSpan.runElement;
6147
+ } else if (isLeftInHyperlink) {
6148
+ parent = boundary.leftSpan.runElement.parentNode;
6149
+ referenceNode = boundary.leftSpan.runElement.nextSibling;
6150
+ }
5550
6151
  }
5551
- const parent = targetSpan.runElement.parentNode;
5552
6152
  if (!parent) {
5553
- if (!fallbackParagraph) return false;
5554
- insertTextRuns(xmlDoc, fallbackParagraph, null, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines);
5555
- return true;
6153
+ if (boundary.rightSpan) {
6154
+ parent = boundary.rightSpan.runElement.parentNode || fallbackParagraph;
6155
+ referenceNode = boundary.rightSpan.runElement;
6156
+ } else if (boundary.leftSpan) {
6157
+ parent = boundary.leftSpan.runElement.parentNode || fallbackParagraph;
6158
+ referenceNode = boundary.leftSpan.runElement.nextSibling;
6159
+ } else {
6160
+ parent = fallbackParagraph;
6161
+ referenceNode = null;
6162
+ }
5556
6163
  }
5557
- const pieces = getRunContentPieces(targetSpan.runElement);
5558
- const targetPiece = pieces.find((piece) => piece.node === targetSpan.textElement);
5559
- const localInsertPos = targetPiece ? targetPiece.start + Math.max(0, Math.min(pos - targetSpan.charStart, targetSpan.charEnd - targetSpan.charStart)) : pos <= targetSpan.charStart ? 0 : getRunTextLength(pieces);
5560
- if (localInsertPos > 0 && localInsertPos < getRunTextLength(pieces)) {
5561
- const beforePieces = sliceRunPieces(xmlDoc, pieces, 0, localInsertPos, false);
5562
- const afterPieces = sliceRunPieces(xmlDoc, pieces, localInsertPos, getRunTextLength(pieces), false);
5563
- insertRunPiecesBefore(xmlDoc, parent, targetSpan.runElement, beforePieces, targetSpan.rPr);
5564
- insertTextRuns(xmlDoc, parent, targetSpan.runElement, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines);
5565
- insertRunPiecesBefore(xmlDoc, parent, targetSpan.runElement, afterPieces, targetSpan.rPr);
5566
- parent.removeChild(targetSpan.runElement);
5567
- return true;
6164
+ if (affinity.bookmark && parent) {
6165
+ if (affinity.bookmark === "outside") {
6166
+ if (referenceNode && isWordElement(referenceNode.previousSibling, "bookmarkStart")) {
6167
+ referenceNode = referenceNode.previousSibling;
6168
+ }
6169
+ if (boundary.leftSpan && isWordElement(boundary.leftSpan.runElement.nextSibling, "bookmarkEnd")) {
6170
+ referenceNode = boundary.leftSpan.runElement.nextSibling.nextSibling;
6171
+ }
6172
+ } else if (affinity.bookmark === "inside") {
6173
+ if (referenceNode && isWordElement(referenceNode, "bookmarkStart")) {
6174
+ referenceNode = referenceNode.nextSibling;
6175
+ }
6176
+ if (boundary.leftSpan && isWordElement(boundary.leftSpan.runElement.nextSibling, "bookmarkEnd")) {
6177
+ referenceNode = boundary.leftSpan.runElement.nextSibling;
6178
+ }
6179
+ }
6180
+ }
6181
+ if (affinity.comment && parent) {
6182
+ if (affinity.comment === "outside") {
6183
+ if (referenceNode && isWordElement(referenceNode.previousSibling, "commentRangeStart")) {
6184
+ referenceNode = referenceNode.previousSibling;
6185
+ }
6186
+ if (boundary.leftSpan && isWordElement(boundary.leftSpan.runElement.nextSibling, "commentRangeEnd")) {
6187
+ let afterComment = boundary.leftSpan.runElement.nextSibling.nextSibling;
6188
+ if (afterComment && (isWordElement(afterComment, "commentReference") || isWordElement(afterComment, "r"))) {
6189
+ const hasCRef = Array.from(afterComment.childNodes || []).some((n) => isWordElement(n, "commentReference"));
6190
+ if (hasCRef) afterComment = afterComment.nextSibling;
6191
+ }
6192
+ referenceNode = afterComment;
6193
+ }
6194
+ } else if (affinity.comment === "inside") {
6195
+ if (referenceNode && isWordElement(referenceNode, "commentRangeStart")) {
6196
+ referenceNode = referenceNode.nextSibling;
6197
+ }
6198
+ if (boundary.leftSpan && isWordElement(boundary.leftSpan.runElement.nextSibling, "commentRangeEnd")) {
6199
+ referenceNode = boundary.leftSpan.runElement.nextSibling;
6200
+ }
6201
+ }
6202
+ }
6203
+ if (generateRedlines && affinity.revision === "coalesce_same_author") {
6204
+ let insElem = null;
6205
+ let insRef = null;
6206
+ if (boundary.leftSpan && isWordElement(boundary.leftSpan.runElement.parentNode, "ins")) {
6207
+ const candidate = boundary.leftSpan.runElement.parentNode;
6208
+ const candAuthor = candidate.getAttribute("w:author") || candidate.getAttributeNS(NS_W, "author");
6209
+ if (candAuthor === author) {
6210
+ insElem = candidate;
6211
+ insRef = boundary.leftSpan.runElement.nextSibling;
6212
+ }
6213
+ } else if (boundary.rightSpan && isWordElement(boundary.rightSpan.runElement.parentNode, "ins")) {
6214
+ const candidate = boundary.rightSpan.runElement.parentNode;
6215
+ const candAuthor = candidate.getAttribute("w:author") || candidate.getAttributeNS(NS_W, "author");
6216
+ if (candAuthor === author) {
6217
+ insElem = candidate;
6218
+ insRef = boundary.rightSpan.runElement;
6219
+ }
6220
+ }
6221
+ if (insElem) {
6222
+ const insRun = createTextRun(xmlDoc, text, baseRPr, false);
6223
+ insElem.insertBefore(insRun, insRef);
6224
+ return true;
6225
+ }
5568
6226
  }
5569
- const referenceNode = pos <= targetSpan.charStart ? targetSpan.runElement : targetSpan.runElement.nextSibling;
5570
- insertTextRuns(xmlDoc, parent, referenceNode, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines);
6227
+ insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata);
5571
6228
  return true;
5572
6229
  }
5573
- function insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, formatHints, insertOffset, generateRedlines) {
6230
+ function insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata = null) {
5574
6231
  const applicableHints = getApplicableFormatHints(formatHints, insertOffset, insertOffset + text.length);
5575
6232
  if (applicableHints.length === 0) {
5576
6233
  const insRun = createTextRun(xmlDoc, text, baseRPr, false);
5577
6234
  if (generateRedlines) {
5578
- const insWrapper = createTrackChange(xmlDoc, "ins", insRun, author);
6235
+ const insWrapper = createTrackChange(xmlDoc, "ins", insRun, author, revisionMetadata);
5579
6236
  parent.insertBefore(insWrapper, referenceNode);
5580
6237
  } else {
5581
6238
  parent.insertBefore(insRun, referenceNode);
@@ -5584,7 +6241,7 @@ function insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, fo
5584
6241
  }
5585
6242
  const runs = createFormattedRuns(xmlDoc, text, baseRPr, applicableHints, insertOffset, author, generateRedlines);
5586
6243
  if (generateRedlines) {
5587
- const insWrapper = createTrackChange(xmlDoc, "ins", null, author);
6244
+ const insWrapper = createTrackChange(xmlDoc, "ins", null, author, revisionMetadata);
5588
6245
  runs.forEach((run) => insWrapper.appendChild(run));
5589
6246
  parent.insertBefore(insWrapper, referenceNode);
5590
6247
  } else {
@@ -5593,16 +6250,82 @@ function insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, fo
5593
6250
  }
5594
6251
 
5595
6252
  // engine/surgical-mode.js
5596
- function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true, targetParagraph = null, diffOptions = {}) {
5597
- void originalText;
6253
+ function checkSafeAdjacencyForPairing(spanIndex, startPos, endPos) {
6254
+ const spans = [];
6255
+ forEachOverlappingSpan(spanIndex, startPos, endPos, (span) => spans.push(span));
6256
+ if (spans.length === 0) return { safe: false };
6257
+ const firstRun = spans[0].runElement;
6258
+ const parent = firstRun?.parentNode;
6259
+ if (!parent) return { safe: false };
6260
+ const sameParent = spans.every((s) => s.runElement?.parentNode === parent);
6261
+ if (!sameParent) return { safe: false, structuralBoundary: true };
6262
+ const parentLocal = parent.localName || parent.nodeName.replace(/^.*:/, "");
6263
+ if (["hyperlink", "sdt", "ins", "del", "moveFrom", "moveTo"].includes(parentLocal)) {
6264
+ return { safe: false, structuralBoundary: true };
6265
+ }
6266
+ const structuralTags = /* @__PURE__ */ new Set([
6267
+ "hyperlink",
6268
+ "fldSimple",
6269
+ "sdt",
6270
+ "commentRangeStart",
6271
+ "commentRangeEnd",
6272
+ "commentReference",
6273
+ "bookmarkStart",
6274
+ "bookmarkEnd",
6275
+ "moveFrom",
6276
+ "moveTo",
6277
+ "ins",
6278
+ "del"
6279
+ ]);
6280
+ for (const span of spans) {
6281
+ const run = span.runElement;
6282
+ for (const child of Array.from(run.childNodes || [])) {
6283
+ if (child.nodeType === 1) {
6284
+ const tag = child.localName || child.nodeName.replace(/^.*:/, "");
6285
+ if (structuralTags.has(tag) || tag === "fldChar") {
6286
+ return { safe: false, structuralBoundary: true };
6287
+ }
6288
+ }
6289
+ }
6290
+ }
6291
+ const lastRun = spans[spans.length - 1].runElement;
6292
+ let curr = firstRun;
6293
+ while (curr && curr !== lastRun) {
6294
+ if (curr !== firstRun) {
6295
+ const tag = curr.localName || curr.nodeName.replace(/^.*:/, "");
6296
+ if (structuralTags.has(tag)) {
6297
+ return { safe: false, structuralBoundary: true };
6298
+ }
6299
+ }
6300
+ curr = curr.nextSibling;
6301
+ }
6302
+ function hasStructuralDescendant(node) {
6303
+ if (!node || node.nodeType !== 1) return false;
6304
+ const tag = node.localName || node.nodeName.replace(/^.*:/, "");
6305
+ if (structuralTags.has(tag) || tag === "fldChar") return true;
6306
+ for (const child of Array.from(node.childNodes || [])) {
6307
+ if (child.nodeType === 1 && hasStructuralDescendant(child)) return true;
6308
+ }
6309
+ return false;
6310
+ }
6311
+ if (hasStructuralDescendant(firstRun.previousSibling) || hasStructuralDescendant(lastRun.nextSibling)) {
6312
+ return { safe: false, structuralBoundary: true };
6313
+ }
6314
+ return { safe: true };
6315
+ }
6316
+ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true, targetParagraph = null, diffOptions = {}, options = {}) {
6317
+ void originalText;
5598
6318
  const allParagraphs = targetParagraph ? [targetParagraph] : getDocumentParagraphs(xmlDoc);
5599
6319
  const { fullText, textSpans } = buildSurgicalTextSpans(allParagraphs);
5600
6320
  const diffs = computeWordDiffs(fullText, modifiedText, diffOptions);
5601
6321
  const spanIndex = buildSpanIndex(textSpans);
6322
+ const pairReplacements = options.pairReplacements === true;
6323
+ const warnings = [];
5602
6324
  let originalPos = 0;
5603
6325
  let newPos = 0;
5604
6326
  let hasChanges = false;
5605
- for (const [op, text] of diffs) {
6327
+ for (let i = 0; i < diffs.length; i++) {
6328
+ const [op, text] = diffs[i];
5606
6329
  if (op === 0) {
5607
6330
  const len = text.length;
5608
6331
  const startPos = originalPos;
@@ -5622,21 +6345,73 @@ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, autho
5622
6345
  originalPos += len;
5623
6346
  newPos += len;
5624
6347
  } else if (op === -1) {
5625
- if (processDelete(xmlDoc, spanIndex, originalPos, originalPos + text.length, author, generateRedlines)) {
6348
+ const hasNextInsert = i + 1 < diffs.length && diffs[i + 1][0] === 1;
6349
+ let paired = false;
6350
+ let delMetadata = null;
6351
+ let insMetadata = null;
6352
+ if (pairReplacements && generateRedlines && hasNextInsert) {
6353
+ const nextText = diffs[i + 1][1];
6354
+ const textWithoutNewlines = nextText.replace(/\n/g, " ");
6355
+ if (textWithoutNewlines.trim().length > 0) {
6356
+ const checkResult = checkSafeAdjacencyForPairing(spanIndex, originalPos, originalPos + text.length);
6357
+ if (checkResult.safe) {
6358
+ const event = createReplacementRevisionEvent(author, xmlDoc);
6359
+ delMetadata = { id: event.deletionId, author: event.author, date: event.date };
6360
+ insMetadata = { id: event.insertionId, author: event.author, date: event.date };
6361
+ paired = true;
6362
+ } else if (checkResult.structuralBoundary) {
6363
+ warnings.push("PAIRING_SKIPPED_STRUCTURAL_BOUNDARY");
6364
+ }
6365
+ }
6366
+ }
6367
+ if (processDelete(xmlDoc, spanIndex, originalPos, originalPos + text.length, author, generateRedlines, delMetadata)) {
5626
6368
  hasChanges = true;
5627
6369
  }
5628
6370
  originalPos += text.length;
6371
+ if (paired) {
6372
+ i++;
6373
+ const [, nextText] = diffs[i];
6374
+ const textWithoutNewlines = nextText.replace(/\n/g, " ");
6375
+ if (textWithoutNewlines.trim().length > 0) {
6376
+ const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null, insMetadata, options?.insertionAffinity || null);
6377
+ if (insertResult && typeof insertResult === "object" && insertResult.error) {
6378
+ return withOoxmlSourceType({
6379
+ oxml: serializer.serializeToString(xmlDoc),
6380
+ hasChanges: false,
6381
+ status: "error",
6382
+ error: insertResult.error
6383
+ });
6384
+ }
6385
+ if (insertResult === true) {
6386
+ hasChanges = true;
6387
+ }
6388
+ }
6389
+ newPos += nextText.length;
6390
+ }
5629
6391
  } else if (op === 1) {
5630
6392
  const textWithoutNewlines = text.replace(/\n/g, " ");
5631
6393
  if (textWithoutNewlines.trim().length > 0) {
5632
- if (processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null)) {
6394
+ const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null, null, options?.insertionAffinity || null);
6395
+ if (insertResult && typeof insertResult === "object" && insertResult.error) {
6396
+ return withOoxmlSourceType({
6397
+ oxml: serializer.serializeToString(xmlDoc),
6398
+ hasChanges: false,
6399
+ status: "error",
6400
+ error: insertResult.error
6401
+ });
6402
+ }
6403
+ if (insertResult === true) {
5633
6404
  hasChanges = true;
5634
6405
  }
5635
6406
  }
5636
6407
  newPos += text.length;
5637
6408
  }
5638
6409
  }
5639
- return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges });
6410
+ return withOoxmlSourceType({
6411
+ oxml: serializer.serializeToString(xmlDoc),
6412
+ hasChanges,
6413
+ ...warnings.length > 0 ? { warnings: [...new Set(warnings)] } : {}
6414
+ });
5640
6415
  }
5641
6416
 
5642
6417
  // engine/reconstruction-mapper.js
@@ -5645,8 +6420,8 @@ var DMP = new import_diff_match_patch2.diff_match_patch();
5645
6420
  function localNameOf(node) {
5646
6421
  return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
5647
6422
  }
5648
- function wordAttribute(node, localName) {
5649
- return node?.getAttributeNS?.(NS_W, localName) || node?.getAttribute?.(`w:${localName}`) || node?.getAttribute?.(localName) || "";
6423
+ function wordAttribute(node, localName2) {
6424
+ return node?.getAttributeNS?.(NS_W, localName2) || node?.getAttribute?.(`w:${localName2}`) || node?.getAttribute?.(localName2) || "";
5650
6425
  }
5651
6426
  function createRangeCursorLookup2(ranges) {
5652
6427
  let cursor = 0;
@@ -6021,7 +6796,7 @@ function processHyperlinkForReconstruction(hyperlinkElement, originalFullText, p
6021
6796
  }
6022
6797
 
6023
6798
  // engine/reconstruction-writer.js
6024
- function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, formatHints, generateRedlines = true) {
6799
+ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, formatHints, generateRedlines = true, options = {}) {
6025
6800
  const {
6026
6801
  paragraphs,
6027
6802
  containerFragments,
@@ -6035,7 +6810,14 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
6035
6810
  } = context;
6036
6811
  const createNewParagraph = (pPr) => {
6037
6812
  const newParagraph = createWordElement(xmlDoc, "w:p");
6038
- if (pPr) newParagraph.appendChild(pPr.cloneNode(true));
6813
+ if (pPr) {
6814
+ const clonedPPr = pPr.cloneNode(true);
6815
+ const sectPr = getFirstElementByTagNSOrTag(clonedPPr, NS_W, "sectPr");
6816
+ if (sectPr) {
6817
+ clonedPPr.removeChild(sectPr);
6818
+ }
6819
+ newParagraph.appendChild(clonedPPr);
6820
+ }
6039
6821
  return newParagraph;
6040
6822
  };
6041
6823
  const startInfo = getParagraphInfo(0);
@@ -6046,15 +6828,30 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
6046
6828
  }
6047
6829
  let currentOriginalIndex = 0;
6048
6830
  let currentInsertOffset = 0;
6831
+ const pairReplacements = options?.pairReplacements === true;
6049
6832
  let pendingReplacementStart = null;
6833
+ let pendingReplacementEvent = null;
6050
6834
  const emittedCommentMarkers = /* @__PURE__ */ new WeakSet();
6051
- for (const [op, text] of diffs) {
6835
+ for (let diffIndex = 0; diffIndex < diffs.length; diffIndex++) {
6836
+ const [op, text] = diffs[diffIndex];
6052
6837
  if (op === 0 || op === -1) {
6053
6838
  const type = op === 0 ? "equal" : "delete";
6054
6839
  if (op === 0) {
6055
6840
  pendingReplacementStart = null;
6841
+ pendingReplacementEvent = null;
6056
6842
  } else if (pendingReplacementStart === null) {
6057
6843
  pendingReplacementStart = currentOriginalIndex;
6844
+ let hasInsert = false;
6845
+ for (let k = diffIndex + 1; k < diffs.length; k++) {
6846
+ if (diffs[k][0] === 1) {
6847
+ hasInsert = true;
6848
+ break;
6849
+ }
6850
+ if (diffs[k][0] === 0) break;
6851
+ }
6852
+ if (pairReplacements && generateRedlines && hasInsert) {
6853
+ pendingReplacementEvent = createReplacementRevisionEvent(author, xmlDoc);
6854
+ }
6058
6855
  }
6059
6856
  let offset = 0;
6060
6857
  while (offset < text.length) {
@@ -6080,7 +6877,8 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
6080
6877
  formatHints,
6081
6878
  currentInsertOffset,
6082
6879
  generateRedlines,
6083
- emittedCommentMarkers
6880
+ emittedCommentMarkers,
6881
+ pendingReplacementEvent
6084
6882
  );
6085
6883
  currentParagraph = appendResult.currentParagraph;
6086
6884
  if (op === 0) {
@@ -6112,13 +6910,47 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
6112
6910
  formatHints,
6113
6911
  currentInsertOffset,
6114
6912
  generateRedlines,
6115
- emittedCommentMarkers
6913
+ emittedCommentMarkers,
6914
+ pendingReplacementEvent,
6915
+ pendingReplacementStart !== null
6116
6916
  );
6117
6917
  currentParagraph = appendResult.currentParagraph;
6118
6918
  currentInsertOffset += text.length;
6119
6919
  pendingReplacementStart = null;
6920
+ pendingReplacementEvent = null;
6120
6921
  }
6121
6922
  }
6923
+ if (generateRedlines) {
6924
+ containerFragments.forEach((fragment) => {
6925
+ Array.from(fragment.childNodes).forEach((node) => {
6926
+ if (isWordElement(node, "p")) {
6927
+ const hasVisibleText = Array.from(node.getElementsByTagNameNS(NS_W, "t")).length > 0;
6928
+ const hasDeletedText = Array.from(node.getElementsByTagNameNS(NS_W, "delText")).length > 0;
6929
+ if (paragraphs.length === 1 && !hasVisibleText && (hasDeletedText || context.originalFullText.trim() !== "")) {
6930
+ markParagraphMarkDeleted(xmlDoc, node, author);
6931
+ }
6932
+ }
6933
+ });
6934
+ });
6935
+ }
6936
+ containerFragments.forEach((fragment) => {
6937
+ const createdParagraphs = Array.from(fragment.childNodes).filter((node) => isWordElement(node, "p"));
6938
+ if (createdParagraphs.length > 1) {
6939
+ const firstP = createdParagraphs[0];
6940
+ const lastP = createdParagraphs[createdParagraphs.length - 1];
6941
+ const firstPPr = getFirstElementByTagNSOrTag(firstP, NS_W, "pPr");
6942
+ const sectPr = firstPPr ? getFirstElementByTagNSOrTag(firstPPr, NS_W, "sectPr") : null;
6943
+ if (sectPr) {
6944
+ firstPPr.removeChild(sectPr);
6945
+ let lastPPr = getFirstElementByTagNSOrTag(lastP, NS_W, "pPr");
6946
+ if (!lastPPr) {
6947
+ lastPPr = createWordElement(xmlDoc, "w:pPr");
6948
+ lastP.insertBefore(lastPPr, lastP.firstChild || null);
6949
+ }
6950
+ lastPPr.appendChild(sectPr);
6951
+ }
6952
+ }
6953
+ });
6122
6954
  const paragraphSet = new Set(paragraphs);
6123
6955
  const insertionAnchors = /* @__PURE__ */ new Map();
6124
6956
  paragraphs.forEach((paragraph) => {
@@ -6157,16 +6989,23 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
6157
6989
  const oxml = hasDocumentTarget && serializedDocumentOutput ? serializedDocumentOutput : serializer.serializeToString(xmlDoc);
6158
6990
  return { oxml, hasChanges: true };
6159
6991
  }
6160
- function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, currentParagraphRef, containerFragments, sentinelMapByStart, referenceMap, replacementContainers, getParagraphInfo, createNewParagraph, author, formatHints = [], insertOffset = 0, generateRedlines = true, emittedCommentMarkers = /* @__PURE__ */ new WeakSet()) {
6992
+ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, currentParagraphRef, containerFragments, sentinelMapByStart, referenceMap, replacementContainers, getParagraphInfo, createNewParagraph, author, formatHints = [], insertOffset = 0, generateRedlines = true, emittedCommentMarkers = /* @__PURE__ */ new WeakSet(), replacementEvent = null, deferClosingCommentMarkers = false) {
6161
6993
  let localBaseIndex = baseIndex;
6162
6994
  let localInsertOffset = insertOffset;
6163
6995
  let localParagraph = currentParagraphRef;
6996
+ const deferredCommentMarkers = [];
6164
6997
  const parts = text.split(/([\n\uFFFC]|[\uE000-\uF8FF])/);
6165
6998
  parts.forEach((part) => {
6166
6999
  const sentinelsAtOffset = sentinelMapByStart.get(localBaseIndex) || [];
6167
7000
  const commentMarkers = sentinelsAtOffset.filter((sentinel) => sentinel.isCommentMarker && !emittedCommentMarkers.has(sentinel.node));
7001
+ const closingCommentIds = deferClosingCommentMarkers ? new Set(commentMarkers.filter((marker) => isWordElement(marker.node, "commentRangeEnd")).map((marker) => marker.node.getAttributeNS?.(NS_W, "id") || marker.node.getAttribute?.("w:id") || marker.node.getAttribute?.("id"))) : /* @__PURE__ */ new Set();
6168
7002
  commentMarkers.forEach((marker) => {
6169
7003
  emittedCommentMarkers.add(marker.node);
7004
+ const markerId = marker.node.getAttributeNS?.(NS_W, "id") || marker.node.getAttribute?.("w:id") || marker.node.getAttribute?.("id");
7005
+ if (deferClosingCommentMarkers && closingCommentIds.has(markerId) && (isWordElement(marker.node, "commentRangeEnd") || isWordElement(marker.node, "commentReference"))) {
7006
+ deferredCommentMarkers.push(marker.node);
7007
+ return;
7008
+ }
6170
7009
  if (isWordElement(marker.node, "commentReference")) {
6171
7010
  const run = createWordElement(xmlDoc, "w:r");
6172
7011
  run.appendChild(marker.node.cloneNode(true));
@@ -6246,14 +7085,34 @@ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, curren
6246
7085
  delText.textContent = part;
6247
7086
  run.appendChild(delText);
6248
7087
  if (generateRedlines) {
6249
- const del = createTrackChange(xmlDoc, "del", run, author);
7088
+ let metadata = null;
7089
+ if (replacementEvent) {
7090
+ const id = replacementEvent.usedDeletionId ? createRevisionMetadata(author, xmlDoc, "del").id : replacementEvent.deletionId;
7091
+ replacementEvent.usedDeletionId = true;
7092
+ metadata = {
7093
+ id,
7094
+ author: replacementEvent.author,
7095
+ date: replacementEvent.date
7096
+ };
7097
+ }
7098
+ const del = createTrackChange(xmlDoc, "del", run, author, metadata);
6250
7099
  parent.appendChild(del);
6251
7100
  }
6252
7101
  } else {
6253
7102
  const applicableHints = getApplicableFormatHints(formatHints, localInsertOffset, localInsertOffset + part.length);
6254
7103
  const runs = createFormattedRuns(xmlDoc, part, rPr, applicableHints, localInsertOffset, author, generateRedlines);
6255
7104
  if (type === "insert" && generateRedlines) {
6256
- const ins = createTrackChange(xmlDoc, "ins", null, author);
7105
+ let metadata = null;
7106
+ if (replacementEvent) {
7107
+ const id = replacementEvent.usedInsertionId ? createRevisionMetadata(author, xmlDoc, "ins").id : replacementEvent.insertionId;
7108
+ replacementEvent.usedInsertionId = true;
7109
+ metadata = {
7110
+ id,
7111
+ author: replacementEvent.author,
7112
+ date: replacementEvent.date
7113
+ };
7114
+ }
7115
+ const ins = createTrackChange(xmlDoc, "ins", null, author, metadata);
6257
7116
  runs.forEach((run) => ins.appendChild(run));
6258
7117
  parent.appendChild(ins);
6259
7118
  } else {
@@ -6265,11 +7124,20 @@ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, curren
6265
7124
  }
6266
7125
  localBaseIndex += part.length;
6267
7126
  });
7127
+ deferredCommentMarkers.forEach((marker) => {
7128
+ if (isWordElement(marker, "commentReference")) {
7129
+ const run = createWordElement(xmlDoc, "w:r");
7130
+ run.appendChild(marker.cloneNode(true));
7131
+ localParagraph.appendChild(run);
7132
+ } else {
7133
+ localParagraph.appendChild(marker.cloneNode(true));
7134
+ }
7135
+ });
6268
7136
  return { currentParagraph: localParagraph };
6269
7137
  }
6270
7138
 
6271
7139
  // engine/reconstruction-mode.js
6272
- function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true, diffOptions = {}) {
7140
+ function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true, diffOptions = {}, options = {}) {
6273
7141
  const selectedParagraphs = findReconstructionParagraphRange(xmlDoc, originalText);
6274
7142
  if (selectedParagraphs === null) {
6275
7143
  return withOoxmlSourceType({
@@ -6294,7 +7162,8 @@ function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer,
6294
7162
  serializer,
6295
7163
  author,
6296
7164
  formatHints,
6297
- generateRedlines
7165
+ generateRedlines,
7166
+ options
6298
7167
  ));
6299
7168
  }
6300
7169
 
@@ -6399,7 +7268,7 @@ function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser
6399
7268
  }
6400
7269
  });
6401
7270
  const del = createWordElement(workingDoc, "w:del");
6402
- const metadata = createRevisionMetadata(author, workingDoc);
7271
+ const metadata = createRevisionMetadata(author, workingDoc, "del");
6403
7272
  del.setAttribute("w:id", String(metadata.id));
6404
7273
  del.setAttribute("w:author", metadata.author);
6405
7274
  del.setAttribute("w:date", metadata.date);
@@ -6419,15 +7288,15 @@ function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser
6419
7288
  }
6420
7289
 
6421
7290
  // services/revision-comment-management.js
6422
- function getAttributeByLocalName(node, localName) {
7291
+ function getAttributeByLocalName(node, localName2) {
6423
7292
  if (!node || !node.attributes) return "";
6424
- for (const attr of Array.from(node.attributes)) {
6425
- if ((attr.localName || "").toLowerCase() === localName.toLowerCase()) {
6426
- return String(attr.value || "");
7293
+ for (const attr4 of Array.from(node.attributes)) {
7294
+ if ((attr4.localName || "").toLowerCase() === localName2.toLowerCase()) {
7295
+ return String(attr4.value || "");
6427
7296
  }
6428
7297
  }
6429
7298
  return String(
6430
- node.getAttribute?.(`w:${localName}`) || node.getAttribute?.(localName) || ""
7299
+ node.getAttribute?.(`w:${localName2}`) || node.getAttribute?.(localName2) || ""
6431
7300
  );
6432
7301
  }
6433
7302
  function normalizeAuthor(author) {
@@ -6436,11 +7305,11 @@ function normalizeAuthor(author) {
6436
7305
  function isElement(node) {
6437
7306
  return !!node && node.nodeType === 1;
6438
7307
  }
6439
- function isWordElement5(node, localName) {
6440
- return isElement(node) && node.namespaceURI === NS_W && String(node.localName || "").toLowerCase() === localName.toLowerCase();
7308
+ function isWordElement5(node, localName2) {
7309
+ return isElement(node) && node.namespaceURI === NS_W && String(node.localName || "").toLowerCase() === localName2.toLowerCase();
6441
7310
  }
6442
- function getWordElementsByLocalName(xmlDoc, localName) {
6443
- return Array.from(xmlDoc.getElementsByTagNameNS(NS_W, localName));
7311
+ function getWordElementsByLocalName(xmlDoc, localName2) {
7312
+ return Array.from(xmlDoc.getElementsByTagNameNS(NS_W, localName2));
6444
7313
  }
6445
7314
  function resolveAuthorFilter(options = {}) {
6446
7315
  if (options?.allAuthors === true) {
@@ -6463,19 +7332,37 @@ function authorMatchesNode(node, filter) {
6463
7332
  return !!nodeAuthor && nodeAuthor === filter.normalizedAuthor;
6464
7333
  }
6465
7334
  function parseXmlWithWarnings(oxml, parseFailurePrefix) {
6466
- const parsed = parseOoxmlSafe(oxml, "application/xml");
7335
+ let rawOxml = typeof oxml === "string" ? oxml.replace(/^\uFEFF/, "").trim() : "";
7336
+ let isFragmentWrapped = false;
7337
+ let parsed = parseOoxmlSafe(rawOxml, "application/xml");
7338
+ if (parsed.error && (parsed.error.message.includes("HierarchyRequestError") || parsed.error.message.includes("Only one element"))) {
7339
+ const wrapped = `<w:body xmlns:w="${NS_W}">${rawOxml}</w:body>`;
7340
+ const wrappedParsed = parseOoxmlSafe(wrapped, "application/xml");
7341
+ if (!wrappedParsed.error) {
7342
+ parsed = wrappedParsed;
7343
+ isFragmentWrapped = true;
7344
+ }
7345
+ }
6467
7346
  const parseError = parsed.doc ? getXmlParseError(parsed.doc) : null;
6468
7347
  if (parsed.error || parseError) {
6469
7348
  const message = parsed.error?.message || parseError?.textContent || "parse error";
6470
7349
  return {
6471
7350
  xmlDoc: null,
6472
7351
  serializer: null,
7352
+ isFragmentWrapped: false,
6473
7353
  warning: `${parseFailurePrefix}: ${message}`,
6474
7354
  warnings: parsed.warnings,
6475
7355
  error: { code: "PARSE_ERROR", message }
6476
7356
  };
6477
7357
  }
6478
- return { xmlDoc: parsed.doc, serializer: createSerializer(), warning: null, warnings: parsed.warnings, error: null };
7358
+ return {
7359
+ xmlDoc: parsed.doc,
7360
+ serializer: createSerializer(),
7361
+ isFragmentWrapped,
7362
+ warning: null,
7363
+ warnings: parsed.warnings,
7364
+ error: null
7365
+ };
6479
7366
  }
6480
7367
  function removeNode(node) {
6481
7368
  if (node?.parentNode) {
@@ -6518,10 +7405,16 @@ function mergeParagraphIntoNextAndRemove(paragraph) {
6518
7405
  if (!paragraph?.parentNode) return false;
6519
7406
  const nextParagraph = getNextWordParagraph(paragraph);
6520
7407
  if (!nextParagraph) {
7408
+ if (isWordElement5(paragraph.parentNode, "tc") && getWordElementsByLocalName(paragraph.parentNode, "p").length <= 1) {
7409
+ return false;
7410
+ }
6521
7411
  return removeNode(paragraph);
6522
7412
  }
6523
7413
  const childrenToMove = Array.from(paragraph.childNodes || []).filter((child) => !isWordElement5(child, "pPr"));
6524
- const insertionPoint = nextParagraph.firstChild || null;
7414
+ let insertionPoint = nextParagraph.firstChild || null;
7415
+ if (isWordElement5(insertionPoint, "pPr")) {
7416
+ insertionPoint = insertionPoint.nextSibling || null;
7417
+ }
6525
7418
  for (const child of childrenToMove) {
6526
7419
  nextParagraph.insertBefore(child, insertionPoint);
6527
7420
  }
@@ -6583,14 +7476,15 @@ function acceptTrackedChangesInOoxml(oxml, options = {}) {
6583
7476
  }
6584
7477
  acceptedCount += removeMoveRangeMarkers(xmlDoc, filter);
6585
7478
  const changeTags = ["rPrChange", "pPrChange", "tblPrChange", "trPrChange", "tcPrChange"];
6586
- for (const localName of changeTags) {
6587
- for (const changeNode of getWordElementsByLocalName(xmlDoc, localName)) {
7479
+ for (const localName2 of changeTags) {
7480
+ for (const changeNode of getWordElementsByLocalName(xmlDoc, localName2)) {
6588
7481
  if (!changeNode.parentNode || !authorMatchesNode(changeNode, filter)) continue;
6589
7482
  if (removeNode(changeNode)) acceptedCount += 1;
6590
7483
  }
6591
7484
  }
7485
+ const serializedOxml = parseResult.isFragmentWrapped ? Array.from(xmlDoc.documentElement.childNodes).map((n) => serializer.serializeToString(n)).join("") : serializer.serializeToString(xmlDoc);
6592
7486
  return {
6593
- oxml: serializer.serializeToString(xmlDoc),
7487
+ oxml: serializedOxml,
6594
7488
  hasChanges: acceptedCount > 0,
6595
7489
  acceptedCount,
6596
7490
  warnings
@@ -6609,10 +7503,10 @@ function convertDeletionTextNodes(xmlDoc, delNode) {
6609
7503
  delTextNode.parentNode?.replaceChild(normalText, delTextNode);
6610
7504
  }
6611
7505
  }
6612
- function rejectPropertyChangeNode(changeNode, localName) {
7506
+ function rejectPropertyChangeNode(changeNode, localName2) {
6613
7507
  const parent = changeNode?.parentNode;
6614
7508
  if (!parent) return false;
6615
- const baseLocalName = localName.endsWith("Change") ? localName.slice(0, -"Change".length) : "";
7509
+ const baseLocalName = localName2.endsWith("Change") ? localName2.slice(0, -"Change".length) : "";
6616
7510
  if (!baseLocalName || String(parent.localName || "").toLowerCase() !== baseLocalName.toLowerCase() || parent.namespaceURI !== NS_W) {
6617
7511
  return removeNode(changeNode);
6618
7512
  }
@@ -6638,9 +7532,9 @@ function xmlDocImportNode(xmlDoc, node) {
6638
7532
  }
6639
7533
  return node.cloneNode(true);
6640
7534
  }
6641
- function collectMoveRangeStartIds(xmlDoc, localName, filter) {
7535
+ function collectMoveRangeStartIds(xmlDoc, localName2, filter) {
6642
7536
  const ids = /* @__PURE__ */ new Set();
6643
- for (const node of getWordElementsByLocalName(xmlDoc, localName)) {
7537
+ for (const node of getWordElementsByLocalName(xmlDoc, localName2)) {
6644
7538
  if (!authorMatchesNode(node, filter)) continue;
6645
7539
  const id = getAttributeByLocalName(node, "id");
6646
7540
  if (id) ids.add(id);
@@ -6657,8 +7551,8 @@ function removeMoveRangeMarkers(xmlDoc, filter) {
6657
7551
  ["moveToRangeStart", moveToIds, true],
6658
7552
  ["moveToRangeEnd", moveToIds, false]
6659
7553
  ];
6660
- for (const [localName, ids, isStart] of markerSpecs) {
6661
- for (const node of getWordElementsByLocalName(xmlDoc, localName)) {
7554
+ for (const [localName2, ids, isStart] of markerSpecs) {
7555
+ for (const node of getWordElementsByLocalName(xmlDoc, localName2)) {
6662
7556
  if (!node.parentNode) continue;
6663
7557
  const id = getAttributeByLocalName(node, "id");
6664
7558
  if (!id) continue;
@@ -6727,14 +7621,15 @@ function rejectTrackedChangesInOoxml(oxml, options = {}) {
6727
7621
  }
6728
7622
  rejectedCount += removeMoveRangeMarkers(xmlDoc, filter);
6729
7623
  const changeTags = ["rPrChange", "pPrChange", "tblPrChange", "trPrChange", "tcPrChange"];
6730
- for (const localName of changeTags) {
6731
- for (const changeNode of getWordElementsByLocalName(xmlDoc, localName)) {
7624
+ for (const localName2 of changeTags) {
7625
+ for (const changeNode of getWordElementsByLocalName(xmlDoc, localName2)) {
6732
7626
  if (!changeNode.parentNode || !authorMatchesNode(changeNode, filter)) continue;
6733
- if (rejectPropertyChangeNode(changeNode, localName)) rejectedCount += 1;
7627
+ if (rejectPropertyChangeNode(changeNode, localName2)) rejectedCount += 1;
6734
7628
  }
6735
7629
  }
7630
+ const serializedOxml = parseResult.isFragmentWrapped ? Array.from(xmlDoc.documentElement.childNodes).map((n) => serializer.serializeToString(n)).join("") : serializer.serializeToString(xmlDoc);
6736
7631
  return {
6737
- oxml: serializer.serializeToString(xmlDoc),
7632
+ oxml: serializedOxml,
6738
7633
  hasChanges: rejectedCount > 0,
6739
7634
  rejectedCount,
6740
7635
  warnings
@@ -6773,12 +7668,12 @@ function runIsOnlyCommentReference(runNode) {
6773
7668
  function removeCommentAnchors(xmlDoc, targetIds) {
6774
7669
  let removed = 0;
6775
7670
  const anchorTags = ["commentRangeStart", "commentRangeEnd", "commentReference"];
6776
- for (const localName of anchorTags) {
6777
- for (const node of getWordElementsByLocalName(xmlDoc, localName)) {
7671
+ for (const localName2 of anchorTags) {
7672
+ for (const node of getWordElementsByLocalName(xmlDoc, localName2)) {
6778
7673
  if (!node.parentNode) continue;
6779
7674
  const id = getAttributeByLocalName(node, "id");
6780
7675
  if (!id || !targetIds.has(id)) continue;
6781
- if (localName === "commentReference" && runIsOnlyCommentReference(node.parentNode)) {
7676
+ if (localName2 === "commentReference" && runIsOnlyCommentReference(node.parentNode)) {
6782
7677
  if (removeNode(node.parentNode)) {
6783
7678
  removed += 1;
6784
7679
  }
@@ -6819,8 +7714,8 @@ function deleteCommentsByAuthorInOoxml(oxml, options = {}) {
6819
7714
  warnings.push(...parseResult.warnings || []);
6820
7715
  const { targetIds, commentNodes } = collectCommentTargetIds(xmlDoc, filter);
6821
7716
  if (filter.allAuthors) {
6822
- for (const localName of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
6823
- for (const node of getWordElementsByLocalName(xmlDoc, localName)) {
7717
+ for (const localName2 of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
7718
+ for (const node of getWordElementsByLocalName(xmlDoc, localName2)) {
6824
7719
  const id = getAttributeByLocalName(node, "id");
6825
7720
  if (id) targetIds.add(id);
6826
7721
  }
@@ -6837,7 +7732,36 @@ function deleteCommentsByAuthorInOoxml(oxml, options = {}) {
6837
7732
  };
6838
7733
  }
6839
7734
 
7735
+ // engine/route-selection.js
7736
+ var RECONCILIATION_CAPABILITY_MATRIX = Object.freeze({
7737
+ formatOnly: Object.freeze({ paragraphs: true, formatting: true, tables: "scoped", hyperlinks: "preserved", fields: "preserved", comments: "preserved" }),
7738
+ surgical: Object.freeze({ paragraphs: true, tables: "cell-scoped", hyperlinks: "preserved", fields: "preserved", comments: "preserved", notes: "preserved" }),
7739
+ reconstruction: Object.freeze({ paragraphs: true, hyperlinks: "sentinel-preserved", fields: "sentinel-preserved", comments: "marker-preserved", notes: "reference-preserved" }),
7740
+ table: Object.freeze({ tables: true, paragraphs: true, formatting: "cell-dependent", numbering: false }),
7741
+ listDirect: Object.freeze({ lists: true, numbering: true, paragraphs: "single-source expansion", tables: "embedded-markdown blocks", formatting: "markdown hints" }),
7742
+ listCompatibilityPipeline: Object.freeze({ lists: true, numbering: true, paragraphs: "multi-source patching", compatibility: true })
7743
+ });
7744
+ function recordRouteSelection(options, route, context = {}) {
7745
+ const callback = options?._routeInstrumentation?.onRoute;
7746
+ if (typeof callback !== "function") return;
7747
+ callback(Object.freeze({
7748
+ route,
7749
+ capabilities: RECONCILIATION_CAPABILITY_MATRIX[route] || null,
7750
+ ...context
7751
+ }));
7752
+ }
7753
+
6840
7754
  // engine/oxml-engine.js
7755
+ function getCommentIdsInOoxml(node) {
7756
+ const ids = /* @__PURE__ */ new Set();
7757
+ for (const localName2 of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
7758
+ for (const marker of getElementsByTagNSOrTag(node, NS_W, localName2)) {
7759
+ const id = marker.getAttribute?.("w:id") || marker.getAttribute?.("id");
7760
+ if (id !== "") ids.add(id);
7761
+ }
7762
+ }
7763
+ return [...ids].sort((a, b) => Number(a) - Number(b) || a.localeCompare(b));
7764
+ }
6841
7765
  async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}) {
6842
7766
  const inputOoxml = oxml;
6843
7767
  let workingOoxml = oxml;
@@ -6849,11 +7773,19 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
6849
7773
  let parseWarnings = [];
6850
7774
  const operationWarnings = [];
6851
7775
  let normalizedExistingRevisions = false;
6852
- const keepNormalizedNoOp = options.existingRevisions === "accept-all-first-keep-normalized";
7776
+ const existingRevisionsPolicy = options.existingRevisions || "merge-same-author";
7777
+ const keepNormalizedNoOp = existingRevisionsPolicy === "accept-all-first-keep-normalized";
6853
7778
  const finalize = (result) => {
6854
7779
  const withStatus = { ...result };
6855
7780
  if (normalizedExistingRevisions && withStatus.hasChanges === false && withStatus.status !== "error") {
6856
- if (keepNormalizedNoOp) {
7781
+ if (existingRevisionsPolicy === "merge-same-author") {
7782
+ withStatus.oxml = workingOoxml;
7783
+ withStatus.hasChanges = true;
7784
+ withStatus.warnings = [
7785
+ ...Array.isArray(withStatus.warnings) ? withStatus.warnings : [],
7786
+ "Previous revisions by the same author were reverted to baseline."
7787
+ ];
7788
+ } else if (keepNormalizedNoOp) {
6857
7789
  withStatus.oxml = workingOoxml;
6858
7790
  withStatus.hasChanges = true;
6859
7791
  withStatus.warnings = [
@@ -6900,8 +7832,74 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
6900
7832
  const revisionIdAllocator = options?._revisionIdAllocator instanceof RevisionIdAllocator ? options._revisionIdAllocator : new RevisionIdAllocator();
6901
7833
  seedRevisionIdsFromDocument(xmlDoc, revisionIdAllocator);
6902
7834
  if (containsTrackedChanges(xmlDoc)) {
6903
- const existingRevisionsPolicy = options.existingRevisions || "reject-input";
6904
- if (existingRevisionsPolicy === "accept-all-first" || existingRevisionsPolicy === "accept-all-first-keep-normalized") {
7835
+ if (existingRevisionsPolicy === "merge-same-author") {
7836
+ const authors = getTrackedChangeAuthors(xmlDoc);
7837
+ const currentAuthor = String(author || "").trim().toLowerCase();
7838
+ const isSameAuthor = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === currentAuthor);
7839
+ if (isSameAuthor) {
7840
+ const commentIds = getCommentIdsInOoxml(xmlDoc);
7841
+ if (commentIds.length > 0) {
7842
+ return finalize({
7843
+ oxml: inputOoxml,
7844
+ hasChanges: false,
7845
+ status: "error",
7846
+ error: {
7847
+ code: "COMMENTED_CONTENT_MERGE",
7848
+ message: "Refusing to merge existing revisions in commented content because reverting the prior revisions could remove or orphan comment anchors.",
7849
+ commentIds
7850
+ }
7851
+ });
7852
+ }
7853
+ log("[OxmlEngine] Existing revisions from same author detected; rejecting previous changes to merge against baseline");
7854
+ const rejected = rejectTrackedChangesInOoxml(inputOoxml, { author });
7855
+ if (rejected.status === "error") return finalize(rejected);
7856
+ workingOoxml = rejected.oxml;
7857
+ normalizedExistingRevisions = true;
7858
+ const rejectedParsed = parseOoxmlSafe(workingOoxml, "text/xml");
7859
+ parseWarnings.push(...rejectedParsed.warnings);
7860
+ xmlDoc = rejectedParsed.doc;
7861
+ const rejectedParseError = xmlDoc ? getXmlParseError(xmlDoc) : null;
7862
+ if (rejectedParsed.error || rejectedParseError) {
7863
+ const message = rejectedParsed.error?.message || rejectedParseError?.textContent || "Could not parse OOXML after rejecting same-author revisions.";
7864
+ error("[OxmlEngine] XML parse error after rejecting same-author revisions:", message);
7865
+ return finalize({
7866
+ oxml: inputOoxml,
7867
+ hasChanges: false,
7868
+ status: "error",
7869
+ error: {
7870
+ code: "PARSE_ERROR",
7871
+ message
7872
+ }
7873
+ });
7874
+ }
7875
+ if (containsTrackedChanges(xmlDoc)) {
7876
+ return finalize({
7877
+ oxml: inputOoxml,
7878
+ hasChanges: false,
7879
+ status: "error",
7880
+ error: {
7881
+ code: "UNSAFE_REVISION_NESTING",
7882
+ message: "Existing same-author revisions could not be completely restored to baseline; refusing to layer new revisions over unsupported revision markup."
7883
+ }
7884
+ });
7885
+ }
7886
+ seedRevisionIdsFromDocument(xmlDoc, revisionIdAllocator);
7887
+ const paragraphsInDoc = xmlDoc.documentElement && String(xmlDoc.documentElement.localName || "").toLowerCase() === "p" ? [xmlDoc.documentElement] : getDocumentParagraphs(xmlDoc);
7888
+ const baselineText = paragraphsInDoc.length > 0 ? paragraphsInDoc.map((p) => extractCanonicalParagraphText(p)).join("\n") : "";
7889
+ originalText = baselineText;
7890
+ } else {
7891
+ log("[OxmlEngine] Existing revisions detected from another/unattributed author; refusing per merge-same-author policy");
7892
+ return finalize({
7893
+ oxml: inputOoxml,
7894
+ hasChanges: false,
7895
+ status: "error",
7896
+ error: {
7897
+ code: "EXISTING_REVISIONS",
7898
+ message: `Input OOXML contains tracked changes from another author (${authors.length ? authors.join(", ") : "unattributed"}). Pass existingRevisions: "accept-all-first" or resolve revisions first.`
7899
+ }
7900
+ });
7901
+ }
7902
+ } else if (existingRevisionsPolicy === "accept-all-first" || existingRevisionsPolicy === "accept-all-first-keep-normalized") {
6905
7903
  log("[OxmlEngine] Existing revisions detected; accepting all input revisions before redlining");
6906
7904
  const accepted = acceptTrackedChangesInOoxml(inputOoxml, { allAuthors: true });
6907
7905
  if (accepted.status === "error") return finalize(accepted);
@@ -6933,7 +7931,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
6933
7931
  status: "error",
6934
7932
  error: {
6935
7933
  code: "EXISTING_REVISIONS",
6936
- message: 'Input OOXML contains existing tracked changes. Pass existingRevisions: "accept-all-first" to normalize before redlining.'
7934
+ message: 'Input OOXML contains existing tracked changes and existingRevisions is "reject-input".'
6937
7935
  }
6938
7936
  });
6939
7937
  }
@@ -6951,10 +7949,29 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
6951
7949
  }
6952
7950
  return isolatedResult;
6953
7951
  }
6954
- const sanitizedText = options.sanitizeInput === true ? sanitizeAiResponse(modifiedText) : modifiedText;
7952
+ let sanitizedText = options.sanitizeInput === true ? sanitizeAiResponse(modifiedText) : modifiedText;
6955
7953
  if (sanitizedText !== modifiedText) {
6956
7954
  operationWarnings.push("Input was sanitized; pass sanitizeInput: false to disable.");
6957
7955
  }
7956
+ let structuredAnalysis = null;
7957
+ if (options.structuredContent !== false) {
7958
+ structuredAnalysis = analyzeStructuredContent(sanitizedText);
7959
+ if (!structuredAnalysis.valid) {
7960
+ if (options.explicitStructuredContent === true) {
7961
+ const message = structuredAnalysis.issues.map((issue) => `${issue.code}: ${issue.message}`).join(" ");
7962
+ return finalize({
7963
+ oxml: inputOoxml,
7964
+ hasChanges: false,
7965
+ status: "error",
7966
+ error: { code: "STRUCTURED_CONTENT_INVALID", message },
7967
+ warnings: structuredAnalysis.issues.map((issue) => issue.message)
7968
+ });
7969
+ }
7970
+ structuredAnalysis = null;
7971
+ } else if (structuredAnalysis.requiresStructuredContent) {
7972
+ sanitizedText = structuredAnalysis.normalizedMarkdown;
7973
+ }
7974
+ }
6958
7975
  const { cleanText: cleanModifiedText, formatHints } = preprocessMarkdown(sanitizedText);
6959
7976
  const hasTextChanges = cleanModifiedText.trim() !== originalText.trim();
6960
7977
  const hasFormatHints = formatHints.length > 0;
@@ -7011,6 +8028,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
7011
8028
  log(`[OxmlEngine] Text changes: ${hasTextChanges}, New format hints: ${formatHints.length}, Existing format hints: ${existingFormatHints.length}`);
7012
8029
  const needsFormatRemoval = options.removeFormatting === true && !hasTextChanges && !hasFormatHints && hasExistingFormatting;
7013
8030
  if (!hasTextChanges && !hasFormatHints && !hasExistingFormatting) {
8031
+ recordRouteSelection(options, "noChange");
7014
8032
  log("[OxmlEngine] No text changes, no format hints, and no existing formatting detected");
7015
8033
  return finalizeUnchanged();
7016
8034
  }
@@ -7019,6 +8037,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
7019
8037
  return finalizeUnchanged();
7020
8038
  }
7021
8039
  if (needsFormatRemoval) {
8040
+ recordRouteSelection(options, "formatOnly", { removeFormatting: true });
7022
8041
  log("[OxmlEngine] Format REMOVAL detected: applying surgical replacement in OOXML");
7023
8042
  const tableCellCtx = initialTableCellContext;
7024
8043
  let targetParagraph = tableCellCtx.targetParagraph || null;
@@ -7052,6 +8071,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
7052
8071
  return finalize(removalResult);
7053
8072
  }
7054
8073
  if (!hasTextChanges && hasFormatHints) {
8074
+ recordRouteSelection(options, "formatOnly", { removeFormatting: false });
7055
8075
  log(`[OxmlEngine] Format-only change detected: ${formatHints.length} format hints`);
7056
8076
  const tableCellCtx = initialTableCellContext;
7057
8077
  const precomputedFormatContext = {
@@ -7074,17 +8094,21 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
7074
8094
  const hasTables = tables.length > 0;
7075
8095
  const isMarkdownTable = /^\|.+\|/.test(cleanModifiedText.trim()) && cleanModifiedText.includes("\n");
7076
8096
  const isTargetList = isListTargetLoose(cleanModifiedText);
8097
+ const isStructuredContent = options.structuredContent !== false && structuredAnalysis?.requiresStructuredContent === true;
7077
8098
  const tableCellContext = initialTableCellContext;
7078
8099
  log(`[OxmlEngine] Mode: ${hasTables ? "SURGICAL" : "RECONSTRUCTION"}, formatHints: ${formatHints.length}, isMarkdownTable: ${isMarkdownTable}, isTargetList: ${isTargetList}, isTableCellParagraph: ${tableCellContext.isTableCellParagraph}`);
7079
8100
  try {
7080
8101
  if (isMarkdownTable && !hasTables) {
8102
+ recordRouteSelection(options, "table", { transformation: "text-to-table" });
7081
8103
  log("[OxmlEngine] Text-to-table transformation: generating new table from Markdown");
7082
8104
  return finalize(applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines));
7083
8105
  }
7084
8106
  if (hasTables && isMarkdownTable) {
8107
+ recordRouteSelection(options, "table", { transformation: "table-reconciliation" });
7085
8108
  return finalize(applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines));
7086
8109
  }
7087
8110
  if (hasTables) {
8111
+ recordRouteSelection(options, "surgical", { tableScoped: true });
7088
8112
  const surgicalTarget = tableCellContext.hasTableWrapper && tableCellContext.targetParagraph ? tableCellContext.targetParagraph : null;
7089
8113
  if (surgicalTarget) {
7090
8114
  log("[OxmlEngine] Table cell edit: scoping surgical mode to target paragraph");
@@ -7097,7 +8121,9 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
7097
8121
  author,
7098
8122
  formatHints,
7099
8123
  generateRedlines,
7100
- surgicalTarget
8124
+ surgicalTarget,
8125
+ {},
8126
+ options
7101
8127
  );
7102
8128
  if (tableCellContext.hasTableWrapper && result.hasChanges && tableCellContext.targetParagraph) {
7103
8129
  log("[OxmlEngine] Stripping table wrapper for table cell paragraph (surgical mode)");
@@ -7105,14 +8131,36 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
7105
8131
  }
7106
8132
  return finalize(result);
7107
8133
  }
7108
- if (isTargetList) {
7109
- log("[OxmlEngine] \u{1F3AF} Using reconciliation pipeline for list generation");
7110
- const pipeline = new ReconciliationPipeline({
7111
- author,
7112
- generateRedlines,
7113
- revisionIdAllocator
8134
+ if (isTargetList || isStructuredContent) {
8135
+ const sourceParagraphs = getElementsByTagNSOrTag(xmlDoc, NS_W, "p");
8136
+ const useDirectListGeneration = sourceParagraphs.length === 1;
8137
+ recordRouteSelection(options, useDirectListGeneration ? "listDirect" : "listCompatibilityPipeline", {
8138
+ sourceParagraphCount: sourceParagraphs.length
7114
8139
  });
7115
- const result = await pipeline.execute(workingOoxml, sanitizedText, { xmlDoc });
8140
+ log(`[OxmlEngine] \u{1F3AF} Using ${useDirectListGeneration ? "direct list generation" : "compatibility pipeline"} for list reconciliation`);
8141
+ let result;
8142
+ if (useDirectListGeneration) {
8143
+ const ingested = ingestOoxml(workingOoxml, { xmlDoc });
8144
+ const numberingContext = detectNumberingContext(sourceParagraphs[0]);
8145
+ result = await executeListGeneration({
8146
+ cleanText: cleanModifiedText,
8147
+ numberingContext,
8148
+ originalRunModel: ingested.runModel,
8149
+ originalText: ingested.acceptedText,
8150
+ generateRedlines,
8151
+ author,
8152
+ font: options.font || null,
8153
+ revisionIdAllocator,
8154
+ numberingService: new NumberingService()
8155
+ });
8156
+ } else {
8157
+ const pipeline = new ReconciliationPipeline({
8158
+ author,
8159
+ generateRedlines,
8160
+ revisionIdAllocator
8161
+ });
8162
+ result = await pipeline.execute(workingOoxml, sanitizedText, { xmlDoc });
8163
+ }
7116
8164
  if (result.error?.code === "DIFF_TOKEN_LIMIT") {
7117
8165
  return finalize({ oxml: inputOoxml, hasChanges: false, status: "error", error: result.error });
7118
8166
  }
@@ -7124,10 +8172,15 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
7124
8172
  numberingXml: result.numberingXml
7125
8173
  });
7126
8174
  log(`[OxmlEngine] \u2705 Wrapped OOXML length: ${wrapped.length}`);
7127
- return finalize({ oxml: wrapped, hasChanges: true });
8175
+ return finalize({
8176
+ oxml: wrapped,
8177
+ hasChanges: true,
8178
+ ...Array.isArray(result.warnings) ? { warnings: result.warnings } : {}
8179
+ });
7128
8180
  }
7129
8181
  return finalizeUnchanged();
7130
8182
  }
8183
+ recordRouteSelection(options, "reconstruction");
7131
8184
  return finalize(applyReconstructionMode(
7132
8185
  xmlDoc,
7133
8186
  originalText,
@@ -7135,7 +8188,9 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
7135
8188
  serializer,
7136
8189
  author,
7137
8190
  formatHints,
7138
- generateRedlines
8191
+ generateRedlines,
8192
+ {},
8193
+ options
7139
8194
  ));
7140
8195
  } catch (caught) {
7141
8196
  if (isDiffTokenLimitError(caught)) {
@@ -7154,10 +8209,10 @@ function normalizeTargetText(text) {
7154
8209
  }
7155
8210
  function textSpanVisibleText(span) {
7156
8211
  const node = span?.textElement;
7157
- const localName = String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
7158
- if (localName === "tab") return " ";
7159
- if (localName === "br" || localName === "cr") return "\n";
7160
- if (localName === "noBreakHyphen") return "\u2011";
8212
+ const localName2 = String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
8213
+ if (localName2 === "tab") return " ";
8214
+ if (localName2 === "br" || localName2 === "cr") return "\n";
8215
+ if (localName2 === "noBreakHyphen") return "\u2011";
7161
8216
  return node?.textContent || "";
7162
8217
  }
7163
8218
  function sanitizeAiResponse(text) {
@@ -7182,39 +8237,27 @@ function createTargetNotFoundError(message) {
7182
8237
  error2.code = "TARGET_NOT_FOUND";
7183
8238
  return error2;
7184
8239
  }
8240
+ function createTargetError(code, message, candidates = null) {
8241
+ const error2 = new Error(message);
8242
+ error2.code = code;
8243
+ if (Array.isArray(candidates)) error2.candidates = candidates;
8244
+ return error2;
8245
+ }
7185
8246
  var WORD_MAIN_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
7186
- function getElementsByLocalName(node, localName) {
8247
+ function getElementsByLocalName(node, localName2) {
7187
8248
  if (!node) return [];
7188
8249
  if (typeof node.getElementsByTagNameNS === "function") {
7189
- const namespaced = toArray(node.getElementsByTagNameNS("*", localName));
8250
+ const namespaced = toArray(node.getElementsByTagNameNS("*", localName2));
7190
8251
  if (namespaced.length > 0) return namespaced;
7191
8252
  }
7192
8253
  if (typeof node.getElementsByTagName !== "function") return [];
7193
- const prefixed = toArray(node.getElementsByTagName(`w:${localName}`));
8254
+ const prefixed = toArray(node.getElementsByTagName(`w:${localName2}`));
7194
8255
  if (prefixed.length > 0) return prefixed;
7195
- return toArray(node.getElementsByTagName(localName));
7196
- }
7197
- function toParagraphText(paragraph) {
7198
- let text = "";
7199
- const visit = (node) => {
7200
- for (const child of toArray(node?.childNodes)) {
7201
- if (child?.nodeType !== 1) continue;
7202
- const localName = String(child.localName || child.nodeName || "").replace(/^.*:/, "");
7203
- if (localName === "t") {
7204
- text += child.textContent || "";
7205
- } else if (localName === "tab") {
7206
- text += " ";
7207
- } else {
7208
- visit(child);
7209
- }
7210
- }
7211
- };
7212
- visit(paragraph);
7213
- return text;
8256
+ return toArray(node.getElementsByTagName(localName2));
7214
8257
  }
7215
8258
  function getParagraphText(paragraph) {
7216
8259
  if (!paragraph) return "";
7217
- return toParagraphText(paragraph);
8260
+ return extractCanonicalParagraphText(paragraph);
7218
8261
  }
7219
8262
  function getDocumentParagraphNodes(xmlDoc) {
7220
8263
  if (!xmlDoc) return [];
@@ -7222,6 +8265,75 @@ function getDocumentParagraphNodes(xmlDoc) {
7222
8265
  const searchRoot = bodies.length > 0 ? bodies[0] : xmlDoc;
7223
8266
  return getElementsByLocalName(searchRoot, "p");
7224
8267
  }
8268
+ function getParagraphId2(paragraph) {
8269
+ if (!paragraph) return null;
8270
+ const attribute = toArray(paragraph.attributes).find(
8271
+ (candidate) => String(candidate?.localName || candidate?.name || "").replace(/^.*:/, "") === "paraId"
8272
+ );
8273
+ return attribute?.value || null;
8274
+ }
8275
+ function createParagraphFingerprint(paragraph, metadata = {}) {
8276
+ if (!paragraph) return null;
8277
+ const revisionView = metadata.revisionView === "rejected" ? "rejected" : "accepted";
8278
+ const text = typeof metadata.text === "string" ? metadata.text : extractCanonicalParagraphText(paragraph, { revisionView });
8279
+ const documentIndex = Number.isInteger(metadata.index) ? metadata.index : getDocumentParagraphNodes(paragraph.ownerDocument || paragraph).indexOf(paragraph) + 1;
8280
+ const paragraphId = metadata.paragraphId === void 0 ? getParagraphId2(paragraph) : metadata.paragraphId;
8281
+ const inTable = typeof metadata.inTable === "boolean" ? metadata.inTable : isParagraphInTable(paragraph);
8282
+ const viewPart = revisionView === "rejected" ? "rejected" : "";
8283
+ const identity = `${paragraphId || ""}${documentIndex}${inTable ? "table" : "body"}${viewPart}${text}`;
8284
+ let hash = 2166136261;
8285
+ for (let i = 0; i < identity.length; i++) {
8286
+ hash ^= identity.charCodeAt(i);
8287
+ hash = Math.imul(hash, 16777619) >>> 0;
8288
+ }
8289
+ return `fnv1a32:${hash.toString(16).padStart(8, "0")}`;
8290
+ }
8291
+ function buildParagraphMetadataIndex(xmlDoc, options = {}) {
8292
+ const revisionView = options.revisionView === "rejected" ? "rejected" : "accepted";
8293
+ const paragraphs = getDocumentParagraphNodes(xmlDoc);
8294
+ const entries = [];
8295
+ const byParagraph = /* @__PURE__ */ new Map();
8296
+ const byId = /* @__PURE__ */ new Map();
8297
+ const grouped = /* @__PURE__ */ new Map();
8298
+ for (let offset = 0; offset < paragraphs.length; offset++) {
8299
+ const paragraph = paragraphs[offset];
8300
+ const text = extractCanonicalParagraphText(paragraph, { revisionView });
8301
+ const paragraphId = getParagraphId2(paragraph);
8302
+ const inTable = isParagraphInTable(paragraph);
8303
+ const normalizedText = normalizeWhitespaceForTargeting(text);
8304
+ const entry = Object.freeze({
8305
+ paragraph,
8306
+ index: offset + 1,
8307
+ paragraphId,
8308
+ text,
8309
+ normalizedText,
8310
+ revisionView,
8311
+ fingerprint: createParagraphFingerprint(paragraph, {
8312
+ text,
8313
+ index: offset + 1,
8314
+ paragraphId,
8315
+ inTable,
8316
+ revisionView
8317
+ }),
8318
+ inTable
8319
+ });
8320
+ entries.push(entry);
8321
+ byParagraph.set(paragraph, entry);
8322
+ if (paragraphId && !byId.has(paragraphId)) byId.set(paragraphId, entry);
8323
+ if (normalizedText) {
8324
+ if (!grouped.has(normalizedText)) grouped.set(normalizedText, []);
8325
+ grouped.get(normalizedText).push(entry);
8326
+ }
8327
+ }
8328
+ for (const [key, values] of grouped) grouped.set(key, Object.freeze(values));
8329
+ return Object.freeze({
8330
+ revisionView,
8331
+ entries: Object.freeze(entries),
8332
+ byParagraph,
8333
+ byId,
8334
+ byNormalizedText: grouped
8335
+ });
8336
+ }
7225
8337
  function normalizeWhitespaceForTargeting(text) {
7226
8338
  return String(text || "").replace(/\s+/g, " ").trim();
7227
8339
  }
@@ -7253,40 +8365,49 @@ function splitLeadingParagraphMarker(text) {
7253
8365
  targetRef: Number.parseInt(marker[1], 10)
7254
8366
  };
7255
8367
  }
7256
- function findParagraphByReference(xmlDoc, targetRef) {
8368
+ function findParagraphByReference(xmlDoc, targetRef, paragraphMetadataIndex = null) {
7257
8369
  if (!Number.isInteger(targetRef) || targetRef < 1) return null;
7258
- const paragraphs = getDocumentParagraphNodes(xmlDoc);
7259
- return paragraphs[targetRef - 1] || null;
8370
+ if (paragraphMetadataIndex?.entries) {
8371
+ return paragraphMetadataIndex.entries[targetRef - 1]?.paragraph || null;
8372
+ }
8373
+ return getDocumentParagraphNodes(xmlDoc)[targetRef - 1] || null;
7260
8374
  }
7261
- function findContainingWordElement(node, localName, namespaceUri = WORD_MAIN_NS) {
8375
+ function findContainingWordElement(node, localName2, namespaceUri = WORD_MAIN_NS) {
7262
8376
  let current = node;
7263
8377
  while (current) {
7264
- if (current.nodeType === 1 && current.namespaceURI === namespaceUri && current.localName === localName) {
8378
+ if (current.nodeType === 1 && current.namespaceURI === namespaceUri && current.localName === localName2) {
7265
8379
  return current;
7266
8380
  }
7267
8381
  current = current.parentNode;
7268
8382
  }
7269
8383
  return null;
7270
8384
  }
7271
- function findParagraphByStrictText(xmlDoc, targetText) {
7272
- const paragraphs = getDocumentParagraphNodes(xmlDoc);
8385
+ function findParagraphByStrictText(xmlDoc, targetText, options = {}) {
8386
+ const metadataIndex = options.paragraphMetadataIndex || null;
8387
+ const entries = metadataIndex?.entries || null;
8388
+ const paragraphs = entries ? null : getDocumentParagraphNodes(xmlDoc);
7273
8389
  const normalizedTarget = String(targetText || "").trim();
7274
8390
  if (!normalizedTarget) return null;
7275
- const exact = paragraphs.find((p) => getParagraphText(p).trim() === normalizedTarget);
8391
+ const exact = entries ? entries.find((entry) => entry.text.trim() === normalizedTarget)?.paragraph : paragraphs.find((p) => getParagraphText(p).trim() === normalizedTarget);
7276
8392
  if (exact) return exact;
7277
8393
  const normTarget = normalizeWhitespaceForTargeting(normalizedTarget);
8394
+ if (metadataIndex?.byNormalizedText) {
8395
+ return metadataIndex.byNormalizedText.get(normTarget)?.[0]?.paragraph || null;
8396
+ }
7278
8397
  return paragraphs.find((p) => normalizeWhitespaceForTargeting(getParagraphText(p)) === normTarget) || null;
7279
8398
  }
7280
8399
  function findParagraphByBestTextMatch(xmlDoc, targetText, options = {}) {
7281
8400
  const onInfo = typeof options.onInfo === "function" ? options.onInfo : () => {
7282
8401
  };
7283
- const paragraphs = getDocumentParagraphNodes(xmlDoc);
8402
+ const metadataIndex = options.paragraphMetadataIndex || null;
8403
+ const entries = metadataIndex?.entries || null;
8404
+ const paragraphs = entries ? null : getDocumentParagraphNodes(xmlDoc);
7284
8405
  const normalizedTarget = String(targetText || "").trim();
7285
8406
  if (!normalizedTarget) return null;
7286
- const strictMatch = findParagraphByStrictText(xmlDoc, normalizedTarget);
8407
+ const strictMatch = findParagraphByStrictText(xmlDoc, normalizedTarget, { paragraphMetadataIndex: metadataIndex });
7287
8408
  if (strictMatch) return strictMatch;
7288
8409
  const normTarget = normalizeWhitespaceForTargeting(normalizedTarget);
7289
- const startsWithMatch = paragraphs.find((p) => {
8410
+ const startsWithMatch = entries ? entries.find((entry) => entry.normalizedText.length > 10 && normTarget.startsWith(entry.normalizedText))?.paragraph || null : paragraphs.find((p) => {
7290
8411
  const paragraphText = normalizeWhitespaceForTargeting(getParagraphText(p));
7291
8412
  return paragraphText.length > 10 && normTarget.startsWith(paragraphText);
7292
8413
  });
@@ -7294,7 +8415,7 @@ function findParagraphByBestTextMatch(xmlDoc, targetText, options = {}) {
7294
8415
  onInfo(`[Fuzzy] Prefix match (target starts with paragraph): "${getParagraphText(startsWithMatch).trim().slice(0, 60)}..."`);
7295
8416
  return startsWithMatch;
7296
8417
  }
7297
- const containsMatch = paragraphs.find((p) => {
8418
+ const containsMatch = entries ? entries.find((entry) => entry.normalizedText.length > 15 && normTarget.includes(entry.normalizedText))?.paragraph || null : paragraphs.find((p) => {
7298
8419
  const paragraphText = normalizeWhitespaceForTargeting(getParagraphText(p));
7299
8420
  return paragraphText.length > 15 && normTarget.includes(paragraphText);
7300
8421
  });
@@ -7305,8 +8426,10 @@ function findParagraphByBestTextMatch(xmlDoc, targetText, options = {}) {
7305
8426
  let bestScore = 0;
7306
8427
  let bestParagraph = null;
7307
8428
  const targetWords = new Set(normTarget.toLowerCase().split(/\s+/).filter((word) => word.length > 2));
7308
- for (const paragraph of paragraphs) {
7309
- const paragraphText = getParagraphText(paragraph).trim();
8429
+ const candidateCount = entries?.length ?? paragraphs.length;
8430
+ for (let index = 0; index < candidateCount; index++) {
8431
+ const paragraph = entries?.[index]?.paragraph ?? paragraphs[index];
8432
+ const paragraphText = (entries?.[index]?.text ?? getParagraphText(paragraph)).trim();
7310
8433
  if (!paragraphText) continue;
7311
8434
  const paragraphWords = normalizeWhitespaceForTargeting(paragraphText).toLowerCase().split(/\s+/).filter((word) => word.length > 2);
7312
8435
  const overlap = paragraphWords.filter((word) => targetWords.has(word)).length;
@@ -7328,14 +8451,154 @@ function resolveTargetParagraph(xmlDoc, options = {}) {
7328
8451
  const onWarn = typeof options.onWarn === "function" ? options.onWarn : () => {
7329
8452
  };
7330
8453
  const opType = options.opType || "operation";
7331
- const cleanTargetText = String(options.targetText || "").trim();
7332
- const parsedRef = parseParagraphReference(options.targetRef);
8454
+ const descriptor = options.targetDescriptor && typeof options.targetDescriptor === "object" ? options.targetDescriptor : null;
8455
+ const revisionView = descriptor?.revisionView === "rejected" ? "rejected" : "accepted";
8456
+ const cleanTargetText = String(descriptor?.exactText ?? descriptor?.text ?? options.targetText ?? "").trim();
8457
+ const parsedRef = parseParagraphReference(descriptor?.index ?? descriptor?.paragraphIndex ?? options.targetRef);
8458
+ const strictAmbiguity = options.strictAmbiguity === true;
8459
+ let paragraphMetadataIndex = options.paragraphMetadataIndex || null;
8460
+ if (paragraphMetadataIndex && paragraphMetadataIndex.revisionView !== revisionView) {
8461
+ paragraphMetadataIndex = options.metadataIndices?.[revisionView] || buildParagraphMetadataIndex(xmlDoc, { revisionView });
8462
+ } else if (!paragraphMetadataIndex) {
8463
+ paragraphMetadataIndex = buildParagraphMetadataIndex(xmlDoc, { revisionView });
8464
+ }
8465
+ if (descriptor?.paragraphId) {
8466
+ const byId = findParagraphById(xmlDoc, descriptor.paragraphId, paragraphMetadataIndex);
8467
+ if (!byId) {
8468
+ throw createTargetError(
8469
+ "TARGET_NOT_FOUND",
8470
+ `Target paragraphId not found: "${descriptor.paragraphId}".`
8471
+ );
8472
+ }
8473
+ const cachedEntry = paragraphMetadataIndex?.byParagraph?.get(byId) || null;
8474
+ const actualIndex = cachedEntry?.index ?? getDocumentParagraphNodes(xmlDoc).indexOf(byId) + 1;
8475
+ const actualFingerprint = cachedEntry?.fingerprint || createParagraphFingerprint(byId, { revisionView });
8476
+ const actualInTable = cachedEntry?.inTable ?? isParagraphInTable(byId);
8477
+ const actualText = cachedEntry?.normalizedText || normalizeWhitespaceForTargeting(extractCanonicalParagraphText(byId, { revisionView }));
8478
+ if (parsedRef != null && parsedRef !== actualIndex) {
8479
+ throw createTargetError(
8480
+ "TARGET_INDEX_MISMATCH",
8481
+ `Target paragraphId "${descriptor.paragraphId}" (index ${actualIndex}) does not match requested index ${parsedRef}.`,
8482
+ cachedEntry ? [serializeTargetCandidate(cachedEntry)] : null
8483
+ );
8484
+ }
8485
+ if (descriptor.fingerprint && descriptor.fingerprint !== actualFingerprint) {
8486
+ throw createTargetError(
8487
+ "TARGET_FINGERPRINT_MISMATCH",
8488
+ `Target paragraphId "${descriptor.paragraphId}" no longer matches its source fingerprint.`,
8489
+ cachedEntry ? [serializeTargetCandidate(cachedEntry)] : null
8490
+ );
8491
+ }
8492
+ if (typeof descriptor.inTable === "boolean" && descriptor.inTable !== actualInTable) {
8493
+ throw createTargetError(
8494
+ "TARGET_CONTEXT_MISMATCH",
8495
+ `Target paragraphId "${descriptor.paragraphId}" does not match the requested table context.`,
8496
+ cachedEntry ? [serializeTargetCandidate(cachedEntry)] : null
8497
+ );
8498
+ }
8499
+ if (cleanTargetText && actualText !== normalizeWhitespaceForTargeting(cleanTargetText)) {
8500
+ throw createTargetError(
8501
+ "TARGET_TEXT_MISMATCH",
8502
+ `Target paragraphId "${descriptor.paragraphId}" no longer matches the supplied text.`,
8503
+ cachedEntry ? [serializeTargetCandidate(cachedEntry)] : null
8504
+ );
8505
+ }
8506
+ if (descriptor.occurrence != null) {
8507
+ const textToFind = cleanTargetText || actualText;
8508
+ const textCandidates = findStrictTargetCandidates(xmlDoc, textToFind, paragraphMetadataIndex);
8509
+ const actualOccurrence = textCandidates.findIndex((c) => c.paragraph === byId) + 1;
8510
+ if (actualOccurrence === 0 || actualOccurrence !== descriptor.occurrence) {
8511
+ throw createTargetError(
8512
+ "TARGET_OCCURRENCE_MISMATCH",
8513
+ `Target paragraphId "${descriptor.paragraphId}" matches occurrence ${actualOccurrence}, not requested occurrence ${descriptor.occurrence}.`,
8514
+ textCandidates.map(serializeTargetCandidate)
8515
+ );
8516
+ }
8517
+ }
8518
+ return { paragraph: byId, resolvedBy: "paragraph_id" };
8519
+ }
8520
+ let candidates = [];
8521
+ if (cleanTargetText) {
8522
+ const unfilteredCandidates = findStrictTargetCandidates(xmlDoc, cleanTargetText, paragraphMetadataIndex);
8523
+ candidates = filterTargetCandidates(unfilteredCandidates, descriptor);
8524
+ if (descriptor?.fingerprint && unfilteredCandidates.length > 0 && candidates.length === 0) {
8525
+ throw createTargetError(
8526
+ "TARGET_FINGERPRINT_MISMATCH",
8527
+ "Target text matched, but no paragraph matched the supplied source fingerprint.",
8528
+ unfilteredCandidates.map(serializeTargetCandidate)
8529
+ );
8530
+ }
8531
+ if (typeof descriptor?.inTable === "boolean" && unfilteredCandidates.length > 0 && candidates.length === 0) {
8532
+ throw createTargetError(
8533
+ "TARGET_CONTEXT_MISMATCH",
8534
+ "Target text matched, but no paragraph matched the requested table context.",
8535
+ unfilteredCandidates.map(serializeTargetCandidate)
8536
+ );
8537
+ }
8538
+ if (descriptor?.occurrence) {
8539
+ const occurrenceMatch = candidates[descriptor.occurrence - 1] || null;
8540
+ if (!occurrenceMatch) {
8541
+ throw createTargetError(
8542
+ "TARGET_NOT_FOUND",
8543
+ `Target occurrence ${descriptor.occurrence} was not found.`,
8544
+ candidates.map(serializeTargetCandidate)
8545
+ );
8546
+ }
8547
+ return { paragraph: occurrenceMatch.paragraph, resolvedBy: "occurrence" };
8548
+ }
8549
+ if (strictAmbiguity) {
8550
+ if (parsedRef) {
8551
+ const byReference = candidates.find((candidate) => candidate.index === parsedRef) || null;
8552
+ if (byReference) return { paragraph: byReference.paragraph, resolvedBy: "ref" };
8553
+ if (descriptor?.fingerprint && candidates.length > 0) {
8554
+ throw createTargetError(
8555
+ "TARGET_FINGERPRINT_MISMATCH",
8556
+ `Target fingerprint does not match paragraph reference [P${parsedRef}].`,
8557
+ candidates.map(serializeTargetCandidate)
8558
+ );
8559
+ }
8560
+ if (candidates.length === 1) {
8561
+ return { paragraph: candidates[0].paragraph, resolvedBy: "strict_text_after_ref_drift" };
8562
+ }
8563
+ }
8564
+ if (candidates.length > 1) {
8565
+ throw createTargetError(
8566
+ "AMBIGUOUS_TARGET",
8567
+ `Target text matched ${candidates.length} paragraphs; provide paragraphId, index, occurrence, or fingerprint.`,
8568
+ candidates.map(serializeTargetCandidate)
8569
+ );
8570
+ }
8571
+ if (candidates.length === 0) {
8572
+ throw createTargetNotFoundError(`Target paragraph not found: "${cleanTargetText}"`);
8573
+ }
8574
+ }
8575
+ if (!parsedRef && candidates.length === 1) {
8576
+ const candidate = candidates[0];
8577
+ return {
8578
+ paragraph: candidate.paragraph,
8579
+ resolvedBy: descriptor?.fingerprint ? "fingerprint" : "strict_text"
8580
+ };
8581
+ }
8582
+ }
7333
8583
  if (parsedRef) {
7334
- const byRef = findParagraphByReference(xmlDoc, parsedRef);
8584
+ const byRef = findParagraphByReference(xmlDoc, parsedRef, paragraphMetadataIndex);
7335
8585
  if (byRef) {
8586
+ const cached = paragraphMetadataIndex?.byParagraph?.get(byRef) || null;
8587
+ if (descriptor?.fingerprint && descriptor.fingerprint !== cached?.fingerprint) {
8588
+ throw createTargetError(
8589
+ "TARGET_FINGERPRINT_MISMATCH",
8590
+ `Target fingerprint does not match paragraph reference [P${parsedRef}].`
8591
+ );
8592
+ }
8593
+ if (typeof descriptor?.inTable === "boolean" && descriptor.inTable !== cached?.inTable) {
8594
+ throw createTargetError(
8595
+ "TARGET_CONTEXT_MISMATCH",
8596
+ `Target paragraph reference [P${parsedRef}] does not match requested table context.`
8597
+ );
8598
+ }
7336
8599
  if (cleanTargetText) {
7337
- const strictMatch = findParagraphByStrictText(xmlDoc, cleanTargetText);
7338
- const byRefText = getParagraphText(byRef).trim();
8600
+ const strictMatch = findParagraphByStrictText(xmlDoc, cleanTargetText, { paragraphMetadataIndex });
8601
+ const byRefText = (cached?.text || extractCanonicalParagraphText(byRef, { revisionView })).trim();
7339
8602
  const byRefNorm = normalizeWhitespaceForTargeting(byRefText);
7340
8603
  const targetNorm = normalizeWhitespaceForTargeting(cleanTargetText);
7341
8604
  const hasDrift = byRefNorm !== targetNorm;
@@ -7344,7 +8607,7 @@ function resolveTargetParagraph(xmlDoc, options = {}) {
7344
8607
  return { paragraph: strictMatch, resolvedBy: "strict_text_after_ref_drift" };
7345
8608
  }
7346
8609
  if (hasDrift) {
7347
- const fuzzyMatch = findParagraphByBestTextMatch(xmlDoc, cleanTargetText, { onInfo });
8610
+ const fuzzyMatch = findParagraphByBestTextMatch(xmlDoc, cleanTargetText, { onInfo, paragraphMetadataIndex });
7348
8611
  if (fuzzyMatch && fuzzyMatch !== byRef) {
7349
8612
  onInfo(`[Target] [P${parsedRef}] drifted for ${opType}; using fuzzy text rematch.`);
7350
8613
  return { paragraph: fuzzyMatch, resolvedBy: "fuzzy_text_after_ref_drift" };
@@ -7360,10 +8623,22 @@ function resolveTargetParagraph(xmlDoc, options = {}) {
7360
8623
  }
7361
8624
  onWarn(`[WARN] Target reference [P${parsedRef}] not found; falling back to text matching for ${opType}.`);
7362
8625
  }
7363
- if (cleanTargetText) {
7364
- const strictMatch = findParagraphByStrictText(xmlDoc, cleanTargetText);
7365
- if (strictMatch) return { paragraph: strictMatch, resolvedBy: "strict_text" };
7366
- const fuzzyMatch = findParagraphByBestTextMatch(xmlDoc, cleanTargetText, { onInfo });
8626
+ if (cleanTargetText && !strictAmbiguity) {
8627
+ const strictMatch = findParagraphByStrictText(xmlDoc, cleanTargetText, { paragraphMetadataIndex });
8628
+ if (strictMatch) {
8629
+ const candidateCount = candidates.length;
8630
+ if (candidateCount > 1) {
8631
+ const warningMsg = `AMBIGUOUS_TARGET_HEURISTIC_USED: Target text matched ${candidateCount} paragraphs; permissive resolution chose candidate 1. Migrate to strict targeting (e.g. strictTargets: true with paragraphId, index, occurrence, or fingerprint) before v1.0.0.`;
8632
+ onWarn(warningMsg);
8633
+ return {
8634
+ paragraph: strictMatch,
8635
+ resolvedBy: "strict_text",
8636
+ warnings: [warningMsg]
8637
+ };
8638
+ }
8639
+ return { paragraph: strictMatch, resolvedBy: "strict_text" };
8640
+ }
8641
+ const fuzzyMatch = findParagraphByBestTextMatch(xmlDoc, cleanTargetText, { onInfo, paragraphMetadataIndex });
7367
8642
  if (fuzzyMatch) return { paragraph: fuzzyMatch, resolvedBy: "fuzzy_text" };
7368
8643
  }
7369
8644
  if (cleanTargetText) throw createTargetNotFoundError(`Target paragraph not found: "${cleanTargetText}"`);
@@ -7373,24 +8648,65 @@ function resolveTargetParagraph(xmlDoc, options = {}) {
7373
8648
  function isParagraphInTable(paragraph) {
7374
8649
  return !!findContainingWordElement(paragraph, "tbl");
7375
8650
  }
7376
- function findStrictTargetCandidates(xmlDoc, targetText) {
8651
+ function findStrictTargetCandidates(xmlDoc, targetText, optionsOrIndex = null) {
7377
8652
  const normalizedTarget = normalizeWhitespaceForTargeting(targetText);
7378
8653
  if (!normalizedTarget) return [];
8654
+ const metadataIndex = optionsOrIndex?.byNormalizedText ? optionsOrIndex : optionsOrIndex?.paragraphMetadataIndex || null;
8655
+ const revisionView = optionsOrIndex?.revisionView || metadataIndex?.revisionView || "accepted";
8656
+ if (metadataIndex?.byNormalizedText && metadataIndex.revisionView === revisionView) {
8657
+ return Array.from(metadataIndex.byNormalizedText.get(normalizedTarget) || []);
8658
+ }
7379
8659
  const paragraphs = getDocumentParagraphNodes(xmlDoc);
7380
8660
  const candidates = [];
7381
8661
  for (let i = 0; i < paragraphs.length; i++) {
7382
8662
  const paragraph = paragraphs[i];
7383
- const paragraphText = getParagraphText(paragraph).trim();
8663
+ const paragraphText = extractCanonicalParagraphText(paragraph, { revisionView }).trim();
7384
8664
  if (!paragraphText) continue;
7385
8665
  if (normalizeWhitespaceForTargeting(paragraphText) !== normalizedTarget) continue;
7386
8666
  candidates.push({
7387
8667
  paragraph,
7388
8668
  index: i + 1,
7389
- inTable: isParagraphInTable(paragraph)
8669
+ inTable: isParagraphInTable(paragraph),
8670
+ paragraphId: getParagraphId2(paragraph),
8671
+ fingerprint: createParagraphFingerprint(paragraph, {
8672
+ text: paragraphText,
8673
+ index: i + 1,
8674
+ revisionView
8675
+ }),
8676
+ text: paragraphText,
8677
+ revisionView
7390
8678
  });
7391
8679
  }
7392
8680
  return candidates;
7393
8681
  }
8682
+ function serializeTargetCandidate(candidate) {
8683
+ return {
8684
+ index: candidate.index,
8685
+ paragraphId: candidate.paragraphId || null,
8686
+ text: candidate.text,
8687
+ inTable: candidate.inTable,
8688
+ fingerprint: candidate.fingerprint,
8689
+ revisionView: candidate.revisionView || "accepted"
8690
+ };
8691
+ }
8692
+ function filterTargetCandidates(candidates, descriptor) {
8693
+ let scoped = candidates.slice();
8694
+ if (descriptor?.paragraphId) {
8695
+ scoped = scoped.filter((candidate) => candidate.paragraphId === descriptor.paragraphId);
8696
+ }
8697
+ if (typeof descriptor?.inTable === "boolean") {
8698
+ scoped = scoped.filter((candidate) => candidate.inTable === descriptor.inTable);
8699
+ }
8700
+ if (descriptor?.fingerprint) {
8701
+ scoped = scoped.filter((candidate) => candidate.fingerprint === descriptor.fingerprint);
8702
+ }
8703
+ return scoped;
8704
+ }
8705
+ function findParagraphById(xmlDoc, paragraphId, paragraphMetadataIndex = null) {
8706
+ if (!paragraphId) return null;
8707
+ if (paragraphMetadataIndex?.byId) return paragraphMetadataIndex.byId.get(paragraphId)?.paragraph || null;
8708
+ return getDocumentParagraphNodes(xmlDoc).find((paragraph) => getParagraphId2(paragraph) === paragraphId) || null;
8709
+ }
7394
8710
  function selectBestTargetCandidate(candidates, parsedRef, expectedInTable = null) {
7395
8711
  if (!Array.isArray(candidates) || candidates.length === 0) return null;
7396
8712
  let scoped = candidates.slice();
@@ -7403,16 +8719,18 @@ function selectBestTargetCandidate(candidates, parsedRef, expectedInTable = null
7403
8719
  }
7404
8720
  return scoped[0] || null;
7405
8721
  }
7406
- function buildTargetReferenceSnapshot(xmlDoc) {
7407
- const paragraphs = getDocumentParagraphNodes(xmlDoc);
8722
+ function buildTargetReferenceSnapshot(xmlDoc, paragraphMetadataIndex = null) {
8723
+ const entries = paragraphMetadataIndex?.entries || null;
8724
+ const paragraphs = entries ? null : getDocumentParagraphNodes(xmlDoc);
7408
8725
  const snapshot = /* @__PURE__ */ new Map();
7409
- for (let i = 0; i < paragraphs.length; i++) {
7410
- const paragraph = paragraphs[i];
7411
- const text = getParagraphText(paragraph).trim();
8726
+ const paragraphCount = entries?.length ?? paragraphs.length;
8727
+ for (let i = 0; i < paragraphCount; i++) {
8728
+ const paragraph = entries?.[i]?.paragraph ?? paragraphs[i];
8729
+ const text = (entries?.[i]?.text ?? getParagraphText(paragraph)).trim();
7412
8730
  snapshot.set(i + 1, {
7413
8731
  text,
7414
- normalizedText: normalizeWhitespaceForTargeting(text),
7415
- inTable: isParagraphInTable(paragraph)
8732
+ normalizedText: entries?.[i]?.normalizedText ?? normalizeWhitespaceForTargeting(text),
8733
+ inTable: entries?.[i]?.inTable ?? isParagraphInTable(paragraph)
7416
8734
  });
7417
8735
  }
7418
8736
  return snapshot;
@@ -7429,7 +8747,7 @@ function resolveTargetParagraphWithSnapshot(xmlDoc, options = {}) {
7429
8747
  const expectedText = cleanTargetText || snapshotEntry.text || "";
7430
8748
  const expectedNorm = normalizeWhitespaceForTargeting(expectedText);
7431
8749
  if (!expectedNorm) return resolved;
7432
- const resolvedNorm = normalizeWhitespaceForTargeting(getParagraphText(resolved.paragraph));
8750
+ const resolvedNorm = options.paragraphMetadataIndex?.byParagraph?.get(resolved.paragraph)?.normalizedText || normalizeWhitespaceForTargeting(getParagraphText(resolved.paragraph));
7433
8751
  if (resolvedNorm === expectedNorm) return resolved;
7434
8752
  const candidateTexts = [];
7435
8753
  if (cleanTargetText) candidateTexts.push(cleanTargetText);
@@ -7441,7 +8759,7 @@ function resolveTargetParagraphWithSnapshot(xmlDoc, options = {}) {
7441
8759
  }
7442
8760
  let bestCandidate = null;
7443
8761
  for (const candidateText of candidateTexts) {
7444
- const candidates = findStrictTargetCandidates(xmlDoc, candidateText);
8762
+ const candidates = findStrictTargetCandidates(xmlDoc, candidateText, options.paragraphMetadataIndex || null);
7445
8763
  const selected = selectBestTargetCandidate(candidates, parsedRef, snapshotEntry.inTable);
7446
8764
  if (!selected) continue;
7447
8765
  if (!bestCandidate) bestCandidate = selected;
@@ -7496,11 +8814,11 @@ function resolveParagraphRangeByRefs(xmlDoc, startRef, endRef, options = {}) {
7496
8814
  }
7497
8815
 
7498
8816
  // core/list-targeting.js
7499
- function getFirstDescendantByLocalName(node, localName) {
8817
+ function getFirstDescendantByLocalName(node, localName2) {
7500
8818
  if (!node || typeof node.getElementsByTagNameNS !== "function") return null;
7501
- const namespaced = node.getElementsByTagNameNS(WORD_MAIN_NS, localName);
8819
+ const namespaced = node.getElementsByTagNameNS(WORD_MAIN_NS, localName2);
7502
8820
  if (namespaced.length > 0) return namespaced[0];
7503
- const anyNs = node.getElementsByTagNameNS("*", localName);
8821
+ const anyNs = node.getElementsByTagNameNS("*", localName2);
7504
8822
  return anyNs.length > 0 ? anyNs[0] : null;
7505
8823
  }
7506
8824
  function readValAttribute(element) {
@@ -7511,18 +8829,13 @@ function readValAttribute(element) {
7511
8829
  }
7512
8830
  return element.getAttribute("w:val") || element.getAttribute("val") || null;
7513
8831
  }
7514
- function parseOutlineLevelFromMarker(marker) {
7515
- const normalized = String(marker || "").trim();
7516
- if (!/^\d+(?:\.\d+)+\.?$/.test(normalized)) return null;
7517
- const parts = normalized.replace(/\.$/, "").split(".");
7518
- return Math.max(0, parts.length - 1);
7519
- }
7520
- var REDUNDANT_LIST_PREFIX_REGEX = /^(?:(?:\d+(?:\.\d+)*\.?|\((?:\d+|[a-zA-Z]|[ivxlcIVXLC]+)\)|[a-zA-Z]\.|[ivxlcIVXLC]+\.|[-*+\u2022]))\s+/;
7521
8832
  function stripRedundantLeadingListMarkers(text) {
7522
8833
  let value = String(text || "").trim();
7523
8834
  let passes = 0;
7524
- while (passes < 4 && REDUNDANT_LIST_PREFIX_REGEX.test(value)) {
7525
- value = value.replace(REDUNDANT_LIST_PREFIX_REGEX, "").trimStart();
8835
+ while (passes < 4) {
8836
+ const stripped = stripListMarker(value);
8837
+ if (stripped === value) break;
8838
+ value = stripped.trimStart();
7526
8839
  passes++;
7527
8840
  }
7528
8841
  return value.trim();
@@ -7534,19 +8847,16 @@ function parseModifiedListItems(modifiedText) {
7534
8847
  for (const rawLine of rawLines) {
7535
8848
  const line = rawLine.trimEnd();
7536
8849
  if (!line.trim()) continue;
7537
- const markerMatch = line.match(/^(\s*)((?:\d+(?:\.\d+)*\.?|\((?:\d+|[a-zA-Z]|[ivxlcIVXLC]+)\)|[a-zA-Z]\.|[ivxlcIVXLC]+\.|[-*+\u2022]))\s+(.*)$/);
7538
- if (markerMatch) {
8850
+ const parsed = parseListItem(line, { indentSpaces: 2 });
8851
+ if (parsed) {
7539
8852
  hasListMarkers = true;
7540
- const marker = markerMatch[2];
7541
- const markerType = /^[-*+\u2022]$/.test(marker) ? "bullet" : "numbered";
7542
- const level = Math.floor((markerMatch[1] || "").length / 2);
7543
8853
  items.push({
7544
8854
  kind: "list",
7545
- markerType,
7546
- level,
7547
- marker,
7548
- outlineLevel: markerType === "numbered" ? parseOutlineLevelFromMarker(marker) : null,
7549
- text: stripRedundantLeadingListMarkers(markerMatch[3])
8855
+ markerType: parsed.markerType,
8856
+ level: parsed.level,
8857
+ marker: parsed.marker,
8858
+ outlineLevel: parsed.outlineLevel,
8859
+ text: stripRedundantLeadingListMarkers(parsed.text)
7550
8860
  });
7551
8861
  continue;
7552
8862
  }
@@ -7573,7 +8883,7 @@ function resolveInsertionLevel(item, anchorLevel, baselineLevel) {
7573
8883
  }
7574
8884
  function shouldPromoteBulletInsertionsToChildDepth(parsedItems, normalizedTargetText, anchorLevel) {
7575
8885
  if (!Array.isArray(parsedItems) || parsedItems.length < 2) return false;
7576
- if (!Number.isInteger(anchorLevel) || anchorLevel < 1) return false;
8886
+ if (!Number.isInteger(anchorLevel) || anchorLevel < 0) return false;
7577
8887
  const firstItem = parsedItems[0];
7578
8888
  const trailingListItems = parsedItems.slice(1).filter((item) => item.kind === "list");
7579
8889
  if (trailingListItems.length === 0) return false;
@@ -7633,7 +8943,7 @@ function getParagraphListInfo(paragraph) {
7633
8943
  const numIdEl = getFirstDescendantByLocalName(numPr, "numId");
7634
8944
  if (!numIdEl) return null;
7635
8945
  const numId = readValAttribute(numIdEl);
7636
- if (!numId) return null;
8946
+ if (!numId || numId === "0") return null;
7637
8947
  const ilvlEl = getFirstDescendantByLocalName(numPr, "ilvl");
7638
8948
  const ilvlRaw = readValAttribute(ilvlEl);
7639
8949
  const ilvl = Number.parseInt(ilvlRaw || "0", 10);
@@ -7780,18 +9090,13 @@ function parseMarkdownListContent(content) {
7780
9090
  const items = [];
7781
9091
  for (const line of lines) {
7782
9092
  if (!line.trim()) continue;
7783
- const markerMatch = matchListMarker(line, { allowZeroSpaceAfterMarker: false });
7784
- if (markerMatch) {
7785
- const indent = markerMatch[1] || "";
7786
- const marker = markerMatch[2].trim();
7787
- const text = stripListMarker(line, { allowZeroSpaceAfterMarker: false }).trim();
7788
- const level = Math.floor(indent.length / 2);
7789
- const isBullet = /^[-*+\u2022]$/.test(marker);
9093
+ const parsed = parseListItem(line, { allowZeroSpaceAfterMarker: false, indentSpaces: 2 });
9094
+ if (parsed) {
7790
9095
  items.push({
7791
- type: isBullet ? "bullet" : "numbered",
7792
- level,
7793
- text,
7794
- marker
9096
+ type: parsed.markerType,
9097
+ level: parsed.level,
9098
+ text: parsed.text.trim(),
9099
+ marker: parsed.marker
7795
9100
  });
7796
9101
  continue;
7797
9102
  }
@@ -7815,16 +9120,6 @@ function hasListItems(parsedListData) {
7815
9120
  }
7816
9121
 
7817
9122
  // orchestration/list-markdown.js
7818
- function inferNumberingStyleFromMarker(marker) {
7819
- const m = (marker || "").trim();
7820
- if (!m) return "decimal";
7821
- if (/^\d+(?:\.\d+)*\.?$/.test(m) || /^\(\d+\)$/.test(m)) return "decimal";
7822
- if (/^[ivxlcdm]+\.$/.test(m)) return "lowerRoman";
7823
- if (/^[IVXLCDM]{2,}\.$/.test(m)) return "upperRoman";
7824
- if (/^[a-z]\.$/.test(m)) return "lowerAlpha";
7825
- if (/^[A-Z]\.$/.test(m)) return "upperAlpha";
7826
- return "decimal";
7827
- }
7828
9123
  function buildListMarkdown(itemsWithLevels, listType, numberingStyle) {
7829
9124
  const levelCounters = /* @__PURE__ */ new Map();
7830
9125
  const lines = [];
@@ -7845,7 +9140,6 @@ function buildListMarkdown(itemsWithLevels, listType, numberingStyle) {
7845
9140
  }
7846
9141
  function normalizeListItemsWithLevels(rawItems, options = {}) {
7847
9142
  const indentSpaces = Math.max(1, Number(options.indentSpaces) || 4);
7848
- const markersRegex = /^((?:\d+(?:\.\d+)*\.?|\((?:\d+|[a-zA-Z]|[ivxlcIVXLC]+)\)|[a-zA-Z]\.|\d+\.|[ivxlcIVXLC]+\.|[-*•])\s*)/;
7849
9143
  return (rawItems || []).map((rawItem) => {
7850
9144
  const item = String(rawItem ?? "");
7851
9145
  const indentMatch = item.match(/^(\s*)/);
@@ -7853,10 +9147,10 @@ function normalizeListItemsWithLevels(rawItems, options = {}) {
7853
9147
  const level = Math.floor(indentSize / indentSpaces);
7854
9148
  let stripped = item.trim();
7855
9149
  let removedMarker = null;
7856
- const markerMatch = stripped.match(markersRegex);
9150
+ const markerMatch = matchListMarker(stripped, { allowZeroSpaceAfterMarker: true });
7857
9151
  if (markerMatch) {
7858
- removedMarker = markerMatch[1].trim() || null;
7859
- stripped = stripped.replace(markersRegex, "");
9152
+ removedMarker = markerMatch[2].trim() || null;
9153
+ stripped = stripListMarker(stripped, { allowZeroSpaceAfterMarker: true });
7860
9154
  }
7861
9155
  return {
7862
9156
  text: stripped.trim(),
@@ -8009,10 +9303,10 @@ function clearSingleLineListFallbackExplicitSequence(sequenceState, numberingKey
8009
9303
  if (!numberingKey) return;
8010
9304
  sequenceState.explicitByNumberingKey.delete(String(numberingKey));
8011
9305
  }
8012
- function getDirectWordChild2(element, localName) {
9306
+ function getDirectWordChild2(element, localName2) {
8013
9307
  if (!element) return null;
8014
9308
  return Array.from(element.childNodes || []).find(
8015
- (node) => node && node.nodeType === 1 && node.namespaceURI === "http://schemas.openxmlformats.org/wordprocessingml/2006/main" && node.localName === localName
9309
+ (node) => node && node.nodeType === 1 && node.namespaceURI === "http://schemas.openxmlformats.org/wordprocessingml/2006/main" && node.localName === localName2
8016
9310
  ) || null;
8017
9311
  }
8018
9312
  function enforceListBindingOnParagraphNodes(nodes, options = {}) {
@@ -8198,13 +9492,21 @@ async function executeSingleLineListStructuralFallback(plan, options = {}) {
8198
9492
  }
8199
9493
  const author = options.author || "AI";
8200
9494
  const generateRedlines = options.generateRedlines ?? true;
8201
- const pipeline = options.pipeline || new ReconciliationPipeline({ author, generateRedlines });
8202
- const result = await pipeline.executeListGeneration(
9495
+ const result = options.pipeline ? await options.pipeline.executeListGeneration(
8203
9496
  plan.listInput,
8204
9497
  null,
8205
9498
  null,
8206
9499
  String(plan.originalText || "")
8207
- );
9500
+ ) : await executeListGeneration({
9501
+ cleanText: plan.listInput,
9502
+ numberingContext: null,
9503
+ originalRunModel: [],
9504
+ originalText: String(plan.originalText || ""),
9505
+ generateRedlines,
9506
+ author,
9507
+ revisionIdAllocator: options.revisionIdAllocator || null,
9508
+ numberingService: new NumberingService()
9509
+ });
8208
9510
  const rawOxml = result?.oxml || result?.ooxml || "";
8209
9511
  const oxml = trimTrailingBlankParagraph(rawOxml);
8210
9512
  const generatedNumId = extractFirstParagraphNumIdFromOxml(oxml);
@@ -8379,10 +9681,10 @@ function validateRedlineOoxml(oxml) {
8379
9681
  }
8380
9682
 
8381
9683
  // core/table-targeting.js
8382
- function getDirectWordChildren(element, localName) {
9684
+ function getDirectWordChildren(element, localName2) {
8383
9685
  if (!element) return [];
8384
9686
  return Array.from(element.childNodes || []).filter(
8385
- (node) => node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName === localName
9687
+ (node) => node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName === localName2
8386
9688
  );
8387
9689
  }
8388
9690
  function escapeMarkdownCell(text) {
@@ -8627,8 +9929,8 @@ function hasXmlParseError(doc) {
8627
9929
  if (doc.documentElement.localName === "parsererror") return true;
8628
9930
  return doc.getElementsByTagName("parsererror").length > 0;
8629
9931
  }
8630
- function isDirectWordChild(node, localName) {
8631
- return !!(node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName === localName);
9932
+ function isDirectWordChild(node, localName2) {
9933
+ return !!(node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName === localName2);
8632
9934
  }
8633
9935
  function insertNumberingNodeInSchemaOrder(root, node, kind) {
8634
9936
  if (!root || !node) return;
@@ -8820,17 +10122,17 @@ function getWordParagraphs(doc) {
8820
10122
  if (namespaced.length > 0) return namespaced;
8821
10123
  return Array.from(doc.getElementsByTagNameNS("*", "p")).filter((node) => node?.localName === "p");
8822
10124
  }
8823
- function getDirectWordChild3(node, localName) {
10125
+ function getDirectWordChild3(node, localName2) {
8824
10126
  const children = Array.from(node?.childNodes || []);
8825
10127
  for (const child of children) {
8826
10128
  if (child?.nodeType !== 1) continue;
8827
10129
  if (child.namespaceURI !== NS_W) continue;
8828
- if (child.localName === localName) return child;
10130
+ if (child.localName === localName2) return child;
8829
10131
  }
8830
10132
  return null;
8831
10133
  }
8832
- function getWordDescendants(node, localName) {
8833
- return Array.from(node?.getElementsByTagNameNS?.(NS_W, localName) || []);
10134
+ function getWordDescendants(node, localName2) {
10135
+ return Array.from(node?.getElementsByTagNameNS?.(NS_W, localName2) || []);
8834
10136
  }
8835
10137
  function getWordAttribute(element, names) {
8836
10138
  if (!element) return "";
@@ -8840,32 +10142,6 @@ function getWordAttribute(element, names) {
8840
10142
  }
8841
10143
  return "";
8842
10144
  }
8843
- function hasWordAncestorWithin(node, localName, boundary) {
8844
- let cursor = node?.parentNode || null;
8845
- while (cursor && cursor !== boundary) {
8846
- if (cursor.nodeType === 1 && cursor.namespaceURI === NS_W && cursor.localName === localName) {
8847
- return true;
8848
- }
8849
- cursor = cursor.parentNode;
8850
- }
8851
- return false;
8852
- }
8853
- function readRunText(run) {
8854
- let text = "";
8855
- for (const child of Array.from(run?.childNodes || [])) {
8856
- if (!child || child.nodeType !== 1 || child.namespaceURI !== NS_W) continue;
8857
- if (child.localName === "t") {
8858
- text += child.textContent || "";
8859
- } else if (child.localName === "tab") {
8860
- text += " ";
8861
- } else if (child.localName === "br" || child.localName === "cr") {
8862
- text += "\n";
8863
- } else if (child.localName === "noBreakHyphen") {
8864
- text += "\u2011";
8865
- }
8866
- }
8867
- return text;
8868
- }
8869
10145
  function getRunFormatting(run) {
8870
10146
  const rPr = getDirectWordChild3(run, "rPr");
8871
10147
  if (!rPr) return { bold: false, italic: false };
@@ -8882,9 +10158,8 @@ function collectParagraphSegments(paragraph) {
8882
10158
  const segments = [];
8883
10159
  const runs = Array.from(paragraph?.getElementsByTagNameNS?.(NS_W, "r") || []);
8884
10160
  for (const run of runs) {
8885
- if (hasWordAncestorWithin(run, "del", paragraph)) continue;
8886
- if (hasWordAncestorWithin(run, "moveFrom", paragraph)) continue;
8887
- const text = readRunText(run);
10161
+ if (!isNodeVisibleInRevisionView(run, paragraph, "accepted")) continue;
10162
+ const text = readCanonicalRunText(run, { boundary: paragraph, revisionView: "accepted" });
8888
10163
  if (!text) continue;
8889
10164
  segments.push({
8890
10165
  text,
@@ -8955,9 +10230,9 @@ function paragraphToMarkdown(paragraph) {
8955
10230
  const inline = segments.map((segment) => wrapRunMarkdown(segment.text, segment)).join("");
8956
10231
  const normalizedInline = normalizeInlineWhitespace(inline);
8957
10232
  if (!normalizedInline) return "";
8958
- const headingLevel = parseHeadingLevel(paragraph);
8959
- if (headingLevel != null) {
8960
- return `${"#".repeat(headingLevel)} ${normalizedInline}`;
10233
+ const headingLevel2 = parseHeadingLevel(paragraph);
10234
+ if (headingLevel2 != null) {
10235
+ return `${"#".repeat(headingLevel2)} ${normalizedInline}`;
8961
10236
  }
8962
10237
  const list = parseListInfo(paragraph);
8963
10238
  if (list) {
@@ -9000,13 +10275,512 @@ function ingestWordOoxmlToMarkdownResult(ooxml) {
9000
10275
  return { text: lines.join("\n\n").trim(), status: "ok", warnings: parsed.warnings };
9001
10276
  }
9002
10277
 
10278
+ // services/revision-token.js
10279
+ var textEncoder = new TextEncoder();
10280
+ function normalizeOpcEntryName(name) {
10281
+ if (typeof name !== "string") {
10282
+ throw new TypeError(`Entry name must be a string, got ${typeof name}`);
10283
+ }
10284
+ let normalized = name.replace(/\\/g, "/");
10285
+ normalized = normalized.replace(/^\/+/, "");
10286
+ while (normalized.startsWith("./")) {
10287
+ normalized = normalized.slice(2);
10288
+ }
10289
+ normalized = normalized.replace(/\/+/g, "/");
10290
+ if (!normalized) {
10291
+ throw new Error(`Invalid empty entry name: "${name}"`);
10292
+ }
10293
+ return normalized;
10294
+ }
10295
+ function buildRevisionTokenFraming({ scope, entries = [] }) {
10296
+ if (typeof scope !== "string" || !scope) {
10297
+ throw new TypeError("Revision token scope must be a non-empty string.");
10298
+ }
10299
+ const magicBytes = textEncoder.encode("docx-redline-revision-token\0");
10300
+ const version = 1;
10301
+ const scopeBytes = textEncoder.encode(scope);
10302
+ const normalizedEntries = [];
10303
+ const seenNames = /* @__PURE__ */ new Set();
10304
+ const rawList = Array.isArray(entries) ? entries : entries instanceof Map ? Array.from(entries.entries()) : Object.entries(entries || {});
10305
+ for (const item of rawList) {
10306
+ if (!item) continue;
10307
+ const rawName = Array.isArray(item) ? item[0] : item.name;
10308
+ const rawPayload = Array.isArray(item) ? item[1] : item.payload ?? item.bytes;
10309
+ if (rawName == null) continue;
10310
+ const normName = normalizeOpcEntryName(String(rawName));
10311
+ if (seenNames.has(normName)) {
10312
+ throw new Error(`Duplicate normalized entry path detected: "${normName}"`);
10313
+ }
10314
+ seenNames.add(normName);
10315
+ let payloadBytes;
10316
+ if (typeof rawPayload === "string") {
10317
+ payloadBytes = textEncoder.encode(rawPayload);
10318
+ } else if (rawPayload instanceof Uint8Array) {
10319
+ payloadBytes = rawPayload;
10320
+ } else if (rawPayload && typeof rawPayload.length === "number") {
10321
+ payloadBytes = new Uint8Array(rawPayload);
10322
+ } else {
10323
+ payloadBytes = new Uint8Array(0);
10324
+ }
10325
+ normalizedEntries.push({
10326
+ name: normName,
10327
+ nameBytes: textEncoder.encode(normName),
10328
+ payloadBytes
10329
+ });
10330
+ }
10331
+ normalizedEntries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
10332
+ let totalSize = magicBytes.length + 4 + 4 + scopeBytes.length + 4;
10333
+ for (const e of normalizedEntries) {
10334
+ totalSize += 4 + e.nameBytes.length + 4 + e.payloadBytes.length;
10335
+ }
10336
+ const buffer = new Uint8Array(totalSize);
10337
+ const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
10338
+ let offset = 0;
10339
+ buffer.set(magicBytes, offset);
10340
+ offset += magicBytes.length;
10341
+ view.setUint32(offset, version, false);
10342
+ offset += 4;
10343
+ view.setUint32(offset, scopeBytes.length, false);
10344
+ offset += 4;
10345
+ buffer.set(scopeBytes, offset);
10346
+ offset += scopeBytes.length;
10347
+ view.setUint32(offset, normalizedEntries.length, false);
10348
+ offset += 4;
10349
+ for (const e of normalizedEntries) {
10350
+ view.setUint32(offset, e.nameBytes.length, false);
10351
+ offset += 4;
10352
+ buffer.set(e.nameBytes, offset);
10353
+ offset += e.nameBytes.length;
10354
+ view.setUint32(offset, e.payloadBytes.length, false);
10355
+ offset += 4;
10356
+ buffer.set(e.payloadBytes, offset);
10357
+ offset += e.payloadBytes.length;
10358
+ }
10359
+ return {
10360
+ framing: buffer,
10361
+ scope,
10362
+ version,
10363
+ coveredParts: normalizedEntries.map((e) => e.name)
10364
+ };
10365
+ }
10366
+ async function computeRevisionToken({ scope, entries = [], digestFn = null }) {
10367
+ const { framing, coveredParts, version } = buildRevisionTokenFraming({ scope, entries });
10368
+ let hashHex = "";
10369
+ if (typeof digestFn === "function") {
10370
+ hashHex = await digestFn(framing);
10371
+ } else if (typeof globalThis.crypto?.subtle?.digest === "function") {
10372
+ const hashBuf = await globalThis.crypto.subtle.digest("SHA-256", framing);
10373
+ const hashBytes = new Uint8Array(hashBuf);
10374
+ hashHex = Array.from(hashBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
10375
+ } else {
10376
+ throw new Error("No crypto provider available for SHA-256 revision token computation.");
10377
+ }
10378
+ return {
10379
+ algorithm: "sha256",
10380
+ version,
10381
+ scope,
10382
+ value: hashHex,
10383
+ coveredParts
10384
+ };
10385
+ }
10386
+ function computeRevisionTokenSync({ scope, entries = [], digestFn }) {
10387
+ if (typeof digestFn !== "function") {
10388
+ throw new TypeError("computeRevisionTokenSync requires a synchronous digestFn.");
10389
+ }
10390
+ const { framing, coveredParts, version } = buildRevisionTokenFraming({ scope, entries });
10391
+ const hashHex = digestFn(framing);
10392
+ return {
10393
+ algorithm: "sha256",
10394
+ version,
10395
+ scope,
10396
+ value: hashHex,
10397
+ coveredParts
10398
+ };
10399
+ }
10400
+ function extractDocumentPartsEntries(parts) {
10401
+ const entries = [];
10402
+ if (parts?.documentXml) {
10403
+ entries.push({ name: "word/document.xml", payload: parts.documentXml });
10404
+ }
10405
+ if (parts?.commentsXml) {
10406
+ entries.push({ name: "word/comments.xml", payload: parts.commentsXml });
10407
+ }
10408
+ if (parts?.commentsExtendedXml) {
10409
+ entries.push({ name: "word/commentsExtended.xml", payload: parts.commentsExtendedXml });
10410
+ }
10411
+ if (parts?.numberingXml) {
10412
+ entries.push({ name: "word/numbering.xml", payload: parts.numberingXml });
10413
+ }
10414
+ if (parts?.stylesXml) {
10415
+ entries.push({ name: "word/styles.xml", payload: parts.stylesXml });
10416
+ }
10417
+ if (parts?.parts instanceof Map) {
10418
+ for (const [name, payload] of parts.parts.entries()) {
10419
+ entries.push({ name, payload });
10420
+ }
10421
+ } else if (parts?.additionalParts && typeof parts.additionalParts === "object") {
10422
+ for (const [name, payload] of Object.entries(parts.additionalParts)) {
10423
+ entries.push({ name, payload });
10424
+ }
10425
+ }
10426
+ return entries;
10427
+ }
10428
+ async function computeDocumentPartsRevisionToken(parts, options = {}) {
10429
+ const entries = extractDocumentPartsEntries(parts);
10430
+ return computeRevisionToken({
10431
+ scope: "document-parts",
10432
+ entries,
10433
+ digestFn: options.digestFn
10434
+ });
10435
+ }
10436
+ function validateRevisionToken(token) {
10437
+ if (!token || typeof token !== "object") {
10438
+ return { valid: false, error: { code: "INVALID_REVISION_TOKEN", message: "Revision token must be an object." } };
10439
+ }
10440
+ if (token.algorithm !== "sha256") {
10441
+ return { valid: false, error: { code: "INVALID_REVISION_TOKEN", message: `Unsupported revision token algorithm: "${token.algorithm}". Expected "sha256".` } };
10442
+ }
10443
+ if (token.version !== 1) {
10444
+ return { valid: false, error: { code: "INVALID_REVISION_TOKEN", message: `Unsupported revision token version: "${token.version}". Expected 1.` } };
10445
+ }
10446
+ if (token.scope !== "document-parts" && token.scope !== "package") {
10447
+ return { valid: false, error: { code: "INVALID_REVISION_TOKEN", message: `Unsupported revision token scope: "${token.scope}". Expected "document-parts" or "package".` } };
10448
+ }
10449
+ if (typeof token.value !== "string" || !/^[0-9a-f]{64}$/i.test(token.value.trim())) {
10450
+ return { valid: false, error: { code: "INVALID_REVISION_TOKEN", message: "Revision token value must be a 64-character hex string." } };
10451
+ }
10452
+ return { valid: true };
10453
+ }
10454
+ function areRevisionTokensEqual(a, b) {
10455
+ if (typeof a !== "string" || typeof b !== "string") return false;
10456
+ const aNorm = a.trim().toLowerCase();
10457
+ const bNorm = b.trim().toLowerCase();
10458
+ if (aNorm.length !== bNorm.length) return false;
10459
+ const aBuf = textEncoder.encode(aNorm);
10460
+ const bBuf = textEncoder.encode(bNorm);
10461
+ let diff = 0;
10462
+ for (let i = 0; i < aBuf.length; i++) {
10463
+ diff |= aBuf[i] ^ bBuf[i];
10464
+ }
10465
+ return diff === 0;
10466
+ }
10467
+
10468
+ // services/document-inspection.js
10469
+ var attr2 = (node, name) => node?.getAttribute?.(`w:${name}`) || node?.getAttribute?.(name) || "";
10470
+ var descendants = (node, name) => Array.from(node?.getElementsByTagNameNS?.(NS_W, name) || []);
10471
+ var first = (node, name) => descendants(node, name)[0] || null;
10472
+ function hasAncestor(node, localName2) {
10473
+ let cursor = node?.parentNode;
10474
+ while (cursor) {
10475
+ if (cursor.localName === localName2 && (!cursor.namespaceURI || cursor.namespaceURI === NS_W)) return true;
10476
+ cursor = cursor.parentNode;
10477
+ }
10478
+ return false;
10479
+ }
10480
+ function parseXml2(xml, partName, required = false) {
10481
+ if (!xml) return required ? { error: { code: "MISSING_PART", message: `Missing ${partName}.` } } : { doc: null };
10482
+ const parsed = parseOoxmlSafe(xml, "application/xml");
10483
+ if (!parsed.doc || parsed.error) return { error: { code: "PARSE_ERROR", message: `Could not parse ${partName}: ${parsed.error?.message || "invalid XML"}` } };
10484
+ return { doc: parsed.doc, warnings: parsed.warnings || [] };
10485
+ }
10486
+ function paragraphListProperties(paragraph) {
10487
+ const numPr = first(first(paragraph, "pPr"), "numPr");
10488
+ if (!numPr) return null;
10489
+ const numId = attr2(first(numPr, "numId"), "val");
10490
+ if (!numId || numId === "0") return null;
10491
+ return { numId, level: Number.parseInt(attr2(first(numPr, "ilvl"), "val") || "0", 10) || 0 };
10492
+ }
10493
+ function parseNumbering(numberingDoc) {
10494
+ const abstracts = /* @__PURE__ */ new Map();
10495
+ for (const abstract of descendants(numberingDoc, "abstractNum")) {
10496
+ const levels = /* @__PURE__ */ new Map();
10497
+ for (const level of descendants(abstract, "lvl")) {
10498
+ const ilvl = Number.parseInt(attr2(level, "ilvl") || "0", 10) || 0;
10499
+ levels.set(ilvl, {
10500
+ start: Number.parseInt(attr2(first(level, "start"), "val") || "1", 10) || 1,
10501
+ format: attr2(first(level, "numFmt"), "val") || "decimal",
10502
+ text: attr2(first(level, "lvlText"), "val") || `%${ilvl + 1}.`
10503
+ });
10504
+ }
10505
+ abstracts.set(attr2(abstract, "abstractNumId"), levels);
10506
+ }
10507
+ const nums = /* @__PURE__ */ new Map();
10508
+ for (const num of descendants(numberingDoc, "num")) {
10509
+ const abstractId = attr2(first(num, "abstractNumId"), "val");
10510
+ const levels = new Map(abstracts.get(abstractId) || []);
10511
+ for (const override of descendants(num, "lvlOverride")) {
10512
+ const ilvl = Number.parseInt(attr2(override, "ilvl") || "0", 10) || 0;
10513
+ const embedded = first(override, "lvl");
10514
+ const base = { ...levels.get(ilvl) || { start: 1, format: "decimal", text: `%${ilvl + 1}.` } };
10515
+ if (embedded) {
10516
+ base.start = Number.parseInt(attr2(first(embedded, "start"), "val") || String(base.start), 10) || base.start;
10517
+ base.format = attr2(first(embedded, "numFmt"), "val") || base.format;
10518
+ base.text = attr2(first(embedded, "lvlText"), "val") || base.text;
10519
+ }
10520
+ const startOverride = first(override, "startOverride");
10521
+ if (startOverride) base.start = Number.parseInt(attr2(startOverride, "val") || String(base.start), 10) || base.start;
10522
+ levels.set(ilvl, base);
10523
+ }
10524
+ nums.set(attr2(num, "numId"), levels);
10525
+ }
10526
+ return nums;
10527
+ }
10528
+ function alpha(value, upper) {
10529
+ let n = Math.max(1, value);
10530
+ let out = "";
10531
+ while (n > 0) {
10532
+ n -= 1;
10533
+ out = String.fromCharCode(97 + n % 26) + out;
10534
+ n = Math.floor(n / 26);
10535
+ }
10536
+ return upper ? out.toUpperCase() : out;
10537
+ }
10538
+ function roman(value) {
10539
+ const pairs = [[1e3, "M"], [900, "CM"], [500, "D"], [400, "CD"], [100, "C"], [90, "XC"], [50, "L"], [40, "XL"], [10, "X"], [9, "IX"], [5, "V"], [4, "IV"], [1, "I"]];
10540
+ let n = value;
10541
+ let out = "";
10542
+ for (const [amount, glyph] of pairs) while (n >= amount) {
10543
+ out += glyph;
10544
+ n -= amount;
10545
+ }
10546
+ return out;
10547
+ }
10548
+ function formatCounter(value, format) {
10549
+ if (format === "lowerLetter") return alpha(value, false);
10550
+ if (format === "upperLetter") return alpha(value, true);
10551
+ if (format === "lowerRoman") return roman(value).toLowerCase();
10552
+ if (format === "upperRoman") return roman(value);
10553
+ return String(value);
10554
+ }
10555
+ function createNumberingResolver(numberingDoc) {
10556
+ const nums = numberingDoc ? parseNumbering(numberingDoc) : /* @__PURE__ */ new Map();
10557
+ const counters = /* @__PURE__ */ new Map();
10558
+ return (list) => {
10559
+ if (!list?.numId) return null;
10560
+ const levels = nums.get(String(list.numId));
10561
+ if (!levels) return { ...list, label: null, format: null };
10562
+ const state = counters.get(list.numId) || [];
10563
+ const definition = levels.get(list.level) || { start: 1, format: "decimal", text: `%${list.level + 1}.` };
10564
+ state[list.level] = state[list.level] == null ? definition.start : state[list.level] + 1;
10565
+ state.length = list.level + 1;
10566
+ counters.set(list.numId, state);
10567
+ const label = definition.text.replace(/%([1-9])/g, (_, raw) => {
10568
+ const level = Number(raw) - 1;
10569
+ const levelDef = levels.get(level) || definition;
10570
+ return formatCounter(state[level] ?? levelDef.start, levelDef.format);
10571
+ });
10572
+ return { ...list, label, format: definition.format };
10573
+ };
10574
+ }
10575
+ function headingLevel(paragraph) {
10576
+ const pPr = first(paragraph, "pPr");
10577
+ const style = attr2(first(pPr, "pStyle"), "val");
10578
+ const match = style.match(/^heading\s*([1-9])$/i);
10579
+ if (match) return Math.min(Number(match[1]), 6);
10580
+ const outline = Number.parseInt(attr2(first(pPr, "outlineLvl"), "val"), 10);
10581
+ return Number.isInteger(outline) ? Math.min(outline + 1, 6) : null;
10582
+ }
10583
+ function structuralContext(paragraph, text) {
10584
+ const references = [];
10585
+ for (const [name, type] of [["footnoteReference", "footnote"], ["endnoteReference", "endnote"], ["commentReference", "comment"]]) {
10586
+ for (const node of descendants(paragraph, name)) references.push({ type, id: attr2(node, "id") || null });
10587
+ }
10588
+ let cell = paragraph.parentNode;
10589
+ while (cell && cell.localName !== "tc") cell = cell.parentNode;
10590
+ let row = cell?.parentNode;
10591
+ while (row && row.localName !== "tr") row = row.parentNode;
10592
+ let table = row?.parentNode;
10593
+ while (table && table.localName !== "tbl") table = table.parentNode;
10594
+ const all = paragraph.ownerDocument;
10595
+ return {
10596
+ references,
10597
+ table: table ? {
10598
+ tableIndex: Array.from(all.getElementsByTagNameNS(NS_W, "tbl")).indexOf(table) + 1,
10599
+ rowIndex: Array.from(table.getElementsByTagNameNS(NS_W, "tr")).indexOf(row) + 1,
10600
+ cellIndex: Array.from(row.getElementsByTagNameNS(NS_W, "tc")).indexOf(cell) + 1
10601
+ } : null,
10602
+ empty: text.length === 0
10603
+ };
10604
+ }
10605
+ function revisionAuthors(paragraph) {
10606
+ const authors = /* @__PURE__ */ new Set();
10607
+ for (const name of ["ins", "del", "moveFrom", "moveTo", "rPrChange", "pPrChange"]) {
10608
+ for (const node of descendants(paragraph, name)) if (attr2(node, "author")) authors.add(attr2(node, "author"));
10609
+ }
10610
+ return [...authors].sort();
10611
+ }
10612
+ function readCommentDefinitions(commentsDoc) {
10613
+ const result = /* @__PURE__ */ new Map();
10614
+ for (const comment of descendants(commentsDoc, "comment")) {
10615
+ const paragraphs = descendants(comment, "p");
10616
+ result.set(attr2(comment, "id"), {
10617
+ id: attr2(comment, "id"),
10618
+ author: attr2(comment, "author") || null,
10619
+ date: attr2(comment, "date") || null,
10620
+ text: paragraphs.map((p) => extractCanonicalParagraphText(p)).join("\n"),
10621
+ paraId: paragraphs[0]?.getAttribute?.("w14:paraId") || paragraphs[0]?.getAttribute?.("paraId") || null
10622
+ });
10623
+ }
10624
+ return result;
10625
+ }
10626
+ function attachCommentThreadMetadata(comments, commentsExtendedDoc) {
10627
+ if (!commentsExtendedDoc) return;
10628
+ const byParaId = new Map([...comments.values()].filter((c) => c.paraId).map((c) => [c.paraId.toUpperCase(), c]));
10629
+ for (const entry of Array.from(commentsExtendedDoc.getElementsByTagNameNS("*", "commentEx"))) {
10630
+ const paraId = entry.getAttribute("w15:paraId") || entry.getAttribute("paraId") || "";
10631
+ const parentParaId = entry.getAttribute("w15:paraIdParent") || entry.getAttribute("paraIdParent") || "";
10632
+ const comment = byParaId.get(paraId.toUpperCase());
10633
+ if (!comment) continue;
10634
+ comment.done = (entry.getAttribute("w15:done") || entry.getAttribute("done")) === "1";
10635
+ if (parentParaId) {
10636
+ comment.parentParaId = parentParaId;
10637
+ comment.parentCommentId = byParaId.get(parentParaId.toUpperCase())?.id || null;
10638
+ }
10639
+ }
10640
+ }
10641
+ function collectDocumentCommentAnchors(paragraphNodes, revisionView) {
10642
+ const active = /* @__PURE__ */ new Map();
10643
+ const anchors = /* @__PURE__ */ new Map();
10644
+ let paragraphBoundary = null;
10645
+ const append = (value) => {
10646
+ for (const item of active.values()) item.text += value;
10647
+ };
10648
+ const visit = (node) => {
10649
+ for (const child of Array.from(node?.childNodes || [])) {
10650
+ if (child?.nodeType !== 1) continue;
10651
+ const name = child.localName;
10652
+ if (revisionView === "accepted" && (name === "del" || name === "moveFrom") || revisionView === "rejected" && (name === "ins" || name === "moveTo")) continue;
10653
+ if (name === "commentRangeStart") active.set(attr2(child, "id"), { text: "" });
10654
+ else if (name === "commentRangeEnd") {
10655
+ const id = attr2(child, "id");
10656
+ if (active.has(id)) {
10657
+ anchors.set(id, active.get(id).text);
10658
+ active.delete(id);
10659
+ }
10660
+ } else if (name === "r") append(readCanonicalRunText(child, { revisionView, boundary: paragraphBoundary }));
10661
+ else visit(child);
10662
+ }
10663
+ };
10664
+ paragraphNodes.forEach((paragraph, index) => {
10665
+ paragraphBoundary = paragraph;
10666
+ visit(paragraph);
10667
+ if (index < paragraphNodes.length - 1 && active.size) append("\n");
10668
+ });
10669
+ return anchors;
10670
+ }
10671
+ function inspectDocumentParts(parts, options = {}) {
10672
+ const documentPart = parseXml2(parts?.documentXml, "word/document.xml", true);
10673
+ if (documentPart.error) return { status: "error", error: documentPart.error, paragraphs: [], comments: [], warnings: [] };
10674
+ const commentsPart = parseXml2(parts?.commentsXml, "word/comments.xml");
10675
+ const commentsExtendedPart = parseXml2(parts?.commentsExtendedXml, "word/commentsExtended.xml");
10676
+ const numberingPart = parseXml2(parts?.numberingXml, "word/numbering.xml");
10677
+ const warnings = [...documentPart.warnings || []];
10678
+ if (commentsPart.error) warnings.push(commentsPart.error.message);
10679
+ if (commentsExtendedPart.error) warnings.push(commentsExtendedPart.error.message);
10680
+ if (numberingPart.error) warnings.push(numberingPart.error.message);
10681
+ const comments = readCommentDefinitions(commentsPart.doc);
10682
+ attachCommentThreadMetadata(comments, commentsExtendedPart.doc);
10683
+ const resolveNumbering = createNumberingResolver(numberingPart.doc);
10684
+ let nearestHeading = null;
10685
+ const paragraphNodes = getDocumentParagraphNodes(documentPart.doc);
10686
+ const commentAnchors = collectDocumentCommentAnchors(paragraphNodes, options.revisionView || "accepted");
10687
+ let paragraphs = paragraphNodes.map((paragraph, zeroIndex) => {
10688
+ const text = extractCanonicalParagraphText(paragraph, { revisionView: options.revisionView || "accepted" });
10689
+ const level = headingLevel(paragraph);
10690
+ if (level) nearestHeading = { level, text };
10691
+ const ids = [...new Set([...descendants(paragraph, "commentRangeStart"), ...descendants(paragraph, "commentReference")].map((node) => attr2(node, "id")).filter(Boolean))];
10692
+ const authors = revisionAuthors(paragraph);
10693
+ const list = resolveNumbering(paragraphListProperties(paragraph));
10694
+ const styleId = attr2(first(first(paragraph, "pPr"), "pStyle"), "val") || null;
10695
+ const structure = structuralContext(paragraph, text);
10696
+ const index = zeroIndex + 1;
10697
+ const provision = list?.label && list.format !== "bullet" ? list.label : null;
10698
+ const headingText = nearestHeading?.text || null;
10699
+ const humanReference = [provision, headingText, text.slice(0, options.excerptLength || 120)].filter(Boolean).join(" \u2014 ");
10700
+ const segments = extractParagraphRevisionSegments(paragraph);
10701
+ return {
10702
+ index,
10703
+ ref: `P${index}`,
10704
+ paragraphId: getParagraphId2(paragraph),
10705
+ fingerprint: createParagraphFingerprint(paragraph),
10706
+ text,
10707
+ exactText: text,
10708
+ excerpt: text.slice(0, options.excerptLength || 120),
10709
+ humanReference,
10710
+ inTable: hasAncestor(paragraph, "tc"),
10711
+ table: structure.table,
10712
+ styleId,
10713
+ headingLevel: level,
10714
+ nearestHeading,
10715
+ list,
10716
+ structuralReferences: structure.references,
10717
+ hasRevisions: authors.length > 0,
10718
+ revisionAuthors: authors,
10719
+ commentIds: ids,
10720
+ segments
10721
+ };
10722
+ });
10723
+ for (const paragraph of paragraphs) for (const id of paragraph.commentIds) {
10724
+ const definition = comments.get(id) || { id, author: null, date: null, text: "" };
10725
+ definition.paragraphIndex ?? (definition.paragraphIndex = paragraph.index);
10726
+ definition.targetRef ?? (definition.targetRef = paragraph.ref);
10727
+ definition.anchoredText ?? (definition.anchoredText = commentAnchors.get(id) || paragraph.text);
10728
+ comments.set(id, definition);
10729
+ }
10730
+ if (options.revisedOnly) paragraphs = paragraphs.filter((item) => item.hasRevisions);
10731
+ if (options.inTable != null) paragraphs = paragraphs.filter((item) => item.inTable === !!options.inTable);
10732
+ if (options.skipEmpty) paragraphs = paragraphs.filter((item) => item.text.length > 0);
10733
+ if (options.search) {
10734
+ const needle = String(options.search).toLowerCase();
10735
+ paragraphs = paragraphs.filter((item) => item.text.toLowerCase().includes(needle));
10736
+ }
10737
+ if (Array.isArray(options.indexes)) {
10738
+ const indexes = new Set(options.indexes);
10739
+ paragraphs = paragraphs.filter((item) => indexes.has(item.index));
10740
+ }
10741
+ if (options.range) {
10742
+ const start = Number(options.range.start ?? options.range[0]);
10743
+ const end = Number(options.range.end ?? options.range[1]);
10744
+ paragraphs = paragraphs.filter((item) => item.index >= start && item.index <= end);
10745
+ }
10746
+ const allRevisionAuthors = [...new Set(paragraphs.flatMap((item) => item.revisionAuthors))].sort();
10747
+ const coveredEntries = extractDocumentPartsEntries(parts);
10748
+ const coveredParts = coveredEntries.map((e) => e.name).sort();
10749
+ let revisionToken = null;
10750
+ if (typeof options.digestFn === "function") {
10751
+ revisionToken = computeRevisionTokenSync({
10752
+ scope: "document-parts",
10753
+ entries: coveredEntries,
10754
+ digestFn: options.digestFn
10755
+ });
10756
+ }
10757
+ return {
10758
+ status: "ok",
10759
+ revisionToken,
10760
+ coveredParts,
10761
+ paragraphs,
10762
+ comments: [...comments.values()],
10763
+ revisionAuthors: allRevisionAuthors,
10764
+ commentAuthors: [...new Set([...comments.values()].map((item) => item.author).filter(Boolean))].sort(),
10765
+ counts: { paragraphs: paragraphs.length, comments: comments.size, revisedParagraphs: paragraphs.filter((item) => item.hasRevisions).length },
10766
+ warnings
10767
+ };
10768
+ }
10769
+
9003
10770
  // services/comment-builders.js
9004
- function buildCommentElement(commentId, author, content, date) {
10771
+ var NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml";
10772
+ var NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
10773
+ function createCommentParaId(commentId) {
10774
+ const numeric = Number.parseInt(String(commentId), 10);
10775
+ const value = Number.isFinite(numeric) ? 1879048192 + (numeric >>> 0) >>> 0 : 1879048192;
10776
+ return value.toString(16).toUpperCase().padStart(8, "0").slice(-8);
10777
+ }
10778
+ function buildCommentElement(commentId, author, content, date, paraId = createCommentParaId(commentId)) {
9005
10779
  const initials = author.split(" ").map((word) => word[0]).join("").toUpperCase() || "AI";
9006
10780
  const escapedContent = escapeXml(content);
9007
10781
  const escapedAuthor = escapeXml(author);
9008
10782
  return `<w:comment w:id="${commentId}" w:author="${escapedAuthor}" w:date="${date}" w:initials="${initials}">
9009
- <w:p>
10783
+ <w:p w14:paraId="${escapeXml(paraId)}" xmlns:w14="${NS_W14}">
9010
10784
  <w:r><w:t>${escapedContent}</w:t></w:r>
9011
10785
  </w:p>
9012
10786
  </w:comment>`;
@@ -9016,60 +10790,158 @@ function buildCommentsPartXml(comments) {
9016
10790
  return `<w:comments xmlns:w="${NS_W}"></w:comments>`;
9017
10791
  }
9018
10792
  const commentElements = comments.map(
9019
- (comment) => buildCommentElement(comment.id, comment.author, comment.content, comment.date)
10793
+ (comment) => buildCommentElement(comment.id, comment.author, comment.content, comment.date, comment.paraId)
9020
10794
  ).join("\n ");
9021
10795
  return `<w:comments xmlns:w="${NS_W}">
9022
10796
  ${commentElements}
9023
10797
  </w:comments>`;
9024
10798
  }
10799
+ function buildCommentsExtendedPartXml(entries) {
10800
+ const body = (entries || []).map((entry) => {
10801
+ const parent = entry.paraIdParent ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
10802
+ return `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parent} w15:done="${entry.done ? "1" : "0"}"/>`;
10803
+ }).join("");
10804
+ return `<w15:commentsEx xmlns:w15="${NS_W15}">${body}</w15:commentsEx>`;
10805
+ }
10806
+
10807
+ // core/revision-cloning.js
10808
+ function refreshRunPropertyChangeIds(root, allocator = null) {
10809
+ if (!root) return root;
10810
+ const xmlDoc = root.nodeType === 9 ? root : root.ownerDocument;
10811
+ const resolvedAllocator = allocator instanceof RevisionIdAllocator ? allocator : getRevisionIdAllocatorForDocument(xmlDoc) || createRevisionIdAllocator(xmlDoc);
10812
+ const candidates = [root, ...Array.from(root.getElementsByTagName?.("*") || [])];
10813
+ for (const node of candidates) {
10814
+ if (!isWordElement(node, "rPrChange")) continue;
10815
+ const nextId = String(resolvedAllocator.next());
10816
+ if (typeof node.setAttributeNS === "function") {
10817
+ node.setAttributeNS(NS_W, "w:id", nextId);
10818
+ } else {
10819
+ node.setAttribute("w:id", nextId);
10820
+ }
10821
+ }
10822
+ return root;
10823
+ }
9025
10824
 
9026
10825
  // services/comment-locator.js
9027
- function createParagraphTextIndex(paragraph) {
10826
+ function createParagraphTextIndex(paragraph, options = {}) {
10827
+ const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
9028
10828
  const runs = getElementsByTag(paragraph, "w:r");
9029
10829
  const runOffsets = [];
9030
10830
  let fullText = "";
9031
10831
  for (const run of runs) {
10832
+ if (!isNodeVisibleInRevisionView(run, paragraph, revisionView)) continue;
9032
10833
  const start = fullText.length;
9033
- const textNodes = getElementsByTag(run, "w:t");
9034
- for (const textNode of textNodes) {
9035
- fullText += textNode.textContent || "";
9036
- }
10834
+ const text = readCanonicalRunText(run, { revisionView, boundary: paragraph });
10835
+ fullText += text;
9037
10836
  runOffsets.push({ run, start, end: fullText.length });
9038
10837
  }
9039
10838
  return { fullText, runOffsets };
9040
10839
  }
9041
- function findTextInParagraphIndex(paragraphIndex, searchText) {
9042
- const searchIndex = paragraphIndex.fullText.indexOf(searchText);
9043
- if (searchIndex === -1) {
9044
- return { found: false };
10840
+ function candidateOffsets(haystack, needle) {
10841
+ if (!needle) return [];
10842
+ const candidates = [];
10843
+ let offset = haystack.indexOf(needle);
10844
+ while (offset !== -1) {
10845
+ candidates.push({ start: offset, end: offset + needle.length });
10846
+ offset = haystack.indexOf(needle, offset + 1);
9045
10847
  }
9046
- const searchEnd = searchIndex + searchText.length;
10848
+ return candidates;
10849
+ }
10850
+ function spaceEquivalentText(value) {
10851
+ return String(value).replace(/[ \u00a0]/g, " ");
10852
+ }
10853
+ function anchorError(code, message, candidates = []) {
10854
+ return {
10855
+ code,
10856
+ message,
10857
+ ...candidates.length > 0 ? { candidates } : {}
10858
+ };
10859
+ }
10860
+ function attachRunOffsets(paragraphIndex, candidate, resolvedBy) {
9047
10861
  let startRun = null;
9048
10862
  let endRun = null;
9049
10863
  let startOffset = 0;
9050
10864
  let endOffset = 0;
9051
10865
  for (const { run, start, end } of paragraphIndex.runOffsets) {
9052
- if (searchIndex >= start && searchIndex < end) {
10866
+ if (candidate.start >= start && candidate.start < end) {
9053
10867
  startRun = run;
9054
- startOffset = searchIndex - start;
10868
+ startOffset = candidate.start - start;
9055
10869
  }
9056
- if (searchEnd > start && searchEnd <= end) {
10870
+ if (candidate.end > start && candidate.end <= end) {
9057
10871
  endRun = run;
9058
- endOffset = searchEnd - start;
10872
+ endOffset = candidate.end - start;
10873
+ }
10874
+ }
10875
+ if (!startRun || !endRun) return null;
10876
+ return { found: true, resolvedBy, ...candidate, startRun, startOffset, endRun, endOffset };
10877
+ }
10878
+ function findAncestorTag(node, tagNames, boundary = null) {
10879
+ let current = node?.parentNode;
10880
+ while (current && current.nodeType === 1 && current !== boundary && (current.localName || current.nodeName.replace(/^.*:/, "")) !== "p") {
10881
+ const local = current.localName || current.nodeName.replace(/^.*:/, "");
10882
+ if (tagNames.includes(local)) return { node: current, tag: local };
10883
+ current = current.parentNode;
10884
+ }
10885
+ return null;
10886
+ }
10887
+ function validateAnchorLocation(paragraphIndex, location) {
10888
+ if (!location || !location.found) return location;
10889
+ const intersectingRuns = paragraphIndex.runOffsets.filter((entry) => entry.end > location.start && entry.start < location.end).map((entry) => entry.run);
10890
+ for (const run of intersectingRuns) {
10891
+ if (findAncestorTag(run, ["del"])) {
10892
+ return {
10893
+ found: false,
10894
+ error: anchorError("UNSAFE_REVISION_NESTING", "Refusing to attach comment to pending deletion.", [location])
10895
+ };
10896
+ }
10897
+ if (findAncestorTag(run, ["moveFrom", "moveTo"])) {
10898
+ return {
10899
+ found: false,
10900
+ error: anchorError("UNSAFE_REVISION_NESTING", "Refusing to comment on move revision until move lifecycle is designed.", [location])
10901
+ };
9059
10902
  }
9060
10903
  }
10904
+ return location;
10905
+ }
10906
+ function resolveTextInParagraphIndex(paragraphIndex, searchText) {
10907
+ const needle = String(searchText ?? "");
10908
+ const exactCandidates = candidateOffsets(paragraphIndex.fullText, needle);
10909
+ if (exactCandidates.length > 1) {
10910
+ return {
10911
+ found: false,
10912
+ error: anchorError("AMBIGUOUS_ANCHOR", `Anchor text matched ${exactCandidates.length} locations in the target paragraph.`, exactCandidates)
10913
+ };
10914
+ }
10915
+ if (exactCandidates.length === 1) {
10916
+ const resolved = attachRunOffsets(paragraphIndex, exactCandidates[0], "exact_anchor");
10917
+ if (resolved) return validateAnchorLocation(paragraphIndex, resolved);
10918
+ }
10919
+ const normalizedNeedle = spaceEquivalentText(needle);
10920
+ const normalizedParagraph = spaceEquivalentText(paragraphIndex.fullText);
10921
+ const equivalentCandidates = candidateOffsets(normalizedParagraph, normalizedNeedle);
10922
+ if (equivalentCandidates.length > 1) {
10923
+ return {
10924
+ found: false,
10925
+ error: anchorError("AMBIGUOUS_ANCHOR", `Space-equivalent anchor text matched ${equivalentCandidates.length} locations in the target paragraph.`, equivalentCandidates)
10926
+ };
10927
+ }
10928
+ if (equivalentCandidates.length === 1) {
10929
+ const resolved = attachRunOffsets(paragraphIndex, equivalentCandidates[0], "space_equivalent_anchor");
10930
+ if (resolved) return validateAnchorLocation(paragraphIndex, resolved);
10931
+ }
9061
10932
  return {
9062
- found: true,
9063
- startRun,
9064
- startOffset,
9065
- endRun,
9066
- endOffset
10933
+ found: false,
10934
+ error: anchorError("ANCHOR_NOT_FOUND", `Could not find anchor text in the target paragraph: "${needle}".`)
9067
10935
  };
9068
10936
  }
9069
- function cloneRunWithText(xmlDoc, rPr, newText) {
10937
+ function cloneRunWithText(xmlDoc, rPr, newText, revisionIdAllocator, preserveRevisionIds = false) {
9070
10938
  const newRun = createWordElement(xmlDoc, "w:r");
9071
10939
  if (rPr) {
9072
- newRun.appendChild(rPr.cloneNode(true));
10940
+ const clonedRPr = rPr.cloneNode(true);
10941
+ if (!preserveRevisionIds) {
10942
+ refreshRunPropertyChangeIds(clonedRPr, revisionIdAllocator);
10943
+ }
10944
+ newRun.appendChild(clonedRPr);
9073
10945
  }
9074
10946
  const newTextNode = createWordElement(xmlDoc, "w:t");
9075
10947
  newTextNode.setAttribute("xml:space", "preserve");
@@ -9077,9 +10949,9 @@ function cloneRunWithText(xmlDoc, rPr, newText) {
9077
10949
  newRun.appendChild(newTextNode);
9078
10950
  return newRun;
9079
10951
  }
9080
- function injectMarkersIntoParagraph(xmlDoc, paragraph, textToFind, commentId, paragraphIndex = null) {
10952
+ function injectMarkersIntoParagraph(xmlDoc, paragraph, textToFind, commentId, paragraphIndex = null, revisionIdAllocator = null, resolvedLocation = null) {
9081
10953
  const activeIndex = paragraphIndex || createParagraphTextIndex(paragraph);
9082
- const location = findTextInParagraphIndex(activeIndex, textToFind);
10954
+ const location = resolvedLocation || resolveTextInParagraphIndex(activeIndex, textToFind);
9083
10955
  if (!location.found || !location.startRun) {
9084
10956
  return false;
9085
10957
  }
@@ -9112,7 +10984,8 @@ function injectMarkersIntoParagraph(xmlDoc, paragraph, textToFind, commentId, pa
9112
10984
  const rPr = getFirstElementByTag(run, "w:rPr");
9113
10985
  const parent = run.parentNode;
9114
10986
  if (beforeText) {
9115
- parent.insertBefore(cloneRunWithText(xmlDoc, rPr, beforeText), run);
10987
+ parent.insertBefore(cloneRunWithText(xmlDoc, rPr, beforeText, revisionIdAllocator, true), run);
10988
+ refreshRunPropertyChangeIds(rPr, revisionIdAllocator);
9116
10989
  }
9117
10990
  parent.insertBefore(startMarker, run);
9118
10991
  textNode.textContent = highlightedText;
@@ -9123,7 +10996,7 @@ function injectMarkersIntoParagraph(xmlDoc, paragraph, textToFind, commentId, pa
9123
10996
  }
9124
10997
  parent.insertBefore(referenceRun, endMarker.nextSibling || null);
9125
10998
  if (afterText) {
9126
- parent.insertBefore(cloneRunWithText(xmlDoc, rPr, afterText), referenceRun.nextSibling || null);
10999
+ parent.insertBefore(cloneRunWithText(xmlDoc, rPr, afterText, revisionIdAllocator), referenceRun.nextSibling || null);
9127
11000
  }
9128
11001
  return true;
9129
11002
  }
@@ -9134,7 +11007,11 @@ function injectMarkersIntoParagraph(xmlDoc, paragraph, textToFind, commentId, pa
9134
11007
  const highlightedStart = fullText.substring(location.startOffset);
9135
11008
  if (beforeText) {
9136
11009
  const rPr = getFirstElementByTag(location.startRun, "w:rPr");
9137
- location.startRun.parentNode.insertBefore(cloneRunWithText(xmlDoc, rPr, beforeText), location.startRun);
11010
+ location.startRun.parentNode.insertBefore(
11011
+ cloneRunWithText(xmlDoc, rPr, beforeText, revisionIdAllocator, true),
11012
+ location.startRun
11013
+ );
11014
+ refreshRunPropertyChangeIds(rPr, revisionIdAllocator);
9138
11015
  }
9139
11016
  startTextNode.textContent = highlightedStart;
9140
11017
  }
@@ -9149,9 +11026,12 @@ function injectMarkersIntoParagraph(xmlDoc, paragraph, textToFind, commentId, pa
9149
11026
  if (afterText) {
9150
11027
  const rPr = getFirstElementByTag(endRun, "w:rPr");
9151
11028
  if (endRun.nextSibling) {
9152
- endRun.parentNode.insertBefore(cloneRunWithText(xmlDoc, rPr, afterText), endRun.nextSibling);
11029
+ endRun.parentNode.insertBefore(
11030
+ cloneRunWithText(xmlDoc, rPr, afterText, revisionIdAllocator),
11031
+ endRun.nextSibling
11032
+ );
9153
11033
  } else {
9154
- endRun.parentNode.appendChild(cloneRunWithText(xmlDoc, rPr, afterText));
11034
+ endRun.parentNode.appendChild(cloneRunWithText(xmlDoc, rPr, afterText, revisionIdAllocator));
9155
11035
  }
9156
11036
  }
9157
11037
  }
@@ -9252,6 +11132,8 @@ function injectCommentsIntoOoxml(oxml, comments, options = {}) {
9252
11132
  const date = getRevisionTimestamp();
9253
11133
  const warnings = [];
9254
11134
  const placedComments = [];
11135
+ const resolvedAnchors = [];
11136
+ const errors = [];
9255
11137
  if (!comments || comments.length === 0) {
9256
11138
  return {
9257
11139
  oxml,
@@ -9278,19 +11160,22 @@ function injectCommentsIntoOoxml(oxml, comments, options = {}) {
9278
11160
  };
9279
11161
  }
9280
11162
  const xmlDoc = parseResult.xmlDoc;
11163
+ const revisionIdAllocator = createRevisionIdAllocator(xmlDoc);
9281
11164
  const paragraphs = getElementsByTag(xmlDoc, "w:p");
9282
11165
  log(`[CommentEngine] Found ${paragraphs.length} paragraphs, processing ${comments.length} comment requests`);
9283
11166
  const remainingRequestsByParagraph = /* @__PURE__ */ new Map();
9284
11167
  for (const request of comments) {
9285
11168
  const paragraphIndex = request.paragraphIndex - 1;
9286
11169
  if (paragraphIndex < 0 || paragraphIndex >= paragraphs.length) {
9287
- warnings.push(`Paragraph ${request.paragraphIndex} out of range (1-${paragraphs.length})`);
11170
+ const message = `Paragraph ${request.paragraphIndex} out of range (1-${paragraphs.length})`;
11171
+ warnings.push(message);
11172
+ errors.push({ code: "TARGET_NOT_FOUND", message, paragraphIndex: request.paragraphIndex });
9288
11173
  continue;
9289
11174
  }
9290
11175
  remainingRequestsByParagraph.set(paragraphIndex, (remainingRequestsByParagraph.get(paragraphIndex) || 0) + 1);
9291
11176
  }
9292
11177
  const paragraphIndexes = /* @__PURE__ */ new Map();
9293
- for (const request of comments) {
11178
+ for (const [requestIndex, request] of comments.entries()) {
9294
11179
  const paragraphIndex = request.paragraphIndex - 1;
9295
11180
  if (paragraphIndex < 0 || paragraphIndex >= paragraphs.length) {
9296
11181
  continue;
@@ -9301,18 +11186,40 @@ function injectCommentsIntoOoxml(oxml, comments, options = {}) {
9301
11186
  textIndex = createParagraphTextIndex(targetParagraph);
9302
11187
  paragraphIndexes.set(paragraphIndex, textIndex);
9303
11188
  }
9304
- const commentId = getNextRevisionId();
11189
+ const anchorText = String(request.textToFind ?? "");
11190
+ const resolution = resolveTextInParagraphIndex(textIndex, anchorText);
11191
+ const remaining = (remainingRequestsByParagraph.get(paragraphIndex) || 1) - 1;
11192
+ remainingRequestsByParagraph.set(paragraphIndex, remaining);
11193
+ if (!resolution.found) {
11194
+ const error2 = {
11195
+ ...resolution.error,
11196
+ requestIndex: requestIndex + 1,
11197
+ paragraphIndex: request.paragraphIndex
11198
+ };
11199
+ errors.push(error2);
11200
+ warnings.push(error2.message);
11201
+ if (remaining === 0) paragraphIndexes.delete(paragraphIndex);
11202
+ continue;
11203
+ }
11204
+ const commentId = typeof options.commentIdAllocator === "function" ? options.commentIdAllocator() : getNextRevisionId();
9305
11205
  const success = injectMarkersIntoParagraph(
9306
11206
  xmlDoc,
9307
11207
  targetParagraph,
9308
- request.textToFind,
11208
+ anchorText,
9309
11209
  commentId,
9310
- textIndex
11210
+ textIndex,
11211
+ revisionIdAllocator,
11212
+ resolution
9311
11213
  );
9312
- const remaining = (remainingRequestsByParagraph.get(paragraphIndex) || 1) - 1;
9313
- remainingRequestsByParagraph.set(paragraphIndex, remaining);
9314
11214
  if (!success) {
9315
- warnings.push(`Could not find "${request.textToFind.substring(0, 30)}..." in paragraph ${request.paragraphIndex}`);
11215
+ const error2 = {
11216
+ code: "ANCHOR_INSERTION_FAILED",
11217
+ message: `Resolved comment anchor could not be inserted in paragraph ${request.paragraphIndex}.`,
11218
+ requestIndex: requestIndex + 1,
11219
+ paragraphIndex: request.paragraphIndex
11220
+ };
11221
+ errors.push(error2);
11222
+ warnings.push(error2.message);
9316
11223
  if (remaining === 0) {
9317
11224
  paragraphIndexes.delete(paragraphIndex);
9318
11225
  }
@@ -9324,6 +11231,14 @@ function injectCommentsIntoOoxml(oxml, comments, options = {}) {
9324
11231
  author,
9325
11232
  date
9326
11233
  });
11234
+ resolvedAnchors.push({
11235
+ requestIndex: requestIndex + 1,
11236
+ paragraphIndex: request.paragraphIndex,
11237
+ text: anchorText,
11238
+ resolvedBy: resolution.resolvedBy,
11239
+ start: resolution.start,
11240
+ end: resolution.end
11241
+ });
9327
11242
  if (remaining > 0) {
9328
11243
  paragraphIndexes.set(paragraphIndex, createParagraphTextIndex(targetParagraph));
9329
11244
  } else {
@@ -9335,7 +11250,9 @@ function injectCommentsIntoOoxml(oxml, comments, options = {}) {
9335
11250
  oxml,
9336
11251
  hasChanges: false,
9337
11252
  commentsApplied: 0,
9338
- warnings
11253
+ warnings,
11254
+ resolvedAnchors,
11255
+ ...errors.length > 0 ? { status: "error", error: errors[0], errors } : {}
9339
11256
  };
9340
11257
  }
9341
11258
  return {
@@ -9343,13 +11260,107 @@ function injectCommentsIntoOoxml(oxml, comments, options = {}) {
9343
11260
  hasChanges: true,
9344
11261
  commentsXml: buildCommentsPartXml(placedComments),
9345
11262
  commentsApplied: placedComments.length,
9346
- warnings
11263
+ placedComments,
11264
+ warnings,
11265
+ resolvedAnchors,
11266
+ ...errors.length > 0 ? { status: "error", error: errors[0], errors } : {}
9347
11267
  };
9348
11268
  }
9349
11269
  function injectCommentsIntoPackage2(packageOxml, commentsXml) {
9350
11270
  return injectCommentsIntoPackage(packageOxml, commentsXml);
9351
11271
  }
9352
11272
 
11273
+ // services/comment-replies.js
11274
+ var NS_W6 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
11275
+ function attr3(node, qualified, local) {
11276
+ return node?.getAttribute?.(qualified) || node?.getAttribute?.(local) || "";
11277
+ }
11278
+ function parseRequired(xml, partName) {
11279
+ const parsed = parseOoxmlSafe(xml, "application/xml");
11280
+ if (!parsed.doc || parsed.error) {
11281
+ return { error: { code: "PARSE_ERROR", message: `Could not parse ${partName}: ${parsed.error?.message || "invalid XML"}` } };
11282
+ }
11283
+ return { doc: parsed.doc };
11284
+ }
11285
+ function usedParaIds(commentsDoc, extendedDoc) {
11286
+ const ids = /* @__PURE__ */ new Set();
11287
+ for (const p of Array.from(commentsDoc?.getElementsByTagNameNS("*", "p") || [])) {
11288
+ const id = attr3(p, "w14:paraId", "paraId");
11289
+ if (id) ids.add(id.toUpperCase());
11290
+ }
11291
+ for (const ex of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
11292
+ const id = attr3(ex, "w15:paraId", "paraId");
11293
+ if (id) ids.add(id.toUpperCase());
11294
+ }
11295
+ return ids;
11296
+ }
11297
+ function allocateParaId(commentId, occupied) {
11298
+ let candidate = createCommentParaId(commentId);
11299
+ let value = Number.parseInt(candidate, 16) >>> 0;
11300
+ while (occupied.has(candidate)) {
11301
+ value = value + 1 >>> 0;
11302
+ candidate = value.toString(16).toUpperCase().padStart(8, "0");
11303
+ }
11304
+ occupied.add(candidate);
11305
+ return candidate;
11306
+ }
11307
+ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, parentCommentId, commentId, commentContent, author, date = (/* @__PURE__ */ new Date()).toISOString() }) {
11308
+ if (!commentsXml) return { status: "error", error: { code: "COMMENTS_PART_MISSING", message: "A comment reply requires an existing word/comments.xml part." } };
11309
+ const commentsParsed = parseRequired(commentsXml, "word/comments.xml");
11310
+ if (commentsParsed.error) return { status: "error", error: commentsParsed.error };
11311
+ const commentsDoc = commentsParsed.doc;
11312
+ const parent = Array.from(commentsDoc.getElementsByTagNameNS("*", "comment")).find((node) => attr3(node, "w:id", "id") === String(parentCommentId));
11313
+ if (!parent) return { status: "error", error: { code: "PARENT_COMMENT_NOT_FOUND", message: `Parent comment '${parentCommentId}' was not found.` } };
11314
+ let extendedDoc = null;
11315
+ if (commentsExtendedXml) {
11316
+ const parsed = parseRequired(commentsExtendedXml, "word/commentsExtended.xml");
11317
+ if (parsed.error) return { status: "error", error: parsed.error };
11318
+ extendedDoc = parsed.doc;
11319
+ }
11320
+ const occupied = usedParaIds(commentsDoc, extendedDoc);
11321
+ const parentParagraph = Array.from(parent.getElementsByTagNameNS(NS_W6, "p"))[0] || Array.from(parent.getElementsByTagNameNS("*", "p"))[0];
11322
+ if (!parentParagraph) return { status: "error", error: { code: "PARENT_COMMENT_INVALID", message: `Parent comment '${parentCommentId}' has no paragraph.` } };
11323
+ let parentParaId = attr3(parentParagraph, "w14:paraId", "paraId");
11324
+ if (!parentParaId) {
11325
+ parentParaId = allocateParaId(parentCommentId, occupied);
11326
+ parentParagraph.setAttributeNS(NS_W14, "w14:paraId", parentParaId);
11327
+ } else {
11328
+ parentParaId = parentParaId.toUpperCase();
11329
+ }
11330
+ const replyParaId = allocateParaId(commentId, occupied);
11331
+ const replyParsed = parseRequired(`<w:comments xmlns:w="${NS_W6}" xmlns:w14="${NS_W14}">${buildCommentElement(commentId, author, commentContent, date, replyParaId)}</w:comments>`, "reply comment");
11332
+ commentsDoc.documentElement.appendChild(commentsDoc.importNode(replyParsed.doc.documentElement.firstChild, true));
11333
+ if (!extendedDoc) {
11334
+ extendedDoc = parseRequired(buildCommentsExtendedPartXml([]), "word/commentsExtended.xml").doc;
11335
+ }
11336
+ const root = extendedDoc.documentElement;
11337
+ const entries = Array.from(root.getElementsByTagNameNS("*", "commentEx"));
11338
+ if (!entries.some((node) => attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId)) {
11339
+ const parentEx = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
11340
+ parentEx.setAttributeNS(NS_W15, "w15:paraId", parentParaId);
11341
+ parentEx.setAttributeNS(NS_W15, "w15:done", "0");
11342
+ root.appendChild(parentEx);
11343
+ }
11344
+ const replyEx = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
11345
+ replyEx.setAttributeNS(NS_W15, "w15:paraId", replyParaId);
11346
+ replyEx.setAttributeNS(NS_W15, "w15:paraIdParent", parentParaId);
11347
+ replyEx.setAttributeNS(NS_W15, "w15:done", "0");
11348
+ root.appendChild(replyEx);
11349
+ const serializer = createSerializer();
11350
+ return {
11351
+ status: "ok",
11352
+ hasChanges: true,
11353
+ commentsXml: serializer.serializeToString(commentsDoc),
11354
+ commentsExtendedXml: serializer.serializeToString(extendedDoc),
11355
+ commentsXmlMode: "replace",
11356
+ commentsExtendedXmlMode: "replace",
11357
+ commentId,
11358
+ parentCommentId: String(parentCommentId),
11359
+ paraId: replyParaId,
11360
+ parentParaId
11361
+ };
11362
+ }
11363
+
9353
11364
  // engine/formatting-removal.js
9354
11365
  function removeNode2(node) {
9355
11366
  if (node?.parentNode) {
@@ -9411,19 +11422,19 @@ function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
9411
11422
  if (!targetText || !ooxmlString) return ooxmlString;
9412
11423
  const doc = parseOoxml(ooxmlString);
9413
11424
  if (!doc) return ooxmlString;
9414
- const NS_W7 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
9415
- const runs = doc.getElementsByTagNameNS(NS_W7, "r");
9416
- const insertions = doc.getElementsByTagNameNS(NS_W7, "ins");
11425
+ const NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
11426
+ const runs = doc.getElementsByTagNameNS(NS_W8, "r");
11427
+ const insertions = doc.getElementsByTagNameNS(NS_W8, "ins");
9417
11428
  const allRuns = [...Array.from(runs)];
9418
11429
  for (const ins of insertions) {
9419
- const insideRuns = ins.getElementsByTagNameNS(NS_W7, "r");
11430
+ const insideRuns = ins.getElementsByTagNameNS(NS_W8, "r");
9420
11431
  allRuns.push(...Array.from(insideRuns));
9421
11432
  }
9422
11433
  for (const run of allRuns) {
9423
- const textNodes = run.getElementsByTagNameNS(NS_W7, "t");
11434
+ const textNodes = run.getElementsByTagNameNS(NS_W8, "t");
9424
11435
  const runText = Array.from(textNodes).map((t) => t.textContent).join("");
9425
11436
  if (runText.includes(targetText) || runText === targetText) {
9426
- const rPrElements = run.getElementsByTagNameNS(NS_W7, "rPr");
11437
+ const rPrElements = run.getElementsByTagNameNS(NS_W8, "rPr");
9427
11438
  if (rPrElements.length > 0) {
9428
11439
  const rPr = rPrElements[0];
9429
11440
  const newRPr = removeFormattingFromRPr(rPr, formatTypes);
@@ -9458,7 +11469,7 @@ var HIGHLIGHT_COLOR_MAP = {
9458
11469
  "white": "white"
9459
11470
  };
9460
11471
  function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
9461
- const NS_W7 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
11472
+ const NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
9462
11473
  const ooxmlColor = HIGHLIGHT_COLOR_MAP[color.toLowerCase()] || "yellow";
9463
11474
  const generateRedlines = options?.generateRedlines ?? false;
9464
11475
  const author = options?.author || getDefaultAuthor();
@@ -9477,10 +11488,10 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
9477
11488
  }
9478
11489
  });
9479
11490
  }
9480
- const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W7, "highlight");
11491
+ const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W8, "highlight");
9481
11492
  Array.from(existingHighlight).forEach(removeNode2);
9482
11493
  const highlightEl = createWordElement(doc, "w:highlight");
9483
- highlightEl.setAttributeNS(NS_W7, "w:val", ooxmlColor);
11494
+ highlightEl.setAttributeNS(NS_W8, "w:val", ooxmlColor);
9484
11495
  rPrElement.appendChild(highlightEl);
9485
11496
  if (generateRedlines && previousRPrState) {
9486
11497
  const rPrChange = createWordElement(doc, "w:rPrChange");
@@ -9489,7 +11500,7 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
9489
11500
  rPrChange.setAttribute("w:author", metadata.author);
9490
11501
  rPrChange.setAttribute("w:date", metadata.date);
9491
11502
  rPrChange.appendChild(previousRPrState);
9492
- const existingChange = rPrElement.getElementsByTagNameNS(NS_W7, "rPrChange");
11503
+ const existingChange = rPrElement.getElementsByTagNameNS(NS_W8, "rPrChange");
9493
11504
  Array.from(existingChange).forEach(removeNode2);
9494
11505
  rPrElement.appendChild(rPrChange);
9495
11506
  }
@@ -9499,27 +11510,35 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
9499
11510
  if (!targetText || !ooxmlString) return ooxmlString;
9500
11511
  const doc = parseOoxml(ooxmlString);
9501
11512
  if (!doc) return ooxmlString;
11513
+ let revisionIdAllocator;
9502
11514
  if (options?._revisionIdAllocator instanceof RevisionIdAllocator) {
9503
- seedRevisionIdsFromDocument(doc, options._revisionIdAllocator);
11515
+ revisionIdAllocator = options._revisionIdAllocator;
11516
+ seedRevisionIdsFromDocument(doc, revisionIdAllocator);
9504
11517
  } else {
9505
- createRevisionIdAllocator(doc);
11518
+ revisionIdAllocator = createRevisionIdAllocator(doc);
9506
11519
  }
9507
- const NS_W7 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
11520
+ const NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
9508
11521
  const getRunText = (run) => {
9509
- const textNodes = run.getElementsByTagNameNS(NS_W7, "t");
11522
+ const textNodes = run.getElementsByTagNameNS(NS_W8, "t");
9510
11523
  return Array.from(textNodes).map((t) => t.textContent).join("");
9511
11524
  };
9512
- const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W7, "r"));
11525
+ const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W8, "r"));
11526
+ const sourceRunsWithClaimedRevisionIds = /* @__PURE__ */ new WeakSet();
9513
11527
  const cloneRunWithText2 = (sourceRun, text, shouldHighlight) => {
9514
11528
  const clonedRun = sourceRun.cloneNode(true);
9515
- const textNodes = clonedRun.getElementsByTagNameNS(NS_W7, "t");
11529
+ if (sourceRunsWithClaimedRevisionIds.has(sourceRun)) {
11530
+ refreshRunPropertyChangeIds(clonedRun, revisionIdAllocator);
11531
+ } else {
11532
+ sourceRunsWithClaimedRevisionIds.add(sourceRun);
11533
+ }
11534
+ const textNodes = clonedRun.getElementsByTagNameNS(NS_W8, "t");
9516
11535
  Array.from(textNodes).forEach(removeNode2);
9517
11536
  const newText = createWordElement(doc, "w:t");
9518
11537
  newText.setAttribute("xml:space", "preserve");
9519
11538
  newText.textContent = text;
9520
11539
  clonedRun.appendChild(newText);
9521
11540
  if (shouldHighlight) {
9522
- const rPrElements = clonedRun.getElementsByTagNameNS(NS_W7, "rPr");
11541
+ const rPrElements = clonedRun.getElementsByTagNameNS(NS_W8, "rPr");
9523
11542
  const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
9524
11543
  const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
9525
11544
  if (existingRPr) {
@@ -9569,16 +11588,19 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
9569
11588
  }
9570
11589
 
9571
11590
  // services/standalone-docx-plumbing.js
9572
- var NS_W6 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
11591
+ var NS_W7 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
9573
11592
  var NS_CT = "http://schemas.openxmlformats.org/package/2006/content-types";
9574
11593
  var NS_RELS = "http://schemas.openxmlformats.org/package/2006/relationships";
9575
11594
  var NUMBERING_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
9576
11595
  var NUMBERING_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
9577
11596
  var COMMENTS_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
9578
11597
  var COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
11598
+ var COMMENTS_EXTENDED_REL_TYPE = "http://schemas.microsoft.com/office/2011/relationships/commentsExtended";
11599
+ var COMMENTS_EXTENDED_CONTENT_TYPE = "application/vnd.ms-word.commentsExtended+xml";
9579
11600
  var DOCUMENT_PATH = "word/document.xml";
9580
11601
  var NUMBERING_PATH = "word/numbering.xml";
9581
11602
  var COMMENTS_PATH = "word/comments.xml";
11603
+ var COMMENTS_EXTENDED_PATH = "word/commentsExtended.xml";
9582
11604
  var CONTENT_TYPES_PATH = "[Content_Types].xml";
9583
11605
  var DOCUMENT_RELS_PATH = "word/_rels/document.xml.rels";
9584
11606
  function parseXmlStrictStandalone(xmlText, label = "xml") {
@@ -9591,7 +11613,7 @@ function parseXmlStrictStandalone(xmlText, label = "xml") {
9591
11613
  return parsed.doc;
9592
11614
  }
9593
11615
  function isSectionPropertiesElement(node) {
9594
- return !!node && node.nodeType === 1 && node.namespaceURI === NS_W6 && node.localName === "sectPr";
11616
+ return !!node && node.nodeType === 1 && node.namespaceURI === NS_W7 && node.localName === "sectPr";
9595
11617
  }
9596
11618
  function getBodyElementFromDocument(xmlDoc) {
9597
11619
  return xmlDoc.getElementsByTagNameNS("*", "body")[0] || null;
@@ -9627,15 +11649,15 @@ function normalizeBodySectionOrderStandalone(xmlDoc) {
9627
11649
  function sanitizeNestedParagraphsInTables(xmlDoc, options = {}) {
9628
11650
  const onInfo = typeof options?.onInfo === "function" ? options.onInfo : () => {
9629
11651
  };
9630
- const tcs = xmlDoc.getElementsByTagNameNS(NS_W6, "tc");
11652
+ const tcs = xmlDoc.getElementsByTagNameNS(NS_W7, "tc");
9631
11653
  let fixed = 0;
9632
11654
  for (const tc of Array.from(tcs)) {
9633
11655
  const outerParagraphs = Array.from(tc.childNodes || []).filter(
9634
- (node) => node.nodeType === 1 && node.namespaceURI === NS_W6 && node.localName === "p"
11656
+ (node) => node.nodeType === 1 && node.namespaceURI === NS_W7 && node.localName === "p"
9635
11657
  );
9636
11658
  for (const outerParagraph of outerParagraphs) {
9637
11659
  const innerParagraphs = Array.from(outerParagraph.childNodes || []).filter(
9638
- (node) => node.nodeType === 1 && node.namespaceURI === NS_W6 && node.localName === "p"
11660
+ (node) => node.nodeType === 1 && node.namespaceURI === NS_W7 && node.localName === "p"
9639
11661
  );
9640
11662
  for (const innerParagraph of innerParagraphs) {
9641
11663
  tc.insertBefore(innerParagraph, outerParagraph);
@@ -9704,7 +11726,7 @@ function extractReplacementNodesFromOoxml(outputOxml) {
9704
11726
  const replacementNodes2 = body ? Array.from(body.childNodes || []).filter((node) => node.nodeType === 1 && !isSectionPropertiesElement(node)) : Array.from(doc.childNodes || []).filter((node) => node.nodeType === 1);
9705
11727
  return { replacementNodes: replacementNodes2, numberingXml: null, sourceType: "document" };
9706
11728
  }
9707
- const wrapped = `<root xmlns:w="${NS_W6}">${outputOxml}</root>`;
11729
+ const wrapped = `<root xmlns:w="${NS_W7}">${outputOxml}</root>`;
9708
11730
  const fragmentDoc = parseXmlStrictStandalone(wrapped, "OOXML fragment");
9709
11731
  const replacementNodes = Array.from(fragmentDoc.documentElement.childNodes || []).filter((node) => node.nodeType === 1);
9710
11732
  return { replacementNodes, numberingXml: null, sourceType: "fragment" };
@@ -9720,17 +11742,21 @@ function extractReplacementNodesFromOoxml(outputOxml) {
9720
11742
  }
9721
11743
  function upsertContentTypeOverride(ctDoc, partName, contentType) {
9722
11744
  const overrides = Array.from(ctDoc.getElementsByTagNameNS("*", "Override"));
9723
- const hasOverride = overrides.some(
11745
+ const existingOverride = overrides.find(
9724
11746
  (override2) => (override2.getAttribute("PartName") || "").toLowerCase() === String(partName).toLowerCase()
9725
11747
  );
9726
- if (hasOverride) return false;
11748
+ if (existingOverride) {
11749
+ if ((existingOverride.getAttribute("ContentType") || "") === contentType) return false;
11750
+ existingOverride.setAttribute("ContentType", contentType);
11751
+ return true;
11752
+ }
9727
11753
  const override = ctDoc.createElementNS(NS_CT, "Override");
9728
11754
  override.setAttribute("PartName", partName);
9729
11755
  override.setAttribute("ContentType", contentType);
9730
11756
  ctDoc.documentElement.appendChild(override);
9731
11757
  return true;
9732
11758
  }
9733
- function upsertDocumentRelationship(relsDoc, relType, target) {
11759
+ function upsertDocumentRelationship(relsDoc, relType, target, options = {}) {
9734
11760
  const relsRoot = relsDoc.getElementsByTagNameNS("*", "Relationships")[0] || relsDoc.documentElement;
9735
11761
  const rels = Array.from(relsRoot.getElementsByTagNameNS("*", "Relationship"));
9736
11762
  const hasRel = rels.some((rel2) => (rel2.getAttribute("Type") || "") === relType);
@@ -9743,11 +11769,17 @@ function upsertDocumentRelationship(relsDoc, relType, target) {
9743
11769
  maxId = Math.max(maxId, idNum);
9744
11770
  }
9745
11771
  }
11772
+ const newId = `rId${maxId + 1}`;
9746
11773
  const rel = relsDoc.createElementNS(NS_RELS, "Relationship");
9747
- rel.setAttribute("Id", `rId${maxId + 1}`);
11774
+ rel.setAttribute("Id", newId);
9748
11775
  rel.setAttribute("Type", relType);
9749
11776
  rel.setAttribute("Target", target);
9750
11777
  relsRoot.appendChild(rel);
11778
+ if (options?._receiptCollector) {
11779
+ options._receiptCollector.recordRelationship(newId);
11780
+ } else if (options?._documentOperationSession?.receiptCollector) {
11781
+ options._documentOperationSession.receiptCollector.recordRelationship(newId);
11782
+ }
9751
11783
  return true;
9752
11784
  }
9753
11785
  async function readZipText(zip, filePath) {
@@ -9759,9 +11791,13 @@ async function ensureNumberingArtifactsInZip(zip, numberingXmlList, options = {}
9759
11791
  const onInfo = typeof options?.onInfo === "function" ? options.onInfo : () => {
9760
11792
  };
9761
11793
  const mergeNumberingXml = typeof options?.mergeNumberingXml === "function" ? options.mergeNumberingXml : null;
11794
+ const onWarn = typeof options?.onWarn === "function" ? options.onWarn : warn;
9762
11795
  const incomingPayloads = (Array.isArray(numberingXmlList) ? numberingXmlList : [numberingXmlList]).filter(Boolean);
9763
11796
  if (incomingPayloads.length === 0) return;
9764
11797
  const existing = await readZipText(zip, NUMBERING_PATH);
11798
+ if (existing && !mergeNumberingXml) {
11799
+ onWarn("[Deprecation] Replacing an existing numbering.xml without mergeNumberingXml is deprecated and will throw in the next major version.");
11800
+ }
9765
11801
  let mergedNumberingXml = existing || null;
9766
11802
  for (const incomingNumberingXml of incomingPayloads) {
9767
11803
  if (!mergedNumberingXml) {
@@ -9787,7 +11823,7 @@ async function ensureNumberingArtifactsInZip(zip, numberingXmlList, options = {}
9787
11823
  const relsText = await readZipText(zip, DOCUMENT_RELS_PATH);
9788
11824
  if (relsText) {
9789
11825
  const relsDoc = parseXmlStrictStandalone(relsText, DOCUMENT_RELS_PATH);
9790
- if (upsertDocumentRelationship(relsDoc, NUMBERING_REL_TYPE, "numbering.xml")) {
11826
+ if (upsertDocumentRelationship(relsDoc, NUMBERING_REL_TYPE, "numbering.xml", options)) {
9791
11827
  zip.file(DOCUMENT_RELS_PATH, serializer.serializeToString(relsDoc));
9792
11828
  }
9793
11829
  }
@@ -9798,7 +11834,7 @@ async function ensureCommentsArtifactsInZip(zip, commentsXml, options = {}) {
9798
11834
  if (!commentsXml) return;
9799
11835
  const serializer = createSerializer();
9800
11836
  const existingText = await readZipText(zip, COMMENTS_PATH);
9801
- if (!existingText) {
11837
+ if (!existingText || options.replaceExisting === true) {
9802
11838
  onInfo("[Demo] Adding comments.xml");
9803
11839
  zip.file(COMMENTS_PATH, commentsXml);
9804
11840
  } else {
@@ -9806,9 +11842,9 @@ async function ensureCommentsArtifactsInZip(zip, commentsXml, options = {}) {
9806
11842
  const incomingDoc = parseXmlStrictStandalone(commentsXml, "word/comments.xml (incoming)");
9807
11843
  const existingRoot = existingDoc.documentElement;
9808
11844
  const existingIds = new Set(
9809
- Array.from(existingRoot.getElementsByTagNameNS(NS_W6, "comment")).map((comment) => comment.getAttribute("w:id") || comment.getAttribute("id")).filter(Boolean)
11845
+ Array.from(existingRoot.getElementsByTagNameNS(NS_W7, "comment")).map((comment) => comment.getAttribute("w:id") || comment.getAttribute("id")).filter(Boolean)
9810
11846
  );
9811
- for (const incomingComment of Array.from(incomingDoc.documentElement.getElementsByTagNameNS(NS_W6, "comment"))) {
11847
+ for (const incomingComment of Array.from(incomingDoc.documentElement.getElementsByTagNameNS(NS_W7, "comment"))) {
9812
11848
  const id = incomingComment.getAttribute("w:id") || incomingComment.getAttribute("id");
9813
11849
  if (id && existingIds.has(id)) {
9814
11850
  throw new Error(`Duplicate comment id: ${id}`);
@@ -9827,7 +11863,27 @@ async function ensureCommentsArtifactsInZip(zip, commentsXml, options = {}) {
9827
11863
  const relsText = await readZipText(zip, DOCUMENT_RELS_PATH);
9828
11864
  if (relsText) {
9829
11865
  const relsDoc = parseXmlStrictStandalone(relsText, DOCUMENT_RELS_PATH);
9830
- if (upsertDocumentRelationship(relsDoc, COMMENTS_REL_TYPE, "comments.xml")) {
11866
+ if (upsertDocumentRelationship(relsDoc, COMMENTS_REL_TYPE, "comments.xml", options)) {
11867
+ zip.file(DOCUMENT_RELS_PATH, serializer.serializeToString(relsDoc));
11868
+ }
11869
+ }
11870
+ }
11871
+ async function ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXml, options = {}) {
11872
+ if (!commentsExtendedXml) return;
11873
+ parseXmlStrictStandalone(commentsExtendedXml, "word/commentsExtended.xml");
11874
+ zip.file(COMMENTS_EXTENDED_PATH, commentsExtendedXml);
11875
+ const serializer = createSerializer();
11876
+ const ctText = await readZipText(zip, CONTENT_TYPES_PATH);
11877
+ if (ctText) {
11878
+ const ctDoc = parseXmlStrictStandalone(ctText, CONTENT_TYPES_PATH);
11879
+ if (upsertContentTypeOverride(ctDoc, "/word/commentsExtended.xml", COMMENTS_EXTENDED_CONTENT_TYPE)) {
11880
+ zip.file(CONTENT_TYPES_PATH, serializer.serializeToString(ctDoc));
11881
+ }
11882
+ }
11883
+ const relsText = await readZipText(zip, DOCUMENT_RELS_PATH);
11884
+ if (relsText) {
11885
+ const relsDoc = parseXmlStrictStandalone(relsText, DOCUMENT_RELS_PATH);
11886
+ if (upsertDocumentRelationship(relsDoc, COMMENTS_EXTENDED_REL_TYPE, "commentsExtended.xml", options)) {
9831
11887
  zip.file(DOCUMENT_RELS_PATH, serializer.serializeToString(relsDoc));
9832
11888
  }
9833
11889
  }
@@ -9851,12 +11907,12 @@ async function validateDocxPackage(zip) {
9851
11907
  if (sectPrIndexes.length === 1 && sectPrIndexes[0] !== directBodyElements.length - 1) {
9852
11908
  throw new Error("Validation failed: w:sectPr not last");
9853
11909
  }
9854
- const tcs = documentDoc.getElementsByTagNameNS(NS_W6, "tc");
11910
+ const tcs = documentDoc.getElementsByTagNameNS(NS_W7, "tc");
9855
11911
  for (const tc of Array.from(tcs)) {
9856
11912
  for (const child of Array.from(tc.childNodes || []).filter((node) => node.nodeType === 1)) {
9857
- if (child.namespaceURI === NS_W6 && child.localName === "p") {
11913
+ if (child.namespaceURI === NS_W7 && child.localName === "p") {
9858
11914
  const hasNestedParagraph = Array.from(child.childNodes || []).some(
9859
- (node) => node.nodeType === 1 && node.namespaceURI === NS_W6 && node.localName === "p"
11915
+ (node) => node.nodeType === 1 && node.namespaceURI === NS_W7 && node.localName === "p"
9860
11916
  );
9861
11917
  if (hasNestedParagraph) {
9862
11918
  throw new Error("Validation failed: nested w:p");
@@ -9864,17 +11920,77 @@ async function validateDocxPackage(zip) {
9864
11920
  }
9865
11921
  }
9866
11922
  }
9867
- const hasNumberingUsage = documentDoc.getElementsByTagNameNS(NS_W6, "numPr").length > 0;
9868
- const hasCommentUsage = documentDoc.getElementsByTagNameNS(NS_W6, "commentRangeStart").length > 0 || documentDoc.getElementsByTagNameNS(NS_W6, "commentRangeEnd").length > 0 || documentDoc.getElementsByTagNameNS(NS_W6, "commentReference").length > 0;
11923
+ const hasNumberingUsage = documentDoc.getElementsByTagNameNS(NS_W7, "numPr").length > 0;
11924
+ const hasCommentUsage = documentDoc.getElementsByTagNameNS(NS_W7, "commentRangeStart").length > 0 || documentDoc.getElementsByTagNameNS(NS_W7, "commentRangeEnd").length > 0 || documentDoc.getElementsByTagNameNS(NS_W7, "commentReference").length > 0;
9869
11925
  const numberingXml = await readZipText(zip, NUMBERING_PATH);
9870
11926
  const commentsXml = await readZipText(zip, COMMENTS_PATH);
11927
+ const commentsExtendedXml = await readZipText(zip, COMMENTS_EXTENDED_PATH);
11928
+ if (commentsExtendedXml && !commentsXml) {
11929
+ throw new Error("Validation failed: commentsExtended part exists but comments part is missing");
11930
+ }
9871
11931
  if (numberingXml) {
9872
11932
  parseXmlStrictStandalone(numberingXml, NUMBERING_PATH);
9873
11933
  } else if (hasNumberingUsage) {
9874
11934
  throw new Error("Validation failed: numbering used but part missing");
9875
11935
  }
9876
11936
  if (commentsXml) {
9877
- parseXmlStrictStandalone(commentsXml, COMMENTS_PATH);
11937
+ const commentsDoc = parseXmlStrictStandalone(commentsXml, COMMENTS_PATH);
11938
+ const idsFor = (doc, localName2) => Array.from(doc.getElementsByTagNameNS(NS_W7, localName2)).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter((id) => id !== "");
11939
+ const starts = new Set(idsFor(documentDoc, "commentRangeStart"));
11940
+ const ends = new Set(idsFor(documentDoc, "commentRangeEnd"));
11941
+ const references = new Set(idsFor(documentDoc, "commentReference"));
11942
+ const definitionIds = idsFor(commentsDoc, "comment");
11943
+ const definitions = new Set(definitionIds);
11944
+ const sorted = (ids) => Array.from(ids).sort((a, b) => Number(a) - Number(b) || a.localeCompare(b));
11945
+ const difference = (left, right) => sorted(new Set(Array.from(left).filter((id) => !right.has(id))));
11946
+ const duplicateDefinitions = sorted(new Set(definitionIds.filter((id, index) => definitionIds.indexOf(id) !== index)));
11947
+ if (duplicateDefinitions.length > 0) {
11948
+ throw new Error(`Validation failed: duplicate comment definitions for id(s): ${duplicateDefinitions.join(", ")}`);
11949
+ }
11950
+ const startsWithoutEnds = difference(starts, ends);
11951
+ const endsWithoutStarts = difference(ends, starts);
11952
+ if (startsWithoutEnds.length > 0 || endsWithoutStarts.length > 0) {
11953
+ const parts = [];
11954
+ if (startsWithoutEnds.length > 0) parts.push(`start without end: ${startsWithoutEnds.join(", ")}`);
11955
+ if (endsWithoutStarts.length > 0) parts.push(`end without start: ${endsWithoutStarts.join(", ")}`);
11956
+ throw new Error(`Validation failed: unbalanced comment range marker(s) (${parts.join("; ")})`);
11957
+ }
11958
+ const rangesWithoutReferences = difference(/* @__PURE__ */ new Set([...starts, ...ends]), references);
11959
+ if (rangesWithoutReferences.length > 0) {
11960
+ throw new Error(`Validation failed: comment range has no reference for id(s): ${rangesWithoutReferences.join(", ")}`);
11961
+ }
11962
+ const usagesWithoutDefinitions = difference(/* @__PURE__ */ new Set([...starts, ...ends, ...references]), definitions);
11963
+ if (usagesWithoutDefinitions.length > 0) {
11964
+ throw new Error(`Validation failed: comment usage has no definition for id(s): ${usagesWithoutDefinitions.join(", ")}`);
11965
+ }
11966
+ const replyCommentIds = /* @__PURE__ */ new Set();
11967
+ if (commentsExtendedXml) {
11968
+ const extendedDoc = parseXmlStrictStandalone(commentsExtendedXml, COMMENTS_EXTENDED_PATH);
11969
+ const paraIdToCommentId = /* @__PURE__ */ new Map();
11970
+ for (const comment of Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"))) {
11971
+ const id = comment.getAttribute("w:id") || comment.getAttribute("id");
11972
+ const paragraph = Array.from(comment.getElementsByTagNameNS("*", "p"))[0];
11973
+ const paraId = paragraph?.getAttribute("w14:paraId") || paragraph?.getAttribute("paraId");
11974
+ if (paraId) paraIdToCommentId.set(paraId.toUpperCase(), id);
11975
+ }
11976
+ const knownParaIds = new Set(paraIdToCommentId.keys());
11977
+ const extendedParaIds = /* @__PURE__ */ new Set();
11978
+ for (const entry of Array.from(extendedDoc.getElementsByTagNameNS("*", "commentEx"))) {
11979
+ const paraId = entry.getAttribute("w15:paraId") || entry.getAttribute("paraId");
11980
+ const parentParaId = entry.getAttribute("w15:paraIdParent") || entry.getAttribute("paraIdParent");
11981
+ if (!paraId || !knownParaIds.has(paraId.toUpperCase())) throw new Error(`Validation failed: commentsExtended entry has no matching comment paragraph: ${paraId || "(missing)"}`);
11982
+ if (extendedParaIds.has(paraId.toUpperCase())) throw new Error(`Validation failed: duplicate commentsExtended entry for paraId: ${paraId}`);
11983
+ extendedParaIds.add(paraId.toUpperCase());
11984
+ if (parentParaId) {
11985
+ if (!knownParaIds.has(parentParaId.toUpperCase())) throw new Error(`Validation failed: commentsExtended parent paragraph was not found: ${parentParaId}`);
11986
+ replyCommentIds.add(paraIdToCommentId.get(paraId.toUpperCase()));
11987
+ }
11988
+ }
11989
+ }
11990
+ const definitionsWithoutReferences = difference(new Set([...definitions].filter((id) => !replyCommentIds.has(id))), references);
11991
+ if (definitionsWithoutReferences.length > 0) {
11992
+ throw new Error(`Validation failed: comment definition has no document reference for id(s): ${definitionsWithoutReferences.join(", ")}`);
11993
+ }
9878
11994
  } else if (hasCommentUsage) {
9879
11995
  throw new Error("Validation failed: comments used but part missing");
9880
11996
  }
@@ -9916,6 +12032,16 @@ async function validateDocxPackage(zip) {
9916
12032
  throw new Error("Validation failed: comments rel missing");
9917
12033
  }
9918
12034
  }
12035
+ if (commentsExtendedXml) {
12036
+ const hasContentType = Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).some(
12037
+ (override) => (override.getAttribute("PartName") || "").toLowerCase() === "/word/commentsextended.xml" && (override.getAttribute("ContentType") || "") === COMMENTS_EXTENDED_CONTENT_TYPE
12038
+ );
12039
+ const hasRelationship = Array.from(relsDoc.getElementsByTagNameNS("*", "Relationship")).some(
12040
+ (rel) => (rel.getAttribute("Type") || "") === COMMENTS_EXTENDED_REL_TYPE
12041
+ );
12042
+ if (!hasContentType) throw new Error("Validation failed: commentsExtended CT override missing");
12043
+ if (!hasRelationship) throw new Error("Validation failed: commentsExtended rel missing");
12044
+ }
9919
12045
  }
9920
12046
 
9921
12047
  // orchestration/route-plan.js
@@ -10040,6 +12166,239 @@ function extractParagraphIdFromOoxml(ooxml) {
10040
12166
  return match ? match[1] : null;
10041
12167
  }
10042
12168
 
12169
+ // services/receipt-collector.js
12170
+ var ReceiptCollector = class {
12171
+ constructor() {
12172
+ this.receipts = [];
12173
+ this.activeReceipt = null;
12174
+ }
12175
+ beginOperation(operationIndex, operationId = null, authorUsed = null) {
12176
+ this.activeReceipt = {
12177
+ operationIndex: typeof operationIndex === "number" ? operationIndex : 1,
12178
+ ...operationId ? { operationId: String(operationId) } : {},
12179
+ attemptedDisposition: "applied",
12180
+ finalDisposition: "applied",
12181
+ committed: true,
12182
+ ...authorUsed ? { authorUsed: String(authorUsed) } : {},
12183
+ revisionItems: [],
12184
+ commentIds: [],
12185
+ numberingIds: [],
12186
+ relationshipIds: [],
12187
+ affectedTargets: [],
12188
+ warnings: []
12189
+ };
12190
+ }
12191
+ recordRevision(id, kind = "structural", partName = "word/document.xml") {
12192
+ if (!this.activeReceipt || id == null) return;
12193
+ const strId = String(id);
12194
+ if (!this.activeReceipt.revisionItems.some((item) => item.id === strId && item.kind === kind && item.partName === partName)) {
12195
+ this.activeReceipt.revisionItems.push({
12196
+ id: strId,
12197
+ kind,
12198
+ partName
12199
+ });
12200
+ }
12201
+ }
12202
+ recordComment(id, _partName = "word/comments.xml") {
12203
+ if (!this.activeReceipt || id == null) return;
12204
+ const strId = String(id);
12205
+ if (!this.activeReceipt.commentIds.includes(strId)) {
12206
+ this.activeReceipt.commentIds.push(strId);
12207
+ }
12208
+ }
12209
+ recordNumbering(id, _partName = "word/numbering.xml") {
12210
+ if (!this.activeReceipt || id == null) return;
12211
+ const strId = String(id);
12212
+ if (!this.activeReceipt.numberingIds.includes(strId)) {
12213
+ this.activeReceipt.numberingIds.push(strId);
12214
+ }
12215
+ }
12216
+ recordRelationship(id, _partName = "word/_rels/document.xml.rels") {
12217
+ if (!this.activeReceipt || id == null) return;
12218
+ const strId = String(id);
12219
+ if (!this.activeReceipt.relationshipIds.includes(strId)) {
12220
+ this.activeReceipt.relationshipIds.push(strId);
12221
+ }
12222
+ }
12223
+ recordAffectedTarget(target) {
12224
+ if (!this.activeReceipt || !target) return;
12225
+ this.activeReceipt.affectedTargets.push(JSON.parse(JSON.stringify(target)));
12226
+ }
12227
+ recordWarning(warning) {
12228
+ if (!this.activeReceipt || !warning) return;
12229
+ this.activeReceipt.warnings.push(String(warning));
12230
+ }
12231
+ commitOperation(disposition = "applied") {
12232
+ if (!this.activeReceipt) return null;
12233
+ this.activeReceipt.attemptedDisposition = disposition;
12234
+ this.activeReceipt.finalDisposition = disposition;
12235
+ this.activeReceipt.committed = disposition === "applied";
12236
+ const committed = JSON.parse(JSON.stringify(this.activeReceipt));
12237
+ this.receipts.push(committed);
12238
+ this.activeReceipt = null;
12239
+ return committed;
12240
+ }
12241
+ abortOperation(disposition = "refused") {
12242
+ if (!this.activeReceipt) return null;
12243
+ this.activeReceipt.attemptedDisposition = disposition;
12244
+ this.activeReceipt.finalDisposition = disposition;
12245
+ this.activeReceipt.committed = false;
12246
+ const aborted = JSON.parse(JSON.stringify(this.activeReceipt));
12247
+ this.activeReceipt = null;
12248
+ return aborted;
12249
+ }
12250
+ createSavepoint() {
12251
+ return {
12252
+ receipts: JSON.parse(JSON.stringify(this.receipts)),
12253
+ activeReceipt: this.activeReceipt ? JSON.parse(JSON.stringify(this.activeReceipt)) : null
12254
+ };
12255
+ }
12256
+ restoreSavepoint(savepoint) {
12257
+ if (!savepoint) return;
12258
+ this.receipts = Array.isArray(savepoint.receipts) ? JSON.parse(JSON.stringify(savepoint.receipts)) : [];
12259
+ this.activeReceipt = savepoint.activeReceipt ? JSON.parse(JSON.stringify(savepoint.activeReceipt)) : null;
12260
+ }
12261
+ clear() {
12262
+ this.receipts = [];
12263
+ this.activeReceipt = null;
12264
+ }
12265
+ markRolledBack() {
12266
+ for (const receipt of this.receipts) {
12267
+ if (receipt.attemptedDisposition === "applied") {
12268
+ receipt.finalDisposition = "rolled_back";
12269
+ receipt.committed = false;
12270
+ }
12271
+ }
12272
+ this.activeReceipt = null;
12273
+ }
12274
+ getReceipts() {
12275
+ return JSON.parse(JSON.stringify(this.receipts));
12276
+ }
12277
+ getCurrentReceipt() {
12278
+ return this.activeReceipt ? JSON.parse(JSON.stringify(this.activeReceipt)) : null;
12279
+ }
12280
+ };
12281
+ function createEmptyReceipt(operationIndex, operationId = null, authorUsed = null, disposition = "not_attempted") {
12282
+ return {
12283
+ operationIndex: typeof operationIndex === "number" ? operationIndex : 1,
12284
+ ...operationId ? { operationId: String(operationId) } : {},
12285
+ attemptedDisposition: disposition,
12286
+ finalDisposition: disposition,
12287
+ committed: false,
12288
+ ...authorUsed ? { authorUsed: String(authorUsed) } : {},
12289
+ revisionItems: [],
12290
+ commentIds: [],
12291
+ numberingIds: [],
12292
+ relationshipIds: [],
12293
+ affectedTargets: [],
12294
+ warnings: []
12295
+ };
12296
+ }
12297
+ function reconcileReceiptsAgainstOutput(parts, receipts) {
12298
+ if (!Array.isArray(receipts) || receipts.length === 0) {
12299
+ return { valid: true };
12300
+ }
12301
+ const committedReceipts = receipts.filter((r) => r && r.committed === true && r.finalDisposition === "applied");
12302
+ if (committedReceipts.length === 0) {
12303
+ return { valid: true };
12304
+ }
12305
+ const revisionIdSet = /* @__PURE__ */ new Set();
12306
+ if (parts?.documentXml && typeof parts.documentXml === "string") {
12307
+ const revRegex = /<(?:w:)?(?:ins|del|rPrChange|pPrChange|moveFrom|moveTo)\b[^>]*?\b(?:w:)?id="([^"]+)"/g;
12308
+ let m;
12309
+ while ((m = revRegex.exec(parts.documentXml)) !== null) {
12310
+ revisionIdSet.add(m[1]);
12311
+ }
12312
+ }
12313
+ const commentIdSet = /* @__PURE__ */ new Set();
12314
+ if (parts?.commentsXml && typeof parts.commentsXml === "string") {
12315
+ const comRegex = /<(?:w:)?comment\b[^>]*?\b(?:w:)?id="([^"]+)"/g;
12316
+ let m;
12317
+ while ((m = comRegex.exec(parts.commentsXml)) !== null) {
12318
+ commentIdSet.add(m[1]);
12319
+ }
12320
+ }
12321
+ const numberingIdSet = /* @__PURE__ */ new Set();
12322
+ const combinedNumbering = [parts?.numberingXml, ...parts?.numberingXmlParts || []].filter(Boolean).join("\n");
12323
+ if (combinedNumbering) {
12324
+ const numRegex = /<(?:w:)?num\b[^>]*?\b(?:w:)?numId="([^"]+)"/g;
12325
+ let m;
12326
+ while ((m = numRegex.exec(combinedNumbering)) !== null) {
12327
+ numberingIdSet.add(m[1]);
12328
+ }
12329
+ }
12330
+ if (parts?.documentXml && typeof parts.documentXml === "string") {
12331
+ const docNumRegex = /<(?:w:)?numId\b[^>]*?\b(?:w:)?val="([^"]+)"/g;
12332
+ let m;
12333
+ while ((m = docNumRegex.exec(parts.documentXml)) !== null) {
12334
+ numberingIdSet.add(m[1]);
12335
+ }
12336
+ }
12337
+ const relIdSet = /* @__PURE__ */ new Set();
12338
+ if (parts?.relationshipsXml && typeof parts.relationshipsXml === "string") {
12339
+ const relRegex = /<Relationship\b[^>]*?\bId="([^"]+)"/g;
12340
+ let m;
12341
+ while ((m = relRegex.exec(parts.relationshipsXml)) !== null) {
12342
+ relIdSet.add(m[1]);
12343
+ }
12344
+ }
12345
+ for (const receipt of committedReceipts) {
12346
+ if (Array.isArray(receipt.revisionItems)) {
12347
+ for (const item of receipt.revisionItems) {
12348
+ if (item.partName === "word/document.xml" && !revisionIdSet.has(String(item.id))) {
12349
+ return {
12350
+ valid: false,
12351
+ error: {
12352
+ code: "RECEIPT_RECONCILIATION_FAILED",
12353
+ message: `Committed revision id '${item.id}' (kind: ${item.kind}) was not found in word/document.xml.`
12354
+ }
12355
+ };
12356
+ }
12357
+ }
12358
+ }
12359
+ if (Array.isArray(receipt.commentIds)) {
12360
+ for (const id of receipt.commentIds) {
12361
+ if (!commentIdSet.has(String(id))) {
12362
+ return {
12363
+ valid: false,
12364
+ error: {
12365
+ code: "RECEIPT_RECONCILIATION_FAILED",
12366
+ message: `Committed comment id '${id}' was not found in word/comments.xml.`
12367
+ }
12368
+ };
12369
+ }
12370
+ }
12371
+ }
12372
+ if (Array.isArray(receipt.numberingIds)) {
12373
+ for (const id of receipt.numberingIds) {
12374
+ if (!numberingIdSet.has(String(id))) {
12375
+ return {
12376
+ valid: false,
12377
+ error: {
12378
+ code: "RECEIPT_RECONCILIATION_FAILED",
12379
+ message: `Committed numbering id '${id}' was not found in numbering parts or document.`
12380
+ }
12381
+ };
12382
+ }
12383
+ }
12384
+ }
12385
+ if (parts?.relationshipsXml && Array.isArray(receipt.relationshipIds)) {
12386
+ for (const id of receipt.relationshipIds) {
12387
+ if (!relIdSet.has(String(id))) {
12388
+ return {
12389
+ valid: false,
12390
+ error: {
12391
+ code: "RECEIPT_RECONCILIATION_FAILED",
12392
+ message: `Committed relationship id '${id}' was not found in document.xml.rels.`
12393
+ }
12394
+ };
12395
+ }
12396
+ }
12397
+ }
12398
+ }
12399
+ return { valid: true };
12400
+ }
12401
+
10043
12402
  // index.js
10044
12403
  async function applyRedlineToOxml2(oxml, originalText, modifiedText, options = {}) {
10045
12404
  const result = await applyRedlineToOxml(oxml, originalText, modifiedText, options);
@@ -10185,49 +12544,66 @@ export {
10185
12544
  DiffOp,
10186
12545
  NS_W,
10187
12546
  NumberingService,
12547
+ ReceiptCollector,
10188
12548
  ReconciliationPipeline,
10189
12549
  RoutePlanKind,
10190
12550
  RunKind,
10191
12551
  WORD_MAIN_NS,
10192
12552
  acceptTrackedChangesInOoxml,
12553
+ analyzeStructuredContent,
12554
+ applyCommentReplyToParts,
10193
12555
  applyFormattingRemovalToOoxml,
10194
12556
  applyHighlightToOoxml,
10195
12557
  applyRedlineToOxml2 as applyRedlineToOxml,
10196
12558
  applyRedlineToOxmlWithListFallback,
12559
+ areRevisionTokensEqual,
10197
12560
  buildCommentElement,
10198
12561
  buildCommentsPartXml,
10199
12562
  buildExplicitDecimalMultilevelNumberingXml,
10200
12563
  buildListMarkdown,
10201
12564
  buildReconciliationPlan,
12565
+ buildRevisionTokenFraming,
10202
12566
  buildSingleLineListStructuralFallbackPlan,
10203
12567
  buildTargetReferenceSnapshot,
10204
12568
  clearSingleLineListFallbackExplicitSequence,
10205
12569
  collectContiguousListParagraphBlock,
12570
+ computeDocumentPartsRevisionToken,
12571
+ computeRevisionToken,
12572
+ computeRevisionTokenSync,
10206
12573
  configureLogger,
10207
12574
  configureXmlProvider,
10208
12575
  containsTrackedChanges,
10209
12576
  createDynamicNumberingIdState,
12577
+ createEmptyReceipt,
12578
+ createParagraphFingerprint,
10210
12579
  deleteCommentsByAuthorInOoxml,
10211
12580
  enforceListBindingOnParagraphNodes,
10212
12581
  ensureCommentsArtifactsInZip,
12582
+ ensureCommentsExtendedArtifactsInZip,
10213
12583
  ensureNumberingArtifactsInZip,
10214
12584
  escapeXml,
10215
12585
  executeSingleLineListStructuralFallback,
12586
+ extractCanonicalParagraphText,
12587
+ extractDocumentPartsEntries,
10216
12588
  extractFirstParagraphNumId,
10217
12589
  extractParagraphIdFromOoxml,
12590
+ extractParagraphRevisionSegments,
10218
12591
  extractReplacementNodesFromOoxml,
10219
12592
  findContainingWordElement,
10220
12593
  findParagraphByBestTextMatch,
10221
12594
  findParagraphByReference,
10222
12595
  findParagraphByStrictText,
12596
+ findStrictTargetCandidates,
10223
12597
  generateTableOoxml,
10224
12598
  getBodyElementFromDocument,
10225
12599
  getDefaultAuthor,
10226
12600
  getDocumentParagraphNodes,
10227
12601
  getPackagePartName,
12602
+ getParagraphId2 as getParagraphId,
10228
12603
  getParagraphListInfo,
10229
12604
  getParagraphText,
10230
12605
  getPlatform,
12606
+ getTrackedChangeAuthors,
10231
12607
  hasListItems,
10232
12608
  inferNumberingStyleFromMarker,
10233
12609
  inferTableReplacementParagraphBlock,
@@ -10239,12 +12615,15 @@ export {
10239
12615
  injectCommentsIntoOoxml,
10240
12616
  injectCommentsIntoPackage2 as injectCommentsIntoPackage,
10241
12617
  insertBodyElementBeforeSectPr,
12618
+ inspectDocumentParts,
10242
12619
  isLikelyStructuredTableSourceParagraph,
10243
12620
  isMarkdownTableText,
12621
+ isNodeVisibleInRevisionView,
10244
12622
  mergeNumberingXmlBySchemaOrder,
10245
12623
  normalizeBodySectionOrderStandalone,
10246
12624
  normalizeContentEscapesForRouting,
10247
12625
  normalizeListItemsWithLevels,
12626
+ normalizeOpcEntryName,
10248
12627
  normalizeWhitespaceForTargeting,
10249
12628
  overwriteParagraphNumIds,
10250
12629
  parseMarkdownListContent,
@@ -10252,8 +12631,11 @@ export {
10252
12631
  parseOoxmlSafe,
10253
12632
  parseParagraphReference,
10254
12633
  planListInsertionOnlyEdit,
12634
+ planStructuredReplacement,
10255
12635
  preprocessMarkdown,
12636
+ readCanonicalRunText,
10256
12637
  reconcileMarkdownTableOoxml,
12638
+ reconcileReceiptsAgainstOutput,
10257
12639
  recordSingleLineListFallbackExplicitSequence,
10258
12640
  rejectTrackedChangesInOoxml,
10259
12641
  remapNumberingPayloadForDocument,
@@ -10278,6 +12660,7 @@ export {
10278
12660
  synthesizeTableMarkdownFromMultilineCellEdit,
10279
12661
  validateDocxPackage,
10280
12662
  validateRedlineOoxml,
12663
+ validateRevisionToken,
10281
12664
  wrapInDocumentFragment
10282
12665
  };
10283
12666
  //# sourceMappingURL=docx-redline-js.esm.js.map