@ansonlai/docx-redline-js 0.2.0 → 0.4.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 (87) hide show
  1. package/AGENTS.md +36 -10
  2. package/README.md +83 -6
  3. package/adapters/xml-adapter.js +73 -10
  4. package/core/list-targeting.js +3 -0
  5. package/core/paragraph-targeting.js +33 -7
  6. package/core/redline-validation.js +22 -0
  7. package/core/types.js +122 -27
  8. package/core/xml-query.js +3 -1
  9. package/dist/docx-redline-js.esm.js +1148 -572
  10. package/dist/docx-redline-js.esm.js.map +4 -4
  11. package/dist/docx-redline-js.esm.min.js +79 -78
  12. package/dist/docx-redline-js.esm.min.js.map +4 -4
  13. package/docs/TESTING.md +687 -0
  14. package/docs/VALIDATION.md +81 -2
  15. package/docs/WORD-MANUAL-REVIEW.md +138 -0
  16. package/docs/plans/2026-08-30-reliability-testing-improvements.md +488 -0
  17. package/docs/plans/2026-09-01-performance-and-complexity-reduction.md +210 -0
  18. package/docs/plans/{2026-03-01-release-0.1.4-design.md → completed/2026-03-01-release-0.1.4-design.md} +2 -0
  19. package/docs/plans/{2026-03-01-release-0.1.4.md → completed/2026-03-01-release-0.1.4.md} +5 -3
  20. package/docs/plans/{2026-05-31-architectural changes.md → completed/2026-05-31-architectural changes.md } +2 -0
  21. package/docs/plans/completed/2026-08-02-reliability-improvements.md +1155 -0
  22. package/docs/test-comparison-dashboard.html +95 -0
  23. package/docs/validation-reports/2026-08-30-phase-1-word-visual-preflight.md +22 -0
  24. package/docs/validation-reports/2026-08-30-phase-2-word-visual-preflight.md +24 -0
  25. package/docs/validation-reports/2026-08-30-phase-3-coverage.md +73 -0
  26. package/docs/validation-reports/2026-09-02-multilevel-bullets-visual-review.md +82 -0
  27. package/docs/validation-reports/2026-09-02-multimodal-visual-samples.md +114 -0
  28. package/docs/validation-reports/2026-09-02-visual-failures-preflight.md +79 -0
  29. package/engine/format-extraction.js +1 -1
  30. package/engine/formatting-removal.js +95 -104
  31. package/engine/oxml-engine.js +176 -83
  32. package/engine/reconstruction-mapper.js +276 -79
  33. package/engine/reconstruction-mode.js +20 -6
  34. package/engine/reconstruction-writer.js +117 -72
  35. package/engine/run-builders.js +17 -13
  36. package/engine/surgical-diff-application.js +7 -21
  37. package/engine/surgical-mode.js +3 -2
  38. package/engine/table-mode.js +27 -16
  39. package/index.d.ts +95 -3
  40. package/index.js +14 -13
  41. package/orchestration/list-structural-fallback.js +16 -39
  42. package/package.json +23 -5
  43. package/pipeline/diff-engine.js +174 -55
  44. package/pipeline/ingestion-export.js +39 -24
  45. package/pipeline/ingestion-paragraph.js +7 -5
  46. package/pipeline/list-generation.js +27 -18
  47. package/pipeline/patching.js +2 -3
  48. package/pipeline/pipeline.js +65 -36
  49. package/pipeline/serialization.js +13 -5
  50. package/scripts/build-test-dashboard.mjs +43 -0
  51. package/scripts/check-types.mjs +16 -24
  52. package/scripts/export-validation-fixtures.mjs +191 -45
  53. package/scripts/fetch-superdoc-corpus.mjs +61 -0
  54. package/scripts/generate-test-dashboard.mjs +199 -0
  55. package/scripts/inspect-visual-evidence.mjs +271 -0
  56. package/scripts/lib/minimal-zip.mjs +199 -18
  57. package/scripts/lib/word-coverage-catalogue.mjs +207 -0
  58. package/scripts/lib/word-coverage-metadata.mjs +93 -0
  59. package/scripts/lib/zip-reader.mjs +64 -0
  60. package/scripts/package-superdoc-word-fixtures.ps1 +64 -0
  61. package/scripts/prepare-corpus-word-visual-review.mjs +84 -0
  62. package/scripts/prepare-superdoc-word-corpus.mjs +284 -0
  63. package/scripts/prepare-word-review.mjs +77 -0
  64. package/scripts/prepare-word-visual-review.mjs +90 -0
  65. package/scripts/render-agenda-multilevel.mjs +70 -0
  66. package/scripts/render-case22.mjs +73 -0
  67. package/scripts/render-case40.ps1 +35 -0
  68. package/scripts/render-multilevel-bullet-images.py +58 -0
  69. package/scripts/render-multilevel-bullet-visual.ps1 +32 -0
  70. package/scripts/render-multilevel-cases.mjs +80 -0
  71. package/scripts/report-coverage-gaps.mjs +103 -0
  72. package/scripts/report-word-coverage.mjs +71 -0
  73. package/scripts/sample-multimodal-visual-check.mjs +221 -0
  74. package/scripts/test-multilevel-bullet-visual.mjs +187 -0
  75. package/scripts/word-com-corpus-suite.ps1 +43 -0
  76. package/scripts/word-com-corpus-visual-suite.ps1 +116 -0
  77. package/scripts/word-com-differential.ps1 +158 -16
  78. package/scripts/word-com-suite.ps1 +19 -0
  79. package/scripts/word-com-visual-suite.ps1 +132 -0
  80. package/services/comment-engine.js +51 -46
  81. package/services/comment-locator.js +0 -1
  82. package/services/comment-package.js +11 -10
  83. package/services/numbering-service.js +1 -1
  84. package/services/revision-comment-management.js +31 -10
  85. package/services/standalone-docx-plumbing.js +45 -34
  86. package/services/standalone-operation-runner.js +315 -75
  87. package/services/table-reconciliation.js +23 -11
@@ -1,4 +1,4 @@
1
- // @ansonlai/docx-redline-js v0.2.0 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js v0.4.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;
@@ -6,7 +6,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __commonJS = (cb, mod) => function __require() {
9
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
+ try {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ } catch (e) {
12
+ throw mod = 0, e;
13
+ }
10
14
  };
11
15
  var __copyProps = (to, from, except, desc) => {
12
16
  if (from && typeof from === "object" || typeof from === "function") {
@@ -28,7 +32,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
32
  // node_modules/diff-match-patch/index.js
29
33
  var require_diff_match_patch = __commonJS({
30
34
  "node_modules/diff-match-patch/index.js"(exports, module) {
31
- var diff_match_patch2 = function() {
35
+ var diff_match_patch3 = function() {
32
36
  this.Diff_Timeout = 1;
33
37
  this.Diff_EditCost = 4;
34
38
  this.Match_Threshold = 0.5;
@@ -40,10 +44,10 @@ var require_diff_match_patch = __commonJS({
40
44
  var DIFF_DELETE = -1;
41
45
  var DIFF_INSERT = 1;
42
46
  var DIFF_EQUAL = 0;
43
- diff_match_patch2.Diff = function(op, text) {
47
+ diff_match_patch3.Diff = function(op, text) {
44
48
  return [op, text];
45
49
  };
46
- diff_match_patch2.prototype.diff_main = function(text1, text2, opt_checklines, opt_deadline) {
50
+ diff_match_patch3.prototype.diff_main = function(text1, text2, opt_checklines, opt_deadline) {
47
51
  if (typeof opt_deadline == "undefined") {
48
52
  if (this.Diff_Timeout <= 0) {
49
53
  opt_deadline = Number.MAX_VALUE;
@@ -57,7 +61,7 @@ var require_diff_match_patch = __commonJS({
57
61
  }
58
62
  if (text1 == text2) {
59
63
  if (text1) {
60
- return [new diff_match_patch2.Diff(DIFF_EQUAL, text1)];
64
+ return [new diff_match_patch3.Diff(DIFF_EQUAL, text1)];
61
65
  }
62
66
  return [];
63
67
  }
@@ -75,30 +79,30 @@ var require_diff_match_patch = __commonJS({
75
79
  text2 = text2.substring(0, text2.length - commonlength);
76
80
  var diffs = this.diff_compute_(text1, text2, checklines, deadline);
77
81
  if (commonprefix) {
78
- diffs.unshift(new diff_match_patch2.Diff(DIFF_EQUAL, commonprefix));
82
+ diffs.unshift(new diff_match_patch3.Diff(DIFF_EQUAL, commonprefix));
79
83
  }
80
84
  if (commonsuffix) {
81
- diffs.push(new diff_match_patch2.Diff(DIFF_EQUAL, commonsuffix));
85
+ diffs.push(new diff_match_patch3.Diff(DIFF_EQUAL, commonsuffix));
82
86
  }
83
87
  this.diff_cleanupMerge(diffs);
84
88
  return diffs;
85
89
  };
86
- diff_match_patch2.prototype.diff_compute_ = function(text1, text2, checklines, deadline) {
90
+ diff_match_patch3.prototype.diff_compute_ = function(text1, text2, checklines, deadline) {
87
91
  var diffs;
88
92
  if (!text1) {
89
- return [new diff_match_patch2.Diff(DIFF_INSERT, text2)];
93
+ return [new diff_match_patch3.Diff(DIFF_INSERT, text2)];
90
94
  }
91
95
  if (!text2) {
92
- return [new diff_match_patch2.Diff(DIFF_DELETE, text1)];
96
+ return [new diff_match_patch3.Diff(DIFF_DELETE, text1)];
93
97
  }
94
98
  var longtext = text1.length > text2.length ? text1 : text2;
95
99
  var shorttext = text1.length > text2.length ? text2 : text1;
96
100
  var i = longtext.indexOf(shorttext);
97
101
  if (i != -1) {
98
102
  diffs = [
99
- new diff_match_patch2.Diff(DIFF_INSERT, longtext.substring(0, i)),
100
- new diff_match_patch2.Diff(DIFF_EQUAL, shorttext),
101
- new diff_match_patch2.Diff(
103
+ new diff_match_patch3.Diff(DIFF_INSERT, longtext.substring(0, i)),
104
+ new diff_match_patch3.Diff(DIFF_EQUAL, shorttext),
105
+ new diff_match_patch3.Diff(
102
106
  DIFF_INSERT,
103
107
  longtext.substring(i + shorttext.length)
104
108
  )
@@ -110,8 +114,8 @@ var require_diff_match_patch = __commonJS({
110
114
  }
111
115
  if (shorttext.length == 1) {
112
116
  return [
113
- new diff_match_patch2.Diff(DIFF_DELETE, text1),
114
- new diff_match_patch2.Diff(DIFF_INSERT, text2)
117
+ new diff_match_patch3.Diff(DIFF_DELETE, text1),
118
+ new diff_match_patch3.Diff(DIFF_INSERT, text2)
115
119
  ];
116
120
  }
117
121
  var hm = this.diff_halfMatch_(text1, text2);
@@ -124,7 +128,7 @@ var require_diff_match_patch = __commonJS({
124
128
  var diffs_a = this.diff_main(text1_a, text2_a, checklines, deadline);
125
129
  var diffs_b = this.diff_main(text1_b, text2_b, checklines, deadline);
126
130
  return diffs_a.concat(
127
- [new diff_match_patch2.Diff(DIFF_EQUAL, mid_common)],
131
+ [new diff_match_patch3.Diff(DIFF_EQUAL, mid_common)],
128
132
  diffs_b
129
133
  );
130
134
  }
@@ -133,7 +137,7 @@ var require_diff_match_patch = __commonJS({
133
137
  }
134
138
  return this.diff_bisect_(text1, text2, deadline);
135
139
  };
136
- diff_match_patch2.prototype.diff_lineMode_ = function(text1, text2, deadline) {
140
+ diff_match_patch3.prototype.diff_lineMode_ = function(text1, text2, deadline) {
137
141
  var a = this.diff_linesToChars_(text1, text2);
138
142
  text1 = a.chars1;
139
143
  text2 = a.chars2;
@@ -141,7 +145,7 @@ var require_diff_match_patch = __commonJS({
141
145
  var diffs = this.diff_main(text1, text2, false, deadline);
142
146
  this.diff_charsToLines_(diffs, linearray);
143
147
  this.diff_cleanupSemantic(diffs);
144
- diffs.push(new diff_match_patch2.Diff(DIFF_EQUAL, ""));
148
+ diffs.push(new diff_match_patch3.Diff(DIFF_EQUAL, ""));
145
149
  var pointer = 0;
146
150
  var count_delete = 0;
147
151
  var count_insert = 0;
@@ -181,7 +185,7 @@ var require_diff_match_patch = __commonJS({
181
185
  diffs.pop();
182
186
  return diffs;
183
187
  };
184
- diff_match_patch2.prototype.diff_bisect_ = function(text1, text2, deadline) {
188
+ diff_match_patch3.prototype.diff_bisect_ = function(text1, text2, deadline) {
185
189
  var text1_length = text1.length;
186
190
  var text2_length = text2.length;
187
191
  var max_d = Math.ceil((text1_length + text2_length) / 2);
@@ -265,11 +269,11 @@ var require_diff_match_patch = __commonJS({
265
269
  }
266
270
  }
267
271
  return [
268
- new diff_match_patch2.Diff(DIFF_DELETE, text1),
269
- new diff_match_patch2.Diff(DIFF_INSERT, text2)
272
+ new diff_match_patch3.Diff(DIFF_DELETE, text1),
273
+ new diff_match_patch3.Diff(DIFF_INSERT, text2)
270
274
  ];
271
275
  };
272
- diff_match_patch2.prototype.diff_bisectSplit_ = function(text1, text2, x, y, deadline) {
276
+ diff_match_patch3.prototype.diff_bisectSplit_ = function(text1, text2, x, y, deadline) {
273
277
  var text1a = text1.substring(0, x);
274
278
  var text2a = text2.substring(0, y);
275
279
  var text1b = text1.substring(x);
@@ -278,7 +282,7 @@ var require_diff_match_patch = __commonJS({
278
282
  var diffsb = this.diff_main(text1b, text2b, false, deadline);
279
283
  return diffs.concat(diffsb);
280
284
  };
281
- diff_match_patch2.prototype.diff_linesToChars_ = function(text1, text2) {
285
+ diff_match_patch3.prototype.diff_linesToChars_ = function(text1, text2) {
282
286
  var lineArray = [];
283
287
  var lineHash = {};
284
288
  lineArray[0] = "";
@@ -314,7 +318,7 @@ var require_diff_match_patch = __commonJS({
314
318
  var chars2 = diff_linesToCharsMunge_(text2);
315
319
  return { chars1, chars2, lineArray };
316
320
  };
317
- diff_match_patch2.prototype.diff_charsToLines_ = function(diffs, lineArray) {
321
+ diff_match_patch3.prototype.diff_charsToLines_ = function(diffs, lineArray) {
318
322
  for (var i = 0; i < diffs.length; i++) {
319
323
  var chars = diffs[i][1];
320
324
  var text = [];
@@ -324,7 +328,7 @@ var require_diff_match_patch = __commonJS({
324
328
  diffs[i][1] = text.join("");
325
329
  }
326
330
  };
327
- diff_match_patch2.prototype.diff_commonPrefix = function(text1, text2) {
331
+ diff_match_patch3.prototype.diff_commonPrefix = function(text1, text2) {
328
332
  if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) {
329
333
  return 0;
330
334
  }
@@ -343,7 +347,7 @@ var require_diff_match_patch = __commonJS({
343
347
  }
344
348
  return pointermid;
345
349
  };
346
- diff_match_patch2.prototype.diff_commonSuffix = function(text1, text2) {
350
+ diff_match_patch3.prototype.diff_commonSuffix = function(text1, text2) {
347
351
  if (!text1 || !text2 || text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1)) {
348
352
  return 0;
349
353
  }
@@ -362,7 +366,7 @@ var require_diff_match_patch = __commonJS({
362
366
  }
363
367
  return pointermid;
364
368
  };
365
- diff_match_patch2.prototype.diff_commonOverlap_ = function(text1, text2) {
369
+ diff_match_patch3.prototype.diff_commonOverlap_ = function(text1, text2) {
366
370
  var text1_length = text1.length;
367
371
  var text2_length = text2.length;
368
372
  if (text1_length == 0 || text2_length == 0) {
@@ -392,7 +396,7 @@ var require_diff_match_patch = __commonJS({
392
396
  }
393
397
  }
394
398
  };
395
- diff_match_patch2.prototype.diff_halfMatch_ = function(text1, text2) {
399
+ diff_match_patch3.prototype.diff_halfMatch_ = function(text1, text2) {
396
400
  if (this.Diff_Timeout <= 0) {
397
401
  return null;
398
402
  }
@@ -471,7 +475,7 @@ var require_diff_match_patch = __commonJS({
471
475
  var mid_common = hm[4];
472
476
  return [text1_a, text1_b, text2_a, text2_b, mid_common];
473
477
  };
474
- diff_match_patch2.prototype.diff_cleanupSemantic = function(diffs) {
478
+ diff_match_patch3.prototype.diff_cleanupSemantic = function(diffs) {
475
479
  var changes = false;
476
480
  var equalities = [];
477
481
  var equalitiesLength = 0;
@@ -502,7 +506,7 @@ var require_diff_match_patch = __commonJS({
502
506
  diffs.splice(
503
507
  equalities[equalitiesLength - 1],
504
508
  0,
505
- new diff_match_patch2.Diff(DIFF_DELETE, lastEquality)
509
+ new diff_match_patch3.Diff(DIFF_DELETE, lastEquality)
506
510
  );
507
511
  diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
508
512
  equalitiesLength--;
@@ -531,7 +535,7 @@ var require_diff_match_patch = __commonJS({
531
535
  var overlap_length2 = this.diff_commonOverlap_(insertion, deletion);
532
536
  if (overlap_length1 >= overlap_length2) {
533
537
  if (overlap_length1 >= deletion.length / 2 || overlap_length1 >= insertion.length / 2) {
534
- diffs.splice(pointer, 0, new diff_match_patch2.Diff(
538
+ diffs.splice(pointer, 0, new diff_match_patch3.Diff(
535
539
  DIFF_EQUAL,
536
540
  insertion.substring(0, overlap_length1)
537
541
  ));
@@ -541,7 +545,7 @@ var require_diff_match_patch = __commonJS({
541
545
  }
542
546
  } else {
543
547
  if (overlap_length2 >= deletion.length / 2 || overlap_length2 >= insertion.length / 2) {
544
- diffs.splice(pointer, 0, new diff_match_patch2.Diff(
548
+ diffs.splice(pointer, 0, new diff_match_patch3.Diff(
545
549
  DIFF_EQUAL,
546
550
  deletion.substring(0, overlap_length2)
547
551
  ));
@@ -557,21 +561,21 @@ var require_diff_match_patch = __commonJS({
557
561
  pointer++;
558
562
  }
559
563
  };
560
- diff_match_patch2.prototype.diff_cleanupSemanticLossless = function(diffs) {
564
+ diff_match_patch3.prototype.diff_cleanupSemanticLossless = function(diffs) {
561
565
  function diff_cleanupSemanticScore_(one, two) {
562
566
  if (!one || !two) {
563
567
  return 6;
564
568
  }
565
569
  var char1 = one.charAt(one.length - 1);
566
570
  var char2 = two.charAt(0);
567
- var nonAlphaNumeric1 = char1.match(diff_match_patch2.nonAlphaNumericRegex_);
568
- var nonAlphaNumeric2 = char2.match(diff_match_patch2.nonAlphaNumericRegex_);
569
- var whitespace1 = nonAlphaNumeric1 && char1.match(diff_match_patch2.whitespaceRegex_);
570
- var whitespace2 = nonAlphaNumeric2 && char2.match(diff_match_patch2.whitespaceRegex_);
571
- var lineBreak1 = whitespace1 && char1.match(diff_match_patch2.linebreakRegex_);
572
- var lineBreak2 = whitespace2 && char2.match(diff_match_patch2.linebreakRegex_);
573
- var blankLine1 = lineBreak1 && one.match(diff_match_patch2.blanklineEndRegex_);
574
- var blankLine2 = lineBreak2 && two.match(diff_match_patch2.blanklineStartRegex_);
571
+ var nonAlphaNumeric1 = char1.match(diff_match_patch3.nonAlphaNumericRegex_);
572
+ var nonAlphaNumeric2 = char2.match(diff_match_patch3.nonAlphaNumericRegex_);
573
+ var whitespace1 = nonAlphaNumeric1 && char1.match(diff_match_patch3.whitespaceRegex_);
574
+ var whitespace2 = nonAlphaNumeric2 && char2.match(diff_match_patch3.whitespaceRegex_);
575
+ var lineBreak1 = whitespace1 && char1.match(diff_match_patch3.linebreakRegex_);
576
+ var lineBreak2 = whitespace2 && char2.match(diff_match_patch3.linebreakRegex_);
577
+ var blankLine1 = lineBreak1 && one.match(diff_match_patch3.blanklineEndRegex_);
578
+ var blankLine2 = lineBreak2 && two.match(diff_match_patch3.blanklineStartRegex_);
575
579
  if (blankLine1 || blankLine2) {
576
580
  return 5;
577
581
  } else if (lineBreak1 || lineBreak2) {
@@ -633,12 +637,12 @@ var require_diff_match_patch = __commonJS({
633
637
  pointer++;
634
638
  }
635
639
  };
636
- diff_match_patch2.nonAlphaNumericRegex_ = /[^a-zA-Z0-9]/;
637
- diff_match_patch2.whitespaceRegex_ = /\s/;
638
- diff_match_patch2.linebreakRegex_ = /[\r\n]/;
639
- diff_match_patch2.blanklineEndRegex_ = /\n\r?\n$/;
640
- diff_match_patch2.blanklineStartRegex_ = /^\r?\n\r?\n/;
641
- diff_match_patch2.prototype.diff_cleanupEfficiency = function(diffs) {
640
+ diff_match_patch3.nonAlphaNumericRegex_ = /[^a-zA-Z0-9]/;
641
+ diff_match_patch3.whitespaceRegex_ = /\s/;
642
+ diff_match_patch3.linebreakRegex_ = /[\r\n]/;
643
+ diff_match_patch3.blanklineEndRegex_ = /\n\r?\n$/;
644
+ diff_match_patch3.blanklineStartRegex_ = /^\r?\n\r?\n/;
645
+ diff_match_patch3.prototype.diff_cleanupEfficiency = function(diffs) {
642
646
  var changes = false;
643
647
  var equalities = [];
644
648
  var equalitiesLength = 0;
@@ -670,7 +674,7 @@ var require_diff_match_patch = __commonJS({
670
674
  diffs.splice(
671
675
  equalities[equalitiesLength - 1],
672
676
  0,
673
- new diff_match_patch2.Diff(DIFF_DELETE, lastEquality)
677
+ new diff_match_patch3.Diff(DIFF_DELETE, lastEquality)
674
678
  );
675
679
  diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
676
680
  equalitiesLength--;
@@ -692,8 +696,8 @@ var require_diff_match_patch = __commonJS({
692
696
  this.diff_cleanupMerge(diffs);
693
697
  }
694
698
  };
695
- diff_match_patch2.prototype.diff_cleanupMerge = function(diffs) {
696
- diffs.push(new diff_match_patch2.Diff(DIFF_EQUAL, ""));
699
+ diff_match_patch3.prototype.diff_cleanupMerge = function(diffs) {
700
+ diffs.push(new diff_match_patch3.Diff(DIFF_EQUAL, ""));
697
701
  var pointer = 0;
698
702
  var count_delete = 0;
699
703
  var count_insert = 0;
@@ -720,7 +724,7 @@ var require_diff_match_patch = __commonJS({
720
724
  if (pointer - count_delete - count_insert > 0 && diffs[pointer - count_delete - count_insert - 1][0] == DIFF_EQUAL) {
721
725
  diffs[pointer - count_delete - count_insert - 1][1] += text_insert.substring(0, commonlength);
722
726
  } else {
723
- diffs.splice(0, 0, new diff_match_patch2.Diff(
727
+ diffs.splice(0, 0, new diff_match_patch3.Diff(
724
728
  DIFF_EQUAL,
725
729
  text_insert.substring(0, commonlength)
726
730
  ));
@@ -742,7 +746,7 @@ var require_diff_match_patch = __commonJS({
742
746
  diffs.splice(
743
747
  pointer,
744
748
  0,
745
- new diff_match_patch2.Diff(DIFF_DELETE, text_delete)
749
+ new diff_match_patch3.Diff(DIFF_DELETE, text_delete)
746
750
  );
747
751
  pointer++;
748
752
  }
@@ -750,7 +754,7 @@ var require_diff_match_patch = __commonJS({
750
754
  diffs.splice(
751
755
  pointer,
752
756
  0,
753
- new diff_match_patch2.Diff(DIFF_INSERT, text_insert)
757
+ new diff_match_patch3.Diff(DIFF_INSERT, text_insert)
754
758
  );
755
759
  pointer++;
756
760
  }
@@ -793,7 +797,7 @@ var require_diff_match_patch = __commonJS({
793
797
  this.diff_cleanupMerge(diffs);
794
798
  }
795
799
  };
796
- diff_match_patch2.prototype.diff_xIndex = function(diffs, loc) {
800
+ diff_match_patch3.prototype.diff_xIndex = function(diffs, loc) {
797
801
  var chars1 = 0;
798
802
  var chars2 = 0;
799
803
  var last_chars1 = 0;
@@ -817,7 +821,7 @@ var require_diff_match_patch = __commonJS({
817
821
  }
818
822
  return last_chars2 + (loc - last_chars1);
819
823
  };
820
- diff_match_patch2.prototype.diff_prettyHtml = function(diffs) {
824
+ diff_match_patch3.prototype.diff_prettyHtml = function(diffs) {
821
825
  var html = [];
822
826
  var pattern_amp = /&/g;
823
827
  var pattern_lt = /</g;
@@ -841,7 +845,7 @@ var require_diff_match_patch = __commonJS({
841
845
  }
842
846
  return html.join("");
843
847
  };
844
- diff_match_patch2.prototype.diff_text1 = function(diffs) {
848
+ diff_match_patch3.prototype.diff_text1 = function(diffs) {
845
849
  var text = [];
846
850
  for (var x = 0; x < diffs.length; x++) {
847
851
  if (diffs[x][0] !== DIFF_INSERT) {
@@ -850,7 +854,7 @@ var require_diff_match_patch = __commonJS({
850
854
  }
851
855
  return text.join("");
852
856
  };
853
- diff_match_patch2.prototype.diff_text2 = function(diffs) {
857
+ diff_match_patch3.prototype.diff_text2 = function(diffs) {
854
858
  var text = [];
855
859
  for (var x = 0; x < diffs.length; x++) {
856
860
  if (diffs[x][0] !== DIFF_DELETE) {
@@ -859,7 +863,7 @@ var require_diff_match_patch = __commonJS({
859
863
  }
860
864
  return text.join("");
861
865
  };
862
- diff_match_patch2.prototype.diff_levenshtein = function(diffs) {
866
+ diff_match_patch3.prototype.diff_levenshtein = function(diffs) {
863
867
  var levenshtein = 0;
864
868
  var insertions = 0;
865
869
  var deletions = 0;
@@ -883,7 +887,7 @@ var require_diff_match_patch = __commonJS({
883
887
  levenshtein += Math.max(insertions, deletions);
884
888
  return levenshtein;
885
889
  };
886
- diff_match_patch2.prototype.diff_toDelta = function(diffs) {
890
+ diff_match_patch3.prototype.diff_toDelta = function(diffs) {
887
891
  var text = [];
888
892
  for (var x = 0; x < diffs.length; x++) {
889
893
  switch (diffs[x][0]) {
@@ -900,7 +904,7 @@ var require_diff_match_patch = __commonJS({
900
904
  }
901
905
  return text.join(" ").replace(/%20/g, " ");
902
906
  };
903
- diff_match_patch2.prototype.diff_fromDelta = function(text1, delta) {
907
+ diff_match_patch3.prototype.diff_fromDelta = function(text1, delta) {
904
908
  var diffs = [];
905
909
  var diffsLength = 0;
906
910
  var pointer = 0;
@@ -910,7 +914,7 @@ var require_diff_match_patch = __commonJS({
910
914
  switch (tokens[x].charAt(0)) {
911
915
  case "+":
912
916
  try {
913
- diffs[diffsLength++] = new diff_match_patch2.Diff(DIFF_INSERT, decodeURI(param));
917
+ diffs[diffsLength++] = new diff_match_patch3.Diff(DIFF_INSERT, decodeURI(param));
914
918
  } catch (ex) {
915
919
  throw new Error("Illegal escape in diff_fromDelta: " + param);
916
920
  }
@@ -924,9 +928,9 @@ var require_diff_match_patch = __commonJS({
924
928
  }
925
929
  var text = text1.substring(pointer, pointer += n);
926
930
  if (tokens[x].charAt(0) == "=") {
927
- diffs[diffsLength++] = new diff_match_patch2.Diff(DIFF_EQUAL, text);
931
+ diffs[diffsLength++] = new diff_match_patch3.Diff(DIFF_EQUAL, text);
928
932
  } else {
929
- diffs[diffsLength++] = new diff_match_patch2.Diff(DIFF_DELETE, text);
933
+ diffs[diffsLength++] = new diff_match_patch3.Diff(DIFF_DELETE, text);
930
934
  }
931
935
  break;
932
936
  default:
@@ -940,7 +944,7 @@ var require_diff_match_patch = __commonJS({
940
944
  }
941
945
  return diffs;
942
946
  };
943
- diff_match_patch2.prototype.match_main = function(text, pattern, loc) {
947
+ diff_match_patch3.prototype.match_main = function(text, pattern, loc) {
944
948
  if (text == null || pattern == null || loc == null) {
945
949
  throw new Error("Null input. (match_main)");
946
950
  }
@@ -955,7 +959,7 @@ var require_diff_match_patch = __commonJS({
955
959
  return this.match_bitap_(text, pattern, loc);
956
960
  }
957
961
  };
958
- diff_match_patch2.prototype.match_bitap_ = function(text, pattern, loc) {
962
+ diff_match_patch3.prototype.match_bitap_ = function(text, pattern, loc) {
959
963
  if (pattern.length > this.Match_MaxBits) {
960
964
  throw new Error("Pattern too long for this browser.");
961
965
  }
@@ -1026,7 +1030,7 @@ var require_diff_match_patch = __commonJS({
1026
1030
  }
1027
1031
  return best_loc;
1028
1032
  };
1029
- diff_match_patch2.prototype.match_alphabet_ = function(pattern) {
1033
+ diff_match_patch3.prototype.match_alphabet_ = function(pattern) {
1030
1034
  var s = {};
1031
1035
  for (var i = 0; i < pattern.length; i++) {
1032
1036
  s[pattern.charAt(i)] = 0;
@@ -1036,7 +1040,7 @@ var require_diff_match_patch = __commonJS({
1036
1040
  }
1037
1041
  return s;
1038
1042
  };
1039
- diff_match_patch2.prototype.patch_addContext_ = function(patch, text) {
1043
+ diff_match_patch3.prototype.patch_addContext_ = function(patch, text) {
1040
1044
  if (text.length == 0) {
1041
1045
  return;
1042
1046
  }
@@ -1055,21 +1059,21 @@ var require_diff_match_patch = __commonJS({
1055
1059
  padding += this.Patch_Margin;
1056
1060
  var prefix = text.substring(patch.start2 - padding, patch.start2);
1057
1061
  if (prefix) {
1058
- patch.diffs.unshift(new diff_match_patch2.Diff(DIFF_EQUAL, prefix));
1062
+ patch.diffs.unshift(new diff_match_patch3.Diff(DIFF_EQUAL, prefix));
1059
1063
  }
1060
1064
  var suffix = text.substring(
1061
1065
  patch.start2 + patch.length1,
1062
1066
  patch.start2 + patch.length1 + padding
1063
1067
  );
1064
1068
  if (suffix) {
1065
- patch.diffs.push(new diff_match_patch2.Diff(DIFF_EQUAL, suffix));
1069
+ patch.diffs.push(new diff_match_patch3.Diff(DIFF_EQUAL, suffix));
1066
1070
  }
1067
1071
  patch.start1 -= prefix.length;
1068
1072
  patch.start2 -= prefix.length;
1069
1073
  patch.length1 += prefix.length + suffix.length;
1070
1074
  patch.length2 += prefix.length + suffix.length;
1071
1075
  };
1072
- diff_match_patch2.prototype.patch_make = function(a, opt_b, opt_c) {
1076
+ diff_match_patch3.prototype.patch_make = function(a, opt_b, opt_c) {
1073
1077
  var text1, diffs;
1074
1078
  if (typeof a == "string" && typeof opt_b == "string" && typeof opt_c == "undefined") {
1075
1079
  text1 = /** @type {string} */
@@ -1105,7 +1109,7 @@ var require_diff_match_patch = __commonJS({
1105
1109
  return [];
1106
1110
  }
1107
1111
  var patches = [];
1108
- var patch = new diff_match_patch2.patch_obj();
1112
+ var patch = new diff_match_patch3.patch_obj();
1109
1113
  var patchDiffLength = 0;
1110
1114
  var char_count1 = 0;
1111
1115
  var char_count2 = 0;
@@ -1138,7 +1142,7 @@ var require_diff_match_patch = __commonJS({
1138
1142
  if (patchDiffLength) {
1139
1143
  this.patch_addContext_(patch, prepatch_text);
1140
1144
  patches.push(patch);
1141
- patch = new diff_match_patch2.patch_obj();
1145
+ patch = new diff_match_patch3.patch_obj();
1142
1146
  patchDiffLength = 0;
1143
1147
  prepatch_text = postpatch_text;
1144
1148
  char_count1 = char_count2;
@@ -1159,14 +1163,14 @@ var require_diff_match_patch = __commonJS({
1159
1163
  }
1160
1164
  return patches;
1161
1165
  };
1162
- diff_match_patch2.prototype.patch_deepCopy = function(patches) {
1166
+ diff_match_patch3.prototype.patch_deepCopy = function(patches) {
1163
1167
  var patchesCopy = [];
1164
1168
  for (var x = 0; x < patches.length; x++) {
1165
1169
  var patch = patches[x];
1166
- var patchCopy = new diff_match_patch2.patch_obj();
1170
+ var patchCopy = new diff_match_patch3.patch_obj();
1167
1171
  patchCopy.diffs = [];
1168
1172
  for (var y = 0; y < patch.diffs.length; y++) {
1169
- patchCopy.diffs[y] = new diff_match_patch2.Diff(patch.diffs[y][0], patch.diffs[y][1]);
1173
+ patchCopy.diffs[y] = new diff_match_patch3.Diff(patch.diffs[y][0], patch.diffs[y][1]);
1170
1174
  }
1171
1175
  patchCopy.start1 = patch.start1;
1172
1176
  patchCopy.start2 = patch.start2;
@@ -1176,7 +1180,7 @@ var require_diff_match_patch = __commonJS({
1176
1180
  }
1177
1181
  return patchesCopy;
1178
1182
  };
1179
- diff_match_patch2.prototype.patch_apply = function(patches, text) {
1183
+ diff_match_patch3.prototype.patch_apply = function(patches, text) {
1180
1184
  if (patches.length == 0) {
1181
1185
  return [text, []];
1182
1186
  }
@@ -1256,7 +1260,7 @@ var require_diff_match_patch = __commonJS({
1256
1260
  text = text.substring(nullPadding.length, text.length - nullPadding.length);
1257
1261
  return [text, results];
1258
1262
  };
1259
- diff_match_patch2.prototype.patch_addPadding = function(patches) {
1263
+ diff_match_patch3.prototype.patch_addPadding = function(patches) {
1260
1264
  var paddingLength = this.Patch_Margin;
1261
1265
  var nullPadding = "";
1262
1266
  for (var x = 1; x <= paddingLength; x++) {
@@ -1269,7 +1273,7 @@ var require_diff_match_patch = __commonJS({
1269
1273
  var patch = patches[0];
1270
1274
  var diffs = patch.diffs;
1271
1275
  if (diffs.length == 0 || diffs[0][0] != DIFF_EQUAL) {
1272
- diffs.unshift(new diff_match_patch2.Diff(DIFF_EQUAL, nullPadding));
1276
+ diffs.unshift(new diff_match_patch3.Diff(DIFF_EQUAL, nullPadding));
1273
1277
  patch.start1 -= paddingLength;
1274
1278
  patch.start2 -= paddingLength;
1275
1279
  patch.length1 += paddingLength;
@@ -1285,7 +1289,7 @@ var require_diff_match_patch = __commonJS({
1285
1289
  patch = patches[patches.length - 1];
1286
1290
  diffs = patch.diffs;
1287
1291
  if (diffs.length == 0 || diffs[diffs.length - 1][0] != DIFF_EQUAL) {
1288
- diffs.push(new diff_match_patch2.Diff(DIFF_EQUAL, nullPadding));
1292
+ diffs.push(new diff_match_patch3.Diff(DIFF_EQUAL, nullPadding));
1289
1293
  patch.length1 += paddingLength;
1290
1294
  patch.length2 += paddingLength;
1291
1295
  } else if (paddingLength > diffs[diffs.length - 1][1].length) {
@@ -1296,7 +1300,7 @@ var require_diff_match_patch = __commonJS({
1296
1300
  }
1297
1301
  return nullPadding;
1298
1302
  };
1299
- diff_match_patch2.prototype.patch_splitMax = function(patches) {
1303
+ diff_match_patch3.prototype.patch_splitMax = function(patches) {
1300
1304
  var patch_size = this.Match_MaxBits;
1301
1305
  for (var x = 0; x < patches.length; x++) {
1302
1306
  if (patches[x].length1 <= patch_size) {
@@ -1308,13 +1312,13 @@ var require_diff_match_patch = __commonJS({
1308
1312
  var start2 = bigpatch.start2;
1309
1313
  var precontext = "";
1310
1314
  while (bigpatch.diffs.length !== 0) {
1311
- var patch = new diff_match_patch2.patch_obj();
1315
+ var patch = new diff_match_patch3.patch_obj();
1312
1316
  var empty = true;
1313
1317
  patch.start1 = start1 - precontext.length;
1314
1318
  patch.start2 = start2 - precontext.length;
1315
1319
  if (precontext !== "") {
1316
1320
  patch.length1 = patch.length2 = precontext.length;
1317
- patch.diffs.push(new diff_match_patch2.Diff(DIFF_EQUAL, precontext));
1321
+ patch.diffs.push(new diff_match_patch3.Diff(DIFF_EQUAL, precontext));
1318
1322
  }
1319
1323
  while (bigpatch.diffs.length !== 0 && patch.length1 < patch_size - this.Patch_Margin) {
1320
1324
  var diff_type = bigpatch.diffs[0][0];
@@ -1328,7 +1332,7 @@ var require_diff_match_patch = __commonJS({
1328
1332
  patch.length1 += diff_text.length;
1329
1333
  start1 += diff_text.length;
1330
1334
  empty = false;
1331
- patch.diffs.push(new diff_match_patch2.Diff(diff_type, diff_text));
1335
+ patch.diffs.push(new diff_match_patch3.Diff(diff_type, diff_text));
1332
1336
  bigpatch.diffs.shift();
1333
1337
  } else {
1334
1338
  diff_text = diff_text.substring(
@@ -1343,7 +1347,7 @@ var require_diff_match_patch = __commonJS({
1343
1347
  } else {
1344
1348
  empty = false;
1345
1349
  }
1346
- patch.diffs.push(new diff_match_patch2.Diff(diff_type, diff_text));
1350
+ patch.diffs.push(new diff_match_patch3.Diff(diff_type, diff_text));
1347
1351
  if (diff_text == bigpatch.diffs[0][1]) {
1348
1352
  bigpatch.diffs.shift();
1349
1353
  } else {
@@ -1360,7 +1364,7 @@ var require_diff_match_patch = __commonJS({
1360
1364
  if (patch.diffs.length !== 0 && patch.diffs[patch.diffs.length - 1][0] === DIFF_EQUAL) {
1361
1365
  patch.diffs[patch.diffs.length - 1][1] += postcontext;
1362
1366
  } else {
1363
- patch.diffs.push(new diff_match_patch2.Diff(DIFF_EQUAL, postcontext));
1367
+ patch.diffs.push(new diff_match_patch3.Diff(DIFF_EQUAL, postcontext));
1364
1368
  }
1365
1369
  }
1366
1370
  if (!empty) {
@@ -1369,14 +1373,14 @@ var require_diff_match_patch = __commonJS({
1369
1373
  }
1370
1374
  }
1371
1375
  };
1372
- diff_match_patch2.prototype.patch_toText = function(patches) {
1376
+ diff_match_patch3.prototype.patch_toText = function(patches) {
1373
1377
  var text = [];
1374
1378
  for (var x = 0; x < patches.length; x++) {
1375
1379
  text[x] = patches[x];
1376
1380
  }
1377
1381
  return text.join("");
1378
1382
  };
1379
- diff_match_patch2.prototype.patch_fromText = function(textline) {
1383
+ diff_match_patch3.prototype.patch_fromText = function(textline) {
1380
1384
  var patches = [];
1381
1385
  if (!textline) {
1382
1386
  return patches;
@@ -1389,7 +1393,7 @@ var require_diff_match_patch = __commonJS({
1389
1393
  if (!m) {
1390
1394
  throw new Error("Invalid patch string: " + text[textPointer]);
1391
1395
  }
1392
- var patch = new diff_match_patch2.patch_obj();
1396
+ var patch = new diff_match_patch3.patch_obj();
1393
1397
  patches.push(patch);
1394
1398
  patch.start1 = parseInt(m[1], 10);
1395
1399
  if (m[2] === "") {
@@ -1420,11 +1424,11 @@ var require_diff_match_patch = __commonJS({
1420
1424
  throw new Error("Illegal escape in patch_fromText: " + line);
1421
1425
  }
1422
1426
  if (sign == "-") {
1423
- patch.diffs.push(new diff_match_patch2.Diff(DIFF_DELETE, line));
1427
+ patch.diffs.push(new diff_match_patch3.Diff(DIFF_DELETE, line));
1424
1428
  } else if (sign == "+") {
1425
- patch.diffs.push(new diff_match_patch2.Diff(DIFF_INSERT, line));
1429
+ patch.diffs.push(new diff_match_patch3.Diff(DIFF_INSERT, line));
1426
1430
  } else if (sign == " ") {
1427
- patch.diffs.push(new diff_match_patch2.Diff(DIFF_EQUAL, line));
1431
+ patch.diffs.push(new diff_match_patch3.Diff(DIFF_EQUAL, line));
1428
1432
  } else if (sign == "@") {
1429
1433
  break;
1430
1434
  } else if (sign === "") {
@@ -1436,14 +1440,14 @@ var require_diff_match_patch = __commonJS({
1436
1440
  }
1437
1441
  return patches;
1438
1442
  };
1439
- diff_match_patch2.patch_obj = function() {
1443
+ diff_match_patch3.patch_obj = function() {
1440
1444
  this.diffs = [];
1441
1445
  this.start1 = null;
1442
1446
  this.start2 = null;
1443
1447
  this.length1 = 0;
1444
1448
  this.length2 = 0;
1445
1449
  };
1446
- diff_match_patch2.patch_obj.prototype.toString = function() {
1450
+ diff_match_patch3.patch_obj.prototype.toString = function() {
1447
1451
  var coords1, coords2;
1448
1452
  if (this.length1 === 0) {
1449
1453
  coords1 = this.start1 + ",0";
@@ -1477,48 +1481,14 @@ var require_diff_match_patch = __commonJS({
1477
1481
  }
1478
1482
  return text.join("").replace(/%20/g, " ");
1479
1483
  };
1480
- module.exports = diff_match_patch2;
1481
- module.exports["diff_match_patch"] = diff_match_patch2;
1484
+ module.exports = diff_match_patch3;
1485
+ module.exports["diff_match_patch"] = diff_match_patch3;
1482
1486
  module.exports["DIFF_DELETE"] = DIFF_DELETE;
1483
1487
  module.exports["DIFF_INSERT"] = DIFF_INSERT;
1484
1488
  module.exports["DIFF_EQUAL"] = DIFF_EQUAL;
1485
1489
  }
1486
1490
  });
1487
1491
 
1488
- // adapters/xml-adapter.js
1489
- var _DOMParser = globalThis.DOMParser;
1490
- var _XMLSerializer = globalThis.XMLSerializer;
1491
- function configureXmlProvider(options = {}) {
1492
- if (options.DOMParser) _DOMParser = options.DOMParser;
1493
- if (options.XMLSerializer) _XMLSerializer = options.XMLSerializer;
1494
- }
1495
- function createParser() {
1496
- if (!_DOMParser && globalThis.DOMParser) {
1497
- _DOMParser = globalThis.DOMParser;
1498
- }
1499
- if (!_DOMParser) {
1500
- throw new Error("DOMParser is not configured. Call configureXmlProvider({ DOMParser, XMLSerializer }) first.");
1501
- }
1502
- return new _DOMParser();
1503
- }
1504
- function createSerializer() {
1505
- if (!_XMLSerializer && globalThis.XMLSerializer) {
1506
- _XMLSerializer = globalThis.XMLSerializer;
1507
- }
1508
- if (!_XMLSerializer) {
1509
- throw new Error("XMLSerializer is not configured. Call configureXmlProvider({ DOMParser, XMLSerializer }) first.");
1510
- }
1511
- return new _XMLSerializer();
1512
- }
1513
- function parseXml(xmlString, contentType = "text/xml") {
1514
- const parser = createParser();
1515
- return parser.parseFromString(xmlString, contentType);
1516
- }
1517
- function serializeXml(node) {
1518
- const serializer = createSerializer();
1519
- return serializer.serializeToString(node);
1520
- }
1521
-
1522
1492
  // adapters/logger.js
1523
1493
  var _logger = console;
1524
1494
  var LEVELS = Object.freeze({
@@ -1561,6 +1531,86 @@ function error(...args) {
1561
1531
  }))(...args);
1562
1532
  }
1563
1533
 
1534
+ // adapters/xml-adapter.js
1535
+ var _DOMParser = globalThis.DOMParser;
1536
+ var _XMLSerializer = globalThis.XMLSerializer;
1537
+ function configureXmlProvider(options = {}) {
1538
+ if (options.DOMParser) _DOMParser = options.DOMParser;
1539
+ if (options.XMLSerializer) _XMLSerializer = options.XMLSerializer;
1540
+ }
1541
+ function createParser(options = {}) {
1542
+ if (!_DOMParser && globalThis.DOMParser) {
1543
+ _DOMParser = globalThis.DOMParser;
1544
+ }
1545
+ if (!_DOMParser) {
1546
+ throw new Error("DOMParser is not configured. Call configureXmlProvider({ DOMParser, XMLSerializer }) first.");
1547
+ }
1548
+ return new _DOMParser(options);
1549
+ }
1550
+ function createSerializer() {
1551
+ if (!_XMLSerializer && globalThis.XMLSerializer) {
1552
+ _XMLSerializer = globalThis.XMLSerializer;
1553
+ }
1554
+ if (!_XMLSerializer) {
1555
+ throw new Error("XMLSerializer is not configured. Call configureXmlProvider({ DOMParser, XMLSerializer }) first.");
1556
+ }
1557
+ return new _XMLSerializer();
1558
+ }
1559
+ function parseXml(xmlString, contentType = "text/xml") {
1560
+ const result = parseOoxmlSafe(xmlString, contentType);
1561
+ if (result.error) {
1562
+ const parseError = new Error(result.error.message);
1563
+ parseError.code = result.error.code;
1564
+ throw parseError;
1565
+ }
1566
+ return result.doc;
1567
+ }
1568
+ function browserParseError(doc) {
1569
+ if (!doc?.documentElement) return null;
1570
+ if (String(doc.documentElement.localName || doc.documentElement.nodeName).toLowerCase() === "parsererror") {
1571
+ return doc.documentElement;
1572
+ }
1573
+ return doc.getElementsByTagName?.("parsererror")?.[0] || null;
1574
+ }
1575
+ function parseOoxmlSafe(xmlString, contentType = "application/xml") {
1576
+ const warnings = [];
1577
+ if (typeof xmlString !== "string" || xmlString.trim() === "") {
1578
+ return {
1579
+ doc: null,
1580
+ error: { code: "PARSE_ERROR", message: "Input is not a non-empty XML string." },
1581
+ warnings
1582
+ };
1583
+ }
1584
+ const onError = (level, message) => {
1585
+ const diagnostic = String(message || "XML parser diagnostic.");
1586
+ if (level === "fatalError") {
1587
+ error("[XmlAdapter] XML fatal parse error:", diagnostic);
1588
+ } else {
1589
+ warnings.push(diagnostic);
1590
+ warn(`[XmlAdapter] XML ${level || "warning"}:`, diagnostic);
1591
+ }
1592
+ };
1593
+ try {
1594
+ const parser = createParser({ onError });
1595
+ const doc = parser.parseFromString(xmlString, contentType);
1596
+ const parseError = browserParseError(doc);
1597
+ if (!doc?.documentElement || parseError) {
1598
+ const message = parseError?.textContent || "Could not parse XML input.";
1599
+ error("[XmlAdapter] XML parse error:", message);
1600
+ return { doc: null, error: { code: "PARSE_ERROR", message }, warnings };
1601
+ }
1602
+ return { doc, error: null, warnings };
1603
+ } catch (caught) {
1604
+ const message = caught?.message || String(caught || "Could not parse XML input.");
1605
+ error("[XmlAdapter] XML parse error:", message);
1606
+ return { doc: null, error: { code: "PARSE_ERROR", message }, warnings };
1607
+ }
1608
+ }
1609
+ function serializeXml(node) {
1610
+ const serializer = createSerializer();
1611
+ return serializer.serializeToString(node);
1612
+ }
1613
+
1564
1614
  // adapters/config.js
1565
1615
  var _defaultAuthor = "Author";
1566
1616
  var _platform = "Unknown";
@@ -1782,37 +1832,108 @@ function escapeXml(str) {
1782
1832
  if (!str) return "";
1783
1833
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1784
1834
  }
1785
- var revisionIdCounter = 1e3;
1835
+ var DEFAULT_REVISION_ID_START = 1e3;
1836
+ var MAX_PRACTICAL_REVISION_ID = 2147483647;
1837
+ var REVISION_ID_SAFETY_MARGIN = 1e4;
1838
+ var REVISION_ELEMENT_NAMES = /* @__PURE__ */ new Set([
1839
+ "ins",
1840
+ "del",
1841
+ "moveFrom",
1842
+ "moveTo",
1843
+ "rPrChange",
1844
+ "pPrChange",
1845
+ "cellIns",
1846
+ "cellDel",
1847
+ "comment"
1848
+ ]);
1849
+ var revisionAllocatorByDocument = /* @__PURE__ */ new WeakMap();
1850
+ function isRevisionIdElement(element) {
1851
+ 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;
1854
+ return !element.namespaceURI || element.namespaceURI === NS_W || String(element.nodeName || "").startsWith("w:");
1855
+ }
1856
+ function readWordId(element) {
1857
+ const raw = element?.getAttributeNS?.(NS_W, "id") || element?.getAttribute?.("w:id") || element?.getAttribute?.("id");
1858
+ const parsed = Number.parseInt(String(raw ?? ""), 10);
1859
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : null;
1860
+ }
1861
+ var RevisionIdAllocator = class {
1862
+ constructor(startValue = DEFAULT_REVISION_ID_START) {
1863
+ this.startValue = Number.isInteger(startValue) && startValue >= 0 ? startValue : DEFAULT_REVISION_ID_START;
1864
+ this.nextId = this.startValue;
1865
+ this.occupiedIds = /* @__PURE__ */ new Set();
1866
+ }
1867
+ seed(xmlDoc) {
1868
+ 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);
1877
+ }
1878
+ const highRiskBoundary = MAX_PRACTICAL_REVISION_ID - REVISION_ID_SAFETY_MARGIN;
1879
+ this.nextId = maxFound >= highRiskBoundary ? this.startValue : Math.max(this.nextId, maxFound + 1);
1880
+ this.advanceToAvailableId();
1881
+ return this.nextId;
1882
+ }
1883
+ advanceToAvailableId() {
1884
+ const highRiskBoundary = MAX_PRACTICAL_REVISION_ID - REVISION_ID_SAFETY_MARGIN;
1885
+ if (this.nextId >= highRiskBoundary) this.nextId = this.startValue;
1886
+ while (this.occupiedIds.has(this.nextId)) {
1887
+ this.nextId += 1;
1888
+ if (this.nextId >= highRiskBoundary) this.nextId = this.startValue;
1889
+ }
1890
+ }
1891
+ next() {
1892
+ this.advanceToAvailableId();
1893
+ const id = this.nextId;
1894
+ this.occupiedIds.add(id);
1895
+ this.nextId += 1;
1896
+ return id;
1897
+ }
1898
+ };
1899
+ var defaultRevisionIdAllocator = new RevisionIdAllocator();
1900
+ function setRevisionIdAllocatorForDocument(xmlNode, allocator) {
1901
+ const xmlDoc = xmlNode?.nodeType === 9 ? xmlNode : xmlNode?.ownerDocument;
1902
+ if (xmlDoc && allocator instanceof RevisionIdAllocator) {
1903
+ revisionAllocatorByDocument.set(xmlDoc, allocator);
1904
+ }
1905
+ return allocator;
1906
+ }
1907
+ function getRevisionIdAllocatorForDocument(xmlNode) {
1908
+ const xmlDoc = xmlNode?.nodeType === 9 ? xmlNode : xmlNode?.ownerDocument;
1909
+ return xmlDoc ? revisionAllocatorByDocument.get(xmlDoc) || null : null;
1910
+ }
1911
+ function createRevisionIdAllocator(xmlDoc, startValue = DEFAULT_REVISION_ID_START) {
1912
+ const allocator = new RevisionIdAllocator(startValue);
1913
+ allocator.seed(xmlDoc);
1914
+ setRevisionIdAllocatorForDocument(xmlDoc, allocator);
1915
+ return allocator;
1916
+ }
1786
1917
  function getNextRevisionId() {
1787
- return revisionIdCounter++;
1918
+ return defaultRevisionIdAllocator.next();
1788
1919
  }
1789
1920
  function getRevisionTimestamp(date = /* @__PURE__ */ new Date()) {
1790
1921
  return date.toISOString();
1791
1922
  }
1792
- function createRevisionMetadata(author) {
1923
+ function createRevisionMetadata(author, allocatorOrNode = null) {
1793
1924
  const resolvedAuthor = typeof author === "string" && author.trim() ? author.trim() : getDefaultAuthor();
1925
+ const allocator = allocatorOrNode instanceof RevisionIdAllocator ? allocatorOrNode : getRevisionIdAllocatorForDocument(allocatorOrNode) || defaultRevisionIdAllocator;
1794
1926
  return {
1795
- id: getNextRevisionId(),
1927
+ id: allocator.next(),
1796
1928
  author: resolvedAuthor,
1797
1929
  date: getRevisionTimestamp()
1798
1930
  };
1799
1931
  }
1800
- function seedRevisionIdsFromDocument(xmlDoc) {
1801
- let maxFound = -1;
1802
- const elements = Array.from(xmlDoc?.getElementsByTagName?.("*") || []);
1803
- for (const element of elements) {
1804
- for (const attr of Array.from(element.attributes || [])) {
1805
- if ((attr.localName || "").toLowerCase() !== "id") continue;
1806
- const parsed = Number.parseInt(attr.value, 10);
1807
- if (Number.isFinite(parsed)) {
1808
- maxFound = Math.max(maxFound, parsed);
1809
- }
1810
- }
1811
- }
1812
- if (maxFound >= revisionIdCounter) {
1813
- revisionIdCounter = maxFound + 1;
1814
- }
1815
- return revisionIdCounter;
1932
+ function seedRevisionIdsFromDocument(xmlDoc, allocator = defaultRevisionIdAllocator) {
1933
+ const resolvedAllocator = allocator instanceof RevisionIdAllocator ? allocator : defaultRevisionIdAllocator;
1934
+ const nextId = resolvedAllocator.seed(xmlDoc);
1935
+ setRevisionIdAllocatorForDocument(xmlDoc, resolvedAllocator);
1936
+ return nextId;
1816
1937
  }
1817
1938
 
1818
1939
  // core/paragraph-offset-policy.js
@@ -1890,10 +2011,12 @@ function ingestOoxml(ooxmlString, options = {}) {
1890
2011
  return { runModel, acceptedText, pPr: null };
1891
2012
  }
1892
2013
  try {
1893
- const doc = preParsedDoc || (() => {
1894
- const parser = createParser();
1895
- return parser.parseFromString(ooxmlString, "application/xml");
1896
- })();
2014
+ const parsed = preParsedDoc ? { doc: preParsedDoc, error: null } : parseOoxmlSafe(ooxmlString, "application/xml");
2015
+ const doc = parsed.doc;
2016
+ if (parsed.error || !doc) {
2017
+ error("OOXML parse error:", parsed.error?.message);
2018
+ return { runModel, acceptedText, pPr: null, error: parsed.error };
2019
+ }
1897
2020
  const parseError = getXmlParseError(doc);
1898
2021
  if (parseError) {
1899
2022
  error("OOXML parse error:", parseError.textContent);
@@ -2298,55 +2421,126 @@ function extractTrPr(trNode) {
2298
2421
 
2299
2422
  // pipeline/diff-engine.js
2300
2423
  var import_diff_match_patch = __toESM(require_diff_match_patch(), 1);
2301
- var DMP = new import_diff_match_patch.diff_match_patch();
2302
- function wordsToChars(text1, text2) {
2424
+ var TOKEN_CODE_POINT_BASE = 65536;
2425
+ var MAX_DIFF_TOKENS = 262144;
2426
+ var BMP_FIRST_CODE = 1;
2427
+ var BMP_BEFORE_SURROGATES = 55296 - BMP_FIRST_CODE;
2428
+ var BMP_AFTER_SURROGATES = 65536 - 57344;
2429
+ var DMP_SAFE_TOKEN_LIMIT = BMP_BEFORE_SURROGATES + BMP_AFTER_SURROGATES;
2430
+ var DiffTokenLimitError = class extends Error {
2431
+ constructor(limit = MAX_DIFF_TOKENS) {
2432
+ super(`Word diff exceeds the safe limit of ${limit} unique tokens.`);
2433
+ this.name = "DiffTokenLimitError";
2434
+ this.code = "DIFF_TOKEN_LIMIT";
2435
+ this.limit = limit;
2436
+ }
2437
+ };
2438
+ function isDiffTokenLimitError(error2) {
2439
+ return error2?.code === "DIFF_TOKEN_LIMIT";
2440
+ }
2441
+ function createDiffEngine(options = {}) {
2442
+ const timeout = options.diffTimeoutSeconds ?? 0;
2443
+ if (!Number.isFinite(timeout) || timeout < 0) {
2444
+ throw new TypeError("diffTimeoutSeconds must be a finite non-negative number.");
2445
+ }
2446
+ const engine = new import_diff_match_patch.diff_match_patch();
2447
+ engine.Diff_Timeout = timeout;
2448
+ return engine;
2449
+ }
2450
+ function tokenize(text) {
2451
+ const tokens = [];
2452
+ const leading = text.match(/^\s+/);
2453
+ if (leading) tokens.push(leading[0]);
2454
+ const regex = /(\S+)(\s*)/g;
2455
+ regex.lastIndex = leading?.[0].length || 0;
2456
+ let match;
2457
+ while ((match = regex.exec(text)) !== null) {
2458
+ if (match[1]) tokens.push(match[1]);
2459
+ if (match[2]) tokens.push(match[2]);
2460
+ }
2461
+ return tokens;
2462
+ }
2463
+ function wordsToChars(text1, text2, options = {}) {
2303
2464
  const wordArray = [];
2304
2465
  const wordHash = /* @__PURE__ */ new Map();
2305
- function tokenize(text) {
2306
- const tokens = [];
2307
- const regex = /(\S+)(\s*)/g;
2308
- let match;
2309
- while ((match = regex.exec(text)) !== null) {
2310
- if (match[1]) tokens.push(match[1]);
2311
- if (match[2]) tokens.push(match[2]);
2312
- }
2313
- return tokens;
2466
+ const maxTokens = options.maxTokens ?? MAX_DIFF_TOKENS;
2467
+ if (!Number.isInteger(maxTokens) || maxTokens < 1 || maxTokens > MAX_DIFF_TOKENS) {
2468
+ throw new RangeError(`maxTokens must be an integer from 1 to ${MAX_DIFF_TOKENS}.`);
2314
2469
  }
2315
2470
  function mapTokensToChars(tokens) {
2316
2471
  let chars = "";
2472
+ const tokenIds = [];
2317
2473
  for (const token of tokens) {
2318
- if (wordHash.has(token)) {
2319
- chars += String.fromCharCode(wordHash.get(token));
2320
- } else {
2321
- const charCode = wordArray.length;
2474
+ let tokenId = wordHash.get(token);
2475
+ if (tokenId === void 0) {
2476
+ if (wordArray.length >= maxTokens) throw new DiffTokenLimitError(maxTokens);
2477
+ tokenId = wordArray.length;
2322
2478
  wordArray.push(token);
2323
- wordHash.set(token, charCode);
2324
- chars += String.fromCharCode(charCode);
2479
+ wordHash.set(token, tokenId);
2325
2480
  }
2481
+ tokenIds.push(tokenId);
2482
+ chars += String.fromCodePoint(TOKEN_CODE_POINT_BASE + tokenId);
2326
2483
  }
2327
- return chars;
2484
+ return { chars, tokenIds };
2328
2485
  }
2329
2486
  const tokens1 = tokenize(text1);
2330
2487
  const tokens2 = tokenize(text2);
2488
+ const encoded1 = mapTokensToChars(tokens1);
2489
+ const encoded2 = mapTokensToChars(tokens2);
2331
2490
  return {
2332
- chars1: mapTokensToChars(tokens1),
2333
- chars2: mapTokensToChars(tokens2),
2334
- wordArray
2491
+ chars1: encoded1.chars,
2492
+ chars2: encoded2.chars,
2493
+ wordArray,
2494
+ tokenIds1: encoded1.tokenIds,
2495
+ tokenIds2: encoded2.tokenIds
2335
2496
  };
2336
2497
  }
2337
- function charsToWords(diffs, wordArray) {
2338
- const wordDiffs = [];
2339
- for (const [op, chars] of diffs) {
2498
+ function tokenIdToBmpChar(tokenId) {
2499
+ const code = tokenId < BMP_BEFORE_SURROGATES ? BMP_FIRST_CODE + tokenId : 57344 + (tokenId - BMP_BEFORE_SURROGATES);
2500
+ return String.fromCharCode(code);
2501
+ }
2502
+ function bmpCharToTokenId(charCode) {
2503
+ if (charCode >= BMP_FIRST_CODE && charCode < 55296) return charCode - BMP_FIRST_CODE;
2504
+ if (charCode >= 57344 && charCode <= 65535) return BMP_BEFORE_SURROGATES + charCode - 57344;
2505
+ throw new RangeError(`BMP diff token U+${charCode.toString(16).toUpperCase()} has no mapping.`);
2506
+ }
2507
+ function encodeTokenIdsForDmp(tokenIds) {
2508
+ let chars = "";
2509
+ for (const tokenId of tokenIds) chars += tokenIdToBmpChar(tokenId);
2510
+ return chars;
2511
+ }
2512
+ function decodeBmpDiffs(diffs, wordArray) {
2513
+ return diffs.map(([op, chars]) => {
2340
2514
  const parts = [];
2341
- for (let i = 0; i < chars.length; i++) {
2342
- const charCode = chars.charCodeAt(i);
2343
- if (charCode < wordArray.length) {
2344
- parts.push(wordArray[charCode]);
2515
+ for (let index = 0; index < chars.length; index++) {
2516
+ const tokenId = bmpCharToTokenId(chars.charCodeAt(index));
2517
+ if (tokenId >= wordArray.length) {
2518
+ throw new RangeError(`BMP diff token ${tokenId} has no mapping.`);
2345
2519
  }
2520
+ parts.push(wordArray[tokenId]);
2346
2521
  }
2347
- wordDiffs.push([op, parts.join("")]);
2348
- }
2349
- return wordDiffs;
2522
+ return [op, parts.join("")];
2523
+ });
2524
+ }
2525
+ function deterministicLargeTokenDiff(tokenIds1, tokenIds2, wordArray) {
2526
+ let prefixLength = 0;
2527
+ const sharedLength = Math.min(tokenIds1.length, tokenIds2.length);
2528
+ while (prefixLength < sharedLength && tokenIds1[prefixLength] === tokenIds2[prefixLength]) {
2529
+ prefixLength++;
2530
+ }
2531
+ let suffixLength = 0;
2532
+ while (suffixLength < sharedLength - prefixLength && tokenIds1[tokenIds1.length - 1 - suffixLength] === tokenIds2[tokenIds2.length - 1 - suffixLength]) {
2533
+ suffixLength++;
2534
+ }
2535
+ const joinTokens = (ids) => ids.map((id) => wordArray[id]).join("");
2536
+ const diffs = [];
2537
+ if (prefixLength) diffs.push([0, joinTokens(tokenIds1.slice(0, prefixLength))]);
2538
+ const deleted = tokenIds1.slice(prefixLength, tokenIds1.length - suffixLength);
2539
+ const inserted = tokenIds2.slice(prefixLength, tokenIds2.length - suffixLength);
2540
+ if (deleted.length) diffs.push([-1, joinTokens(deleted)]);
2541
+ if (inserted.length) diffs.push([1, joinTokens(inserted)]);
2542
+ if (suffixLength) diffs.push([0, joinTokens(tokenIds1.slice(tokenIds1.length - suffixLength))]);
2543
+ return diffs;
2350
2544
  }
2351
2545
  function computeWordDiffs(originalText, newText, options = {}) {
2352
2546
  if (originalText === newText) {
@@ -2359,12 +2553,16 @@ function computeWordDiffs(originalText, newText, options = {}) {
2359
2553
  return [[-1, originalText]];
2360
2554
  }
2361
2555
  const { cleanupSemantic = true } = options;
2362
- const { chars1, chars2, wordArray } = wordsToChars(originalText, newText);
2363
- const charDiffs = DMP.diff_main(chars1, chars2);
2556
+ const { wordArray, tokenIds1, tokenIds2 } = wordsToChars(originalText, newText, options);
2557
+ if (wordArray.length > DMP_SAFE_TOKEN_LIMIT) {
2558
+ return deterministicLargeTokenDiff(tokenIds1, tokenIds2, wordArray);
2559
+ }
2560
+ const dmp = createDiffEngine(options);
2561
+ const charDiffs = dmp.diff_main(encodeTokenIdsForDmp(tokenIds1), encodeTokenIdsForDmp(tokenIds2));
2364
2562
  if (cleanupSemantic) {
2365
- DMP.diff_cleanupSemantic(charDiffs);
2563
+ dmp.diff_cleanupSemantic(charDiffs);
2366
2564
  }
2367
- return charsToWords(charDiffs, wordArray);
2565
+ return decodeBmpDiffs(charDiffs, wordArray);
2368
2566
  }
2369
2567
  function computeWordLevelDiffOps(originalText, newText, options = {}) {
2370
2568
  if (originalText === newText) {
@@ -2555,7 +2753,6 @@ function applyPatches(splitModel, diffOps, options) {
2555
2753
  function processInsertionOperation(context) {
2556
2754
  const {
2557
2755
  insertOp,
2558
- splitModel,
2559
2756
  styleLookup,
2560
2757
  patchedModel,
2561
2758
  state,
@@ -2820,7 +3017,7 @@ function buildParagraphOnlyPackage(paragraphXml) {
2820
3017
  var XMLNS_ATTR_REGEX2 = /\s+xmlns:[^=]+="[^"]*"/g;
2821
3018
  function serializeToOoxml(patchedModel, pPr, formatHints = [], options = {}) {
2822
3019
  const serializationOptions = normalizeSerializationOptions(options);
2823
- const { author, generateRedlines } = serializationOptions;
3020
+ const { generateRedlines } = serializationOptions;
2824
3021
  const paragraphs = [];
2825
3022
  let currentPPrXml = "";
2826
3023
  let currentPPrElement = null;
@@ -2908,7 +3105,8 @@ function normalizeSerializationOptions(options) {
2908
3105
  return {
2909
3106
  author: getDefaultAuthor(),
2910
3107
  generateRedlines: true,
2911
- font: options
3108
+ font: options,
3109
+ revisionIdAllocator: null
2912
3110
  };
2913
3111
  }
2914
3112
  const normalized = options && typeof options === "object" ? options : {};
@@ -2916,7 +3114,8 @@ function normalizeSerializationOptions(options) {
2916
3114
  return {
2917
3115
  author: resolvedAuthor,
2918
3116
  generateRedlines: normalized.generateRedlines ?? true,
2919
- font: normalized.font ?? null
3117
+ font: normalized.font ?? null,
3118
+ revisionIdAllocator: normalized.revisionIdAllocator ?? null
2920
3119
  };
2921
3120
  }
2922
3121
  function buildRunXmlWithHints(item, formatHints, options = {}) {
@@ -2954,7 +3153,10 @@ function buildSimpleRun(text, rPrXml) {
2954
3153
  return `<w:r>${rPr}<w:t xml:space="preserve">${escapeXml(text)}</w:t></w:r>`;
2955
3154
  }
2956
3155
  function buildDeletionXml(item, options = {}) {
2957
- const metadata = createRevisionMetadata(options.author ?? getDefaultAuthor());
3156
+ const metadata = createRevisionMetadata(
3157
+ options.author ?? getDefaultAuthor(),
3158
+ options.revisionIdAllocator
3159
+ );
2958
3160
  const font = options.font ?? null;
2959
3161
  let rPr = item.rPrXml ? stripNamespaceDeclarations(item.rPrXml) : "";
2960
3162
  if (font) {
@@ -2963,7 +3165,10 @@ function buildDeletionXml(item, options = {}) {
2963
3165
  return `<w:del w:id="${metadata.id}" w:author="${escapeXml(metadata.author)}" w:date="${metadata.date}"><w:r>${rPr}<w:delText xml:space="preserve">${escapeXml(item.text)}</w:delText></w:r></w:del>`;
2964
3166
  }
2965
3167
  function buildInsertionXml(item, formatHints, options = {}) {
2966
- const metadata = createRevisionMetadata(options.author ?? getDefaultAuthor());
3168
+ const metadata = createRevisionMetadata(
3169
+ options.author ?? getDefaultAuthor(),
3170
+ options.revisionIdAllocator
3171
+ );
2967
3172
  const font = options.font ?? null;
2968
3173
  const applicableHints = getApplicableFormatHints(formatHints, item.startOffset, item.endOffset);
2969
3174
  let innerContent = "";
@@ -3283,8 +3488,8 @@ var NumberingService = class {
3283
3488
 
3284
3489
  // services/table-reconciliation.js
3285
3490
  function generateTableOoxml(tableData, options = {}) {
3286
- const { generateRedlines = false, author = "AI" } = options;
3287
- const tableInsertMeta = generateRedlines ? createRevisionMetadata(author) : null;
3491
+ const { generateRedlines = false, author = "AI", revisionIdAllocator = null } = options;
3492
+ const tableInsertMeta = generateRedlines ? createRevisionMetadata(author, revisionIdAllocator) : null;
3288
3493
  const numCols = tableData.headers?.length || (tableData.rows?.[0]?.length || 1);
3289
3494
  const tblPr = `
3290
3495
  <w:tblPr>
@@ -3319,7 +3524,11 @@ function generateTableOoxml(tableData, options = {}) {
3319
3524
  startOffset: 0,
3320
3525
  endOffset: cleanText.length
3321
3526
  }];
3322
- const runsOoxml = serializeToOoxml(runModel, null, formatHints, { author, generateRedlines });
3527
+ const runsOoxml = serializeToOoxml(runModel, null, formatHints, {
3528
+ author,
3529
+ generateRedlines,
3530
+ revisionIdAllocator
3531
+ });
3323
3532
  const tcPr = '<w:tcPr><w:tcW w:w="0" w:type="auto"/></w:tcPr>';
3324
3533
  cellsXml += `<w:tc>${tcPr}${runsOoxml}</w:tc>`;
3325
3534
  }
@@ -3377,7 +3586,7 @@ function diffTablesWithVirtualGrid(oldGrid, newTableData) {
3377
3586
  return operations;
3378
3587
  }
3379
3588
  function serializeVirtualGridToOoxml(grid, operations, options) {
3380
- const { generateRedlines, author } = options;
3589
+ const { generateRedlines, author, revisionIdAllocator = null } = options;
3381
3590
  const opIndex = buildTableOperationIndex(operations);
3382
3591
  let rowsXml = "";
3383
3592
  for (let row = 0; row < grid.rowCount; row++) {
@@ -3407,7 +3616,7 @@ function serializeVirtualGridToOoxml(grid, operations, options) {
3407
3616
  }
3408
3617
  let trPr = grid.trPrList[row] || "<w:trPr/>";
3409
3618
  if (rowDeleteOp && generateRedlines) {
3410
- const metadata = createRevisionMetadata(author);
3619
+ const metadata = createRevisionMetadata(author, revisionIdAllocator);
3411
3620
  const delMark = `<w:del w:id="${metadata.id}" w:author="${escapeXml(metadata.author)}" w:date="${metadata.date}"/>`;
3412
3621
  if (trPr.includes("</w:trPr>")) {
3413
3622
  trPr = trPr.replace("</w:trPr>", `${delMark}</w:trPr>`);
@@ -3420,7 +3629,7 @@ function serializeVirtualGridToOoxml(grid, operations, options) {
3420
3629
  const insertOps = opIndex.rowInsertOperations;
3421
3630
  for (const op of insertOps) {
3422
3631
  let cellsXml = "";
3423
- const rowInsertMeta = generateRedlines ? createRevisionMetadata(author) : null;
3632
+ const rowInsertMeta = generateRedlines ? createRevisionMetadata(author, revisionIdAllocator) : null;
3424
3633
  for (const cellText of op.cells) {
3425
3634
  const { cleanText, formatHints } = preprocessMarkdown(cellText);
3426
3635
  const runModel = [{
@@ -3431,7 +3640,11 @@ function serializeVirtualGridToOoxml(grid, operations, options) {
3431
3640
  startOffset: 0,
3432
3641
  endOffset: cleanText.length
3433
3642
  }];
3434
- const runsOoxml = serializeToOoxml(runModel, null, formatHints, { author, generateRedlines });
3643
+ const runsOoxml = serializeToOoxml(runModel, null, formatHints, {
3644
+ author,
3645
+ generateRedlines,
3646
+ revisionIdAllocator
3647
+ });
3435
3648
  cellsXml += `<w:tc><w:tcPr><w:tcW w:w="0" w:type="auto"/></w:tcPr>${runsOoxml}</w:tc>`;
3436
3649
  }
3437
3650
  let trPr = "<w:trPr/>";
@@ -3469,7 +3682,7 @@ function buildTableOperationIndex(operations) {
3469
3682
  };
3470
3683
  }
3471
3684
  function reconcileCellContent(cell, newText, options) {
3472
- const { generateRedlines, author } = options;
3685
+ const { generateRedlines, author, revisionIdAllocator = null } = options;
3473
3686
  const { cleanText, formatHints } = preprocessMarkdown(newText);
3474
3687
  const oldText = cell.getText();
3475
3688
  const diffOps = computeWordLevelDiffOps(oldText, cleanText);
@@ -3480,7 +3693,11 @@ function reconcileCellContent(cell, newText, options) {
3480
3693
  author,
3481
3694
  formatHints
3482
3695
  });
3483
- const runsOoxml = serializeToOoxml(patchedModel, baseBlock.pPr, formatHints, { author, generateRedlines });
3696
+ const runsOoxml = serializeToOoxml(patchedModel, baseBlock.pPr, formatHints, {
3697
+ author,
3698
+ generateRedlines,
3699
+ revisionIdAllocator
3700
+ });
3484
3701
  return runsOoxml;
3485
3702
  }
3486
3703
  function serializeCellBlocks(blocks) {
@@ -3489,7 +3706,7 @@ function serializeCellBlocks(blocks) {
3489
3706
  return runsOoxml;
3490
3707
  }).join("");
3491
3708
  }
3492
- function buildTcXml(cell, content, options) {
3709
+ function buildTcXml(cell, content, _options) {
3493
3710
  let tcPr = cell.tcPrXml;
3494
3711
  if (cell.colSpan > 1 && !tcPr.includes("gridSpan")) {
3495
3712
  tcPr = tcPr.replace("</w:tcPr>", `<w:gridSpan w:val="${cell.colSpan}"/></w:tcPr>`);
@@ -3543,6 +3760,7 @@ async function executeListGeneration(options) {
3543
3760
  generateRedlines = true,
3544
3761
  author = "AI",
3545
3762
  font = null,
3763
+ revisionIdAllocator = null,
3546
3764
  numberingService
3547
3765
  } = options;
3548
3766
  const normalizedListText = normalizeCompositeListMarkers(cleanText);
@@ -3578,10 +3796,15 @@ async function executeListGeneration(options) {
3578
3796
  results.push(serializeToOoxml(deletionRuns, null, [], {
3579
3797
  author,
3580
3798
  generateRedlines,
3581
- font
3799
+ font,
3800
+ revisionIdAllocator
3582
3801
  }));
3583
3802
  }
3584
- results.push(generateTableOoxml(tableData, { generateRedlines, author }));
3803
+ results.push(generateTableOoxml(tableData, {
3804
+ generateRedlines,
3805
+ author,
3806
+ revisionIdAllocator
3807
+ }));
3585
3808
  i = tableBlock.endIndex;
3586
3809
  continue;
3587
3810
  }
@@ -3596,6 +3819,7 @@ async function executeListGeneration(options) {
3596
3819
  generateRedlines,
3597
3820
  author,
3598
3821
  font,
3822
+ revisionIdAllocator,
3599
3823
  deletionRuns
3600
3824
  );
3601
3825
  results.push(entry.ooxml);
@@ -3686,7 +3910,7 @@ function collectMarkdownTableBlock(lineMetadata, index) {
3686
3910
  endIndex: cursor - 1
3687
3911
  };
3688
3912
  }
3689
- function buildListEntry(line, lineIndex, indentStep, numberingContext, numberingService, generateRedlines, author, font, deletionRuns) {
3913
+ function buildListEntry(line, lineIndex, indentStep, numberingContext, numberingService, generateRedlines, author, font, revisionIdAllocator, deletionRuns) {
3690
3914
  let pPrXml = "";
3691
3915
  let segmentText = "";
3692
3916
  if (line.headerMatch) {
@@ -3723,7 +3947,8 @@ function buildListEntry(line, lineIndex, indentStep, numberingContext, numbering
3723
3947
  ooxml: serializeToOoxml(runModel, pPrXml, formatHints, {
3724
3948
  author,
3725
3949
  generateRedlines,
3726
- font
3950
+ font,
3951
+ revisionIdAllocator
3727
3952
  })
3728
3953
  };
3729
3954
  }
@@ -3754,6 +3979,7 @@ var ReconciliationPipeline = class {
3754
3979
  this.validationMode = options.validationMode ?? "auto";
3755
3980
  this.numberingService = options.numberingService || new NumberingService();
3756
3981
  this.font = options.font || null;
3982
+ this.revisionIdAllocator = options.revisionIdAllocator || null;
3757
3983
  this.platform = options.platform ?? getPlatform();
3758
3984
  this.isWebPlatform = options.isWebPlatform ?? isWebPlatform(this.platform);
3759
3985
  this.enableEventLoopYielding = options.enableEventLoopYielding ?? this.isWebPlatform;
@@ -3772,10 +3998,18 @@ var ReconciliationPipeline = class {
3772
3998
  async execute(originalOoxml, newText, options = {}) {
3773
3999
  const warnings = [];
3774
4000
  try {
3775
- const doc = options.xmlDoc || (() => {
3776
- const parser = createParser();
3777
- return parser.parseFromString(originalOoxml, "application/xml");
3778
- })();
4001
+ const parsed = options.xmlDoc ? { doc: options.xmlDoc, error: null, warnings: [] } : parseOoxmlSafe(originalOoxml, "application/xml");
4002
+ if (parsed.error || !parsed.doc) {
4003
+ return {
4004
+ ooxml: originalOoxml,
4005
+ isValid: false,
4006
+ status: "error",
4007
+ error: parsed.error,
4008
+ warnings: parsed.warnings || []
4009
+ };
4010
+ }
4011
+ warnings.push(...parsed.warnings || []);
4012
+ const doc = parsed.doc;
3779
4013
  const pElement = getFirstElementByTagNS(doc, "*", "p");
3780
4014
  const { runModel, acceptedText, pPr } = ingestOoxml(originalOoxml, { xmlDoc: doc });
3781
4015
  const numberingContext = pElement ? detectNumberingContext(pElement) : null;
@@ -3807,8 +4041,12 @@ var ReconciliationPipeline = class {
3807
4041
  }
3808
4042
  await this.maybeYield(runModel.length, Math.max(acceptedText.length, cleanText.length));
3809
4043
  const paragraphCount = runModel.filter((r) => r.kind === RunKind.PARAGRAPH_START).length;
4044
+ const sourceIsMarkedMultilineList = isListTargetStrict(acceptedText) || isListTargetLoose(acceptedText);
4045
+ const canPatchExistingMarkedList = isTargetList && sourceIsMarkedMultilineList && paragraphCount > 1 && acceptedText !== cleanText;
3810
4046
  log(`[Reconcile] isTargetList: ${isTargetList}, paragraphCount: ${paragraphCount}`);
3811
- if (isTargetList) {
4047
+ if (canPatchExistingMarkedList) {
4048
+ log("[Reconcile] Existing marked list edit detected; using run-aware patching to preserve formatting and paragraph boundaries.");
4049
+ } else if (isTargetList) {
3812
4050
  log("[Reconcile] \u{1F3AF} ENTERING LIST GENERATION PATH");
3813
4051
  log(`[Reconcile] cleanText preview: ${cleanText.substring(0, 100)}...`);
3814
4052
  log(`[Reconcile] acceptedText preview: ${acceptedText.substring(0, 100)}...`);
@@ -3827,7 +4065,8 @@ var ReconciliationPipeline = class {
3827
4065
  await this.maybeYield(patchedModel.length, Math.max(acceptedText.length, cleanText.length));
3828
4066
  const resultOoxml = serializeToOoxml(patchedModel, pPr, formatHints, {
3829
4067
  author: this.author,
3830
- generateRedlines: this.generateRedlines
4068
+ generateRedlines: this.generateRedlines,
4069
+ revisionIdAllocator: this.revisionIdAllocator
3831
4070
  });
3832
4071
  if (this.shouldRunValidation()) {
3833
4072
  const validation = this.validateBasic(resultOoxml);
@@ -3845,7 +4084,8 @@ var ReconciliationPipeline = class {
3845
4084
  return {
3846
4085
  ooxml: originalOoxml,
3847
4086
  isValid: false,
3848
- warnings: [`Pipeline error: ${error2.message}`]
4087
+ warnings: [`Pipeline error: ${error2.message}`],
4088
+ error: error2?.code ? { code: error2.code, message: error2.message } : void 0
3849
4089
  };
3850
4090
  }
3851
4091
  }
@@ -3859,8 +4099,12 @@ var ReconciliationPipeline = class {
3859
4099
  const errors = [];
3860
4100
  try {
3861
4101
  const wrappedXml = `<root xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">${ooxml}</root>`;
3862
- const parser = createParser();
3863
- const doc = parser.parseFromString(wrappedXml, "application/xml");
4102
+ const parsed = parseOoxmlSafe(wrappedXml, "application/xml");
4103
+ const doc = parsed.doc;
4104
+ if (parsed.error || !doc) {
4105
+ errors.push("Generated OOXML is not well-formed XML: " + (parsed.error?.message || "parse error"));
4106
+ return { isValid: false, errors };
4107
+ }
3864
4108
  const parseError = getXmlParseError(doc);
3865
4109
  if (parseError) {
3866
4110
  errors.push("Generated OOXML is not well-formed XML: " + parseError.textContent.substring(0, 100));
@@ -3931,6 +4175,7 @@ var ReconciliationPipeline = class {
3931
4175
  generateRedlines: this.generateRedlines,
3932
4176
  author: this.author,
3933
4177
  font: this.font,
4178
+ revisionIdAllocator: this.revisionIdAllocator,
3934
4179
  numberingService: this.numberingService
3935
4180
  });
3936
4181
  }
@@ -3960,7 +4205,8 @@ var ReconciliationPipeline = class {
3960
4205
  }
3961
4206
  const tableOoxml = generateTableOoxml(tableData, {
3962
4207
  generateRedlines: this.generateRedlines,
3963
- author: this.author
4208
+ author: this.author,
4209
+ revisionIdAllocator: this.revisionIdAllocator
3964
4210
  });
3965
4211
  return {
3966
4212
  ooxml: tableOoxml,
@@ -4336,7 +4582,7 @@ function extractFormattingFromOoxml(xmlDoc) {
4336
4582
  // engine/run-builders.js
4337
4583
  function createTrackChange(xmlDoc, type, run, author) {
4338
4584
  const wrapper = createWordElement(xmlDoc, type === "ins" ? "w:ins" : "w:del");
4339
- const metadata = createRevisionMetadata(author);
4585
+ const metadata = createRevisionMetadata(author, xmlDoc);
4340
4586
  wrapper.setAttribute("w:id", String(metadata.id));
4341
4587
  wrapper.setAttribute("w:author", metadata.author);
4342
4588
  wrapper.setAttribute("w:date", metadata.date);
@@ -4378,7 +4624,7 @@ function markParagraphMark(xmlDoc, paragraph, author, type) {
4378
4624
  }
4379
4625
  }
4380
4626
  const marker = createWordElement(xmlDoc, type === "ins" ? "w:ins" : "w:del");
4381
- const metadata = createRevisionMetadata(author);
4627
+ const metadata = createRevisionMetadata(author, xmlDoc);
4382
4628
  marker.setAttribute("w:id", String(metadata.id));
4383
4629
  marker.setAttribute("w:author", metadata.author);
4384
4630
  marker.setAttribute("w:date", metadata.date);
@@ -4425,11 +4671,11 @@ function createFormattedRuns(xmlDoc, text, baseRPr, formatHints, baseOffset, aut
4425
4671
  const applicableHints = formatHints.filter(
4426
4672
  (h) => h.start <= segmentBaseOffset && h.end >= segmentEndOffset
4427
4673
  );
4428
- const combinedFormat = {};
4674
+ const combinedFormat = { ...extractFormatFromRPr(baseRPr) };
4429
4675
  applicableHints.forEach((h) => {
4430
4676
  if (h.format) Object.assign(combinedFormat, h.format);
4431
4677
  });
4432
- const formattedRPr = injectFormattingToRPr(xmlDoc, baseRPr, combinedFormat, author, generateRedlines);
4678
+ const formattedRPr = applicableHints.length > 0 ? injectFormattingToRPr(xmlDoc, baseRPr, combinedFormat, author, generateRedlines) : baseRPr?.cloneNode(true) || null;
4433
4679
  runs.push(createTextRunWithRPrElement(xmlDoc, segment, formattedRPr, false));
4434
4680
  }
4435
4681
  return runs;
@@ -4517,7 +4763,7 @@ function injectFormattingToRPr(xmlDoc, baseRPr, format, author, generateRedlines
4517
4763
  }
4518
4764
  function snapshotAndAttachRPrChange(xmlDoc, rPr, author, dateStr, sourceNode) {
4519
4765
  const rPrChange = createWordElement(xmlDoc, "w:rPrChange");
4520
- const metadata = createRevisionMetadata(author);
4766
+ const metadata = createRevisionMetadata(author, xmlDoc);
4521
4767
  rPrChange.setAttribute("w:id", String(metadata.id));
4522
4768
  rPrChange.setAttribute("w:author", metadata.author);
4523
4769
  rPrChange.setAttribute("w:date", dateStr || metadata.date);
@@ -4549,6 +4795,7 @@ function isWordElement3(node, localName) {
4549
4795
  return nodeName === `w:${localName}` || nodeName === localName;
4550
4796
  }
4551
4797
  function buildParagraphInfos(xmlDoc, paragraphs, textSpans) {
4798
+ void xmlDoc;
4552
4799
  const spansByParagraph = /* @__PURE__ */ new Map();
4553
4800
  for (const span of textSpans) {
4554
4801
  if (!span || !span.paragraph) continue;
@@ -4753,6 +5000,7 @@ function normalizePrecomputedFormatContext(precomputedContext) {
4753
5000
  };
4754
5001
  }
4755
5002
  function applyFormatRemovalAsSurgicalReplacement(xmlDoc, textSpans, existingFormatHints, serializer, author, generateRedlines = true) {
5003
+ void textSpans;
4756
5004
  let hasAnyChanges = false;
4757
5005
  const processedRuns = /* @__PURE__ */ new Set();
4758
5006
  log(`[OxmlEngine] Surgical format removal: ${existingFormatHints.length} hints to process (using w:rPrChange)`);
@@ -5208,26 +5456,11 @@ function cloneRunPiece(xmlDoc, sourceNode, text, asDeletedText) {
5208
5456
 
5209
5457
  // engine/surgical-diff-application.js
5210
5458
  function reconcileFormattingForTextSpan(xmlDoc, span, start, end, applicableHints, author, generateRedlines) {
5211
- const desiredFormat = {};
5212
- if (applicableHints.length > 0) {
5213
- applicableHints.forEach((h) => Object.assign(desiredFormat, h.format));
5214
- }
5459
+ if (applicableHints.length === 0) return false;
5215
5460
  const rPr = span.rPr;
5216
- const hasElement = (localName) => {
5217
- if (!rPr) return false;
5218
- for (let node = rPr.firstChild; node; node = node.nextSibling) {
5219
- if (isWordElement(node, localName)) {
5220
- return true;
5221
- }
5222
- }
5223
- return false;
5224
- };
5225
- const existingFormat = {
5226
- bold: hasElement("b"),
5227
- italic: hasElement("i"),
5228
- underline: hasElement("u"),
5229
- strikethrough: hasElement("strike")
5230
- };
5461
+ const existingFormat = extractFormatFromRPr(rPr);
5462
+ const desiredFormat = { ...existingFormat };
5463
+ applicableHints.forEach((h) => Object.assign(desiredFormat, h.format));
5231
5464
  const formatsToCheck = ["bold", "italic", "underline", "strikethrough"];
5232
5465
  const changesNeeded = formatsToCheck.some((f) => !!desiredFormat[f] !== existingFormat[f]);
5233
5466
  if (!changesNeeded) return false;
@@ -5360,10 +5593,11 @@ function insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, fo
5360
5593
  }
5361
5594
 
5362
5595
  // engine/surgical-mode.js
5363
- function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true, targetParagraph = null) {
5596
+ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true, targetParagraph = null, diffOptions = {}) {
5597
+ void originalText;
5364
5598
  const allParagraphs = targetParagraph ? [targetParagraph] : getDocumentParagraphs(xmlDoc);
5365
5599
  const { fullText, textSpans } = buildSurgicalTextSpans(allParagraphs);
5366
- const diffs = computeWordDiffs(fullText, modifiedText);
5600
+ const diffs = computeWordDiffs(fullText, modifiedText, diffOptions);
5367
5601
  const spanIndex = buildSpanIndex(textSpans);
5368
5602
  let originalPos = 0;
5369
5603
  let newPos = 0;
@@ -5406,10 +5640,28 @@ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, autho
5406
5640
  }
5407
5641
 
5408
5642
  // engine/reconstruction-mapper.js
5643
+ var import_diff_match_patch2 = __toESM(require_diff_match_patch(), 1);
5644
+ var DMP = new import_diff_match_patch2.diff_match_patch();
5645
+ function localNameOf(node) {
5646
+ return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
5647
+ }
5648
+ function wordAttribute(node, localName) {
5649
+ return node?.getAttributeNS?.(NS_W, localName) || node?.getAttribute?.(`w:${localName}`) || node?.getAttribute?.(localName) || "";
5650
+ }
5409
5651
  function createRangeCursorLookup2(ranges) {
5410
5652
  let cursor = 0;
5411
5653
  return {
5412
5654
  at(index) {
5655
+ if (cursor > 0 && (!ranges[cursor] || index < ranges[cursor].start)) {
5656
+ let low = 0;
5657
+ let high = cursor - 1;
5658
+ while (low <= high) {
5659
+ const middle = Math.floor((low + high) / 2);
5660
+ if (ranges[middle].end <= index) low = middle + 1;
5661
+ else high = middle - 1;
5662
+ }
5663
+ cursor = low;
5664
+ }
5413
5665
  while (cursor < ranges.length && ranges[cursor].end <= index) {
5414
5666
  cursor++;
5415
5667
  }
@@ -5430,11 +5682,11 @@ function indexSentinelsByStart(sentinelMap) {
5430
5682
  });
5431
5683
  return sentinelMapByStart;
5432
5684
  }
5433
- function buildReconstructionMapping(xmlDoc, modifiedText) {
5685
+ function buildReconstructionMapping(xmlDoc, modifiedText, selectedParagraphs = null) {
5434
5686
  const rootElement = xmlDoc.documentElement;
5435
- const isBodyRoot = rootElement.nodeName === "w:body" || rootElement.nodeName.endsWith(":package");
5436
- const paragraphs = getDocumentParagraphs(xmlDoc);
5437
- let body = getFirstElementByTag(xmlDoc, "w:body");
5687
+ const isBodyRoot = isWordElement(rootElement, "body") || localNameOf(rootElement) === "package";
5688
+ const paragraphs = selectedParagraphs || getDocumentParagraphs(xmlDoc);
5689
+ let body = getFirstElementByTagNSOrTag(xmlDoc, NS_W, "body");
5438
5690
  if (!body && isBodyRoot) body = rootElement;
5439
5691
  let originalFullText = "";
5440
5692
  const propertyMap = [];
@@ -5442,7 +5694,8 @@ function buildReconstructionMapping(xmlDoc, modifiedText) {
5442
5694
  const sentinelMap = [];
5443
5695
  const referenceMap = /* @__PURE__ */ new Map();
5444
5696
  const tokenToCharMap = /* @__PURE__ */ new Map();
5445
- let nextCharCode = 57344;
5697
+ const breakChars = /* @__PURE__ */ new Set();
5698
+ const characterState = { nextCharCode: 57344 };
5446
5699
  const uniqueContainers = /* @__PURE__ */ new Set();
5447
5700
  paragraphs.forEach((paragraph, paragraphIndex) => {
5448
5701
  const paragraphStart = originalFullText.length;
@@ -5454,15 +5707,13 @@ function buildReconstructionMapping(xmlDoc, modifiedText) {
5454
5707
  sentinelMap,
5455
5708
  referenceMap,
5456
5709
  tokenToCharMap,
5457
- nextCharCode
5710
+ characterState,
5711
+ breakChars
5458
5712
  );
5459
- if (referenceMap.size > tokenToCharMap.size) {
5460
- nextCharCode++;
5461
- }
5462
5713
  });
5463
5714
  originalFullText = appendParagraphBoundary(originalFullText, paragraphIndex, paragraphs.length);
5464
5715
  const paragraphEnd = originalFullText.length;
5465
- const pPr = getFirstElementByTag(paragraph, "w:pPr");
5716
+ const pPr = getFirstElementByTagNSOrTag(paragraph, NS_W, "pPr");
5466
5717
  const container = paragraph.parentNode;
5467
5718
  if (container) uniqueContainers.add(container);
5468
5719
  paragraphMap.push({
@@ -5472,7 +5723,13 @@ function buildReconstructionMapping(xmlDoc, modifiedText) {
5472
5723
  container: container || body
5473
5724
  });
5474
5725
  });
5475
- let processedModifiedText = modifiedText;
5726
+ let displayOriginalText = "";
5727
+ for (let index = 0; index < originalFullText.length; index++) {
5728
+ const char = originalFullText[index];
5729
+ displayOriginalText += breakChars.has(char) ? "\n" : char;
5730
+ }
5731
+ let processedModifiedText = preserveZeroWidthSentinels(displayOriginalText, modifiedText, sentinelMap);
5732
+ processedModifiedText = preserveStructuralBreaks(displayOriginalText, originalFullText, processedModifiedText, breakChars);
5476
5733
  tokenToCharMap.forEach((char, tokenString) => {
5477
5734
  const escapedToken = tokenString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
5478
5735
  processedModifiedText = processedModifiedText.replace(new RegExp(escapedToken, "g"), char);
@@ -5528,6 +5785,124 @@ function buildReconstructionMapping(xmlDoc, modifiedText) {
5528
5785
  isParagraphStart: (index) => paragraphStarts.has(index)
5529
5786
  };
5530
5787
  }
5788
+ function findReconstructionParagraphRange(xmlDoc, originalText) {
5789
+ const paragraphs = getDocumentParagraphs(xmlDoc);
5790
+ if (paragraphs.length === 0) return [];
5791
+ const wanted = normalizeComparisonText(originalText);
5792
+ const paragraphTexts = paragraphs.map(extractParagraphVisibleText);
5793
+ if (!wanted) {
5794
+ const emptyIndex = paragraphTexts.findIndex((text) => text === "");
5795
+ return emptyIndex >= 0 ? [paragraphs[emptyIndex]] : null;
5796
+ }
5797
+ if (paragraphs.length === 1 && paragraphTexts[0] === "") {
5798
+ return paragraphs;
5799
+ }
5800
+ const comparisons = [
5801
+ (text) => text,
5802
+ (text) => text.trim(),
5803
+ (text) => text.replace(/\s+/g, " ").trim()
5804
+ ];
5805
+ for (const compare of comparisons) {
5806
+ const expected = compare(wanted);
5807
+ for (let start = 0; start < paragraphs.length; start++) {
5808
+ let combined = "";
5809
+ for (let end = start; end < paragraphs.length; end++) {
5810
+ combined += (end === start ? "" : "\n") + paragraphTexts[end];
5811
+ const candidate = compare(combined);
5812
+ if (candidate === expected) return paragraphs.slice(start, end + 1);
5813
+ }
5814
+ }
5815
+ }
5816
+ return null;
5817
+ }
5818
+ function normalizeComparisonText(text) {
5819
+ return String(text ?? "").replace(/\r\n?/g, "\n").replace(/\u00a0/g, " ");
5820
+ }
5821
+ function extractParagraphVisibleText(paragraph) {
5822
+ let text = "";
5823
+ const visit = (node) => {
5824
+ for (const child of Array.from(node?.childNodes || [])) {
5825
+ if (child.nodeType !== 1) continue;
5826
+ if (isWordElement(child, "pPr") || isWordElement(child, "del") || isWordElement(child, "moveFrom")) continue;
5827
+ if (isWordElement(child, "t")) text += child.textContent || "";
5828
+ else if (isWordElement(child, "tab")) text += " ";
5829
+ else if (isWordElement(child, "br") || isWordElement(child, "cr")) text += "\n";
5830
+ else if (isWordElement(child, "noBreakHyphen")) text += "\u2011";
5831
+ else visit(child);
5832
+ }
5833
+ };
5834
+ visit(paragraph);
5835
+ return normalizeComparisonText(text);
5836
+ }
5837
+ function preserveStructuralBreaks(displayOriginalText, internalOriginalText, modifiedText, breakChars) {
5838
+ if (breakChars.size === 0) return modifiedText;
5839
+ const diffs = DMP.diff_main(displayOriginalText, modifiedText);
5840
+ let originalOffset = 0;
5841
+ let result = "";
5842
+ for (const [op, text] of diffs) {
5843
+ if (op === 0) {
5844
+ for (let index = 0; index < text.length; index++) {
5845
+ const internalChar = internalOriginalText[originalOffset + index];
5846
+ result += breakChars.has(internalChar) ? internalChar : text[index];
5847
+ }
5848
+ originalOffset += text.length;
5849
+ } else if (op === -1) {
5850
+ originalOffset += text.length;
5851
+ } else {
5852
+ result += text;
5853
+ }
5854
+ }
5855
+ return result;
5856
+ }
5857
+ function preserveZeroWidthSentinels(displayOriginalText, modifiedText, sentinelMap) {
5858
+ const sentinelsByInternalOffset = /* @__PURE__ */ new Map();
5859
+ sentinelMap.forEach((sentinel) => {
5860
+ if (sentinel.zeroWidth) sentinelsByInternalOffset.set(sentinel.start, sentinel);
5861
+ });
5862
+ if (sentinelsByInternalOffset.size === 0) return modifiedText;
5863
+ let visibleOriginalText = "";
5864
+ let visibleOffset = 0;
5865
+ const sentinelsByVisibleBoundary = /* @__PURE__ */ new Map();
5866
+ for (let internalOffset = 0; internalOffset < displayOriginalText.length; internalOffset++) {
5867
+ const sentinel = sentinelsByInternalOffset.get(internalOffset);
5868
+ if (sentinel) {
5869
+ if (!sentinelsByVisibleBoundary.has(visibleOffset)) sentinelsByVisibleBoundary.set(visibleOffset, []);
5870
+ sentinelsByVisibleBoundary.get(visibleOffset).push({
5871
+ char: displayOriginalText[internalOffset],
5872
+ affinity: sentinel.affinity || "right",
5873
+ emitted: false
5874
+ });
5875
+ continue;
5876
+ }
5877
+ visibleOriginalText += displayOriginalText[internalOffset];
5878
+ visibleOffset++;
5879
+ }
5880
+ const diffs = DMP.diff_main(visibleOriginalText, modifiedText);
5881
+ let originalOffset = 0;
5882
+ let result = "";
5883
+ const emitSentinels = (boundary, affinity) => {
5884
+ const sentinels = sentinelsByVisibleBoundary.get(boundary) || [];
5885
+ for (const sentinel of sentinels) {
5886
+ if (sentinel.emitted || affinity && sentinel.affinity !== affinity) continue;
5887
+ result += sentinel.char;
5888
+ sentinel.emitted = true;
5889
+ }
5890
+ };
5891
+ for (const [op, text] of diffs) {
5892
+ if (op === 1) {
5893
+ emitSentinels(originalOffset, "left");
5894
+ result += text;
5895
+ continue;
5896
+ }
5897
+ for (let index = 0; index < text.length; index++) {
5898
+ emitSentinels(originalOffset);
5899
+ if (op === 0) result += text[index];
5900
+ originalOffset++;
5901
+ }
5902
+ }
5903
+ emitSentinels(originalOffset);
5904
+ return result;
5905
+ }
5531
5906
  function preserveReferencePlaceholders(originalFullText, modifiedText, referenceMap) {
5532
5907
  let result = modifiedText;
5533
5908
  for (const referenceChar of referenceMap.keys()) {
@@ -5547,28 +5922,28 @@ function preserveReferencePlaceholders(originalFullText, modifiedText, reference
5547
5922
  }
5548
5923
  return result;
5549
5924
  }
5550
- function processChildNode(child, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, nextCharCode) {
5551
- if (child.nodeName === "w:r") {
5552
- return processRunForReconstruction(child, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, nextCharCode);
5925
+ function processChildNode(child, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, characterState, breakChars) {
5926
+ if (isWordElement(child, "r")) {
5927
+ return processRunForReconstruction(child, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, characterState, breakChars);
5553
5928
  }
5554
- if (child.nodeName === "w:hyperlink") {
5929
+ if (isWordElement(child, "hyperlink")) {
5555
5930
  return processHyperlinkForReconstruction(child, originalFullText, propertyMap);
5556
5931
  }
5557
- if (["w:sdt", "w:oMath", "m:oMath", "w:bookmarkStart", "w:bookmarkEnd"].includes(child.nodeName)) {
5932
+ if (isWordElement(child, "sdt") || isWordElement(child, "oMath") || localNameOf(child) === "oMath" || isWordElement(child, "bookmarkStart") || isWordElement(child, "bookmarkEnd")) {
5558
5933
  sentinelMap.push({ start: originalFullText.length, node: child });
5559
5934
  return originalFullText + "\uFFFC";
5560
5935
  }
5561
- if (["w:commentRangeStart", "w:commentRangeEnd"].includes(child.nodeName)) {
5936
+ if (isWordElement(child, "commentRangeStart") || isWordElement(child, "commentRangeEnd")) {
5562
5937
  sentinelMap.push({ start: originalFullText.length, node: child, isCommentMarker: true });
5563
5938
  return originalFullText;
5564
5939
  }
5565
5940
  return originalFullText;
5566
5941
  }
5567
- function processRunForReconstruction(runElement, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, nextCharCode) {
5942
+ function processRunForReconstruction(runElement, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, characterState, breakChars) {
5568
5943
  let fullText = originalFullText;
5569
- const rPr = getFirstElementByTag(runElement, "w:rPr");
5944
+ const rPr = getFirstElementByTagNSOrTag(runElement, NS_W, "rPr");
5570
5945
  Array.from(runElement.childNodes).forEach((runChild) => {
5571
- if (runChild.nodeName === "w:t") {
5946
+ if (isWordElement(runChild, "t")) {
5572
5947
  const textContent = runChild.textContent || "";
5573
5948
  if (textContent.length > 0) {
5574
5949
  propertyMap.push({
@@ -5578,38 +5953,47 @@ function processRunForReconstruction(runElement, originalFullText, propertyMap,
5578
5953
  });
5579
5954
  fullText += textContent;
5580
5955
  }
5581
- } else if (runChild.nodeName === "w:br" || runChild.nodeName === "w:cr") {
5582
- fullText += "\n";
5956
+ } else if (isWordElement(runChild, "br") || isWordElement(runChild, "cr")) {
5957
+ const char = String.fromCharCode(characterState.nextCharCode++);
5958
+ referenceMap.set(char, runChild);
5959
+ breakChars.add(char);
5960
+ fullText += char;
5583
5961
  propertyMap.push({ start: fullText.length - 1, end: fullText.length, rPr });
5584
- } else if (runChild.nodeName === "w:tab") {
5962
+ } else if (isWordElement(runChild, "tab")) {
5585
5963
  fullText += " ";
5586
5964
  propertyMap.push({ start: fullText.length - 1, end: fullText.length, rPr });
5587
- } else if (runChild.nodeName === "w:noBreakHyphen") {
5965
+ } else if (isWordElement(runChild, "noBreakHyphen")) {
5588
5966
  fullText += "\u2011";
5589
5967
  propertyMap.push({ start: fullText.length - 1, end: fullText.length, rPr });
5590
- } else if (["w:drawing", "w:pict", "w:object", "w:fldChar", "w:instrText", "w:sym"].includes(runChild.nodeName)) {
5591
- const textBoxContent = getFirstElementByTag(runChild, "w:txbxContent");
5592
- const hasTextBox = runChild.nodeName === "w:pict" && !!textBoxContent;
5968
+ } else if (["drawing", "pict", "object", "fldChar", "instrText", "sym"].some((name) => isWordElement(runChild, name))) {
5969
+ const textBoxContent = getFirstElementByTagNSOrTag(runChild, NS_W, "txbxContent");
5970
+ const hasTextBox = isWordElement(runChild, "pict") && !!textBoxContent;
5971
+ const isFieldStructure = isWordElement(runChild, "fldChar") || isWordElement(runChild, "instrText");
5972
+ const fieldCharType = isWordElement(runChild, "fldChar") ? runChild.getAttributeNS?.(NS_W, "fldCharType") || runChild.getAttribute("w:fldCharType") || runChild.getAttribute("fldCharType") : null;
5593
5973
  sentinelMap.push({
5594
5974
  start: fullText.length,
5595
5975
  node: runChild,
5976
+ wrapInRun: true,
5977
+ rPr,
5978
+ zeroWidth: isFieldStructure,
5979
+ affinity: fieldCharType === "end" ? "left" : "right",
5596
5980
  isTextBox: hasTextBox,
5597
5981
  originalContainer: hasTextBox ? textBoxContent : void 0
5598
5982
  });
5599
5983
  fullText += "\uFFFC";
5600
5984
  propertyMap.push({ start: fullText.length - 1, end: fullText.length, rPr });
5601
- } else if (runChild.nodeName === "w:footnoteReference" || runChild.nodeName === "w:endnoteReference") {
5602
- const id = runChild.getAttribute("w:id");
5985
+ } else if (isWordElement(runChild, "footnoteReference") || isWordElement(runChild, "endnoteReference")) {
5986
+ const id = wordAttribute(runChild, "id");
5603
5987
  if (id) {
5604
- const type = runChild.nodeName === "w:footnoteReference" ? "FN" : "EN";
5988
+ const type = isWordElement(runChild, "footnoteReference") ? "FN" : "EN";
5605
5989
  const tokenString = `{{__${type}_${id}__}}`;
5606
- const char = String.fromCharCode(nextCharCode);
5990
+ const char = String.fromCharCode(characterState.nextCharCode++);
5607
5991
  referenceMap.set(char, runChild);
5608
5992
  tokenToCharMap.set(tokenString, char);
5609
5993
  fullText += char;
5610
5994
  propertyMap.push({ start: fullText.length - 1, end: fullText.length, rPr });
5611
5995
  }
5612
- } else if (runChild.nodeName === "w:commentReference") {
5996
+ } else if (isWordElement(runChild, "commentReference")) {
5613
5997
  sentinelMap.push({ start: fullText.length, node: runChild, isCommentMarker: true });
5614
5998
  }
5615
5999
  });
@@ -5618,9 +6002,9 @@ function processRunForReconstruction(runElement, originalFullText, propertyMap,
5618
6002
  function processHyperlinkForReconstruction(hyperlinkElement, originalFullText, propertyMap) {
5619
6003
  let fullText = originalFullText;
5620
6004
  Array.from(hyperlinkElement.childNodes).forEach((hyperlinkChild) => {
5621
- if (hyperlinkChild.nodeName !== "w:r") return;
5622
- const rPr = getFirstElementByTag(hyperlinkChild, "w:rPr");
5623
- const texts = getElementsByTag(hyperlinkChild, "w:t");
6005
+ if (!isWordElement(hyperlinkChild, "r")) return;
6006
+ const rPr = getFirstElementByTagNSOrTag(hyperlinkChild, NS_W, "rPr");
6007
+ const texts = getElementsByTagNSOrTag(hyperlinkChild, NS_W, "t");
5624
6008
  texts.forEach((textNode) => {
5625
6009
  const textContent = textNode.textContent || "";
5626
6010
  if (textContent.length === 0) return;
@@ -5640,7 +6024,6 @@ function processHyperlinkForReconstruction(hyperlinkElement, originalFullText, p
5640
6024
  function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, formatHints, generateRedlines = true) {
5641
6025
  const {
5642
6026
  paragraphs,
5643
- paragraphMap,
5644
6027
  containerFragments,
5645
6028
  sentinelMapByStart,
5646
6029
  referenceMap,
@@ -5663,10 +6046,16 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
5663
6046
  }
5664
6047
  let currentOriginalIndex = 0;
5665
6048
  let currentInsertOffset = 0;
6049
+ let pendingReplacementStart = null;
5666
6050
  const emittedCommentMarkers = /* @__PURE__ */ new WeakSet();
5667
6051
  for (const [op, text] of diffs) {
5668
6052
  if (op === 0 || op === -1) {
5669
6053
  const type = op === 0 ? "equal" : "delete";
6054
+ if (op === 0) {
6055
+ pendingReplacementStart = null;
6056
+ } else if (pendingReplacementStart === null) {
6057
+ pendingReplacementStart = currentOriginalIndex;
6058
+ }
5670
6059
  let offset = 0;
5671
6060
  while (offset < text.length) {
5672
6061
  const chunkStart = currentOriginalIndex + offset;
@@ -5703,7 +6092,8 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
5703
6092
  continue;
5704
6093
  }
5705
6094
  if (op === 1) {
5706
- const properties = currentOriginalIndex > 0 && !isParagraphStart(currentOriginalIndex) ? getRunProperties(currentOriginalIndex - 1) : getRunProperties(currentOriginalIndex);
6095
+ const propertyIndex = pendingReplacementStart !== null ? pendingReplacementStart : currentOriginalIndex > 0 && !isParagraphStart(currentOriginalIndex) ? currentOriginalIndex - 1 : currentOriginalIndex;
6096
+ const properties = getRunProperties(propertyIndex);
5707
6097
  const appendResult = appendTextToCurrent(
5708
6098
  xmlDoc,
5709
6099
  text,
@@ -5726,29 +6116,46 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
5726
6116
  );
5727
6117
  currentParagraph = appendResult.currentParagraph;
5728
6118
  currentInsertOffset += text.length;
6119
+ pendingReplacementStart = null;
5729
6120
  }
5730
6121
  }
6122
+ const paragraphSet = new Set(paragraphs);
6123
+ const insertionAnchors = /* @__PURE__ */ new Map();
6124
+ paragraphs.forEach((paragraph) => {
6125
+ const container = paragraph.parentNode;
6126
+ if (!container || insertionAnchors.has(container)) return;
6127
+ let anchor = paragraph.nextSibling;
6128
+ while (anchor && paragraphSet.has(anchor)) anchor = anchor.nextSibling;
6129
+ insertionAnchors.set(container, anchor);
6130
+ });
5731
6131
  paragraphs.forEach((paragraph) => {
5732
6132
  if (paragraph.parentNode) {
5733
6133
  paragraph.parentNode.removeChild(paragraph);
5734
6134
  }
5735
6135
  });
6136
+ let hasDocumentTarget = false;
6137
+ let serializedDocumentOutput = "";
5736
6138
  containerFragments.forEach((fragment, container) => {
5737
6139
  const replacement = replacementContainers.get(container);
5738
6140
  const target = replacement || container;
5739
6141
  if (target.nodeType === 9) {
5740
- const firstChild = fragment.firstChild;
5741
- if (firstChild) {
5742
- target.appendChild(firstChild);
5743
- while (fragment.firstChild) {
5744
- target.documentElement.appendChild(fragment.firstChild);
5745
- }
6142
+ hasDocumentTarget = true;
6143
+ if (fragment.childNodes.length === 1) {
6144
+ target.appendChild(fragment.firstChild);
6145
+ } else {
6146
+ serializedDocumentOutput = Array.from(fragment.childNodes).map((node) => serializer.serializeToString(node)).join("");
5746
6147
  }
5747
6148
  return;
5748
6149
  }
5749
- target.appendChild(fragment);
6150
+ const anchor = replacement ? null : insertionAnchors.get(container);
6151
+ if (anchor && anchor.parentNode === target) {
6152
+ target.insertBefore(fragment, anchor);
6153
+ } else {
6154
+ target.appendChild(fragment);
6155
+ }
5750
6156
  });
5751
- return { oxml: serializer.serializeToString(xmlDoc), hasChanges: true };
6157
+ const oxml = hasDocumentTarget && serializedDocumentOutput ? serializedDocumentOutput : serializer.serializeToString(xmlDoc);
6158
+ return { oxml, hasChanges: true };
5752
6159
  }
5753
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()) {
5754
6161
  let localBaseIndex = baseIndex;
@@ -5760,7 +6167,7 @@ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, curren
5760
6167
  const commentMarkers = sentinelsAtOffset.filter((sentinel) => sentinel.isCommentMarker && !emittedCommentMarkers.has(sentinel.node));
5761
6168
  commentMarkers.forEach((marker) => {
5762
6169
  emittedCommentMarkers.add(marker.node);
5763
- if (marker.node.nodeName === "w:commentReference") {
6170
+ if (isWordElement(marker.node, "commentReference")) {
5764
6171
  const run = createWordElement(xmlDoc, "w:r");
5765
6172
  run.appendChild(marker.node.cloneNode(true));
5766
6173
  localParagraph.appendChild(run);
@@ -5772,9 +6179,9 @@ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, curren
5772
6179
  const info = getParagraphInfo(localBaseIndex + 1);
5773
6180
  const nextParagraph = createNewParagraph(info.pPr);
5774
6181
  if (generateRedlines && type === "insert") {
5775
- markParagraphMarkInserted(xmlDoc, nextParagraph, author);
6182
+ markParagraphMarkInserted(xmlDoc, localParagraph, author);
5776
6183
  } else if (generateRedlines && type === "delete") {
5777
- markParagraphMarkDeleted(xmlDoc, nextParagraph, author);
6184
+ markParagraphMarkDeleted(xmlDoc, localParagraph, author);
5778
6185
  }
5779
6186
  const fragment = containerFragments.get(info.container);
5780
6187
  if (fragment) {
@@ -5790,13 +6197,20 @@ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, curren
5790
6197
  if (sentinel) {
5791
6198
  const clone = sentinel.node.cloneNode(true);
5792
6199
  if (sentinel.isTextBox && sentinel.originalContainer) {
5793
- const newContainer = getFirstElementByTag(clone, "w:txbxContent");
6200
+ const newContainer = getFirstElementByTagNSOrTag(clone, NS_W, "txbxContent");
5794
6201
  if (newContainer) {
5795
6202
  while (newContainer.firstChild) newContainer.removeChild(newContainer.firstChild);
5796
6203
  replacementContainers.set(sentinel.originalContainer, newContainer);
5797
6204
  }
5798
6205
  }
5799
- localParagraph.appendChild(clone);
6206
+ if (sentinel.wrapInRun) {
6207
+ const run = createWordElement(xmlDoc, "w:r");
6208
+ if (sentinel.rPr) run.appendChild(sentinel.rPr.cloneNode(true));
6209
+ run.appendChild(clone);
6210
+ localParagraph.appendChild(run);
6211
+ } else {
6212
+ localParagraph.appendChild(clone);
6213
+ }
5800
6214
  }
5801
6215
  localBaseIndex++;
5802
6216
  if (type !== "delete") localInsertOffset++;
@@ -5855,12 +6269,24 @@ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, curren
5855
6269
  }
5856
6270
 
5857
6271
  // engine/reconstruction-mode.js
5858
- function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true) {
5859
- const mapping = buildReconstructionMapping(xmlDoc, modifiedText);
6272
+ function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true, diffOptions = {}) {
6273
+ const selectedParagraphs = findReconstructionParagraphRange(xmlDoc, originalText);
6274
+ if (selectedParagraphs === null) {
6275
+ return withOoxmlSourceType({
6276
+ oxml: serializer.serializeToString(xmlDoc),
6277
+ hasChanges: false,
6278
+ status: "error",
6279
+ error: {
6280
+ code: "PARTIAL_TARGET",
6281
+ message: "Original text did not identify a complete contiguous paragraph range for reconstruction."
6282
+ }
6283
+ });
6284
+ }
6285
+ const mapping = buildReconstructionMapping(xmlDoc, modifiedText, selectedParagraphs);
5860
6286
  if (mapping.paragraphs.length === 0) {
5861
6287
  return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
5862
6288
  }
5863
- const diffs = computeWordDiffs(mapping.originalFullText, mapping.processedModifiedText);
6289
+ const diffs = computeWordDiffs(mapping.originalFullText, mapping.processedModifiedText, diffOptions);
5864
6290
  return withOoxmlSourceType(applyReconstructionDiffs(
5865
6291
  xmlDoc,
5866
6292
  diffs,
@@ -5889,11 +6315,15 @@ function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, auth
5889
6315
  if (operations.length === 0) {
5890
6316
  return noChanges(serializer, xmlDoc);
5891
6317
  }
5892
- const options = { generateRedlines, author };
6318
+ const options = {
6319
+ generateRedlines,
6320
+ author,
6321
+ revisionIdAllocator: getRevisionIdAllocatorForDocument(xmlDoc)
6322
+ };
5893
6323
  const reconciledOxml = serializeVirtualGridToOoxml(oldGrid, operations, options);
5894
6324
  const wrappedOxml = `<root xmlns:w="${NS_W}">${reconciledOxml}</root>`;
5895
- const reconcileParser = parser || createParser();
5896
- const reconciledDoc = reconcileParser.parseFromString(wrappedOxml, "application/xml");
6325
+ const reconciledDoc = parseOoxmlSafe(wrappedOxml, "application/xml").doc;
6326
+ if (!reconciledDoc) return noChanges(serializer, xmlDoc);
5897
6327
  const parseError = getXmlParseError(reconciledDoc);
5898
6328
  if (parseError) {
5899
6329
  error("[OxmlEngine] Failed to parse reconciled table OOXML:", parseError.textContent);
@@ -5909,14 +6339,15 @@ function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, auth
5909
6339
  return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: true });
5910
6340
  }
5911
6341
  function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines) {
6342
+ const revisionIdAllocator = getRevisionIdAllocatorForDocument(xmlDoc);
5912
6343
  const tableData = parseTable(modifiedText);
5913
6344
  if (!tableData || tableData.rows.length === 0 && tableData.headers.length === 0) {
5914
6345
  log("[OxmlEngine] Failed to parse table data from Markdown");
5915
6346
  return noChanges(serializer, xmlDoc);
5916
6347
  }
5917
- const tableOoxml = generateTableOoxml(tableData, { generateRedlines, author });
5918
- const activeParser = parser || createParser();
5919
- const tableDoc = activeParser.parseFromString(`<root xmlns:w="${NS_W}">${tableOoxml}</root>`, "application/xml");
6348
+ const tableOoxml = generateTableOoxml(tableData, { generateRedlines, author, revisionIdAllocator });
6349
+ const tableDoc = parseOoxmlSafe(`<root xmlns:w="${NS_W}">${tableOoxml}</root>`, "application/xml").doc;
6350
+ if (!tableDoc) return noChanges(serializer, xmlDoc);
5920
6351
  const tableParseError = getXmlParseError(tableDoc);
5921
6352
  if (tableParseError) {
5922
6353
  error("[OxmlEngine] Failed to parse generated table OOXML:", tableParseError.textContent);
@@ -5939,13 +6370,15 @@ function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser
5939
6370
  let firstParagraph = paragraphs[0];
5940
6371
  let parent = firstParagraph.parentNode;
5941
6372
  if (parent && parent.nodeType === 9) {
5942
- const wrappedDoc = activeParser.parseFromString(
6373
+ const wrappedDoc = parseOoxmlSafe(
5943
6374
  `<w:document xmlns:w="${NS_W}"><w:body/></w:document>`,
5944
6375
  "application/xml"
5945
- );
6376
+ ).doc;
6377
+ if (!wrappedDoc) return noChanges(serializer, workingDoc);
5946
6378
  const wrappedBody = getFirstElementByTagNS(wrappedDoc, NS_W, "body");
5947
6379
  paragraphs.forEach((p) => wrappedBody.appendChild(wrappedDoc.importNode(p, true)));
5948
6380
  workingDoc = wrappedDoc;
6381
+ setRevisionIdAllocatorForDocument(workingDoc, revisionIdAllocator);
5949
6382
  paragraphs = getElementsByTagNS(workingDoc, NS_W, "p");
5950
6383
  firstParagraph = paragraphs[0];
5951
6384
  parent = firstParagraph.parentNode;
@@ -5966,7 +6399,7 @@ function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser
5966
6399
  }
5967
6400
  });
5968
6401
  const del = createWordElement(workingDoc, "w:del");
5969
- const metadata = createRevisionMetadata(author);
6402
+ const metadata = createRevisionMetadata(author, workingDoc);
5970
6403
  del.setAttribute("w:id", String(metadata.id));
5971
6404
  del.setAttribute("w:author", metadata.author);
5972
6405
  del.setAttribute("w:date", metadata.date);
@@ -6030,17 +6463,19 @@ function authorMatchesNode(node, filter) {
6030
6463
  return !!nodeAuthor && nodeAuthor === filter.normalizedAuthor;
6031
6464
  }
6032
6465
  function parseXmlWithWarnings(oxml, parseFailurePrefix) {
6033
- const parser = createParser();
6034
- const xmlDoc = parser.parseFromString(oxml, "application/xml");
6035
- const parseError = getXmlParseError(xmlDoc);
6036
- if (parseError) {
6466
+ const parsed = parseOoxmlSafe(oxml, "application/xml");
6467
+ const parseError = parsed.doc ? getXmlParseError(parsed.doc) : null;
6468
+ if (parsed.error || parseError) {
6469
+ const message = parsed.error?.message || parseError?.textContent || "parse error";
6037
6470
  return {
6038
6471
  xmlDoc: null,
6039
6472
  serializer: null,
6040
- warning: `${parseFailurePrefix}: ${parseError.textContent || "parse error"}`
6473
+ warning: `${parseFailurePrefix}: ${message}`,
6474
+ warnings: parsed.warnings,
6475
+ error: { code: "PARSE_ERROR", message }
6041
6476
  };
6042
6477
  }
6043
- return { xmlDoc, serializer: createSerializer(), warning: null };
6478
+ return { xmlDoc: parsed.doc, serializer: createSerializer(), warning: null, warnings: parsed.warnings, error: null };
6044
6479
  }
6045
6480
  function removeNode(node) {
6046
6481
  if (node?.parentNode) {
@@ -6100,9 +6535,17 @@ function acceptTrackedChangesInOoxml(oxml, options = {}) {
6100
6535
  }
6101
6536
  const parseResult = parseXmlWithWarnings(oxml, "Failed to parse OOXML");
6102
6537
  if (!parseResult.xmlDoc) {
6103
- return { oxml, hasChanges: false, acceptedCount: 0, warnings: [parseResult.warning] };
6538
+ return {
6539
+ oxml,
6540
+ hasChanges: false,
6541
+ acceptedCount: 0,
6542
+ status: "error",
6543
+ error: parseResult.error,
6544
+ warnings: [...parseResult.warnings || [], parseResult.warning]
6545
+ };
6104
6546
  }
6105
6547
  const { xmlDoc, serializer } = parseResult;
6548
+ warnings.push(...parseResult.warnings || []);
6106
6549
  let acceptedCount = 0;
6107
6550
  for (const insNode of getWordElementsByLocalName(xmlDoc, "ins")) {
6108
6551
  if (!insNode.parentNode || !authorMatchesNode(insNode, filter)) continue;
@@ -6234,9 +6677,17 @@ function rejectTrackedChangesInOoxml(oxml, options = {}) {
6234
6677
  }
6235
6678
  const parseResult = parseXmlWithWarnings(oxml, "Failed to parse OOXML");
6236
6679
  if (!parseResult.xmlDoc) {
6237
- return { oxml, hasChanges: false, rejectedCount: 0, warnings: [parseResult.warning] };
6680
+ return {
6681
+ oxml,
6682
+ hasChanges: false,
6683
+ rejectedCount: 0,
6684
+ status: "error",
6685
+ error: parseResult.error,
6686
+ warnings: [...parseResult.warnings || [], parseResult.warning]
6687
+ };
6238
6688
  }
6239
6689
  const { xmlDoc, serializer } = parseResult;
6690
+ warnings.push(...parseResult.warnings || []);
6240
6691
  let rejectedCount = 0;
6241
6692
  for (const insNode of getWordElementsByLocalName(xmlDoc, "ins")) {
6242
6693
  if (!insNode.parentNode || !authorMatchesNode(insNode, filter)) continue;
@@ -6359,10 +6810,13 @@ function deleteCommentsByAuthorInOoxml(oxml, options = {}) {
6359
6810
  hasChanges: false,
6360
6811
  commentsRemoved: 0,
6361
6812
  referencesRemoved: 0,
6362
- warnings: [parseResult.warning]
6813
+ status: "error",
6814
+ error: parseResult.error,
6815
+ warnings: [...parseResult.warnings || [], parseResult.warning]
6363
6816
  };
6364
6817
  }
6365
6818
  const { xmlDoc, serializer } = parseResult;
6819
+ warnings.push(...parseResult.warnings || []);
6366
6820
  const { targetIds, commentNodes } = collectCommentTargetIds(xmlDoc, filter);
6367
6821
  if (filter.allAuthors) {
6368
6822
  for (const localName of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
@@ -6385,66 +6839,96 @@ function deleteCommentsByAuthorInOoxml(oxml, options = {}) {
6385
6839
 
6386
6840
  // engine/oxml-engine.js
6387
6841
  async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}) {
6842
+ const inputOoxml = oxml;
6843
+ let workingOoxml = oxml;
6844
+ originalText = typeof originalText === "string" ? originalText : String(originalText ?? "");
6845
+ modifiedText = typeof modifiedText === "string" ? modifiedText : String(modifiedText ?? "");
6388
6846
  const generateRedlines = options.generateRedlines ?? true;
6389
6847
  const author = options.author || getDefaultAuthor();
6390
- const parser = createParser();
6391
6848
  const serializer = createSerializer();
6849
+ let parseWarnings = [];
6850
+ const operationWarnings = [];
6851
+ let normalizedExistingRevisions = false;
6852
+ const keepNormalizedNoOp = options.existingRevisions === "accept-all-first-keep-normalized";
6392
6853
  const finalize = (result) => {
6393
6854
  const withStatus = { ...result };
6855
+ if (normalizedExistingRevisions && withStatus.hasChanges === false && withStatus.status !== "error") {
6856
+ if (keepNormalizedNoOp) {
6857
+ withStatus.oxml = workingOoxml;
6858
+ withStatus.hasChanges = true;
6859
+ withStatus.warnings = [
6860
+ ...Array.isArray(withStatus.warnings) ? withStatus.warnings : [],
6861
+ "Existing revisions were accepted before redlining."
6862
+ ];
6863
+ } else {
6864
+ withStatus.oxml = inputOoxml;
6865
+ }
6866
+ }
6867
+ const warnings = [...parseWarnings, ...operationWarnings, ...Array.isArray(withStatus.warnings) ? withStatus.warnings : []];
6868
+ if (warnings.length > 0) {
6869
+ withStatus.warnings = [...new Set(warnings)];
6870
+ }
6394
6871
  if (!withStatus.status) {
6395
6872
  withStatus.status = withStatus.hasChanges ? "ok" : "no-op";
6396
6873
  }
6397
6874
  return withOoxmlSourceType(withStatus);
6398
6875
  };
6399
- const noChanges2 = () => finalize({ oxml, hasChanges: false });
6400
- let xmlDoc;
6401
- try {
6402
- xmlDoc = parser.parseFromString(oxml, "text/xml");
6403
- } catch (e) {
6404
- error("[OxmlEngine] Failed to parse OXML:", e);
6405
- return finalize({
6406
- oxml,
6407
- hasChanges: false,
6408
- status: "error",
6409
- error: { code: "PARSE_ERROR", message: "Could not parse OOXML input." }
6410
- });
6411
- }
6412
- const parseError = getXmlParseError(xmlDoc);
6413
- if (parseError) {
6414
- error("[OxmlEngine] XML parse error:", parseError.textContent);
6876
+ const finalizeUnchanged = () => {
6877
+ if (normalizedExistingRevisions && keepNormalizedNoOp) {
6878
+ return finalize({
6879
+ oxml: workingOoxml,
6880
+ hasChanges: true,
6881
+ warnings: ["Existing revisions were accepted before redlining."]
6882
+ });
6883
+ }
6884
+ return finalize({ oxml: inputOoxml, hasChanges: false });
6885
+ };
6886
+ const parsed = parseOoxmlSafe(inputOoxml, "text/xml");
6887
+ parseWarnings = parsed.warnings;
6888
+ let xmlDoc = parsed.doc;
6889
+ const parseError = xmlDoc ? getXmlParseError(xmlDoc) : null;
6890
+ if (parsed.error || parseError) {
6891
+ const message = parsed.error?.message || parseError?.textContent || "Could not parse OOXML input.";
6892
+ error("[OxmlEngine] XML parse error:", message);
6415
6893
  return finalize({
6416
- oxml,
6894
+ oxml: inputOoxml,
6417
6895
  hasChanges: false,
6418
6896
  status: "error",
6419
- error: { code: "PARSE_ERROR", message: parseError.textContent || "Could not parse OOXML input." }
6897
+ error: { code: "PARSE_ERROR", message }
6420
6898
  });
6421
6899
  }
6422
- seedRevisionIdsFromDocument(xmlDoc);
6900
+ const revisionIdAllocator = options?._revisionIdAllocator instanceof RevisionIdAllocator ? options._revisionIdAllocator : new RevisionIdAllocator();
6901
+ seedRevisionIdsFromDocument(xmlDoc, revisionIdAllocator);
6423
6902
  if (containsTrackedChanges(xmlDoc)) {
6424
6903
  const existingRevisionsPolicy = options.existingRevisions || "reject-input";
6425
- if (existingRevisionsPolicy === "accept-all-first") {
6904
+ if (existingRevisionsPolicy === "accept-all-first" || existingRevisionsPolicy === "accept-all-first-keep-normalized") {
6426
6905
  log("[OxmlEngine] Existing revisions detected; accepting all input revisions before redlining");
6427
- const accepted = acceptTrackedChangesInOoxml(oxml, { allAuthors: true });
6428
- oxml = accepted.oxml;
6429
- xmlDoc = parser.parseFromString(oxml, "text/xml");
6430
- const acceptedParseError = getXmlParseError(xmlDoc);
6431
- if (acceptedParseError) {
6432
- error("[OxmlEngine] XML parse error after accepting existing revisions:", acceptedParseError.textContent);
6906
+ const accepted = acceptTrackedChangesInOoxml(inputOoxml, { allAuthors: true });
6907
+ if (accepted.status === "error") return finalize(accepted);
6908
+ workingOoxml = accepted.oxml;
6909
+ normalizedExistingRevisions = true;
6910
+ const acceptedParsed = parseOoxmlSafe(workingOoxml, "text/xml");
6911
+ parseWarnings.push(...acceptedParsed.warnings);
6912
+ xmlDoc = acceptedParsed.doc;
6913
+ const acceptedParseError = xmlDoc ? getXmlParseError(xmlDoc) : null;
6914
+ if (acceptedParsed.error || acceptedParseError) {
6915
+ const message = acceptedParsed.error?.message || acceptedParseError?.textContent || "Could not parse OOXML after accepting existing revisions.";
6916
+ error("[OxmlEngine] XML parse error after accepting existing revisions:", message);
6433
6917
  return finalize({
6434
- oxml,
6918
+ oxml: inputOoxml,
6435
6919
  hasChanges: false,
6436
6920
  status: "error",
6437
6921
  error: {
6438
6922
  code: "PARSE_ERROR",
6439
- message: "Could not parse OOXML after accepting existing revisions."
6923
+ message
6440
6924
  }
6441
6925
  });
6442
6926
  }
6443
- seedRevisionIdsFromDocument(xmlDoc);
6927
+ seedRevisionIdsFromDocument(xmlDoc, revisionIdAllocator);
6444
6928
  } else {
6445
6929
  log("[OxmlEngine] Existing revisions detected; rejecting input per existingRevisions policy");
6446
6930
  return finalize({
6447
- oxml,
6931
+ oxml: inputOoxml,
6448
6932
  hasChanges: false,
6449
6933
  status: "error",
6450
6934
  error: {
@@ -6458,22 +6942,33 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
6458
6942
  if (initialTableCellContext.hasTableWrapper && initialTableCellContext.targetParagraph && !options._isolatedTableCell) {
6459
6943
  log("[OxmlEngine] Isolating table-cell paragraph before diff");
6460
6944
  const isolatedOxml = serializeParagraphOnly(xmlDoc, initialTableCellContext.targetParagraph, serializer);
6461
- return applyRedlineToOxml(isolatedOxml, originalText, modifiedText, {
6945
+ const isolatedResult = await applyRedlineToOxml(isolatedOxml, originalText, modifiedText, {
6462
6946
  ...options,
6463
6947
  _isolatedTableCell: true
6464
6948
  });
6949
+ if (!isolatedResult.hasChanges && isolatedResult.status === "no-op") {
6950
+ return finalizeUnchanged();
6951
+ }
6952
+ return isolatedResult;
6953
+ }
6954
+ const sanitizedText = options.sanitizeInput === true ? sanitizeAiResponse(modifiedText) : modifiedText;
6955
+ if (sanitizedText !== modifiedText) {
6956
+ operationWarnings.push("Input was sanitized; pass sanitizeInput: false to disable.");
6465
6957
  }
6466
- const sanitizedText = sanitizeAiResponse(modifiedText);
6467
6958
  const { cleanText: cleanModifiedText, formatHints } = preprocessMarkdown(sanitizedText);
6468
6959
  const hasTextChanges = cleanModifiedText.trim() !== originalText.trim();
6469
6960
  const hasFormatHints = formatHints.length > 0;
6470
6961
  const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
6471
6962
  const hasExistingFormatting = existingFormatHints.length > 0;
6472
6963
  const visibleText = textSpans.map((span) => textSpanVisibleText(span)).join("");
6473
- if (hasTextChanges && typeof originalText === "string" && originalText.trim() && !originalText.includes("\n") && !visibleText.includes(originalText.trim()) && !visibleText.replace(/[\t\n\u2011]/g, "").includes(originalText.trim().replace(/[\t\n\u2011]/g, "")) && !normalizeTargetText(visibleText).includes(normalizeTargetText(originalText))) {
6964
+ const targetFound = originalText.includes("\n") || originalText.includes("\r") ? originalText.split(/\r?\n/).map(normalizeTargetText).filter(Boolean).every((line) => paragraphs.some((paragraph) => {
6965
+ const paragraphText = textSpans.filter((span) => span.paragraph === paragraph).map(textSpanVisibleText).join("");
6966
+ return normalizeTargetText(paragraphText).includes(line);
6967
+ })) : visibleText.includes(originalText.trim()) || visibleText.replace(/[\t\n\u2011]/g, "").includes(originalText.trim().replace(/[\t\n\u2011]/g, "")) || normalizeTargetText(visibleText).includes(normalizeTargetText(originalText));
6968
+ if (hasTextChanges && typeof originalText === "string" && originalText.trim() && !targetFound) {
6474
6969
  log("[OxmlEngine] Target text not found in OOXML");
6475
6970
  return finalize({
6476
- oxml,
6971
+ oxml: inputOoxml,
6477
6972
  hasChanges: false,
6478
6973
  status: "error",
6479
6974
  error: {
@@ -6514,10 +7009,14 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
6514
7009
  );
6515
7010
  };
6516
7011
  log(`[OxmlEngine] Text changes: ${hasTextChanges}, New format hints: ${formatHints.length}, Existing format hints: ${existingFormatHints.length}`);
6517
- const needsFormatRemoval = !hasTextChanges && !hasFormatHints && hasExistingFormatting;
7012
+ const needsFormatRemoval = options.removeFormatting === true && !hasTextChanges && !hasFormatHints && hasExistingFormatting;
6518
7013
  if (!hasTextChanges && !hasFormatHints && !hasExistingFormatting) {
6519
7014
  log("[OxmlEngine] No text changes, no format hints, and no existing formatting detected");
6520
- return noChanges2();
7015
+ return finalizeUnchanged();
7016
+ }
7017
+ if (!hasTextChanges && !hasFormatHints && hasExistingFormatting && !needsFormatRemoval) {
7018
+ log("[OxmlEngine] No text or explicit formatting changes; preserving existing formatting");
7019
+ return finalizeUnchanged();
6521
7020
  }
6522
7021
  if (needsFormatRemoval) {
6523
7022
  log("[OxmlEngine] Format REMOVAL detected: applying surgical replacement in OOXML");
@@ -6577,50 +7076,78 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
6577
7076
  const isTargetList = isListTargetLoose(cleanModifiedText);
6578
7077
  const tableCellContext = initialTableCellContext;
6579
7078
  log(`[OxmlEngine] Mode: ${hasTables ? "SURGICAL" : "RECONSTRUCTION"}, formatHints: ${formatHints.length}, isMarkdownTable: ${isMarkdownTable}, isTargetList: ${isTargetList}, isTableCellParagraph: ${tableCellContext.isTableCellParagraph}`);
6580
- if (isMarkdownTable && !hasTables) {
6581
- log("[OxmlEngine] Text-to-table transformation: generating new table from Markdown");
6582
- return finalize(applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer, parser, author, generateRedlines));
6583
- }
6584
- if (hasTables && isMarkdownTable) {
6585
- return finalize(applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, parser, author, generateRedlines));
6586
- }
6587
- if (hasTables) {
6588
- const surgicalTarget = tableCellContext.hasTableWrapper && tableCellContext.targetParagraph ? tableCellContext.targetParagraph : null;
6589
- if (surgicalTarget) {
6590
- log("[OxmlEngine] Table cell edit: scoping surgical mode to target paragraph");
7079
+ try {
7080
+ if (isMarkdownTable && !hasTables) {
7081
+ log("[OxmlEngine] Text-to-table transformation: generating new table from Markdown");
7082
+ return finalize(applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines));
7083
+ }
7084
+ if (hasTables && isMarkdownTable) {
7085
+ return finalize(applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines));
7086
+ }
7087
+ if (hasTables) {
7088
+ const surgicalTarget = tableCellContext.hasTableWrapper && tableCellContext.targetParagraph ? tableCellContext.targetParagraph : null;
7089
+ if (surgicalTarget) {
7090
+ log("[OxmlEngine] Table cell edit: scoping surgical mode to target paragraph");
7091
+ }
7092
+ const result = applySurgicalMode(
7093
+ xmlDoc,
7094
+ originalText,
7095
+ cleanModifiedText,
7096
+ serializer,
7097
+ author,
7098
+ formatHints,
7099
+ generateRedlines,
7100
+ surgicalTarget
7101
+ );
7102
+ if (tableCellContext.hasTableWrapper && result.hasChanges && tableCellContext.targetParagraph) {
7103
+ log("[OxmlEngine] Stripping table wrapper for table cell paragraph (surgical mode)");
7104
+ return finalize({ oxml: serializeParagraphOnly(xmlDoc, tableCellContext.targetParagraph, serializer), hasChanges: true });
7105
+ }
7106
+ return finalize(result);
7107
+ }
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
7114
+ });
7115
+ const result = await pipeline.execute(workingOoxml, sanitizedText, { xmlDoc });
7116
+ if (result.error?.code === "DIFF_TOKEN_LIMIT") {
7117
+ return finalize({ oxml: inputOoxml, hasChanges: false, status: "error", error: result.error });
7118
+ }
7119
+ if (result.isValid && result.ooxml && result.ooxml !== workingOoxml) {
7120
+ const includeNumbering = result.includeNumbering === true;
7121
+ log(`[OxmlEngine] Wrapping list OOXML with numbering definitions, includeNumbering=${includeNumbering}`);
7122
+ const wrapped = wrapInDocumentFragment(result.ooxml, {
7123
+ includeNumbering,
7124
+ numberingXml: result.numberingXml
7125
+ });
7126
+ log(`[OxmlEngine] \u2705 Wrapped OOXML length: ${wrapped.length}`);
7127
+ return finalize({ oxml: wrapped, hasChanges: true });
7128
+ }
7129
+ return finalizeUnchanged();
6591
7130
  }
6592
- const result = applySurgicalMode(
7131
+ return finalize(applyReconstructionMode(
6593
7132
  xmlDoc,
6594
7133
  originalText,
6595
7134
  cleanModifiedText,
6596
7135
  serializer,
6597
7136
  author,
6598
7137
  formatHints,
6599
- generateRedlines,
6600
- surgicalTarget
6601
- );
6602
- if (tableCellContext.hasTableWrapper && result.hasChanges && tableCellContext.targetParagraph) {
6603
- log("[OxmlEngine] Stripping table wrapper for table cell paragraph (surgical mode)");
6604
- return finalize({ oxml: serializeParagraphOnly(xmlDoc, tableCellContext.targetParagraph, serializer), hasChanges: true });
6605
- }
6606
- return finalize(result);
6607
- }
6608
- if (isTargetList) {
6609
- log("[OxmlEngine] \u{1F3AF} Using reconciliation pipeline for list generation");
6610
- const pipeline = new ReconciliationPipeline({ author, generateRedlines });
6611
- const result = await pipeline.execute(oxml, modifiedText, { xmlDoc });
6612
- if (result.isValid && result.ooxml && result.ooxml !== oxml) {
6613
- log(`[OxmlEngine] Wrapping list OOXML with numbering definitions, includeNumbering=${result.includeNumbering}`);
6614
- const wrapped = wrapInDocumentFragment(result.ooxml, {
6615
- includeNumbering: result.includeNumbering ?? true,
6616
- numberingXml: result.numberingXml
7138
+ generateRedlines
7139
+ ));
7140
+ } catch (caught) {
7141
+ if (isDiffTokenLimitError(caught)) {
7142
+ return finalize({
7143
+ oxml: inputOoxml,
7144
+ hasChanges: false,
7145
+ status: "error",
7146
+ error: { code: caught.code, message: caught.message }
6617
7147
  });
6618
- log(`[OxmlEngine] \u2705 Wrapped OOXML length: ${wrapped.length}`);
6619
- return finalize({ oxml: wrapped, hasChanges: true });
6620
7148
  }
6621
- return noChanges2();
7149
+ throw caught;
6622
7150
  }
6623
- return finalize(applyReconstructionMode(xmlDoc, originalText, cleanModifiedText, serializer, author, formatHints, generateRedlines));
6624
7151
  }
6625
7152
  function normalizeTargetText(text) {
6626
7153
  return String(text || "").replace(/[\t\n\u2011]/g, " ").replace(/\s+/g, " ").trim();
@@ -6634,15 +7161,13 @@ function textSpanVisibleText(span) {
6634
7161
  return node?.textContent || "";
6635
7162
  }
6636
7163
  function sanitizeAiResponse(text) {
6637
- let cleaned = text;
6638
- cleaned = cleaned.replace(/^(Here is the redline:|Here is the text:|Sure, I can help:|Here's the updated text:)\s*/i, "");
6639
- cleaned = cleaned.replace(/\$\\text\{/g, "").replace(/\}\$/g, "");
6640
- cleaned = cleaned.replace(/\$([^0-9\n]+?)\$/g, "$1");
6641
- cleaned = cleaned.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n");
6642
- return cleaned;
7164
+ return String(text ?? "").replace(
7165
+ /^(?:Here is the redline:|Here is the text:|Sure, I can help:|Here's the updated text:)[ \t]*\r?\n/i,
7166
+ ""
7167
+ );
6643
7168
  }
6644
7169
  function parseOoxml(ooxmlString) {
6645
- return parseXml(ooxmlString, "application/xml");
7170
+ return parseOoxmlSafe(ooxmlString, "application/xml").doc;
6646
7171
  }
6647
7172
  function serializeOoxml(doc) {
6648
7173
  return serializeXml(doc);
@@ -6652,6 +7177,11 @@ function serializeOoxml(doc) {
6652
7177
  function toArray(nodeList) {
6653
7178
  return Array.from(nodeList || []);
6654
7179
  }
7180
+ function createTargetNotFoundError(message) {
7181
+ const error2 = new Error(message);
7182
+ error2.code = "TARGET_NOT_FOUND";
7183
+ return error2;
7184
+ }
6655
7185
  var WORD_MAIN_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
6656
7186
  function getElementsByLocalName(node, localName) {
6657
7187
  if (!node) return [];
@@ -6665,8 +7195,22 @@ function getElementsByLocalName(node, localName) {
6665
7195
  return toArray(node.getElementsByTagName(localName));
6666
7196
  }
6667
7197
  function toParagraphText(paragraph) {
6668
- const textNodes = getElementsByLocalName(paragraph, "t");
6669
- return textNodes.map((node) => node.textContent || "").join("");
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;
6670
7214
  }
6671
7215
  function getParagraphText(paragraph) {
6672
7216
  if (!paragraph) return "";
@@ -6822,9 +7366,9 @@ function resolveTargetParagraph(xmlDoc, options = {}) {
6822
7366
  const fuzzyMatch = findParagraphByBestTextMatch(xmlDoc, cleanTargetText, { onInfo });
6823
7367
  if (fuzzyMatch) return { paragraph: fuzzyMatch, resolvedBy: "fuzzy_text" };
6824
7368
  }
6825
- if (cleanTargetText) throw new Error(`Target paragraph not found: "${cleanTargetText}"`);
6826
- if (parsedRef) throw new Error(`Target paragraph reference not found: [P${parsedRef}]`);
6827
- throw new Error('Operation target missing: provide "target" text or "targetRef" ([P#]).');
7369
+ if (cleanTargetText) throw createTargetNotFoundError(`Target paragraph not found: "${cleanTargetText}"`);
7370
+ if (parsedRef) throw createTargetNotFoundError(`Target paragraph reference not found: [P${parsedRef}]`);
7371
+ throw createTargetNotFoundError('Operation target missing: provide "target" text or "targetRef" ([P#]).');
6828
7372
  }
6829
7373
  function isParagraphInTable(paragraph) {
6830
7374
  return !!findContainingWordElement(paragraph, "tbl");
@@ -6911,7 +7455,9 @@ function resolveTargetParagraphWithSnapshot(xmlDoc, options = {}) {
6911
7455
  onInfo(`[Target] [P${parsedRef}] appears stale after prior edits; using strict text rematch for ${opType}.`);
6912
7456
  return { paragraph: bestCandidate.paragraph, resolvedBy: "strict_text_after_ref_drift" };
6913
7457
  }
6914
- return resolved;
7458
+ throw createTargetNotFoundError(
7459
+ `Target paragraph [P${parsedRef}] no longer matches its batch-start anchor.`
7460
+ );
6915
7461
  }
6916
7462
  function resolveParagraphRangeByRefs(xmlDoc, startRef, endRef, options = {}) {
6917
7463
  if (!xmlDoc || !startRef || !endRef) return null;
@@ -7037,6 +7583,9 @@ function shouldPromoteBulletInsertionsToChildDepth(parsedItems, normalizedTarget
7037
7583
  return isNormalizedTextEqual(firstItem.text, normalizedTargetText);
7038
7584
  }
7039
7585
  if (firstItem?.kind === "list" && firstItem.markerType === "numbered") {
7586
+ const firstLevel = firstItem.level || 0;
7587
+ const alreadyIndented = trailingListItems.some((item) => (item.level || 0) > firstLevel);
7588
+ if (alreadyIndented) return false;
7040
7589
  return isNormalizedTextEqual(firstItem.text, normalizedTargetText);
7041
7590
  }
7042
7591
  return false;
@@ -7479,10 +8028,7 @@ function enforceListBindingOnParagraphNodes(nodes, options = {}) {
7479
8028
  if (!ownerDoc) continue;
7480
8029
  let pPr = getDirectWordChild2(paragraph, "pPr");
7481
8030
  if (!pPr) {
7482
- pPr = ownerDoc.createElementNS(
7483
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
7484
- "w:pPr"
7485
- );
8031
+ pPr = createWordElement(ownerDoc, "w:pPr");
7486
8032
  paragraph.insertBefore(pPr, paragraph.firstChild);
7487
8033
  }
7488
8034
  if (clearParagraphPropertyChanges) {
@@ -7495,27 +8041,18 @@ function enforceListBindingOnParagraphNodes(nodes, options = {}) {
7495
8041
  }
7496
8042
  let numPr = getDirectWordChild2(pPr, "numPr");
7497
8043
  if (!numPr) {
7498
- numPr = ownerDoc.createElementNS(
7499
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
7500
- "w:numPr"
7501
- );
8044
+ numPr = createWordElement(ownerDoc, "w:numPr");
7502
8045
  pPr.appendChild(numPr);
7503
8046
  }
7504
8047
  let ilvlEl = getDirectWordChild2(numPr, "ilvl");
7505
8048
  if (!ilvlEl) {
7506
- ilvlEl = ownerDoc.createElementNS(
7507
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
7508
- "w:ilvl"
7509
- );
8049
+ ilvlEl = createWordElement(ownerDoc, "w:ilvl");
7510
8050
  numPr.appendChild(ilvlEl);
7511
8051
  }
7512
8052
  ilvlEl.setAttribute("w:val", String(ilvl));
7513
8053
  let numIdEl = getDirectWordChild2(numPr, "numId");
7514
8054
  if (!numIdEl) {
7515
- numIdEl = ownerDoc.createElementNS(
7516
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
7517
- "w:numId"
7518
- );
8055
+ numIdEl = createWordElement(ownerDoc, "w:numId");
7519
8056
  numPr.appendChild(numIdEl);
7520
8057
  }
7521
8058
  numIdEl.setAttribute("w:val", String(numId));
@@ -7524,8 +8061,8 @@ function enforceListBindingOnParagraphNodes(nodes, options = {}) {
7524
8061
  return updated;
7525
8062
  }
7526
8063
  function getFirstParagraphFromOxml(oxml) {
7527
- const parser = createParser();
7528
- const doc = parser.parseFromString(String(oxml || ""), "application/xml");
8064
+ const doc = parseOoxmlSafe(oxml, "application/xml").doc;
8065
+ if (!doc) return null;
7529
8066
  const parseError = getXmlParseError(doc);
7530
8067
  if (parseError) return null;
7531
8068
  const paragraphs = getDocumentParagraphNodes(doc);
@@ -7551,8 +8088,8 @@ function setElementVal(element, value) {
7551
8088
  element.setAttribute("w:val", String(value));
7552
8089
  }
7553
8090
  function extractFirstParagraphNumIdFromOxml(oxml) {
7554
- const parser = createParser();
7555
- const doc = parser.parseFromString(String(oxml || ""), "application/xml");
8091
+ const doc = parseOoxmlSafe(oxml, "application/xml").doc;
8092
+ if (!doc) return null;
7556
8093
  const parseError = getXmlParseError(doc);
7557
8094
  if (parseError) return null;
7558
8095
  const paragraphs = getDocumentParagraphNodes(doc);
@@ -7568,9 +8105,9 @@ function extractFirstParagraphNumIdFromOxml(oxml) {
7568
8105
  function applyStartOverrideToNumberingXml(numberingXml, targetNumId, startAt, options = {}) {
7569
8106
  if (!numberingXml || !targetNumId || !Number.isInteger(startAt) || startAt < 1) return numberingXml;
7570
8107
  const setAbstractStartOverride = options.setAbstractStartOverride !== false;
7571
- const parser = createParser();
7572
8108
  const serializer = createSerializer();
7573
- const numberingDoc = parser.parseFromString(String(numberingXml || ""), "application/xml");
8109
+ const numberingDoc = parseOoxmlSafe(numberingXml, "application/xml").doc;
8110
+ if (!numberingDoc) return numberingXml;
7574
8111
  const parseError = getXmlParseError(numberingDoc);
7575
8112
  if (parseError) return numberingXml;
7576
8113
  const nums = Array.from(numberingDoc.getElementsByTagNameNS("*", "num"));
@@ -7586,19 +8123,13 @@ function applyStartOverrideToNumberingXml(numberingXml, targetNumId, startAt, op
7586
8123
  return ilvl === 0;
7587
8124
  }) || null;
7588
8125
  if (!lvlOverride) {
7589
- lvlOverride = numberingDoc.createElementNS(
7590
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
7591
- "w:lvlOverride"
7592
- );
8126
+ lvlOverride = createWordElement(numberingDoc, "w:lvlOverride");
7593
8127
  lvlOverride.setAttribute("w:ilvl", "0");
7594
8128
  target.appendChild(lvlOverride);
7595
8129
  }
7596
8130
  let startOverride = Array.from(lvlOverride.getElementsByTagNameNS("*", "startOverride"))[0] || null;
7597
8131
  if (!startOverride) {
7598
- startOverride = numberingDoc.createElementNS(
7599
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
7600
- "w:startOverride"
7601
- );
8132
+ startOverride = createWordElement(numberingDoc, "w:startOverride");
7602
8133
  lvlOverride.appendChild(startOverride);
7603
8134
  }
7604
8135
  setElementVal(startOverride, startAt);
@@ -7614,19 +8145,13 @@ function applyStartOverrideToNumberingXml(numberingXml, targetNumId, startAt, op
7614
8145
  return ilvl === 0;
7615
8146
  }) || null;
7616
8147
  if (!lvl) {
7617
- lvl = numberingDoc.createElementNS(
7618
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
7619
- "w:lvl"
7620
- );
8148
+ lvl = createWordElement(numberingDoc, "w:lvl");
7621
8149
  lvl.setAttribute("w:ilvl", "0");
7622
8150
  abstractNum.appendChild(lvl);
7623
8151
  }
7624
8152
  let startNode = Array.from(lvl.getElementsByTagNameNS("*", "start"))[0] || null;
7625
8153
  if (!startNode) {
7626
- startNode = numberingDoc.createElementNS(
7627
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
7628
- "w:start"
7629
- );
8154
+ startNode = createWordElement(numberingDoc, "w:start");
7630
8155
  lvl.insertBefore(startNode, lvl.firstChild);
7631
8156
  }
7632
8157
  setElementVal(startNode, startAt);
@@ -7718,20 +8243,20 @@ async function executeSingleLineListStructuralFallback(plan, options = {}) {
7718
8243
  // core/redline-validation.js
7719
8244
  var REVISION_ID_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "rPrChange", "pPrChange"]);
7720
8245
  var REVISION_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T/;
7721
- function localNameOf(node) {
8246
+ function localNameOf2(node) {
7722
8247
  return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
7723
8248
  }
7724
8249
  function elementsByLocalName(root, name) {
7725
- return Array.from(root.getElementsByTagName("*")).filter((el) => localNameOf(el) === name);
8250
+ return Array.from(root.getElementsByTagName("*")).filter((el) => localNameOf2(el) === name);
7726
8251
  }
7727
- function wordAttribute(node, name) {
8252
+ function wordAttribute2(node, name) {
7728
8253
  return node.getAttribute(`w:${name}`) || node.getAttribute(name) || "";
7729
8254
  }
7730
8255
  function xmlSpaceAttribute(node) {
7731
8256
  return node.getAttribute("xml:space") || node.getAttribute("space") || node.getAttributeNS?.("http://www.w3.org/XML/1998/namespace", "space") || "";
7732
8257
  }
7733
8258
  function isParagraphMarkRevision(node) {
7734
- return localNameOf(node.parentNode) === "rPr";
8259
+ return localNameOf2(node.parentNode) === "rPr";
7735
8260
  }
7736
8261
  function parseOoxmlForValidation(oxml) {
7737
8262
  const attempt = (xml) => {
@@ -7767,13 +8292,28 @@ function validateRedlineOoxml(oxml) {
7767
8292
  const insElements = elementsByLocalName(doc, "ins");
7768
8293
  const delElements = elementsByLocalName(doc, "del");
7769
8294
  const revisions = insElements.concat(delElements);
8295
+ for (const paragraph of elementsByLocalName(doc, "p")) {
8296
+ const nested = Array.from(paragraph.getElementsByTagName("*")).find((el) => el !== paragraph && localNameOf2(el) === "p");
8297
+ if (nested) {
8298
+ addIssue("NESTED_PARAGRAPH", "error", `<${paragraph.nodeName}> contains nested <${nested.nodeName}>.`);
8299
+ }
8300
+ }
8301
+ for (const body of elementsByLocalName(doc, "body")) {
8302
+ const children = Array.from(body.childNodes || []).filter((child) => child.nodeType === 1);
8303
+ const sectPrIndexes = children.map((child, index) => localNameOf2(child) === "sectPr" ? index : -1).filter((index) => index >= 0);
8304
+ if (sectPrIndexes.length > 1) {
8305
+ addIssue("MULTIPLE_BODY_SECTPR", "error", "<w:body> contains multiple direct <w:sectPr> elements.");
8306
+ } else if (sectPrIndexes.length === 1 && sectPrIndexes[0] !== children.length - 1) {
8307
+ addIssue("SECTPR_NOT_LAST", "error", "<w:sectPr> is not the last element child of <w:body>.");
8308
+ }
8309
+ }
7770
8310
  for (const revision of revisions) {
7771
- const nested = Array.from(revision.getElementsByTagName("*")).filter((el) => el !== revision && ["ins", "del"].includes(localNameOf(el)));
8311
+ const nested = Array.from(revision.getElementsByTagName("*")).filter((el) => el !== revision && ["ins", "del"].includes(localNameOf2(el)));
7772
8312
  if (nested.length > 0) {
7773
8313
  addIssue(
7774
8314
  "NESTED_REVISION",
7775
8315
  "error",
7776
- `<${revision.nodeName}> (w:id="${wordAttribute(revision, "id")}") contains nested <${nested[0].nodeName}>.`
8316
+ `<${revision.nodeName}> (w:id="${wordAttribute2(revision, "id")}") contains nested <${nested[0].nodeName}>.`
7777
8317
  );
7778
8318
  }
7779
8319
  }
@@ -7783,15 +8323,15 @@ function validateRedlineOoxml(oxml) {
7783
8323
  addIssue(
7784
8324
  "DEL_CONTAINS_T",
7785
8325
  "error",
7786
- `<w:del> (w:id="${wordAttribute(del, "id")}") contains <w:t>; deleted text must use <w:delText>.`
8326
+ `<w:del> (w:id="${wordAttribute2(del, "id")}") contains <w:t>; deleted text must use <w:delText>.`
7787
8327
  );
7788
8328
  }
7789
8329
  }
7790
8330
  for (const revision of revisions) {
7791
8331
  const missing = [];
7792
- if (!wordAttribute(revision, "id")) missing.push("w:id");
7793
- if (!wordAttribute(revision, "author")) missing.push("w:author");
7794
- if (!REVISION_DATE_PATTERN.test(wordAttribute(revision, "date"))) missing.push("w:date");
8332
+ if (!wordAttribute2(revision, "id")) missing.push("w:id");
8333
+ if (!wordAttribute2(revision, "author")) missing.push("w:author");
8334
+ if (!REVISION_DATE_PATTERN.test(wordAttribute2(revision, "date"))) missing.push("w:date");
7795
8335
  if (missing.length > 0) {
7796
8336
  addIssue(
7797
8337
  "MISSING_REVISION_METADATA",
@@ -7802,8 +8342,8 @@ function validateRedlineOoxml(oxml) {
7802
8342
  }
7803
8343
  const seenIds = /* @__PURE__ */ new Set();
7804
8344
  for (const node of Array.from(doc.getElementsByTagName("*"))) {
7805
- if (!REVISION_ID_ELEMENTS.has(localNameOf(node))) continue;
7806
- const id = wordAttribute(node, "id");
8345
+ if (!REVISION_ID_ELEMENTS.has(localNameOf2(node))) continue;
8346
+ const id = wordAttribute2(node, "id");
7807
8347
  if (!id) continue;
7808
8348
  if (seenIds.has(id)) {
7809
8349
  addIssue("DUPLICATE_REVISION_ID", "error", `Revision id ${id} appears more than once.`);
@@ -7831,7 +8371,7 @@ function validateRedlineOoxml(oxml) {
7831
8371
  addIssue(
7832
8372
  "EMPTY_REVISION_WRAPPER",
7833
8373
  "warning",
7834
- `<${revision.nodeName}> (w:id="${wordAttribute(revision, "id")}") wraps no content.`
8374
+ `<${revision.nodeName}> (w:id="${wordAttribute2(revision, "id")}") wraps no content.`
7835
8375
  );
7836
8376
  }
7837
8377
  }
@@ -8270,20 +8810,9 @@ function hasParserError(doc) {
8270
8810
  return !!getXmlParseError(doc);
8271
8811
  }
8272
8812
  function parseWordOoxml(ooxml) {
8273
- const source = typeof ooxml === "string" ? ooxml : String(ooxml || "");
8274
- if (!source.trim()) {
8275
- return null;
8276
- }
8277
- try {
8278
- const parser = createParser();
8279
- const doc = parser.parseFromString(source, "application/xml");
8280
- if (hasParserError(doc)) {
8281
- return null;
8282
- }
8283
- return doc;
8284
- } catch {
8285
- return null;
8286
- }
8813
+ const parsed = parseOoxmlSafe(ooxml, "application/xml");
8814
+ if (parsed.error || hasParserError(parsed.doc)) return { ...parsed, doc: null };
8815
+ return parsed;
8287
8816
  }
8288
8817
  function getWordParagraphs(doc) {
8289
8818
  if (!doc) return [];
@@ -8438,25 +8967,37 @@ function paragraphToMarkdown(paragraph) {
8438
8967
  return normalizedInline;
8439
8968
  }
8440
8969
  function ingestWordOoxmlToPlainText(ooxml) {
8441
- const doc = parseWordOoxml(ooxml);
8442
- if (!doc) return "";
8970
+ return ingestWordOoxmlToPlainTextResult(ooxml).text;
8971
+ }
8972
+ function ingestWordOoxmlToPlainTextResult(ooxml) {
8973
+ const parsed = parseWordOoxml(ooxml);
8974
+ if (!parsed.doc) {
8975
+ return { text: "", status: "error", error: parsed.error, warnings: parsed.warnings };
8976
+ }
8977
+ const doc = parsed.doc;
8443
8978
  const paragraphs = getWordParagraphs(doc);
8444
8979
  if (paragraphs.length === 0) {
8445
8980
  const fallback = normalizeInlineWhitespace(doc.documentElement?.textContent || "");
8446
- return fallback;
8981
+ return { text: fallback, status: "ok", warnings: parsed.warnings };
8447
8982
  }
8448
8983
  const lines = paragraphs.map(paragraphToPlainText);
8449
- return lines.join("\n\n").trim();
8984
+ return { text: lines.join("\n\n").trim(), status: "ok", warnings: parsed.warnings };
8450
8985
  }
8451
8986
  function ingestWordOoxmlToMarkdown(ooxml) {
8452
- const doc = parseWordOoxml(ooxml);
8453
- if (!doc) return "";
8987
+ return ingestWordOoxmlToMarkdownResult(ooxml).text;
8988
+ }
8989
+ function ingestWordOoxmlToMarkdownResult(ooxml) {
8990
+ const parsed = parseWordOoxml(ooxml);
8991
+ if (!parsed.doc) {
8992
+ return { text: "", status: "error", error: parsed.error, warnings: parsed.warnings };
8993
+ }
8994
+ const doc = parsed.doc;
8454
8995
  const paragraphs = getWordParagraphs(doc);
8455
8996
  if (paragraphs.length === 0) {
8456
- return "";
8997
+ return { text: "", status: "ok", warnings: parsed.warnings };
8457
8998
  }
8458
8999
  const lines = paragraphs.map(paragraphToMarkdown);
8459
- return lines.join("\n\n").trim();
9000
+ return { text: lines.join("\n\n").trim(), status: "ok", warnings: parsed.warnings };
8460
9001
  }
8461
9002
 
8462
9003
  // services/comment-builders.js
@@ -8628,12 +9169,12 @@ function injectMarkersIntoParagraph(xmlDoc, paragraph, textToFind, commentId, pa
8628
9169
  var PKG_NS = "http://schemas.microsoft.com/office/2006/xmlPackage";
8629
9170
  var RELS_NS = "http://schemas.openxmlformats.org/package/2006/relationships";
8630
9171
  function injectCommentsIntoPackage(packageOxml, commentsXml) {
8631
- const parser = createParser();
8632
9172
  const serializer = createSerializer();
8633
- const pkgDoc = parser.parseFromString(packageOxml, "text/xml");
8634
- const parseError = getXmlParseError(pkgDoc);
8635
- if (parseError) {
8636
- error("[CommentEngine] Failed to parse package:", parseError.textContent);
9173
+ const packageParsed = parseOoxmlSafe(packageOxml, "text/xml");
9174
+ const pkgDoc = packageParsed.doc;
9175
+ const parseError = pkgDoc ? getXmlParseError(pkgDoc) : null;
9176
+ if (packageParsed.error || parseError) {
9177
+ error("[CommentEngine] Failed to parse package:", packageParsed.error?.message || parseError?.textContent);
8637
9178
  return packageOxml;
8638
9179
  }
8639
9180
  const pkgPackage = pkgDoc.documentElement;
@@ -8641,7 +9182,8 @@ function injectCommentsIntoPackage(packageOxml, commentsXml) {
8641
9182
  commentsPart.setAttribute("pkg:name", "/word/comments.xml");
8642
9183
  commentsPart.setAttribute("pkg:contentType", "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml");
8643
9184
  const commentsXmlData = pkgDoc.createElementNS(PKG_NS, "pkg:xmlData");
8644
- const commentsDoc = parser.parseFromString(commentsXml, "text/xml");
9185
+ const commentsDoc = parseOoxmlSafe(commentsXml, "text/xml").doc;
9186
+ if (!commentsDoc) return packageOxml;
8645
9187
  commentsXmlData.appendChild(pkgDoc.importNode(commentsDoc.documentElement, true));
8646
9188
  commentsPart.appendChild(commentsXmlData);
8647
9189
  pkgPackage.appendChild(commentsPart);
@@ -8691,17 +9233,19 @@ function injectCommentsIntoPackage(packageOxml, commentsXml) {
8691
9233
  }
8692
9234
 
8693
9235
  // services/comment-engine.js
8694
- function parseDocumentOxml(oxml, parser, parseFailureWarning) {
8695
- try {
8696
- const xmlDoc = parser.parseFromString(oxml, "text/xml");
8697
- const parseError = getXmlParseError(xmlDoc);
8698
- if (parseError) {
8699
- return { xmlDoc: null, warning: parseFailureWarning(parseError.textContent || "parse error") };
8700
- }
8701
- return { xmlDoc, warning: null };
8702
- } catch (error2) {
8703
- return { xmlDoc: null, warning: parseFailureWarning(error2.message) };
9236
+ function parseDocumentOxml(oxml, parseFailureWarning) {
9237
+ const parsed = parseOoxmlSafe(oxml, "text/xml");
9238
+ const parseError = parsed.doc ? getXmlParseError(parsed.doc) : null;
9239
+ if (parsed.error || parseError) {
9240
+ const message = parsed.error?.message || parseError?.textContent || "parse error";
9241
+ return {
9242
+ xmlDoc: null,
9243
+ warning: parseFailureWarning(message),
9244
+ warnings: parsed.warnings,
9245
+ error: { code: "PARSE_ERROR", message }
9246
+ };
8704
9247
  }
9248
+ return { xmlDoc: parsed.doc, warning: null, warnings: parsed.warnings, error: null };
8705
9249
  }
8706
9250
  function injectCommentsIntoOoxml(oxml, comments, options = {}) {
8707
9251
  const author = options?.author || getDefaultAuthor();
@@ -8711,23 +9255,26 @@ function injectCommentsIntoOoxml(oxml, comments, options = {}) {
8711
9255
  if (!comments || comments.length === 0) {
8712
9256
  return {
8713
9257
  oxml,
9258
+ hasChanges: false,
8714
9259
  commentsApplied: 0,
8715
9260
  warnings: ["No comments to inject"]
8716
9261
  };
8717
9262
  }
8718
- const parser = createParser();
8719
9263
  const serializer = createSerializer();
8720
9264
  const parseResult = parseDocumentOxml(
8721
9265
  oxml,
8722
- parser,
8723
9266
  (warning) => `Failed to parse OXML: ${warning}`
8724
9267
  );
9268
+ warnings.push(...parseResult.warnings || []);
8725
9269
  if (!parseResult.xmlDoc) {
8726
9270
  error("[CommentEngine] Parse failure:", parseResult.warning);
8727
9271
  return {
8728
9272
  oxml,
9273
+ hasChanges: false,
8729
9274
  commentsApplied: 0,
8730
- warnings: [parseResult.warning]
9275
+ status: "error",
9276
+ error: parseResult.error,
9277
+ warnings: [...warnings, parseResult.warning]
8731
9278
  };
8732
9279
  }
8733
9280
  const xmlDoc = parseResult.xmlDoc;
@@ -8786,12 +9333,14 @@ function injectCommentsIntoOoxml(oxml, comments, options = {}) {
8786
9333
  if (placedComments.length === 0) {
8787
9334
  return {
8788
9335
  oxml,
9336
+ hasChanges: false,
8789
9337
  commentsApplied: 0,
8790
9338
  warnings
8791
9339
  };
8792
9340
  }
8793
9341
  return {
8794
9342
  oxml: serializer.serializeToString(xmlDoc),
9343
+ hasChanges: true,
8795
9344
  commentsXml: buildCommentsPartXml(placedComments),
8796
9345
  commentsApplied: placedComments.length,
8797
9346
  warnings
@@ -8802,6 +9351,11 @@ function injectCommentsIntoPackage2(packageOxml, commentsXml) {
8802
9351
  }
8803
9352
 
8804
9353
  // engine/formatting-removal.js
9354
+ function removeNode2(node) {
9355
+ if (node?.parentNode) {
9356
+ node.parentNode.removeChild(node);
9357
+ }
9358
+ }
8805
9359
  function removeFormattingFromRPr(rPr, formatTypes = ["all"]) {
8806
9360
  if (!rPr) return null;
8807
9361
  const rPrClone = rPr.cloneNode(true);
@@ -8825,7 +9379,7 @@ function removeFormattingFromRPr(rPr, formatTypes = ["all"]) {
8825
9379
  ];
8826
9380
  toRemove.forEach((tag) => {
8827
9381
  const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace("w:", "")}`);
8828
- elements.forEach((el) => el.remove());
9382
+ elements.forEach(removeNode2);
8829
9383
  });
8830
9384
  } else {
8831
9385
  const tagMap = {
@@ -8847,7 +9401,7 @@ function removeFormattingFromRPr(rPr, formatTypes = ["all"]) {
8847
9401
  const tag = tagMap[type];
8848
9402
  if (tag) {
8849
9403
  const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace("w:", "")}`);
8850
- elements.forEach((el) => el.remove());
9404
+ elements.forEach(removeNode2);
8851
9405
  }
8852
9406
  });
8853
9407
  }
@@ -8856,6 +9410,7 @@ function removeFormattingFromRPr(rPr, formatTypes = ["all"]) {
8856
9410
  function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
8857
9411
  if (!targetText || !ooxmlString) return ooxmlString;
8858
9412
  const doc = parseOoxml(ooxmlString);
9413
+ if (!doc) return ooxmlString;
8859
9414
  const NS_W7 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
8860
9415
  const runs = doc.getElementsByTagNameNS(NS_W7, "r");
8861
9416
  const insertions = doc.getElementsByTagNameNS(NS_W7, "ins");
@@ -8877,7 +9432,7 @@ function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
8877
9432
  rPr.parentNode.replaceChild(newRPr, rPr);
8878
9433
  }
8879
9434
  } else {
8880
- rPr.remove();
9435
+ removeNode2(rPr);
8881
9436
  }
8882
9437
  }
8883
9438
  }
@@ -8923,19 +9478,19 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
8923
9478
  });
8924
9479
  }
8925
9480
  const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W7, "highlight");
8926
- Array.from(existingHighlight).forEach((el) => el.remove());
9481
+ Array.from(existingHighlight).forEach(removeNode2);
8927
9482
  const highlightEl = createWordElement(doc, "w:highlight");
8928
9483
  highlightEl.setAttributeNS(NS_W7, "w:val", ooxmlColor);
8929
9484
  rPrElement.appendChild(highlightEl);
8930
9485
  if (generateRedlines && previousRPrState) {
8931
9486
  const rPrChange = createWordElement(doc, "w:rPrChange");
8932
- const metadata = createRevisionMetadata(author);
9487
+ const metadata = createRevisionMetadata(author, doc);
8933
9488
  rPrChange.setAttribute("w:id", String(metadata.id));
8934
9489
  rPrChange.setAttribute("w:author", metadata.author);
8935
9490
  rPrChange.setAttribute("w:date", metadata.date);
8936
9491
  rPrChange.appendChild(previousRPrState);
8937
9492
  const existingChange = rPrElement.getElementsByTagNameNS(NS_W7, "rPrChange");
8938
- Array.from(existingChange).forEach((el) => el.remove());
9493
+ Array.from(existingChange).forEach(removeNode2);
8939
9494
  rPrElement.appendChild(rPrChange);
8940
9495
  }
8941
9496
  return rPrElement;
@@ -8943,64 +9498,70 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
8943
9498
  function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", options = {}) {
8944
9499
  if (!targetText || !ooxmlString) return ooxmlString;
8945
9500
  const doc = parseOoxml(ooxmlString);
9501
+ if (!doc) return ooxmlString;
9502
+ if (options?._revisionIdAllocator instanceof RevisionIdAllocator) {
9503
+ seedRevisionIdsFromDocument(doc, options._revisionIdAllocator);
9504
+ } else {
9505
+ createRevisionIdAllocator(doc);
9506
+ }
8946
9507
  const NS_W7 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
8947
9508
  const getRunText = (run) => {
8948
9509
  const textNodes = run.getElementsByTagNameNS(NS_W7, "t");
8949
9510
  return Array.from(textNodes).map((t) => t.textContent).join("");
8950
9511
  };
8951
9512
  const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W7, "r"));
8952
- for (let i = 0; i < allRuns.length; i++) {
8953
- const run = allRuns[i];
9513
+ const cloneRunWithText2 = (sourceRun, text, shouldHighlight) => {
9514
+ const clonedRun = sourceRun.cloneNode(true);
9515
+ const textNodes = clonedRun.getElementsByTagNameNS(NS_W7, "t");
9516
+ Array.from(textNodes).forEach(removeNode2);
9517
+ const newText = createWordElement(doc, "w:t");
9518
+ newText.setAttribute("xml:space", "preserve");
9519
+ newText.textContent = text;
9520
+ clonedRun.appendChild(newText);
9521
+ if (shouldHighlight) {
9522
+ const rPrElements = clonedRun.getElementsByTagNameNS(NS_W7, "rPr");
9523
+ const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
9524
+ const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
9525
+ if (existingRPr) {
9526
+ clonedRun.replaceChild(newRPr, existingRPr);
9527
+ } else {
9528
+ clonedRun.insertBefore(newRPr, clonedRun.firstChild);
9529
+ }
9530
+ }
9531
+ return clonedRun;
9532
+ };
9533
+ for (const run of allRuns) {
8954
9534
  const runText = getRunText(run);
8955
9535
  if (!runText) continue;
8956
- const matchIndex = runText.indexOf(targetText);
8957
- if (matchIndex === -1) continue;
9536
+ const matchIndexes = [];
9537
+ let searchOffset = 0;
9538
+ while (searchOffset <= runText.length - targetText.length) {
9539
+ const matchIndex = runText.indexOf(targetText, searchOffset);
9540
+ if (matchIndex === -1) break;
9541
+ matchIndexes.push(matchIndex);
9542
+ searchOffset = matchIndex + targetText.length;
9543
+ }
9544
+ if (matchIndexes.length === 0) continue;
8958
9545
  const parent = run.parentNode;
8959
9546
  if (!parent) {
8960
9547
  console.warn("[Highlight] Run parent is null; skipping. Likely already processed.");
8961
9548
  continue;
8962
9549
  }
8963
- const prefixText = runText.substring(0, matchIndex);
8964
- const matchText = runText.substring(matchIndex, matchIndex + targetText.length);
8965
- const suffixText = runText.substring(matchIndex + targetText.length);
8966
9550
  const fragment = doc.createDocumentFragment();
8967
- if (prefixText.length > 0) {
8968
- const prefixRun = run.cloneNode(true);
8969
- const tNodes = prefixRun.getElementsByTagNameNS(NS_W7, "t");
8970
- Array.from(tNodes).forEach((t) => t.remove());
8971
- const newT = createWordElement(doc, "w:t");
8972
- newT.setAttribute("xml:space", "preserve");
8973
- newT.textContent = prefixText;
8974
- prefixRun.appendChild(newT);
8975
- fragment.appendChild(prefixRun);
8976
- }
8977
- if (matchText.length > 0) {
8978
- const matchRun = run.cloneNode(true);
8979
- const tNodes = matchRun.getElementsByTagNameNS(NS_W7, "t");
8980
- Array.from(tNodes).forEach((t) => t.remove());
8981
- const newT = createWordElement(doc, "w:t");
8982
- newT.setAttribute("xml:space", "preserve");
8983
- newT.textContent = matchText;
8984
- matchRun.appendChild(newT);
8985
- const rPrElements = matchRun.getElementsByTagNameNS(NS_W7, "rPr");
8986
- const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
8987
- const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
8988
- if (existingRPr) {
8989
- matchRun.replaceChild(newRPr, existingRPr);
8990
- } else {
8991
- matchRun.insertBefore(newRPr, matchRun.firstChild);
9551
+ let cursor = 0;
9552
+ for (const matchIndex of matchIndexes) {
9553
+ if (matchIndex > cursor) {
9554
+ fragment.appendChild(cloneRunWithText2(run, runText.slice(cursor, matchIndex), false));
8992
9555
  }
8993
- fragment.appendChild(matchRun);
9556
+ fragment.appendChild(cloneRunWithText2(
9557
+ run,
9558
+ runText.slice(matchIndex, matchIndex + targetText.length),
9559
+ true
9560
+ ));
9561
+ cursor = matchIndex + targetText.length;
8994
9562
  }
8995
- if (suffixText.length > 0) {
8996
- const suffixRun = run.cloneNode(true);
8997
- const tNodes = suffixRun.getElementsByTagNameNS(NS_W7, "t");
8998
- Array.from(tNodes).forEach((t) => t.remove());
8999
- const newT = createWordElement(doc, "w:t");
9000
- newT.setAttribute("xml:space", "preserve");
9001
- newT.textContent = suffixText;
9002
- suffixRun.appendChild(newT);
9003
- fragment.appendChild(suffixRun);
9563
+ if (cursor < runText.length) {
9564
+ fragment.appendChild(cloneRunWithText2(run, runText.slice(cursor), false));
9004
9565
  }
9005
9566
  parent.replaceChild(fragment, run);
9006
9567
  }
@@ -9021,13 +9582,13 @@ var COMMENTS_PATH = "word/comments.xml";
9021
9582
  var CONTENT_TYPES_PATH = "[Content_Types].xml";
9022
9583
  var DOCUMENT_RELS_PATH = "word/_rels/document.xml.rels";
9023
9584
  function parseXmlStrictStandalone(xmlText, label = "xml") {
9024
- const parser = createParser();
9025
- const xmlDoc = parser.parseFromString(xmlText, "application/xml");
9026
- const parseError = xmlDoc.getElementsByTagName("parsererror")[0];
9027
- if (parseError) {
9028
- throw new Error(`[XML parse error] ${label}: ${parseError.textContent || "Unknown"}`);
9585
+ const parsed = parseOoxmlSafe(xmlText, "application/xml");
9586
+ if (parsed.error || !parsed.doc) {
9587
+ const error2 = new Error(`[XML parse error] ${label}: ${parsed.error?.message || "Unknown"}`);
9588
+ error2.code = "PARSE_ERROR";
9589
+ throw error2;
9029
9590
  }
9030
- return xmlDoc;
9591
+ return parsed.doc;
9031
9592
  }
9032
9593
  function isSectionPropertiesElement(node) {
9033
9594
  return !!node && node.nodeType === 1 && node.namespaceURI === NS_W6 && node.localName === "sectPr";
@@ -9091,9 +9652,8 @@ function getPackagePartName(partElement) {
9091
9652
  return partElement.getAttribute("pkg:name") || partElement.getAttribute("name") || "";
9092
9653
  }
9093
9654
  function extractFromPackageXml(packageXml) {
9094
- const parser = createParser();
9095
9655
  const serializer = createSerializer();
9096
- const pkgDoc = parser.parseFromString(packageXml, "application/xml");
9656
+ const pkgDoc = parseXmlStrictStandalone(packageXml, "package OOXML");
9097
9657
  const parts = Array.from(pkgDoc.getElementsByTagNameNS("*", "part"));
9098
9658
  const documentPart = parts.find((part) => getPackagePartName(part) === "/word/document.xml");
9099
9659
  if (!documentPart) {
@@ -9126,23 +9686,37 @@ function extractFromPackageXml(packageXml) {
9126
9686
  }
9127
9687
  function extractReplacementNodesFromOoxml(outputOxml) {
9128
9688
  if (typeof outputOxml !== "string" || !outputOxml.trim()) {
9129
- throw new Error("Reconciliation engine returned no OOXML payload for this operation");
9130
- }
9131
- if (outputOxml.includes("<pkg:package")) {
9132
- return extractFromPackageXml(outputOxml);
9689
+ return {
9690
+ replacementNodes: [],
9691
+ numberingXml: null,
9692
+ sourceType: "fragment",
9693
+ status: "error",
9694
+ error: { code: "PARSE_ERROR", message: "Reconciliation engine returned no OOXML payload for this operation" }
9695
+ };
9133
9696
  }
9134
- if (outputOxml.includes("<w:document")) {
9135
- const parser2 = createParser();
9136
- const doc = parser2.parseFromString(outputOxml, "application/xml");
9137
- const body = doc.getElementsByTagNameNS("*", "body")[0];
9138
- const replacementNodes2 = body ? Array.from(body.childNodes || []).filter((node) => node.nodeType === 1 && !isSectionPropertiesElement(node)) : Array.from(doc.childNodes || []).filter((node) => node.nodeType === 1);
9139
- return { replacementNodes: replacementNodes2, numberingXml: null, sourceType: "document" };
9697
+ try {
9698
+ if (outputOxml.includes("<pkg:package")) {
9699
+ return extractFromPackageXml(outputOxml);
9700
+ }
9701
+ if (outputOxml.includes("<w:document")) {
9702
+ const doc = parseXmlStrictStandalone(outputOxml, "document OOXML");
9703
+ const body = doc.getElementsByTagNameNS("*", "body")[0];
9704
+ const replacementNodes2 = body ? Array.from(body.childNodes || []).filter((node) => node.nodeType === 1 && !isSectionPropertiesElement(node)) : Array.from(doc.childNodes || []).filter((node) => node.nodeType === 1);
9705
+ return { replacementNodes: replacementNodes2, numberingXml: null, sourceType: "document" };
9706
+ }
9707
+ const wrapped = `<root xmlns:w="${NS_W6}">${outputOxml}</root>`;
9708
+ const fragmentDoc = parseXmlStrictStandalone(wrapped, "OOXML fragment");
9709
+ const replacementNodes = Array.from(fragmentDoc.documentElement.childNodes || []).filter((node) => node.nodeType === 1);
9710
+ return { replacementNodes, numberingXml: null, sourceType: "fragment" };
9711
+ } catch (caught) {
9712
+ return {
9713
+ replacementNodes: [],
9714
+ numberingXml: null,
9715
+ sourceType: "fragment",
9716
+ status: "error",
9717
+ error: { code: "PARSE_ERROR", message: caught?.message || "Could not parse OOXML payload." }
9718
+ };
9140
9719
  }
9141
- const wrapped = `<root xmlns:w="${NS_W6}">${outputOxml}</root>`;
9142
- const parser = createParser();
9143
- const fragmentDoc = parser.parseFromString(wrapped, "application/xml");
9144
- const replacementNodes = Array.from(fragmentDoc.documentElement.childNodes || []).filter((node) => node.nodeType === 1);
9145
- return { replacementNodes, numberingXml: null, sourceType: "fragment" };
9146
9720
  }
9147
9721
  function upsertContentTypeOverride(ctDoc, partName, contentType) {
9148
9722
  const overrides = Array.from(ctDoc.getElementsByTagNameNS("*", "Override"));
@@ -9202,18 +9776,17 @@ async function ensureNumberingArtifactsInZip(zip, numberingXmlList, options = {}
9202
9776
  onInfo("[Demo] Merging numbering.xml payload(s) into existing numbering definitions");
9203
9777
  }
9204
9778
  zip.file(NUMBERING_PATH, mergedNumberingXml);
9205
- const parser = createParser();
9206
9779
  const serializer = createSerializer();
9207
9780
  const ctText = await readZipText(zip, CONTENT_TYPES_PATH);
9208
9781
  if (ctText) {
9209
- const ctDoc = parser.parseFromString(ctText, "application/xml");
9782
+ const ctDoc = parseXmlStrictStandalone(ctText, CONTENT_TYPES_PATH);
9210
9783
  if (upsertContentTypeOverride(ctDoc, "/word/numbering.xml", NUMBERING_CONTENT_TYPE)) {
9211
9784
  zip.file(CONTENT_TYPES_PATH, serializer.serializeToString(ctDoc));
9212
9785
  }
9213
9786
  }
9214
9787
  const relsText = await readZipText(zip, DOCUMENT_RELS_PATH);
9215
9788
  if (relsText) {
9216
- const relsDoc = parser.parseFromString(relsText, "application/xml");
9789
+ const relsDoc = parseXmlStrictStandalone(relsText, DOCUMENT_RELS_PATH);
9217
9790
  if (upsertDocumentRelationship(relsDoc, NUMBERING_REL_TYPE, "numbering.xml")) {
9218
9791
  zip.file(DOCUMENT_RELS_PATH, serializer.serializeToString(relsDoc));
9219
9792
  }
@@ -9223,7 +9796,6 @@ async function ensureCommentsArtifactsInZip(zip, commentsXml, options = {}) {
9223
9796
  const onInfo = typeof options?.onInfo === "function" ? options.onInfo : () => {
9224
9797
  };
9225
9798
  if (!commentsXml) return;
9226
- const parser = createParser();
9227
9799
  const serializer = createSerializer();
9228
9800
  const existingText = await readZipText(zip, COMMENTS_PATH);
9229
9801
  if (!existingText) {
@@ -9247,14 +9819,14 @@ async function ensureCommentsArtifactsInZip(zip, commentsXml, options = {}) {
9247
9819
  }
9248
9820
  const ctText = await readZipText(zip, CONTENT_TYPES_PATH);
9249
9821
  if (ctText) {
9250
- const ctDoc = parser.parseFromString(ctText, "application/xml");
9822
+ const ctDoc = parseXmlStrictStandalone(ctText, CONTENT_TYPES_PATH);
9251
9823
  if (upsertContentTypeOverride(ctDoc, "/word/comments.xml", COMMENTS_CONTENT_TYPE)) {
9252
9824
  zip.file(CONTENT_TYPES_PATH, serializer.serializeToString(ctDoc));
9253
9825
  }
9254
9826
  }
9255
9827
  const relsText = await readZipText(zip, DOCUMENT_RELS_PATH);
9256
9828
  if (relsText) {
9257
- const relsDoc = parser.parseFromString(relsText, "application/xml");
9829
+ const relsDoc = parseXmlStrictStandalone(relsText, DOCUMENT_RELS_PATH);
9258
9830
  if (upsertDocumentRelationship(relsDoc, COMMENTS_REL_TYPE, "comments.xml")) {
9259
9831
  zip.file(DOCUMENT_RELS_PATH, serializer.serializeToString(relsDoc));
9260
9832
  }
@@ -9517,10 +10089,12 @@ async function reconcileMarkdownTableOoxml(oxml, originalText, markdownTable, op
9517
10089
  }
9518
10090
  async function applyRedlineToOxmlWithListFallback(oxml, originalText, modifiedText, options = {}) {
9519
10091
  const allowExistingListForFallback = options.listFallbackAllowExistingList !== false;
10092
+ const fallbackModifiedText = options.sanitizeInput === true ? sanitizeAiResponse(modifiedText) : modifiedText;
10093
+ const sanitizationWarnings = fallbackModifiedText !== modifiedText ? ["Input was sanitized; pass sanitizeInput: false to disable."] : [];
9520
10094
  const plan = buildSingleLineListStructuralFallbackPlan({
9521
10095
  oxml,
9522
10096
  originalText,
9523
- modifiedText,
10097
+ modifiedText: fallbackModifiedText,
9524
10098
  allowExistingList: allowExistingListForFallback
9525
10099
  });
9526
10100
  const preferListFallback = options.preferListStructuralFallback !== false;
@@ -9540,7 +10114,7 @@ async function applyRedlineToOxmlWithListFallback(oxml, originalText, modifiedTe
9540
10114
  return withOoxmlSourceType({
9541
10115
  oxml: wrappedOxml2,
9542
10116
  hasChanges: true,
9543
- warnings: fallbackWarnings2,
10117
+ warnings: [...sanitizationWarnings, ...fallbackWarnings2],
9544
10118
  listStructuralFallbackApplied: true,
9545
10119
  listStructuralFallbackKey: fallbackResult2.listStructuralFallbackKey || null,
9546
10120
  listStructuralFallbackNumberingXml: fallbackResult2.numberingXml || null
@@ -9659,7 +10233,9 @@ export {
9659
10233
  inferTableReplacementParagraphBlock,
9660
10234
  ingestOoxml,
9661
10235
  ingestWordOoxmlToMarkdown,
10236
+ ingestWordOoxmlToMarkdownResult,
9662
10237
  ingestWordOoxmlToPlainText,
10238
+ ingestWordOoxmlToPlainTextResult,
9663
10239
  injectCommentsIntoOoxml,
9664
10240
  injectCommentsIntoPackage2 as injectCommentsIntoPackage,
9665
10241
  insertBodyElementBeforeSectPr,
@@ -9673,8 +10249,8 @@ export {
9673
10249
  overwriteParagraphNumIds,
9674
10250
  parseMarkdownListContent,
9675
10251
  parseOoxml,
10252
+ parseOoxmlSafe,
9676
10253
  parseParagraphReference,
9677
- parseXmlStrictStandalone,
9678
10254
  planListInsertionOnlyEdit,
9679
10255
  preprocessMarkdown,
9680
10256
  reconcileMarkdownTableOoxml,