@ansonlai/docx-redline-js 0.2.1 → 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 (86) 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 +1124 -545
  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 +84 -99
  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 +3 -3
  36. package/engine/surgical-mode.js +3 -2
  37. package/engine/table-mode.js +27 -16
  38. package/index.d.ts +95 -3
  39. package/index.js +14 -13
  40. package/orchestration/list-structural-fallback.js +16 -39
  41. package/package.json +22 -4
  42. package/pipeline/diff-engine.js +174 -55
  43. package/pipeline/ingestion-export.js +39 -24
  44. package/pipeline/ingestion-paragraph.js +7 -5
  45. package/pipeline/list-generation.js +27 -18
  46. package/pipeline/patching.js +2 -3
  47. package/pipeline/pipeline.js +65 -36
  48. package/pipeline/serialization.js +13 -5
  49. package/scripts/build-test-dashboard.mjs +43 -0
  50. package/scripts/check-types.mjs +16 -24
  51. package/scripts/export-validation-fixtures.mjs +191 -45
  52. package/scripts/fetch-superdoc-corpus.mjs +61 -0
  53. package/scripts/generate-test-dashboard.mjs +199 -0
  54. package/scripts/inspect-visual-evidence.mjs +271 -0
  55. package/scripts/lib/minimal-zip.mjs +199 -18
  56. package/scripts/lib/word-coverage-catalogue.mjs +207 -0
  57. package/scripts/lib/word-coverage-metadata.mjs +93 -0
  58. package/scripts/lib/zip-reader.mjs +64 -0
  59. package/scripts/package-superdoc-word-fixtures.ps1 +64 -0
  60. package/scripts/prepare-corpus-word-visual-review.mjs +84 -0
  61. package/scripts/prepare-superdoc-word-corpus.mjs +284 -0
  62. package/scripts/prepare-word-review.mjs +77 -0
  63. package/scripts/prepare-word-visual-review.mjs +90 -0
  64. package/scripts/render-agenda-multilevel.mjs +70 -0
  65. package/scripts/render-case22.mjs +73 -0
  66. package/scripts/render-case40.ps1 +35 -0
  67. package/scripts/render-multilevel-bullet-images.py +58 -0
  68. package/scripts/render-multilevel-bullet-visual.ps1 +32 -0
  69. package/scripts/render-multilevel-cases.mjs +80 -0
  70. package/scripts/report-coverage-gaps.mjs +103 -0
  71. package/scripts/report-word-coverage.mjs +71 -0
  72. package/scripts/sample-multimodal-visual-check.mjs +221 -0
  73. package/scripts/test-multilevel-bullet-visual.mjs +187 -0
  74. package/scripts/word-com-corpus-suite.ps1 +43 -0
  75. package/scripts/word-com-corpus-visual-suite.ps1 +116 -0
  76. package/scripts/word-com-differential.ps1 +158 -16
  77. package/scripts/word-com-suite.ps1 +19 -0
  78. package/scripts/word-com-visual-suite.ps1 +132 -0
  79. package/services/comment-engine.js +51 -46
  80. package/services/comment-locator.js +0 -1
  81. package/services/comment-package.js +11 -10
  82. package/services/numbering-service.js +1 -1
  83. package/services/revision-comment-management.js +31 -10
  84. package/services/standalone-docx-plumbing.js +45 -34
  85. package/services/standalone-operation-runner.js +315 -75
  86. package/services/table-reconciliation.js +23 -11
@@ -1,4 +1,4 @@
1
- // @ansonlai/docx-redline-js v0.2.1 — 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;
@@ -32,7 +32,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
32
32
  // node_modules/diff-match-patch/index.js
33
33
  var require_diff_match_patch = __commonJS({
34
34
  "node_modules/diff-match-patch/index.js"(exports, module) {
35
- var diff_match_patch2 = function() {
35
+ var diff_match_patch3 = function() {
36
36
  this.Diff_Timeout = 1;
37
37
  this.Diff_EditCost = 4;
38
38
  this.Match_Threshold = 0.5;
@@ -44,10 +44,10 @@ var require_diff_match_patch = __commonJS({
44
44
  var DIFF_DELETE = -1;
45
45
  var DIFF_INSERT = 1;
46
46
  var DIFF_EQUAL = 0;
47
- diff_match_patch2.Diff = function(op, text) {
47
+ diff_match_patch3.Diff = function(op, text) {
48
48
  return [op, text];
49
49
  };
50
- 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) {
51
51
  if (typeof opt_deadline == "undefined") {
52
52
  if (this.Diff_Timeout <= 0) {
53
53
  opt_deadline = Number.MAX_VALUE;
@@ -61,7 +61,7 @@ var require_diff_match_patch = __commonJS({
61
61
  }
62
62
  if (text1 == text2) {
63
63
  if (text1) {
64
- return [new diff_match_patch2.Diff(DIFF_EQUAL, text1)];
64
+ return [new diff_match_patch3.Diff(DIFF_EQUAL, text1)];
65
65
  }
66
66
  return [];
67
67
  }
@@ -79,30 +79,30 @@ var require_diff_match_patch = __commonJS({
79
79
  text2 = text2.substring(0, text2.length - commonlength);
80
80
  var diffs = this.diff_compute_(text1, text2, checklines, deadline);
81
81
  if (commonprefix) {
82
- diffs.unshift(new diff_match_patch2.Diff(DIFF_EQUAL, commonprefix));
82
+ diffs.unshift(new diff_match_patch3.Diff(DIFF_EQUAL, commonprefix));
83
83
  }
84
84
  if (commonsuffix) {
85
- diffs.push(new diff_match_patch2.Diff(DIFF_EQUAL, commonsuffix));
85
+ diffs.push(new diff_match_patch3.Diff(DIFF_EQUAL, commonsuffix));
86
86
  }
87
87
  this.diff_cleanupMerge(diffs);
88
88
  return diffs;
89
89
  };
90
- diff_match_patch2.prototype.diff_compute_ = function(text1, text2, checklines, deadline) {
90
+ diff_match_patch3.prototype.diff_compute_ = function(text1, text2, checklines, deadline) {
91
91
  var diffs;
92
92
  if (!text1) {
93
- return [new diff_match_patch2.Diff(DIFF_INSERT, text2)];
93
+ return [new diff_match_patch3.Diff(DIFF_INSERT, text2)];
94
94
  }
95
95
  if (!text2) {
96
- return [new diff_match_patch2.Diff(DIFF_DELETE, text1)];
96
+ return [new diff_match_patch3.Diff(DIFF_DELETE, text1)];
97
97
  }
98
98
  var longtext = text1.length > text2.length ? text1 : text2;
99
99
  var shorttext = text1.length > text2.length ? text2 : text1;
100
100
  var i = longtext.indexOf(shorttext);
101
101
  if (i != -1) {
102
102
  diffs = [
103
- new diff_match_patch2.Diff(DIFF_INSERT, longtext.substring(0, i)),
104
- new diff_match_patch2.Diff(DIFF_EQUAL, shorttext),
105
- 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(
106
106
  DIFF_INSERT,
107
107
  longtext.substring(i + shorttext.length)
108
108
  )
@@ -114,8 +114,8 @@ var require_diff_match_patch = __commonJS({
114
114
  }
115
115
  if (shorttext.length == 1) {
116
116
  return [
117
- new diff_match_patch2.Diff(DIFF_DELETE, text1),
118
- 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)
119
119
  ];
120
120
  }
121
121
  var hm = this.diff_halfMatch_(text1, text2);
@@ -128,7 +128,7 @@ var require_diff_match_patch = __commonJS({
128
128
  var diffs_a = this.diff_main(text1_a, text2_a, checklines, deadline);
129
129
  var diffs_b = this.diff_main(text1_b, text2_b, checklines, deadline);
130
130
  return diffs_a.concat(
131
- [new diff_match_patch2.Diff(DIFF_EQUAL, mid_common)],
131
+ [new diff_match_patch3.Diff(DIFF_EQUAL, mid_common)],
132
132
  diffs_b
133
133
  );
134
134
  }
@@ -137,7 +137,7 @@ var require_diff_match_patch = __commonJS({
137
137
  }
138
138
  return this.diff_bisect_(text1, text2, deadline);
139
139
  };
140
- diff_match_patch2.prototype.diff_lineMode_ = function(text1, text2, deadline) {
140
+ diff_match_patch3.prototype.diff_lineMode_ = function(text1, text2, deadline) {
141
141
  var a = this.diff_linesToChars_(text1, text2);
142
142
  text1 = a.chars1;
143
143
  text2 = a.chars2;
@@ -145,7 +145,7 @@ var require_diff_match_patch = __commonJS({
145
145
  var diffs = this.diff_main(text1, text2, false, deadline);
146
146
  this.diff_charsToLines_(diffs, linearray);
147
147
  this.diff_cleanupSemantic(diffs);
148
- diffs.push(new diff_match_patch2.Diff(DIFF_EQUAL, ""));
148
+ diffs.push(new diff_match_patch3.Diff(DIFF_EQUAL, ""));
149
149
  var pointer = 0;
150
150
  var count_delete = 0;
151
151
  var count_insert = 0;
@@ -185,7 +185,7 @@ var require_diff_match_patch = __commonJS({
185
185
  diffs.pop();
186
186
  return diffs;
187
187
  };
188
- diff_match_patch2.prototype.diff_bisect_ = function(text1, text2, deadline) {
188
+ diff_match_patch3.prototype.diff_bisect_ = function(text1, text2, deadline) {
189
189
  var text1_length = text1.length;
190
190
  var text2_length = text2.length;
191
191
  var max_d = Math.ceil((text1_length + text2_length) / 2);
@@ -269,11 +269,11 @@ var require_diff_match_patch = __commonJS({
269
269
  }
270
270
  }
271
271
  return [
272
- new diff_match_patch2.Diff(DIFF_DELETE, text1),
273
- 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)
274
274
  ];
275
275
  };
276
- 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) {
277
277
  var text1a = text1.substring(0, x);
278
278
  var text2a = text2.substring(0, y);
279
279
  var text1b = text1.substring(x);
@@ -282,7 +282,7 @@ var require_diff_match_patch = __commonJS({
282
282
  var diffsb = this.diff_main(text1b, text2b, false, deadline);
283
283
  return diffs.concat(diffsb);
284
284
  };
285
- diff_match_patch2.prototype.diff_linesToChars_ = function(text1, text2) {
285
+ diff_match_patch3.prototype.diff_linesToChars_ = function(text1, text2) {
286
286
  var lineArray = [];
287
287
  var lineHash = {};
288
288
  lineArray[0] = "";
@@ -318,7 +318,7 @@ var require_diff_match_patch = __commonJS({
318
318
  var chars2 = diff_linesToCharsMunge_(text2);
319
319
  return { chars1, chars2, lineArray };
320
320
  };
321
- diff_match_patch2.prototype.diff_charsToLines_ = function(diffs, lineArray) {
321
+ diff_match_patch3.prototype.diff_charsToLines_ = function(diffs, lineArray) {
322
322
  for (var i = 0; i < diffs.length; i++) {
323
323
  var chars = diffs[i][1];
324
324
  var text = [];
@@ -328,7 +328,7 @@ var require_diff_match_patch = __commonJS({
328
328
  diffs[i][1] = text.join("");
329
329
  }
330
330
  };
331
- diff_match_patch2.prototype.diff_commonPrefix = function(text1, text2) {
331
+ diff_match_patch3.prototype.diff_commonPrefix = function(text1, text2) {
332
332
  if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) {
333
333
  return 0;
334
334
  }
@@ -347,7 +347,7 @@ var require_diff_match_patch = __commonJS({
347
347
  }
348
348
  return pointermid;
349
349
  };
350
- diff_match_patch2.prototype.diff_commonSuffix = function(text1, text2) {
350
+ diff_match_patch3.prototype.diff_commonSuffix = function(text1, text2) {
351
351
  if (!text1 || !text2 || text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1)) {
352
352
  return 0;
353
353
  }
@@ -366,7 +366,7 @@ var require_diff_match_patch = __commonJS({
366
366
  }
367
367
  return pointermid;
368
368
  };
369
- diff_match_patch2.prototype.diff_commonOverlap_ = function(text1, text2) {
369
+ diff_match_patch3.prototype.diff_commonOverlap_ = function(text1, text2) {
370
370
  var text1_length = text1.length;
371
371
  var text2_length = text2.length;
372
372
  if (text1_length == 0 || text2_length == 0) {
@@ -396,7 +396,7 @@ var require_diff_match_patch = __commonJS({
396
396
  }
397
397
  }
398
398
  };
399
- diff_match_patch2.prototype.diff_halfMatch_ = function(text1, text2) {
399
+ diff_match_patch3.prototype.diff_halfMatch_ = function(text1, text2) {
400
400
  if (this.Diff_Timeout <= 0) {
401
401
  return null;
402
402
  }
@@ -475,7 +475,7 @@ var require_diff_match_patch = __commonJS({
475
475
  var mid_common = hm[4];
476
476
  return [text1_a, text1_b, text2_a, text2_b, mid_common];
477
477
  };
478
- diff_match_patch2.prototype.diff_cleanupSemantic = function(diffs) {
478
+ diff_match_patch3.prototype.diff_cleanupSemantic = function(diffs) {
479
479
  var changes = false;
480
480
  var equalities = [];
481
481
  var equalitiesLength = 0;
@@ -506,7 +506,7 @@ var require_diff_match_patch = __commonJS({
506
506
  diffs.splice(
507
507
  equalities[equalitiesLength - 1],
508
508
  0,
509
- new diff_match_patch2.Diff(DIFF_DELETE, lastEquality)
509
+ new diff_match_patch3.Diff(DIFF_DELETE, lastEquality)
510
510
  );
511
511
  diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
512
512
  equalitiesLength--;
@@ -535,7 +535,7 @@ var require_diff_match_patch = __commonJS({
535
535
  var overlap_length2 = this.diff_commonOverlap_(insertion, deletion);
536
536
  if (overlap_length1 >= overlap_length2) {
537
537
  if (overlap_length1 >= deletion.length / 2 || overlap_length1 >= insertion.length / 2) {
538
- diffs.splice(pointer, 0, new diff_match_patch2.Diff(
538
+ diffs.splice(pointer, 0, new diff_match_patch3.Diff(
539
539
  DIFF_EQUAL,
540
540
  insertion.substring(0, overlap_length1)
541
541
  ));
@@ -545,7 +545,7 @@ var require_diff_match_patch = __commonJS({
545
545
  }
546
546
  } else {
547
547
  if (overlap_length2 >= deletion.length / 2 || overlap_length2 >= insertion.length / 2) {
548
- diffs.splice(pointer, 0, new diff_match_patch2.Diff(
548
+ diffs.splice(pointer, 0, new diff_match_patch3.Diff(
549
549
  DIFF_EQUAL,
550
550
  deletion.substring(0, overlap_length2)
551
551
  ));
@@ -561,21 +561,21 @@ var require_diff_match_patch = __commonJS({
561
561
  pointer++;
562
562
  }
563
563
  };
564
- diff_match_patch2.prototype.diff_cleanupSemanticLossless = function(diffs) {
564
+ diff_match_patch3.prototype.diff_cleanupSemanticLossless = function(diffs) {
565
565
  function diff_cleanupSemanticScore_(one, two) {
566
566
  if (!one || !two) {
567
567
  return 6;
568
568
  }
569
569
  var char1 = one.charAt(one.length - 1);
570
570
  var char2 = two.charAt(0);
571
- var nonAlphaNumeric1 = char1.match(diff_match_patch2.nonAlphaNumericRegex_);
572
- var nonAlphaNumeric2 = char2.match(diff_match_patch2.nonAlphaNumericRegex_);
573
- var whitespace1 = nonAlphaNumeric1 && char1.match(diff_match_patch2.whitespaceRegex_);
574
- var whitespace2 = nonAlphaNumeric2 && char2.match(diff_match_patch2.whitespaceRegex_);
575
- var lineBreak1 = whitespace1 && char1.match(diff_match_patch2.linebreakRegex_);
576
- var lineBreak2 = whitespace2 && char2.match(diff_match_patch2.linebreakRegex_);
577
- var blankLine1 = lineBreak1 && one.match(diff_match_patch2.blanklineEndRegex_);
578
- 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_);
579
579
  if (blankLine1 || blankLine2) {
580
580
  return 5;
581
581
  } else if (lineBreak1 || lineBreak2) {
@@ -637,12 +637,12 @@ var require_diff_match_patch = __commonJS({
637
637
  pointer++;
638
638
  }
639
639
  };
640
- diff_match_patch2.nonAlphaNumericRegex_ = /[^a-zA-Z0-9]/;
641
- diff_match_patch2.whitespaceRegex_ = /\s/;
642
- diff_match_patch2.linebreakRegex_ = /[\r\n]/;
643
- diff_match_patch2.blanklineEndRegex_ = /\n\r?\n$/;
644
- diff_match_patch2.blanklineStartRegex_ = /^\r?\n\r?\n/;
645
- 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) {
646
646
  var changes = false;
647
647
  var equalities = [];
648
648
  var equalitiesLength = 0;
@@ -674,7 +674,7 @@ var require_diff_match_patch = __commonJS({
674
674
  diffs.splice(
675
675
  equalities[equalitiesLength - 1],
676
676
  0,
677
- new diff_match_patch2.Diff(DIFF_DELETE, lastEquality)
677
+ new diff_match_patch3.Diff(DIFF_DELETE, lastEquality)
678
678
  );
679
679
  diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
680
680
  equalitiesLength--;
@@ -696,8 +696,8 @@ var require_diff_match_patch = __commonJS({
696
696
  this.diff_cleanupMerge(diffs);
697
697
  }
698
698
  };
699
- diff_match_patch2.prototype.diff_cleanupMerge = function(diffs) {
700
- 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, ""));
701
701
  var pointer = 0;
702
702
  var count_delete = 0;
703
703
  var count_insert = 0;
@@ -724,7 +724,7 @@ var require_diff_match_patch = __commonJS({
724
724
  if (pointer - count_delete - count_insert > 0 && diffs[pointer - count_delete - count_insert - 1][0] == DIFF_EQUAL) {
725
725
  diffs[pointer - count_delete - count_insert - 1][1] += text_insert.substring(0, commonlength);
726
726
  } else {
727
- diffs.splice(0, 0, new diff_match_patch2.Diff(
727
+ diffs.splice(0, 0, new diff_match_patch3.Diff(
728
728
  DIFF_EQUAL,
729
729
  text_insert.substring(0, commonlength)
730
730
  ));
@@ -746,7 +746,7 @@ var require_diff_match_patch = __commonJS({
746
746
  diffs.splice(
747
747
  pointer,
748
748
  0,
749
- new diff_match_patch2.Diff(DIFF_DELETE, text_delete)
749
+ new diff_match_patch3.Diff(DIFF_DELETE, text_delete)
750
750
  );
751
751
  pointer++;
752
752
  }
@@ -754,7 +754,7 @@ var require_diff_match_patch = __commonJS({
754
754
  diffs.splice(
755
755
  pointer,
756
756
  0,
757
- new diff_match_patch2.Diff(DIFF_INSERT, text_insert)
757
+ new diff_match_patch3.Diff(DIFF_INSERT, text_insert)
758
758
  );
759
759
  pointer++;
760
760
  }
@@ -797,7 +797,7 @@ var require_diff_match_patch = __commonJS({
797
797
  this.diff_cleanupMerge(diffs);
798
798
  }
799
799
  };
800
- diff_match_patch2.prototype.diff_xIndex = function(diffs, loc) {
800
+ diff_match_patch3.prototype.diff_xIndex = function(diffs, loc) {
801
801
  var chars1 = 0;
802
802
  var chars2 = 0;
803
803
  var last_chars1 = 0;
@@ -821,7 +821,7 @@ var require_diff_match_patch = __commonJS({
821
821
  }
822
822
  return last_chars2 + (loc - last_chars1);
823
823
  };
824
- diff_match_patch2.prototype.diff_prettyHtml = function(diffs) {
824
+ diff_match_patch3.prototype.diff_prettyHtml = function(diffs) {
825
825
  var html = [];
826
826
  var pattern_amp = /&/g;
827
827
  var pattern_lt = /</g;
@@ -845,7 +845,7 @@ var require_diff_match_patch = __commonJS({
845
845
  }
846
846
  return html.join("");
847
847
  };
848
- diff_match_patch2.prototype.diff_text1 = function(diffs) {
848
+ diff_match_patch3.prototype.diff_text1 = function(diffs) {
849
849
  var text = [];
850
850
  for (var x = 0; x < diffs.length; x++) {
851
851
  if (diffs[x][0] !== DIFF_INSERT) {
@@ -854,7 +854,7 @@ var require_diff_match_patch = __commonJS({
854
854
  }
855
855
  return text.join("");
856
856
  };
857
- diff_match_patch2.prototype.diff_text2 = function(diffs) {
857
+ diff_match_patch3.prototype.diff_text2 = function(diffs) {
858
858
  var text = [];
859
859
  for (var x = 0; x < diffs.length; x++) {
860
860
  if (diffs[x][0] !== DIFF_DELETE) {
@@ -863,7 +863,7 @@ var require_diff_match_patch = __commonJS({
863
863
  }
864
864
  return text.join("");
865
865
  };
866
- diff_match_patch2.prototype.diff_levenshtein = function(diffs) {
866
+ diff_match_patch3.prototype.diff_levenshtein = function(diffs) {
867
867
  var levenshtein = 0;
868
868
  var insertions = 0;
869
869
  var deletions = 0;
@@ -887,7 +887,7 @@ var require_diff_match_patch = __commonJS({
887
887
  levenshtein += Math.max(insertions, deletions);
888
888
  return levenshtein;
889
889
  };
890
- diff_match_patch2.prototype.diff_toDelta = function(diffs) {
890
+ diff_match_patch3.prototype.diff_toDelta = function(diffs) {
891
891
  var text = [];
892
892
  for (var x = 0; x < diffs.length; x++) {
893
893
  switch (diffs[x][0]) {
@@ -904,7 +904,7 @@ var require_diff_match_patch = __commonJS({
904
904
  }
905
905
  return text.join(" ").replace(/%20/g, " ");
906
906
  };
907
- diff_match_patch2.prototype.diff_fromDelta = function(text1, delta) {
907
+ diff_match_patch3.prototype.diff_fromDelta = function(text1, delta) {
908
908
  var diffs = [];
909
909
  var diffsLength = 0;
910
910
  var pointer = 0;
@@ -914,7 +914,7 @@ var require_diff_match_patch = __commonJS({
914
914
  switch (tokens[x].charAt(0)) {
915
915
  case "+":
916
916
  try {
917
- diffs[diffsLength++] = new diff_match_patch2.Diff(DIFF_INSERT, decodeURI(param));
917
+ diffs[diffsLength++] = new diff_match_patch3.Diff(DIFF_INSERT, decodeURI(param));
918
918
  } catch (ex) {
919
919
  throw new Error("Illegal escape in diff_fromDelta: " + param);
920
920
  }
@@ -928,9 +928,9 @@ var require_diff_match_patch = __commonJS({
928
928
  }
929
929
  var text = text1.substring(pointer, pointer += n);
930
930
  if (tokens[x].charAt(0) == "=") {
931
- diffs[diffsLength++] = new diff_match_patch2.Diff(DIFF_EQUAL, text);
931
+ diffs[diffsLength++] = new diff_match_patch3.Diff(DIFF_EQUAL, text);
932
932
  } else {
933
- diffs[diffsLength++] = new diff_match_patch2.Diff(DIFF_DELETE, text);
933
+ diffs[diffsLength++] = new diff_match_patch3.Diff(DIFF_DELETE, text);
934
934
  }
935
935
  break;
936
936
  default:
@@ -944,7 +944,7 @@ var require_diff_match_patch = __commonJS({
944
944
  }
945
945
  return diffs;
946
946
  };
947
- diff_match_patch2.prototype.match_main = function(text, pattern, loc) {
947
+ diff_match_patch3.prototype.match_main = function(text, pattern, loc) {
948
948
  if (text == null || pattern == null || loc == null) {
949
949
  throw new Error("Null input. (match_main)");
950
950
  }
@@ -959,7 +959,7 @@ var require_diff_match_patch = __commonJS({
959
959
  return this.match_bitap_(text, pattern, loc);
960
960
  }
961
961
  };
962
- diff_match_patch2.prototype.match_bitap_ = function(text, pattern, loc) {
962
+ diff_match_patch3.prototype.match_bitap_ = function(text, pattern, loc) {
963
963
  if (pattern.length > this.Match_MaxBits) {
964
964
  throw new Error("Pattern too long for this browser.");
965
965
  }
@@ -1030,7 +1030,7 @@ var require_diff_match_patch = __commonJS({
1030
1030
  }
1031
1031
  return best_loc;
1032
1032
  };
1033
- diff_match_patch2.prototype.match_alphabet_ = function(pattern) {
1033
+ diff_match_patch3.prototype.match_alphabet_ = function(pattern) {
1034
1034
  var s = {};
1035
1035
  for (var i = 0; i < pattern.length; i++) {
1036
1036
  s[pattern.charAt(i)] = 0;
@@ -1040,7 +1040,7 @@ var require_diff_match_patch = __commonJS({
1040
1040
  }
1041
1041
  return s;
1042
1042
  };
1043
- diff_match_patch2.prototype.patch_addContext_ = function(patch, text) {
1043
+ diff_match_patch3.prototype.patch_addContext_ = function(patch, text) {
1044
1044
  if (text.length == 0) {
1045
1045
  return;
1046
1046
  }
@@ -1059,21 +1059,21 @@ var require_diff_match_patch = __commonJS({
1059
1059
  padding += this.Patch_Margin;
1060
1060
  var prefix = text.substring(patch.start2 - padding, patch.start2);
1061
1061
  if (prefix) {
1062
- patch.diffs.unshift(new diff_match_patch2.Diff(DIFF_EQUAL, prefix));
1062
+ patch.diffs.unshift(new diff_match_patch3.Diff(DIFF_EQUAL, prefix));
1063
1063
  }
1064
1064
  var suffix = text.substring(
1065
1065
  patch.start2 + patch.length1,
1066
1066
  patch.start2 + patch.length1 + padding
1067
1067
  );
1068
1068
  if (suffix) {
1069
- patch.diffs.push(new diff_match_patch2.Diff(DIFF_EQUAL, suffix));
1069
+ patch.diffs.push(new diff_match_patch3.Diff(DIFF_EQUAL, suffix));
1070
1070
  }
1071
1071
  patch.start1 -= prefix.length;
1072
1072
  patch.start2 -= prefix.length;
1073
1073
  patch.length1 += prefix.length + suffix.length;
1074
1074
  patch.length2 += prefix.length + suffix.length;
1075
1075
  };
1076
- 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) {
1077
1077
  var text1, diffs;
1078
1078
  if (typeof a == "string" && typeof opt_b == "string" && typeof opt_c == "undefined") {
1079
1079
  text1 = /** @type {string} */
@@ -1109,7 +1109,7 @@ var require_diff_match_patch = __commonJS({
1109
1109
  return [];
1110
1110
  }
1111
1111
  var patches = [];
1112
- var patch = new diff_match_patch2.patch_obj();
1112
+ var patch = new diff_match_patch3.patch_obj();
1113
1113
  var patchDiffLength = 0;
1114
1114
  var char_count1 = 0;
1115
1115
  var char_count2 = 0;
@@ -1142,7 +1142,7 @@ var require_diff_match_patch = __commonJS({
1142
1142
  if (patchDiffLength) {
1143
1143
  this.patch_addContext_(patch, prepatch_text);
1144
1144
  patches.push(patch);
1145
- patch = new diff_match_patch2.patch_obj();
1145
+ patch = new diff_match_patch3.patch_obj();
1146
1146
  patchDiffLength = 0;
1147
1147
  prepatch_text = postpatch_text;
1148
1148
  char_count1 = char_count2;
@@ -1163,14 +1163,14 @@ var require_diff_match_patch = __commonJS({
1163
1163
  }
1164
1164
  return patches;
1165
1165
  };
1166
- diff_match_patch2.prototype.patch_deepCopy = function(patches) {
1166
+ diff_match_patch3.prototype.patch_deepCopy = function(patches) {
1167
1167
  var patchesCopy = [];
1168
1168
  for (var x = 0; x < patches.length; x++) {
1169
1169
  var patch = patches[x];
1170
- var patchCopy = new diff_match_patch2.patch_obj();
1170
+ var patchCopy = new diff_match_patch3.patch_obj();
1171
1171
  patchCopy.diffs = [];
1172
1172
  for (var y = 0; y < patch.diffs.length; y++) {
1173
- 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]);
1174
1174
  }
1175
1175
  patchCopy.start1 = patch.start1;
1176
1176
  patchCopy.start2 = patch.start2;
@@ -1180,7 +1180,7 @@ var require_diff_match_patch = __commonJS({
1180
1180
  }
1181
1181
  return patchesCopy;
1182
1182
  };
1183
- diff_match_patch2.prototype.patch_apply = function(patches, text) {
1183
+ diff_match_patch3.prototype.patch_apply = function(patches, text) {
1184
1184
  if (patches.length == 0) {
1185
1185
  return [text, []];
1186
1186
  }
@@ -1260,7 +1260,7 @@ var require_diff_match_patch = __commonJS({
1260
1260
  text = text.substring(nullPadding.length, text.length - nullPadding.length);
1261
1261
  return [text, results];
1262
1262
  };
1263
- diff_match_patch2.prototype.patch_addPadding = function(patches) {
1263
+ diff_match_patch3.prototype.patch_addPadding = function(patches) {
1264
1264
  var paddingLength = this.Patch_Margin;
1265
1265
  var nullPadding = "";
1266
1266
  for (var x = 1; x <= paddingLength; x++) {
@@ -1273,7 +1273,7 @@ var require_diff_match_patch = __commonJS({
1273
1273
  var patch = patches[0];
1274
1274
  var diffs = patch.diffs;
1275
1275
  if (diffs.length == 0 || diffs[0][0] != DIFF_EQUAL) {
1276
- diffs.unshift(new diff_match_patch2.Diff(DIFF_EQUAL, nullPadding));
1276
+ diffs.unshift(new diff_match_patch3.Diff(DIFF_EQUAL, nullPadding));
1277
1277
  patch.start1 -= paddingLength;
1278
1278
  patch.start2 -= paddingLength;
1279
1279
  patch.length1 += paddingLength;
@@ -1289,7 +1289,7 @@ var require_diff_match_patch = __commonJS({
1289
1289
  patch = patches[patches.length - 1];
1290
1290
  diffs = patch.diffs;
1291
1291
  if (diffs.length == 0 || diffs[diffs.length - 1][0] != DIFF_EQUAL) {
1292
- diffs.push(new diff_match_patch2.Diff(DIFF_EQUAL, nullPadding));
1292
+ diffs.push(new diff_match_patch3.Diff(DIFF_EQUAL, nullPadding));
1293
1293
  patch.length1 += paddingLength;
1294
1294
  patch.length2 += paddingLength;
1295
1295
  } else if (paddingLength > diffs[diffs.length - 1][1].length) {
@@ -1300,7 +1300,7 @@ var require_diff_match_patch = __commonJS({
1300
1300
  }
1301
1301
  return nullPadding;
1302
1302
  };
1303
- diff_match_patch2.prototype.patch_splitMax = function(patches) {
1303
+ diff_match_patch3.prototype.patch_splitMax = function(patches) {
1304
1304
  var patch_size = this.Match_MaxBits;
1305
1305
  for (var x = 0; x < patches.length; x++) {
1306
1306
  if (patches[x].length1 <= patch_size) {
@@ -1312,13 +1312,13 @@ var require_diff_match_patch = __commonJS({
1312
1312
  var start2 = bigpatch.start2;
1313
1313
  var precontext = "";
1314
1314
  while (bigpatch.diffs.length !== 0) {
1315
- var patch = new diff_match_patch2.patch_obj();
1315
+ var patch = new diff_match_patch3.patch_obj();
1316
1316
  var empty = true;
1317
1317
  patch.start1 = start1 - precontext.length;
1318
1318
  patch.start2 = start2 - precontext.length;
1319
1319
  if (precontext !== "") {
1320
1320
  patch.length1 = patch.length2 = precontext.length;
1321
- patch.diffs.push(new diff_match_patch2.Diff(DIFF_EQUAL, precontext));
1321
+ patch.diffs.push(new diff_match_patch3.Diff(DIFF_EQUAL, precontext));
1322
1322
  }
1323
1323
  while (bigpatch.diffs.length !== 0 && patch.length1 < patch_size - this.Patch_Margin) {
1324
1324
  var diff_type = bigpatch.diffs[0][0];
@@ -1332,7 +1332,7 @@ var require_diff_match_patch = __commonJS({
1332
1332
  patch.length1 += diff_text.length;
1333
1333
  start1 += diff_text.length;
1334
1334
  empty = false;
1335
- 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));
1336
1336
  bigpatch.diffs.shift();
1337
1337
  } else {
1338
1338
  diff_text = diff_text.substring(
@@ -1347,7 +1347,7 @@ var require_diff_match_patch = __commonJS({
1347
1347
  } else {
1348
1348
  empty = false;
1349
1349
  }
1350
- 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));
1351
1351
  if (diff_text == bigpatch.diffs[0][1]) {
1352
1352
  bigpatch.diffs.shift();
1353
1353
  } else {
@@ -1364,7 +1364,7 @@ var require_diff_match_patch = __commonJS({
1364
1364
  if (patch.diffs.length !== 0 && patch.diffs[patch.diffs.length - 1][0] === DIFF_EQUAL) {
1365
1365
  patch.diffs[patch.diffs.length - 1][1] += postcontext;
1366
1366
  } else {
1367
- patch.diffs.push(new diff_match_patch2.Diff(DIFF_EQUAL, postcontext));
1367
+ patch.diffs.push(new diff_match_patch3.Diff(DIFF_EQUAL, postcontext));
1368
1368
  }
1369
1369
  }
1370
1370
  if (!empty) {
@@ -1373,14 +1373,14 @@ var require_diff_match_patch = __commonJS({
1373
1373
  }
1374
1374
  }
1375
1375
  };
1376
- diff_match_patch2.prototype.patch_toText = function(patches) {
1376
+ diff_match_patch3.prototype.patch_toText = function(patches) {
1377
1377
  var text = [];
1378
1378
  for (var x = 0; x < patches.length; x++) {
1379
1379
  text[x] = patches[x];
1380
1380
  }
1381
1381
  return text.join("");
1382
1382
  };
1383
- diff_match_patch2.prototype.patch_fromText = function(textline) {
1383
+ diff_match_patch3.prototype.patch_fromText = function(textline) {
1384
1384
  var patches = [];
1385
1385
  if (!textline) {
1386
1386
  return patches;
@@ -1393,7 +1393,7 @@ var require_diff_match_patch = __commonJS({
1393
1393
  if (!m) {
1394
1394
  throw new Error("Invalid patch string: " + text[textPointer]);
1395
1395
  }
1396
- var patch = new diff_match_patch2.patch_obj();
1396
+ var patch = new diff_match_patch3.patch_obj();
1397
1397
  patches.push(patch);
1398
1398
  patch.start1 = parseInt(m[1], 10);
1399
1399
  if (m[2] === "") {
@@ -1424,11 +1424,11 @@ var require_diff_match_patch = __commonJS({
1424
1424
  throw new Error("Illegal escape in patch_fromText: " + line);
1425
1425
  }
1426
1426
  if (sign == "-") {
1427
- patch.diffs.push(new diff_match_patch2.Diff(DIFF_DELETE, line));
1427
+ patch.diffs.push(new diff_match_patch3.Diff(DIFF_DELETE, line));
1428
1428
  } else if (sign == "+") {
1429
- patch.diffs.push(new diff_match_patch2.Diff(DIFF_INSERT, line));
1429
+ patch.diffs.push(new diff_match_patch3.Diff(DIFF_INSERT, line));
1430
1430
  } else if (sign == " ") {
1431
- patch.diffs.push(new diff_match_patch2.Diff(DIFF_EQUAL, line));
1431
+ patch.diffs.push(new diff_match_patch3.Diff(DIFF_EQUAL, line));
1432
1432
  } else if (sign == "@") {
1433
1433
  break;
1434
1434
  } else if (sign === "") {
@@ -1440,14 +1440,14 @@ var require_diff_match_patch = __commonJS({
1440
1440
  }
1441
1441
  return patches;
1442
1442
  };
1443
- diff_match_patch2.patch_obj = function() {
1443
+ diff_match_patch3.patch_obj = function() {
1444
1444
  this.diffs = [];
1445
1445
  this.start1 = null;
1446
1446
  this.start2 = null;
1447
1447
  this.length1 = 0;
1448
1448
  this.length2 = 0;
1449
1449
  };
1450
- diff_match_patch2.patch_obj.prototype.toString = function() {
1450
+ diff_match_patch3.patch_obj.prototype.toString = function() {
1451
1451
  var coords1, coords2;
1452
1452
  if (this.length1 === 0) {
1453
1453
  coords1 = this.start1 + ",0";
@@ -1481,48 +1481,14 @@ var require_diff_match_patch = __commonJS({
1481
1481
  }
1482
1482
  return text.join("").replace(/%20/g, " ");
1483
1483
  };
1484
- module.exports = diff_match_patch2;
1485
- module.exports["diff_match_patch"] = diff_match_patch2;
1484
+ module.exports = diff_match_patch3;
1485
+ module.exports["diff_match_patch"] = diff_match_patch3;
1486
1486
  module.exports["DIFF_DELETE"] = DIFF_DELETE;
1487
1487
  module.exports["DIFF_INSERT"] = DIFF_INSERT;
1488
1488
  module.exports["DIFF_EQUAL"] = DIFF_EQUAL;
1489
1489
  }
1490
1490
  });
1491
1491
 
1492
- // adapters/xml-adapter.js
1493
- var _DOMParser = globalThis.DOMParser;
1494
- var _XMLSerializer = globalThis.XMLSerializer;
1495
- function configureXmlProvider(options = {}) {
1496
- if (options.DOMParser) _DOMParser = options.DOMParser;
1497
- if (options.XMLSerializer) _XMLSerializer = options.XMLSerializer;
1498
- }
1499
- function createParser() {
1500
- if (!_DOMParser && globalThis.DOMParser) {
1501
- _DOMParser = globalThis.DOMParser;
1502
- }
1503
- if (!_DOMParser) {
1504
- throw new Error("DOMParser is not configured. Call configureXmlProvider({ DOMParser, XMLSerializer }) first.");
1505
- }
1506
- return new _DOMParser();
1507
- }
1508
- function createSerializer() {
1509
- if (!_XMLSerializer && globalThis.XMLSerializer) {
1510
- _XMLSerializer = globalThis.XMLSerializer;
1511
- }
1512
- if (!_XMLSerializer) {
1513
- throw new Error("XMLSerializer is not configured. Call configureXmlProvider({ DOMParser, XMLSerializer }) first.");
1514
- }
1515
- return new _XMLSerializer();
1516
- }
1517
- function parseXml(xmlString, contentType = "text/xml") {
1518
- const parser = createParser();
1519
- return parser.parseFromString(xmlString, contentType);
1520
- }
1521
- function serializeXml(node) {
1522
- const serializer = createSerializer();
1523
- return serializer.serializeToString(node);
1524
- }
1525
-
1526
1492
  // adapters/logger.js
1527
1493
  var _logger = console;
1528
1494
  var LEVELS = Object.freeze({
@@ -1565,6 +1531,86 @@ function error(...args) {
1565
1531
  }))(...args);
1566
1532
  }
1567
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
+
1568
1614
  // adapters/config.js
1569
1615
  var _defaultAuthor = "Author";
1570
1616
  var _platform = "Unknown";
@@ -1786,37 +1832,108 @@ function escapeXml(str) {
1786
1832
  if (!str) return "";
1787
1833
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1788
1834
  }
1789
- 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
+ }
1790
1917
  function getNextRevisionId() {
1791
- return revisionIdCounter++;
1918
+ return defaultRevisionIdAllocator.next();
1792
1919
  }
1793
1920
  function getRevisionTimestamp(date = /* @__PURE__ */ new Date()) {
1794
1921
  return date.toISOString();
1795
1922
  }
1796
- function createRevisionMetadata(author) {
1923
+ function createRevisionMetadata(author, allocatorOrNode = null) {
1797
1924
  const resolvedAuthor = typeof author === "string" && author.trim() ? author.trim() : getDefaultAuthor();
1925
+ const allocator = allocatorOrNode instanceof RevisionIdAllocator ? allocatorOrNode : getRevisionIdAllocatorForDocument(allocatorOrNode) || defaultRevisionIdAllocator;
1798
1926
  return {
1799
- id: getNextRevisionId(),
1927
+ id: allocator.next(),
1800
1928
  author: resolvedAuthor,
1801
1929
  date: getRevisionTimestamp()
1802
1930
  };
1803
1931
  }
1804
- function seedRevisionIdsFromDocument(xmlDoc) {
1805
- let maxFound = -1;
1806
- const elements = Array.from(xmlDoc?.getElementsByTagName?.("*") || []);
1807
- for (const element of elements) {
1808
- for (const attr of Array.from(element.attributes || [])) {
1809
- if ((attr.localName || "").toLowerCase() !== "id") continue;
1810
- const parsed = Number.parseInt(attr.value, 10);
1811
- if (Number.isFinite(parsed)) {
1812
- maxFound = Math.max(maxFound, parsed);
1813
- }
1814
- }
1815
- }
1816
- if (maxFound >= revisionIdCounter) {
1817
- revisionIdCounter = maxFound + 1;
1818
- }
1819
- 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;
1820
1937
  }
1821
1938
 
1822
1939
  // core/paragraph-offset-policy.js
@@ -1894,10 +2011,12 @@ function ingestOoxml(ooxmlString, options = {}) {
1894
2011
  return { runModel, acceptedText, pPr: null };
1895
2012
  }
1896
2013
  try {
1897
- const doc = preParsedDoc || (() => {
1898
- const parser = createParser();
1899
- return parser.parseFromString(ooxmlString, "application/xml");
1900
- })();
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
+ }
1901
2020
  const parseError = getXmlParseError(doc);
1902
2021
  if (parseError) {
1903
2022
  error("OOXML parse error:", parseError.textContent);
@@ -2302,55 +2421,126 @@ function extractTrPr(trNode) {
2302
2421
 
2303
2422
  // pipeline/diff-engine.js
2304
2423
  var import_diff_match_patch = __toESM(require_diff_match_patch(), 1);
2305
- var DMP = new import_diff_match_patch.diff_match_patch();
2306
- 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 = {}) {
2307
2464
  const wordArray = [];
2308
2465
  const wordHash = /* @__PURE__ */ new Map();
2309
- function tokenize(text) {
2310
- const tokens = [];
2311
- const regex = /(\S+)(\s*)/g;
2312
- let match;
2313
- while ((match = regex.exec(text)) !== null) {
2314
- if (match[1]) tokens.push(match[1]);
2315
- if (match[2]) tokens.push(match[2]);
2316
- }
2317
- 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}.`);
2318
2469
  }
2319
2470
  function mapTokensToChars(tokens) {
2320
2471
  let chars = "";
2472
+ const tokenIds = [];
2321
2473
  for (const token of tokens) {
2322
- if (wordHash.has(token)) {
2323
- chars += String.fromCharCode(wordHash.get(token));
2324
- } else {
2325
- 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;
2326
2478
  wordArray.push(token);
2327
- wordHash.set(token, charCode);
2328
- chars += String.fromCharCode(charCode);
2479
+ wordHash.set(token, tokenId);
2329
2480
  }
2481
+ tokenIds.push(tokenId);
2482
+ chars += String.fromCodePoint(TOKEN_CODE_POINT_BASE + tokenId);
2330
2483
  }
2331
- return chars;
2484
+ return { chars, tokenIds };
2332
2485
  }
2333
2486
  const tokens1 = tokenize(text1);
2334
2487
  const tokens2 = tokenize(text2);
2488
+ const encoded1 = mapTokensToChars(tokens1);
2489
+ const encoded2 = mapTokensToChars(tokens2);
2335
2490
  return {
2336
- chars1: mapTokensToChars(tokens1),
2337
- chars2: mapTokensToChars(tokens2),
2338
- wordArray
2491
+ chars1: encoded1.chars,
2492
+ chars2: encoded2.chars,
2493
+ wordArray,
2494
+ tokenIds1: encoded1.tokenIds,
2495
+ tokenIds2: encoded2.tokenIds
2339
2496
  };
2340
2497
  }
2341
- function charsToWords(diffs, wordArray) {
2342
- const wordDiffs = [];
2343
- 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]) => {
2344
2514
  const parts = [];
2345
- for (let i = 0; i < chars.length; i++) {
2346
- const charCode = chars.charCodeAt(i);
2347
- if (charCode < wordArray.length) {
2348
- 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.`);
2349
2519
  }
2520
+ parts.push(wordArray[tokenId]);
2350
2521
  }
2351
- wordDiffs.push([op, parts.join("")]);
2352
- }
2353
- 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;
2354
2544
  }
2355
2545
  function computeWordDiffs(originalText, newText, options = {}) {
2356
2546
  if (originalText === newText) {
@@ -2363,12 +2553,16 @@ function computeWordDiffs(originalText, newText, options = {}) {
2363
2553
  return [[-1, originalText]];
2364
2554
  }
2365
2555
  const { cleanupSemantic = true } = options;
2366
- const { chars1, chars2, wordArray } = wordsToChars(originalText, newText);
2367
- 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));
2368
2562
  if (cleanupSemantic) {
2369
- DMP.diff_cleanupSemantic(charDiffs);
2563
+ dmp.diff_cleanupSemantic(charDiffs);
2370
2564
  }
2371
- return charsToWords(charDiffs, wordArray);
2565
+ return decodeBmpDiffs(charDiffs, wordArray);
2372
2566
  }
2373
2567
  function computeWordLevelDiffOps(originalText, newText, options = {}) {
2374
2568
  if (originalText === newText) {
@@ -2559,7 +2753,6 @@ function applyPatches(splitModel, diffOps, options) {
2559
2753
  function processInsertionOperation(context) {
2560
2754
  const {
2561
2755
  insertOp,
2562
- splitModel,
2563
2756
  styleLookup,
2564
2757
  patchedModel,
2565
2758
  state,
@@ -2824,7 +3017,7 @@ function buildParagraphOnlyPackage(paragraphXml) {
2824
3017
  var XMLNS_ATTR_REGEX2 = /\s+xmlns:[^=]+="[^"]*"/g;
2825
3018
  function serializeToOoxml(patchedModel, pPr, formatHints = [], options = {}) {
2826
3019
  const serializationOptions = normalizeSerializationOptions(options);
2827
- const { author, generateRedlines } = serializationOptions;
3020
+ const { generateRedlines } = serializationOptions;
2828
3021
  const paragraphs = [];
2829
3022
  let currentPPrXml = "";
2830
3023
  let currentPPrElement = null;
@@ -2912,7 +3105,8 @@ function normalizeSerializationOptions(options) {
2912
3105
  return {
2913
3106
  author: getDefaultAuthor(),
2914
3107
  generateRedlines: true,
2915
- font: options
3108
+ font: options,
3109
+ revisionIdAllocator: null
2916
3110
  };
2917
3111
  }
2918
3112
  const normalized = options && typeof options === "object" ? options : {};
@@ -2920,7 +3114,8 @@ function normalizeSerializationOptions(options) {
2920
3114
  return {
2921
3115
  author: resolvedAuthor,
2922
3116
  generateRedlines: normalized.generateRedlines ?? true,
2923
- font: normalized.font ?? null
3117
+ font: normalized.font ?? null,
3118
+ revisionIdAllocator: normalized.revisionIdAllocator ?? null
2924
3119
  };
2925
3120
  }
2926
3121
  function buildRunXmlWithHints(item, formatHints, options = {}) {
@@ -2958,7 +3153,10 @@ function buildSimpleRun(text, rPrXml) {
2958
3153
  return `<w:r>${rPr}<w:t xml:space="preserve">${escapeXml(text)}</w:t></w:r>`;
2959
3154
  }
2960
3155
  function buildDeletionXml(item, options = {}) {
2961
- const metadata = createRevisionMetadata(options.author ?? getDefaultAuthor());
3156
+ const metadata = createRevisionMetadata(
3157
+ options.author ?? getDefaultAuthor(),
3158
+ options.revisionIdAllocator
3159
+ );
2962
3160
  const font = options.font ?? null;
2963
3161
  let rPr = item.rPrXml ? stripNamespaceDeclarations(item.rPrXml) : "";
2964
3162
  if (font) {
@@ -2967,7 +3165,10 @@ function buildDeletionXml(item, options = {}) {
2967
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>`;
2968
3166
  }
2969
3167
  function buildInsertionXml(item, formatHints, options = {}) {
2970
- const metadata = createRevisionMetadata(options.author ?? getDefaultAuthor());
3168
+ const metadata = createRevisionMetadata(
3169
+ options.author ?? getDefaultAuthor(),
3170
+ options.revisionIdAllocator
3171
+ );
2971
3172
  const font = options.font ?? null;
2972
3173
  const applicableHints = getApplicableFormatHints(formatHints, item.startOffset, item.endOffset);
2973
3174
  let innerContent = "";
@@ -3287,8 +3488,8 @@ var NumberingService = class {
3287
3488
 
3288
3489
  // services/table-reconciliation.js
3289
3490
  function generateTableOoxml(tableData, options = {}) {
3290
- const { generateRedlines = false, author = "AI" } = options;
3291
- const tableInsertMeta = generateRedlines ? createRevisionMetadata(author) : null;
3491
+ const { generateRedlines = false, author = "AI", revisionIdAllocator = null } = options;
3492
+ const tableInsertMeta = generateRedlines ? createRevisionMetadata(author, revisionIdAllocator) : null;
3292
3493
  const numCols = tableData.headers?.length || (tableData.rows?.[0]?.length || 1);
3293
3494
  const tblPr = `
3294
3495
  <w:tblPr>
@@ -3323,7 +3524,11 @@ function generateTableOoxml(tableData, options = {}) {
3323
3524
  startOffset: 0,
3324
3525
  endOffset: cleanText.length
3325
3526
  }];
3326
- const runsOoxml = serializeToOoxml(runModel, null, formatHints, { author, generateRedlines });
3527
+ const runsOoxml = serializeToOoxml(runModel, null, formatHints, {
3528
+ author,
3529
+ generateRedlines,
3530
+ revisionIdAllocator
3531
+ });
3327
3532
  const tcPr = '<w:tcPr><w:tcW w:w="0" w:type="auto"/></w:tcPr>';
3328
3533
  cellsXml += `<w:tc>${tcPr}${runsOoxml}</w:tc>`;
3329
3534
  }
@@ -3381,7 +3586,7 @@ function diffTablesWithVirtualGrid(oldGrid, newTableData) {
3381
3586
  return operations;
3382
3587
  }
3383
3588
  function serializeVirtualGridToOoxml(grid, operations, options) {
3384
- const { generateRedlines, author } = options;
3589
+ const { generateRedlines, author, revisionIdAllocator = null } = options;
3385
3590
  const opIndex = buildTableOperationIndex(operations);
3386
3591
  let rowsXml = "";
3387
3592
  for (let row = 0; row < grid.rowCount; row++) {
@@ -3411,7 +3616,7 @@ function serializeVirtualGridToOoxml(grid, operations, options) {
3411
3616
  }
3412
3617
  let trPr = grid.trPrList[row] || "<w:trPr/>";
3413
3618
  if (rowDeleteOp && generateRedlines) {
3414
- const metadata = createRevisionMetadata(author);
3619
+ const metadata = createRevisionMetadata(author, revisionIdAllocator);
3415
3620
  const delMark = `<w:del w:id="${metadata.id}" w:author="${escapeXml(metadata.author)}" w:date="${metadata.date}"/>`;
3416
3621
  if (trPr.includes("</w:trPr>")) {
3417
3622
  trPr = trPr.replace("</w:trPr>", `${delMark}</w:trPr>`);
@@ -3424,7 +3629,7 @@ function serializeVirtualGridToOoxml(grid, operations, options) {
3424
3629
  const insertOps = opIndex.rowInsertOperations;
3425
3630
  for (const op of insertOps) {
3426
3631
  let cellsXml = "";
3427
- const rowInsertMeta = generateRedlines ? createRevisionMetadata(author) : null;
3632
+ const rowInsertMeta = generateRedlines ? createRevisionMetadata(author, revisionIdAllocator) : null;
3428
3633
  for (const cellText of op.cells) {
3429
3634
  const { cleanText, formatHints } = preprocessMarkdown(cellText);
3430
3635
  const runModel = [{
@@ -3435,7 +3640,11 @@ function serializeVirtualGridToOoxml(grid, operations, options) {
3435
3640
  startOffset: 0,
3436
3641
  endOffset: cleanText.length
3437
3642
  }];
3438
- const runsOoxml = serializeToOoxml(runModel, null, formatHints, { author, generateRedlines });
3643
+ const runsOoxml = serializeToOoxml(runModel, null, formatHints, {
3644
+ author,
3645
+ generateRedlines,
3646
+ revisionIdAllocator
3647
+ });
3439
3648
  cellsXml += `<w:tc><w:tcPr><w:tcW w:w="0" w:type="auto"/></w:tcPr>${runsOoxml}</w:tc>`;
3440
3649
  }
3441
3650
  let trPr = "<w:trPr/>";
@@ -3473,7 +3682,7 @@ function buildTableOperationIndex(operations) {
3473
3682
  };
3474
3683
  }
3475
3684
  function reconcileCellContent(cell, newText, options) {
3476
- const { generateRedlines, author } = options;
3685
+ const { generateRedlines, author, revisionIdAllocator = null } = options;
3477
3686
  const { cleanText, formatHints } = preprocessMarkdown(newText);
3478
3687
  const oldText = cell.getText();
3479
3688
  const diffOps = computeWordLevelDiffOps(oldText, cleanText);
@@ -3484,7 +3693,11 @@ function reconcileCellContent(cell, newText, options) {
3484
3693
  author,
3485
3694
  formatHints
3486
3695
  });
3487
- const runsOoxml = serializeToOoxml(patchedModel, baseBlock.pPr, formatHints, { author, generateRedlines });
3696
+ const runsOoxml = serializeToOoxml(patchedModel, baseBlock.pPr, formatHints, {
3697
+ author,
3698
+ generateRedlines,
3699
+ revisionIdAllocator
3700
+ });
3488
3701
  return runsOoxml;
3489
3702
  }
3490
3703
  function serializeCellBlocks(blocks) {
@@ -3493,7 +3706,7 @@ function serializeCellBlocks(blocks) {
3493
3706
  return runsOoxml;
3494
3707
  }).join("");
3495
3708
  }
3496
- function buildTcXml(cell, content, options) {
3709
+ function buildTcXml(cell, content, _options) {
3497
3710
  let tcPr = cell.tcPrXml;
3498
3711
  if (cell.colSpan > 1 && !tcPr.includes("gridSpan")) {
3499
3712
  tcPr = tcPr.replace("</w:tcPr>", `<w:gridSpan w:val="${cell.colSpan}"/></w:tcPr>`);
@@ -3547,6 +3760,7 @@ async function executeListGeneration(options) {
3547
3760
  generateRedlines = true,
3548
3761
  author = "AI",
3549
3762
  font = null,
3763
+ revisionIdAllocator = null,
3550
3764
  numberingService
3551
3765
  } = options;
3552
3766
  const normalizedListText = normalizeCompositeListMarkers(cleanText);
@@ -3582,10 +3796,15 @@ async function executeListGeneration(options) {
3582
3796
  results.push(serializeToOoxml(deletionRuns, null, [], {
3583
3797
  author,
3584
3798
  generateRedlines,
3585
- font
3799
+ font,
3800
+ revisionIdAllocator
3586
3801
  }));
3587
3802
  }
3588
- results.push(generateTableOoxml(tableData, { generateRedlines, author }));
3803
+ results.push(generateTableOoxml(tableData, {
3804
+ generateRedlines,
3805
+ author,
3806
+ revisionIdAllocator
3807
+ }));
3589
3808
  i = tableBlock.endIndex;
3590
3809
  continue;
3591
3810
  }
@@ -3600,6 +3819,7 @@ async function executeListGeneration(options) {
3600
3819
  generateRedlines,
3601
3820
  author,
3602
3821
  font,
3822
+ revisionIdAllocator,
3603
3823
  deletionRuns
3604
3824
  );
3605
3825
  results.push(entry.ooxml);
@@ -3690,7 +3910,7 @@ function collectMarkdownTableBlock(lineMetadata, index) {
3690
3910
  endIndex: cursor - 1
3691
3911
  };
3692
3912
  }
3693
- function buildListEntry(line, lineIndex, indentStep, numberingContext, numberingService, generateRedlines, author, font, deletionRuns) {
3913
+ function buildListEntry(line, lineIndex, indentStep, numberingContext, numberingService, generateRedlines, author, font, revisionIdAllocator, deletionRuns) {
3694
3914
  let pPrXml = "";
3695
3915
  let segmentText = "";
3696
3916
  if (line.headerMatch) {
@@ -3727,7 +3947,8 @@ function buildListEntry(line, lineIndex, indentStep, numberingContext, numbering
3727
3947
  ooxml: serializeToOoxml(runModel, pPrXml, formatHints, {
3728
3948
  author,
3729
3949
  generateRedlines,
3730
- font
3950
+ font,
3951
+ revisionIdAllocator
3731
3952
  })
3732
3953
  };
3733
3954
  }
@@ -3758,6 +3979,7 @@ var ReconciliationPipeline = class {
3758
3979
  this.validationMode = options.validationMode ?? "auto";
3759
3980
  this.numberingService = options.numberingService || new NumberingService();
3760
3981
  this.font = options.font || null;
3982
+ this.revisionIdAllocator = options.revisionIdAllocator || null;
3761
3983
  this.platform = options.platform ?? getPlatform();
3762
3984
  this.isWebPlatform = options.isWebPlatform ?? isWebPlatform(this.platform);
3763
3985
  this.enableEventLoopYielding = options.enableEventLoopYielding ?? this.isWebPlatform;
@@ -3776,10 +3998,18 @@ var ReconciliationPipeline = class {
3776
3998
  async execute(originalOoxml, newText, options = {}) {
3777
3999
  const warnings = [];
3778
4000
  try {
3779
- const doc = options.xmlDoc || (() => {
3780
- const parser = createParser();
3781
- return parser.parseFromString(originalOoxml, "application/xml");
3782
- })();
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;
3783
4013
  const pElement = getFirstElementByTagNS(doc, "*", "p");
3784
4014
  const { runModel, acceptedText, pPr } = ingestOoxml(originalOoxml, { xmlDoc: doc });
3785
4015
  const numberingContext = pElement ? detectNumberingContext(pElement) : null;
@@ -3811,8 +4041,12 @@ var ReconciliationPipeline = class {
3811
4041
  }
3812
4042
  await this.maybeYield(runModel.length, Math.max(acceptedText.length, cleanText.length));
3813
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;
3814
4046
  log(`[Reconcile] isTargetList: ${isTargetList}, paragraphCount: ${paragraphCount}`);
3815
- 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) {
3816
4050
  log("[Reconcile] \u{1F3AF} ENTERING LIST GENERATION PATH");
3817
4051
  log(`[Reconcile] cleanText preview: ${cleanText.substring(0, 100)}...`);
3818
4052
  log(`[Reconcile] acceptedText preview: ${acceptedText.substring(0, 100)}...`);
@@ -3831,7 +4065,8 @@ var ReconciliationPipeline = class {
3831
4065
  await this.maybeYield(patchedModel.length, Math.max(acceptedText.length, cleanText.length));
3832
4066
  const resultOoxml = serializeToOoxml(patchedModel, pPr, formatHints, {
3833
4067
  author: this.author,
3834
- generateRedlines: this.generateRedlines
4068
+ generateRedlines: this.generateRedlines,
4069
+ revisionIdAllocator: this.revisionIdAllocator
3835
4070
  });
3836
4071
  if (this.shouldRunValidation()) {
3837
4072
  const validation = this.validateBasic(resultOoxml);
@@ -3849,7 +4084,8 @@ var ReconciliationPipeline = class {
3849
4084
  return {
3850
4085
  ooxml: originalOoxml,
3851
4086
  isValid: false,
3852
- warnings: [`Pipeline error: ${error2.message}`]
4087
+ warnings: [`Pipeline error: ${error2.message}`],
4088
+ error: error2?.code ? { code: error2.code, message: error2.message } : void 0
3853
4089
  };
3854
4090
  }
3855
4091
  }
@@ -3863,8 +4099,12 @@ var ReconciliationPipeline = class {
3863
4099
  const errors = [];
3864
4100
  try {
3865
4101
  const wrappedXml = `<root xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">${ooxml}</root>`;
3866
- const parser = createParser();
3867
- 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
+ }
3868
4108
  const parseError = getXmlParseError(doc);
3869
4109
  if (parseError) {
3870
4110
  errors.push("Generated OOXML is not well-formed XML: " + parseError.textContent.substring(0, 100));
@@ -3935,6 +4175,7 @@ var ReconciliationPipeline = class {
3935
4175
  generateRedlines: this.generateRedlines,
3936
4176
  author: this.author,
3937
4177
  font: this.font,
4178
+ revisionIdAllocator: this.revisionIdAllocator,
3938
4179
  numberingService: this.numberingService
3939
4180
  });
3940
4181
  }
@@ -3964,7 +4205,8 @@ var ReconciliationPipeline = class {
3964
4205
  }
3965
4206
  const tableOoxml = generateTableOoxml(tableData, {
3966
4207
  generateRedlines: this.generateRedlines,
3967
- author: this.author
4208
+ author: this.author,
4209
+ revisionIdAllocator: this.revisionIdAllocator
3968
4210
  });
3969
4211
  return {
3970
4212
  ooxml: tableOoxml,
@@ -4340,7 +4582,7 @@ function extractFormattingFromOoxml(xmlDoc) {
4340
4582
  // engine/run-builders.js
4341
4583
  function createTrackChange(xmlDoc, type, run, author) {
4342
4584
  const wrapper = createWordElement(xmlDoc, type === "ins" ? "w:ins" : "w:del");
4343
- const metadata = createRevisionMetadata(author);
4585
+ const metadata = createRevisionMetadata(author, xmlDoc);
4344
4586
  wrapper.setAttribute("w:id", String(metadata.id));
4345
4587
  wrapper.setAttribute("w:author", metadata.author);
4346
4588
  wrapper.setAttribute("w:date", metadata.date);
@@ -4382,7 +4624,7 @@ function markParagraphMark(xmlDoc, paragraph, author, type) {
4382
4624
  }
4383
4625
  }
4384
4626
  const marker = createWordElement(xmlDoc, type === "ins" ? "w:ins" : "w:del");
4385
- const metadata = createRevisionMetadata(author);
4627
+ const metadata = createRevisionMetadata(author, xmlDoc);
4386
4628
  marker.setAttribute("w:id", String(metadata.id));
4387
4629
  marker.setAttribute("w:author", metadata.author);
4388
4630
  marker.setAttribute("w:date", metadata.date);
@@ -4521,7 +4763,7 @@ function injectFormattingToRPr(xmlDoc, baseRPr, format, author, generateRedlines
4521
4763
  }
4522
4764
  function snapshotAndAttachRPrChange(xmlDoc, rPr, author, dateStr, sourceNode) {
4523
4765
  const rPrChange = createWordElement(xmlDoc, "w:rPrChange");
4524
- const metadata = createRevisionMetadata(author);
4766
+ const metadata = createRevisionMetadata(author, xmlDoc);
4525
4767
  rPrChange.setAttribute("w:id", String(metadata.id));
4526
4768
  rPrChange.setAttribute("w:author", metadata.author);
4527
4769
  rPrChange.setAttribute("w:date", dateStr || metadata.date);
@@ -5351,11 +5593,11 @@ function insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, fo
5351
5593
  }
5352
5594
 
5353
5595
  // engine/surgical-mode.js
5354
- 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 = {}) {
5355
5597
  void originalText;
5356
5598
  const allParagraphs = targetParagraph ? [targetParagraph] : getDocumentParagraphs(xmlDoc);
5357
5599
  const { fullText, textSpans } = buildSurgicalTextSpans(allParagraphs);
5358
- const diffs = computeWordDiffs(fullText, modifiedText);
5600
+ const diffs = computeWordDiffs(fullText, modifiedText, diffOptions);
5359
5601
  const spanIndex = buildSpanIndex(textSpans);
5360
5602
  let originalPos = 0;
5361
5603
  let newPos = 0;
@@ -5398,10 +5640,28 @@ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, autho
5398
5640
  }
5399
5641
 
5400
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
+ }
5401
5651
  function createRangeCursorLookup2(ranges) {
5402
5652
  let cursor = 0;
5403
5653
  return {
5404
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
+ }
5405
5665
  while (cursor < ranges.length && ranges[cursor].end <= index) {
5406
5666
  cursor++;
5407
5667
  }
@@ -5422,11 +5682,11 @@ function indexSentinelsByStart(sentinelMap) {
5422
5682
  });
5423
5683
  return sentinelMapByStart;
5424
5684
  }
5425
- function buildReconstructionMapping(xmlDoc, modifiedText) {
5685
+ function buildReconstructionMapping(xmlDoc, modifiedText, selectedParagraphs = null) {
5426
5686
  const rootElement = xmlDoc.documentElement;
5427
- const isBodyRoot = rootElement.nodeName === "w:body" || rootElement.nodeName.endsWith(":package");
5428
- const paragraphs = getDocumentParagraphs(xmlDoc);
5429
- 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");
5430
5690
  if (!body && isBodyRoot) body = rootElement;
5431
5691
  let originalFullText = "";
5432
5692
  const propertyMap = [];
@@ -5434,7 +5694,8 @@ function buildReconstructionMapping(xmlDoc, modifiedText) {
5434
5694
  const sentinelMap = [];
5435
5695
  const referenceMap = /* @__PURE__ */ new Map();
5436
5696
  const tokenToCharMap = /* @__PURE__ */ new Map();
5437
- let nextCharCode = 57344;
5697
+ const breakChars = /* @__PURE__ */ new Set();
5698
+ const characterState = { nextCharCode: 57344 };
5438
5699
  const uniqueContainers = /* @__PURE__ */ new Set();
5439
5700
  paragraphs.forEach((paragraph, paragraphIndex) => {
5440
5701
  const paragraphStart = originalFullText.length;
@@ -5446,15 +5707,13 @@ function buildReconstructionMapping(xmlDoc, modifiedText) {
5446
5707
  sentinelMap,
5447
5708
  referenceMap,
5448
5709
  tokenToCharMap,
5449
- nextCharCode
5710
+ characterState,
5711
+ breakChars
5450
5712
  );
5451
- if (referenceMap.size > tokenToCharMap.size) {
5452
- nextCharCode++;
5453
- }
5454
5713
  });
5455
5714
  originalFullText = appendParagraphBoundary(originalFullText, paragraphIndex, paragraphs.length);
5456
5715
  const paragraphEnd = originalFullText.length;
5457
- const pPr = getFirstElementByTag(paragraph, "w:pPr");
5716
+ const pPr = getFirstElementByTagNSOrTag(paragraph, NS_W, "pPr");
5458
5717
  const container = paragraph.parentNode;
5459
5718
  if (container) uniqueContainers.add(container);
5460
5719
  paragraphMap.push({
@@ -5464,7 +5723,13 @@ function buildReconstructionMapping(xmlDoc, modifiedText) {
5464
5723
  container: container || body
5465
5724
  });
5466
5725
  });
5467
- 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);
5468
5733
  tokenToCharMap.forEach((char, tokenString) => {
5469
5734
  const escapedToken = tokenString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
5470
5735
  processedModifiedText = processedModifiedText.replace(new RegExp(escapedToken, "g"), char);
@@ -5520,6 +5785,124 @@ function buildReconstructionMapping(xmlDoc, modifiedText) {
5520
5785
  isParagraphStart: (index) => paragraphStarts.has(index)
5521
5786
  };
5522
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
+ }
5523
5906
  function preserveReferencePlaceholders(originalFullText, modifiedText, referenceMap) {
5524
5907
  let result = modifiedText;
5525
5908
  for (const referenceChar of referenceMap.keys()) {
@@ -5539,28 +5922,28 @@ function preserveReferencePlaceholders(originalFullText, modifiedText, reference
5539
5922
  }
5540
5923
  return result;
5541
5924
  }
5542
- function processChildNode(child, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, nextCharCode) {
5543
- if (child.nodeName === "w:r") {
5544
- 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);
5545
5928
  }
5546
- if (child.nodeName === "w:hyperlink") {
5929
+ if (isWordElement(child, "hyperlink")) {
5547
5930
  return processHyperlinkForReconstruction(child, originalFullText, propertyMap);
5548
5931
  }
5549
- 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")) {
5550
5933
  sentinelMap.push({ start: originalFullText.length, node: child });
5551
5934
  return originalFullText + "\uFFFC";
5552
5935
  }
5553
- if (["w:commentRangeStart", "w:commentRangeEnd"].includes(child.nodeName)) {
5936
+ if (isWordElement(child, "commentRangeStart") || isWordElement(child, "commentRangeEnd")) {
5554
5937
  sentinelMap.push({ start: originalFullText.length, node: child, isCommentMarker: true });
5555
5938
  return originalFullText;
5556
5939
  }
5557
5940
  return originalFullText;
5558
5941
  }
5559
- function processRunForReconstruction(runElement, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, nextCharCode) {
5942
+ function processRunForReconstruction(runElement, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, characterState, breakChars) {
5560
5943
  let fullText = originalFullText;
5561
- const rPr = getFirstElementByTag(runElement, "w:rPr");
5944
+ const rPr = getFirstElementByTagNSOrTag(runElement, NS_W, "rPr");
5562
5945
  Array.from(runElement.childNodes).forEach((runChild) => {
5563
- if (runChild.nodeName === "w:t") {
5946
+ if (isWordElement(runChild, "t")) {
5564
5947
  const textContent = runChild.textContent || "";
5565
5948
  if (textContent.length > 0) {
5566
5949
  propertyMap.push({
@@ -5570,38 +5953,47 @@ function processRunForReconstruction(runElement, originalFullText, propertyMap,
5570
5953
  });
5571
5954
  fullText += textContent;
5572
5955
  }
5573
- } else if (runChild.nodeName === "w:br" || runChild.nodeName === "w:cr") {
5574
- 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;
5575
5961
  propertyMap.push({ start: fullText.length - 1, end: fullText.length, rPr });
5576
- } else if (runChild.nodeName === "w:tab") {
5962
+ } else if (isWordElement(runChild, "tab")) {
5577
5963
  fullText += " ";
5578
5964
  propertyMap.push({ start: fullText.length - 1, end: fullText.length, rPr });
5579
- } else if (runChild.nodeName === "w:noBreakHyphen") {
5965
+ } else if (isWordElement(runChild, "noBreakHyphen")) {
5580
5966
  fullText += "\u2011";
5581
5967
  propertyMap.push({ start: fullText.length - 1, end: fullText.length, rPr });
5582
- } else if (["w:drawing", "w:pict", "w:object", "w:fldChar", "w:instrText", "w:sym"].includes(runChild.nodeName)) {
5583
- const textBoxContent = getFirstElementByTag(runChild, "w:txbxContent");
5584
- 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;
5585
5973
  sentinelMap.push({
5586
5974
  start: fullText.length,
5587
5975
  node: runChild,
5976
+ wrapInRun: true,
5977
+ rPr,
5978
+ zeroWidth: isFieldStructure,
5979
+ affinity: fieldCharType === "end" ? "left" : "right",
5588
5980
  isTextBox: hasTextBox,
5589
5981
  originalContainer: hasTextBox ? textBoxContent : void 0
5590
5982
  });
5591
5983
  fullText += "\uFFFC";
5592
5984
  propertyMap.push({ start: fullText.length - 1, end: fullText.length, rPr });
5593
- } else if (runChild.nodeName === "w:footnoteReference" || runChild.nodeName === "w:endnoteReference") {
5594
- const id = runChild.getAttribute("w:id");
5985
+ } else if (isWordElement(runChild, "footnoteReference") || isWordElement(runChild, "endnoteReference")) {
5986
+ const id = wordAttribute(runChild, "id");
5595
5987
  if (id) {
5596
- const type = runChild.nodeName === "w:footnoteReference" ? "FN" : "EN";
5988
+ const type = isWordElement(runChild, "footnoteReference") ? "FN" : "EN";
5597
5989
  const tokenString = `{{__${type}_${id}__}}`;
5598
- const char = String.fromCharCode(nextCharCode);
5990
+ const char = String.fromCharCode(characterState.nextCharCode++);
5599
5991
  referenceMap.set(char, runChild);
5600
5992
  tokenToCharMap.set(tokenString, char);
5601
5993
  fullText += char;
5602
5994
  propertyMap.push({ start: fullText.length - 1, end: fullText.length, rPr });
5603
5995
  }
5604
- } else if (runChild.nodeName === "w:commentReference") {
5996
+ } else if (isWordElement(runChild, "commentReference")) {
5605
5997
  sentinelMap.push({ start: fullText.length, node: runChild, isCommentMarker: true });
5606
5998
  }
5607
5999
  });
@@ -5610,9 +6002,9 @@ function processRunForReconstruction(runElement, originalFullText, propertyMap,
5610
6002
  function processHyperlinkForReconstruction(hyperlinkElement, originalFullText, propertyMap) {
5611
6003
  let fullText = originalFullText;
5612
6004
  Array.from(hyperlinkElement.childNodes).forEach((hyperlinkChild) => {
5613
- if (hyperlinkChild.nodeName !== "w:r") return;
5614
- const rPr = getFirstElementByTag(hyperlinkChild, "w:rPr");
5615
- 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");
5616
6008
  texts.forEach((textNode) => {
5617
6009
  const textContent = textNode.textContent || "";
5618
6010
  if (textContent.length === 0) return;
@@ -5632,7 +6024,6 @@ function processHyperlinkForReconstruction(hyperlinkElement, originalFullText, p
5632
6024
  function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, formatHints, generateRedlines = true) {
5633
6025
  const {
5634
6026
  paragraphs,
5635
- paragraphMap,
5636
6027
  containerFragments,
5637
6028
  sentinelMapByStart,
5638
6029
  referenceMap,
@@ -5655,10 +6046,16 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
5655
6046
  }
5656
6047
  let currentOriginalIndex = 0;
5657
6048
  let currentInsertOffset = 0;
6049
+ let pendingReplacementStart = null;
5658
6050
  const emittedCommentMarkers = /* @__PURE__ */ new WeakSet();
5659
6051
  for (const [op, text] of diffs) {
5660
6052
  if (op === 0 || op === -1) {
5661
6053
  const type = op === 0 ? "equal" : "delete";
6054
+ if (op === 0) {
6055
+ pendingReplacementStart = null;
6056
+ } else if (pendingReplacementStart === null) {
6057
+ pendingReplacementStart = currentOriginalIndex;
6058
+ }
5662
6059
  let offset = 0;
5663
6060
  while (offset < text.length) {
5664
6061
  const chunkStart = currentOriginalIndex + offset;
@@ -5695,7 +6092,8 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
5695
6092
  continue;
5696
6093
  }
5697
6094
  if (op === 1) {
5698
- 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);
5699
6097
  const appendResult = appendTextToCurrent(
5700
6098
  xmlDoc,
5701
6099
  text,
@@ -5718,29 +6116,46 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
5718
6116
  );
5719
6117
  currentParagraph = appendResult.currentParagraph;
5720
6118
  currentInsertOffset += text.length;
6119
+ pendingReplacementStart = null;
5721
6120
  }
5722
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
+ });
5723
6131
  paragraphs.forEach((paragraph) => {
5724
6132
  if (paragraph.parentNode) {
5725
6133
  paragraph.parentNode.removeChild(paragraph);
5726
6134
  }
5727
6135
  });
6136
+ let hasDocumentTarget = false;
6137
+ let serializedDocumentOutput = "";
5728
6138
  containerFragments.forEach((fragment, container) => {
5729
6139
  const replacement = replacementContainers.get(container);
5730
6140
  const target = replacement || container;
5731
6141
  if (target.nodeType === 9) {
5732
- const firstChild = fragment.firstChild;
5733
- if (firstChild) {
5734
- target.appendChild(firstChild);
5735
- while (fragment.firstChild) {
5736
- target.documentElement.appendChild(fragment.firstChild);
5737
- }
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("");
5738
6147
  }
5739
6148
  return;
5740
6149
  }
5741
- 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
+ }
5742
6156
  });
5743
- return { oxml: serializer.serializeToString(xmlDoc), hasChanges: true };
6157
+ const oxml = hasDocumentTarget && serializedDocumentOutput ? serializedDocumentOutput : serializer.serializeToString(xmlDoc);
6158
+ return { oxml, hasChanges: true };
5744
6159
  }
5745
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()) {
5746
6161
  let localBaseIndex = baseIndex;
@@ -5752,7 +6167,7 @@ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, curren
5752
6167
  const commentMarkers = sentinelsAtOffset.filter((sentinel) => sentinel.isCommentMarker && !emittedCommentMarkers.has(sentinel.node));
5753
6168
  commentMarkers.forEach((marker) => {
5754
6169
  emittedCommentMarkers.add(marker.node);
5755
- if (marker.node.nodeName === "w:commentReference") {
6170
+ if (isWordElement(marker.node, "commentReference")) {
5756
6171
  const run = createWordElement(xmlDoc, "w:r");
5757
6172
  run.appendChild(marker.node.cloneNode(true));
5758
6173
  localParagraph.appendChild(run);
@@ -5764,9 +6179,9 @@ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, curren
5764
6179
  const info = getParagraphInfo(localBaseIndex + 1);
5765
6180
  const nextParagraph = createNewParagraph(info.pPr);
5766
6181
  if (generateRedlines && type === "insert") {
5767
- markParagraphMarkInserted(xmlDoc, nextParagraph, author);
6182
+ markParagraphMarkInserted(xmlDoc, localParagraph, author);
5768
6183
  } else if (generateRedlines && type === "delete") {
5769
- markParagraphMarkDeleted(xmlDoc, nextParagraph, author);
6184
+ markParagraphMarkDeleted(xmlDoc, localParagraph, author);
5770
6185
  }
5771
6186
  const fragment = containerFragments.get(info.container);
5772
6187
  if (fragment) {
@@ -5782,13 +6197,20 @@ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, curren
5782
6197
  if (sentinel) {
5783
6198
  const clone = sentinel.node.cloneNode(true);
5784
6199
  if (sentinel.isTextBox && sentinel.originalContainer) {
5785
- const newContainer = getFirstElementByTag(clone, "w:txbxContent");
6200
+ const newContainer = getFirstElementByTagNSOrTag(clone, NS_W, "txbxContent");
5786
6201
  if (newContainer) {
5787
6202
  while (newContainer.firstChild) newContainer.removeChild(newContainer.firstChild);
5788
6203
  replacementContainers.set(sentinel.originalContainer, newContainer);
5789
6204
  }
5790
6205
  }
5791
- 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
+ }
5792
6214
  }
5793
6215
  localBaseIndex++;
5794
6216
  if (type !== "delete") localInsertOffset++;
@@ -5847,12 +6269,24 @@ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, curren
5847
6269
  }
5848
6270
 
5849
6271
  // engine/reconstruction-mode.js
5850
- function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true) {
5851
- 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);
5852
6286
  if (mapping.paragraphs.length === 0) {
5853
6287
  return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
5854
6288
  }
5855
- const diffs = computeWordDiffs(mapping.originalFullText, mapping.processedModifiedText);
6289
+ const diffs = computeWordDiffs(mapping.originalFullText, mapping.processedModifiedText, diffOptions);
5856
6290
  return withOoxmlSourceType(applyReconstructionDiffs(
5857
6291
  xmlDoc,
5858
6292
  diffs,
@@ -5881,11 +6315,15 @@ function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, auth
5881
6315
  if (operations.length === 0) {
5882
6316
  return noChanges(serializer, xmlDoc);
5883
6317
  }
5884
- const options = { generateRedlines, author };
6318
+ const options = {
6319
+ generateRedlines,
6320
+ author,
6321
+ revisionIdAllocator: getRevisionIdAllocatorForDocument(xmlDoc)
6322
+ };
5885
6323
  const reconciledOxml = serializeVirtualGridToOoxml(oldGrid, operations, options);
5886
6324
  const wrappedOxml = `<root xmlns:w="${NS_W}">${reconciledOxml}</root>`;
5887
- const reconcileParser = parser || createParser();
5888
- const reconciledDoc = reconcileParser.parseFromString(wrappedOxml, "application/xml");
6325
+ const reconciledDoc = parseOoxmlSafe(wrappedOxml, "application/xml").doc;
6326
+ if (!reconciledDoc) return noChanges(serializer, xmlDoc);
5889
6327
  const parseError = getXmlParseError(reconciledDoc);
5890
6328
  if (parseError) {
5891
6329
  error("[OxmlEngine] Failed to parse reconciled table OOXML:", parseError.textContent);
@@ -5901,14 +6339,15 @@ function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, auth
5901
6339
  return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: true });
5902
6340
  }
5903
6341
  function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines) {
6342
+ const revisionIdAllocator = getRevisionIdAllocatorForDocument(xmlDoc);
5904
6343
  const tableData = parseTable(modifiedText);
5905
6344
  if (!tableData || tableData.rows.length === 0 && tableData.headers.length === 0) {
5906
6345
  log("[OxmlEngine] Failed to parse table data from Markdown");
5907
6346
  return noChanges(serializer, xmlDoc);
5908
6347
  }
5909
- const tableOoxml = generateTableOoxml(tableData, { generateRedlines, author });
5910
- const activeParser = parser || createParser();
5911
- 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);
5912
6351
  const tableParseError = getXmlParseError(tableDoc);
5913
6352
  if (tableParseError) {
5914
6353
  error("[OxmlEngine] Failed to parse generated table OOXML:", tableParseError.textContent);
@@ -5931,13 +6370,15 @@ function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser
5931
6370
  let firstParagraph = paragraphs[0];
5932
6371
  let parent = firstParagraph.parentNode;
5933
6372
  if (parent && parent.nodeType === 9) {
5934
- const wrappedDoc = activeParser.parseFromString(
6373
+ const wrappedDoc = parseOoxmlSafe(
5935
6374
  `<w:document xmlns:w="${NS_W}"><w:body/></w:document>`,
5936
6375
  "application/xml"
5937
- );
6376
+ ).doc;
6377
+ if (!wrappedDoc) return noChanges(serializer, workingDoc);
5938
6378
  const wrappedBody = getFirstElementByTagNS(wrappedDoc, NS_W, "body");
5939
6379
  paragraphs.forEach((p) => wrappedBody.appendChild(wrappedDoc.importNode(p, true)));
5940
6380
  workingDoc = wrappedDoc;
6381
+ setRevisionIdAllocatorForDocument(workingDoc, revisionIdAllocator);
5941
6382
  paragraphs = getElementsByTagNS(workingDoc, NS_W, "p");
5942
6383
  firstParagraph = paragraphs[0];
5943
6384
  parent = firstParagraph.parentNode;
@@ -5958,7 +6399,7 @@ function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser
5958
6399
  }
5959
6400
  });
5960
6401
  const del = createWordElement(workingDoc, "w:del");
5961
- const metadata = createRevisionMetadata(author);
6402
+ const metadata = createRevisionMetadata(author, workingDoc);
5962
6403
  del.setAttribute("w:id", String(metadata.id));
5963
6404
  del.setAttribute("w:author", metadata.author);
5964
6405
  del.setAttribute("w:date", metadata.date);
@@ -6022,17 +6463,19 @@ function authorMatchesNode(node, filter) {
6022
6463
  return !!nodeAuthor && nodeAuthor === filter.normalizedAuthor;
6023
6464
  }
6024
6465
  function parseXmlWithWarnings(oxml, parseFailurePrefix) {
6025
- const parser = createParser();
6026
- const xmlDoc = parser.parseFromString(oxml, "application/xml");
6027
- const parseError = getXmlParseError(xmlDoc);
6028
- 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";
6029
6470
  return {
6030
6471
  xmlDoc: null,
6031
6472
  serializer: null,
6032
- warning: `${parseFailurePrefix}: ${parseError.textContent || "parse error"}`
6473
+ warning: `${parseFailurePrefix}: ${message}`,
6474
+ warnings: parsed.warnings,
6475
+ error: { code: "PARSE_ERROR", message }
6033
6476
  };
6034
6477
  }
6035
- return { xmlDoc, serializer: createSerializer(), warning: null };
6478
+ return { xmlDoc: parsed.doc, serializer: createSerializer(), warning: null, warnings: parsed.warnings, error: null };
6036
6479
  }
6037
6480
  function removeNode(node) {
6038
6481
  if (node?.parentNode) {
@@ -6092,9 +6535,17 @@ function acceptTrackedChangesInOoxml(oxml, options = {}) {
6092
6535
  }
6093
6536
  const parseResult = parseXmlWithWarnings(oxml, "Failed to parse OOXML");
6094
6537
  if (!parseResult.xmlDoc) {
6095
- 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
+ };
6096
6546
  }
6097
6547
  const { xmlDoc, serializer } = parseResult;
6548
+ warnings.push(...parseResult.warnings || []);
6098
6549
  let acceptedCount = 0;
6099
6550
  for (const insNode of getWordElementsByLocalName(xmlDoc, "ins")) {
6100
6551
  if (!insNode.parentNode || !authorMatchesNode(insNode, filter)) continue;
@@ -6226,9 +6677,17 @@ function rejectTrackedChangesInOoxml(oxml, options = {}) {
6226
6677
  }
6227
6678
  const parseResult = parseXmlWithWarnings(oxml, "Failed to parse OOXML");
6228
6679
  if (!parseResult.xmlDoc) {
6229
- 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
+ };
6230
6688
  }
6231
6689
  const { xmlDoc, serializer } = parseResult;
6690
+ warnings.push(...parseResult.warnings || []);
6232
6691
  let rejectedCount = 0;
6233
6692
  for (const insNode of getWordElementsByLocalName(xmlDoc, "ins")) {
6234
6693
  if (!insNode.parentNode || !authorMatchesNode(insNode, filter)) continue;
@@ -6351,10 +6810,13 @@ function deleteCommentsByAuthorInOoxml(oxml, options = {}) {
6351
6810
  hasChanges: false,
6352
6811
  commentsRemoved: 0,
6353
6812
  referencesRemoved: 0,
6354
- warnings: [parseResult.warning]
6813
+ status: "error",
6814
+ error: parseResult.error,
6815
+ warnings: [...parseResult.warnings || [], parseResult.warning]
6355
6816
  };
6356
6817
  }
6357
6818
  const { xmlDoc, serializer } = parseResult;
6819
+ warnings.push(...parseResult.warnings || []);
6358
6820
  const { targetIds, commentNodes } = collectCommentTargetIds(xmlDoc, filter);
6359
6821
  if (filter.allAuthors) {
6360
6822
  for (const localName of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
@@ -6377,66 +6839,96 @@ function deleteCommentsByAuthorInOoxml(oxml, options = {}) {
6377
6839
 
6378
6840
  // engine/oxml-engine.js
6379
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 ?? "");
6380
6846
  const generateRedlines = options.generateRedlines ?? true;
6381
6847
  const author = options.author || getDefaultAuthor();
6382
- const parser = createParser();
6383
6848
  const serializer = createSerializer();
6849
+ let parseWarnings = [];
6850
+ const operationWarnings = [];
6851
+ let normalizedExistingRevisions = false;
6852
+ const keepNormalizedNoOp = options.existingRevisions === "accept-all-first-keep-normalized";
6384
6853
  const finalize = (result) => {
6385
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
+ }
6386
6871
  if (!withStatus.status) {
6387
6872
  withStatus.status = withStatus.hasChanges ? "ok" : "no-op";
6388
6873
  }
6389
6874
  return withOoxmlSourceType(withStatus);
6390
6875
  };
6391
- const noChanges2 = () => finalize({ oxml, hasChanges: false });
6392
- let xmlDoc;
6393
- try {
6394
- xmlDoc = parser.parseFromString(oxml, "text/xml");
6395
- } catch (e) {
6396
- error("[OxmlEngine] Failed to parse OXML:", e);
6397
- return finalize({
6398
- oxml,
6399
- hasChanges: false,
6400
- status: "error",
6401
- error: { code: "PARSE_ERROR", message: "Could not parse OOXML input." }
6402
- });
6403
- }
6404
- const parseError = getXmlParseError(xmlDoc);
6405
- if (parseError) {
6406
- 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);
6407
6893
  return finalize({
6408
- oxml,
6894
+ oxml: inputOoxml,
6409
6895
  hasChanges: false,
6410
6896
  status: "error",
6411
- error: { code: "PARSE_ERROR", message: parseError.textContent || "Could not parse OOXML input." }
6897
+ error: { code: "PARSE_ERROR", message }
6412
6898
  });
6413
6899
  }
6414
- seedRevisionIdsFromDocument(xmlDoc);
6900
+ const revisionIdAllocator = options?._revisionIdAllocator instanceof RevisionIdAllocator ? options._revisionIdAllocator : new RevisionIdAllocator();
6901
+ seedRevisionIdsFromDocument(xmlDoc, revisionIdAllocator);
6415
6902
  if (containsTrackedChanges(xmlDoc)) {
6416
6903
  const existingRevisionsPolicy = options.existingRevisions || "reject-input";
6417
- if (existingRevisionsPolicy === "accept-all-first") {
6904
+ if (existingRevisionsPolicy === "accept-all-first" || existingRevisionsPolicy === "accept-all-first-keep-normalized") {
6418
6905
  log("[OxmlEngine] Existing revisions detected; accepting all input revisions before redlining");
6419
- const accepted = acceptTrackedChangesInOoxml(oxml, { allAuthors: true });
6420
- oxml = accepted.oxml;
6421
- xmlDoc = parser.parseFromString(oxml, "text/xml");
6422
- const acceptedParseError = getXmlParseError(xmlDoc);
6423
- if (acceptedParseError) {
6424
- 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);
6425
6917
  return finalize({
6426
- oxml,
6918
+ oxml: inputOoxml,
6427
6919
  hasChanges: false,
6428
6920
  status: "error",
6429
6921
  error: {
6430
6922
  code: "PARSE_ERROR",
6431
- message: "Could not parse OOXML after accepting existing revisions."
6923
+ message
6432
6924
  }
6433
6925
  });
6434
6926
  }
6435
- seedRevisionIdsFromDocument(xmlDoc);
6927
+ seedRevisionIdsFromDocument(xmlDoc, revisionIdAllocator);
6436
6928
  } else {
6437
6929
  log("[OxmlEngine] Existing revisions detected; rejecting input per existingRevisions policy");
6438
6930
  return finalize({
6439
- oxml,
6931
+ oxml: inputOoxml,
6440
6932
  hasChanges: false,
6441
6933
  status: "error",
6442
6934
  error: {
@@ -6450,22 +6942,33 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
6450
6942
  if (initialTableCellContext.hasTableWrapper && initialTableCellContext.targetParagraph && !options._isolatedTableCell) {
6451
6943
  log("[OxmlEngine] Isolating table-cell paragraph before diff");
6452
6944
  const isolatedOxml = serializeParagraphOnly(xmlDoc, initialTableCellContext.targetParagraph, serializer);
6453
- return applyRedlineToOxml(isolatedOxml, originalText, modifiedText, {
6945
+ const isolatedResult = await applyRedlineToOxml(isolatedOxml, originalText, modifiedText, {
6454
6946
  ...options,
6455
6947
  _isolatedTableCell: true
6456
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.");
6457
6957
  }
6458
- const sanitizedText = sanitizeAiResponse(modifiedText);
6459
6958
  const { cleanText: cleanModifiedText, formatHints } = preprocessMarkdown(sanitizedText);
6460
6959
  const hasTextChanges = cleanModifiedText.trim() !== originalText.trim();
6461
6960
  const hasFormatHints = formatHints.length > 0;
6462
6961
  const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
6463
6962
  const hasExistingFormatting = existingFormatHints.length > 0;
6464
6963
  const visibleText = textSpans.map((span) => textSpanVisibleText(span)).join("");
6465
- 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) {
6466
6969
  log("[OxmlEngine] Target text not found in OOXML");
6467
6970
  return finalize({
6468
- oxml,
6971
+ oxml: inputOoxml,
6469
6972
  hasChanges: false,
6470
6973
  status: "error",
6471
6974
  error: {
@@ -6506,10 +7009,14 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
6506
7009
  );
6507
7010
  };
6508
7011
  log(`[OxmlEngine] Text changes: ${hasTextChanges}, New format hints: ${formatHints.length}, Existing format hints: ${existingFormatHints.length}`);
6509
- const needsFormatRemoval = !hasTextChanges && !hasFormatHints && hasExistingFormatting;
7012
+ const needsFormatRemoval = options.removeFormatting === true && !hasTextChanges && !hasFormatHints && hasExistingFormatting;
6510
7013
  if (!hasTextChanges && !hasFormatHints && !hasExistingFormatting) {
6511
7014
  log("[OxmlEngine] No text changes, no format hints, and no existing formatting detected");
6512
- 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();
6513
7020
  }
6514
7021
  if (needsFormatRemoval) {
6515
7022
  log("[OxmlEngine] Format REMOVAL detected: applying surgical replacement in OOXML");
@@ -6569,50 +7076,78 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
6569
7076
  const isTargetList = isListTargetLoose(cleanModifiedText);
6570
7077
  const tableCellContext = initialTableCellContext;
6571
7078
  log(`[OxmlEngine] Mode: ${hasTables ? "SURGICAL" : "RECONSTRUCTION"}, formatHints: ${formatHints.length}, isMarkdownTable: ${isMarkdownTable}, isTargetList: ${isTargetList}, isTableCellParagraph: ${tableCellContext.isTableCellParagraph}`);
6572
- if (isMarkdownTable && !hasTables) {
6573
- log("[OxmlEngine] Text-to-table transformation: generating new table from Markdown");
6574
- return finalize(applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer, parser, author, generateRedlines));
6575
- }
6576
- if (hasTables && isMarkdownTable) {
6577
- return finalize(applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, parser, author, generateRedlines));
6578
- }
6579
- if (hasTables) {
6580
- const surgicalTarget = tableCellContext.hasTableWrapper && tableCellContext.targetParagraph ? tableCellContext.targetParagraph : null;
6581
- if (surgicalTarget) {
6582
- 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));
6583
7083
  }
6584
- const result = applySurgicalMode(
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();
7130
+ }
7131
+ return finalize(applyReconstructionMode(
6585
7132
  xmlDoc,
6586
7133
  originalText,
6587
7134
  cleanModifiedText,
6588
7135
  serializer,
6589
7136
  author,
6590
7137
  formatHints,
6591
- generateRedlines,
6592
- surgicalTarget
6593
- );
6594
- if (tableCellContext.hasTableWrapper && result.hasChanges && tableCellContext.targetParagraph) {
6595
- log("[OxmlEngine] Stripping table wrapper for table cell paragraph (surgical mode)");
6596
- return finalize({ oxml: serializeParagraphOnly(xmlDoc, tableCellContext.targetParagraph, serializer), hasChanges: true });
6597
- }
6598
- return finalize(result);
6599
- }
6600
- if (isTargetList) {
6601
- log("[OxmlEngine] \u{1F3AF} Using reconciliation pipeline for list generation");
6602
- const pipeline = new ReconciliationPipeline({ author, generateRedlines });
6603
- const result = await pipeline.execute(oxml, modifiedText, { xmlDoc });
6604
- if (result.isValid && result.ooxml && result.ooxml !== oxml) {
6605
- log(`[OxmlEngine] Wrapping list OOXML with numbering definitions, includeNumbering=${result.includeNumbering}`);
6606
- const wrapped = wrapInDocumentFragment(result.ooxml, {
6607
- includeNumbering: result.includeNumbering ?? true,
6608
- 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 }
6609
7147
  });
6610
- log(`[OxmlEngine] \u2705 Wrapped OOXML length: ${wrapped.length}`);
6611
- return finalize({ oxml: wrapped, hasChanges: true });
6612
7148
  }
6613
- return noChanges2();
7149
+ throw caught;
6614
7150
  }
6615
- return finalize(applyReconstructionMode(xmlDoc, originalText, cleanModifiedText, serializer, author, formatHints, generateRedlines));
6616
7151
  }
6617
7152
  function normalizeTargetText(text) {
6618
7153
  return String(text || "").replace(/[\t\n\u2011]/g, " ").replace(/\s+/g, " ").trim();
@@ -6626,15 +7161,13 @@ function textSpanVisibleText(span) {
6626
7161
  return node?.textContent || "";
6627
7162
  }
6628
7163
  function sanitizeAiResponse(text) {
6629
- let cleaned = text;
6630
- cleaned = cleaned.replace(/^(Here is the redline:|Here is the text:|Sure, I can help:|Here's the updated text:)\s*/i, "");
6631
- cleaned = cleaned.replace(/\$\\text\{/g, "").replace(/\}\$/g, "");
6632
- cleaned = cleaned.replace(/\$([^0-9\n]+?)\$/g, "$1");
6633
- cleaned = cleaned.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n");
6634
- 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
+ );
6635
7168
  }
6636
7169
  function parseOoxml(ooxmlString) {
6637
- return parseXml(ooxmlString, "application/xml");
7170
+ return parseOoxmlSafe(ooxmlString, "application/xml").doc;
6638
7171
  }
6639
7172
  function serializeOoxml(doc) {
6640
7173
  return serializeXml(doc);
@@ -6644,6 +7177,11 @@ function serializeOoxml(doc) {
6644
7177
  function toArray(nodeList) {
6645
7178
  return Array.from(nodeList || []);
6646
7179
  }
7180
+ function createTargetNotFoundError(message) {
7181
+ const error2 = new Error(message);
7182
+ error2.code = "TARGET_NOT_FOUND";
7183
+ return error2;
7184
+ }
6647
7185
  var WORD_MAIN_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
6648
7186
  function getElementsByLocalName(node, localName) {
6649
7187
  if (!node) return [];
@@ -6657,8 +7195,22 @@ function getElementsByLocalName(node, localName) {
6657
7195
  return toArray(node.getElementsByTagName(localName));
6658
7196
  }
6659
7197
  function toParagraphText(paragraph) {
6660
- const textNodes = getElementsByLocalName(paragraph, "t");
6661
- 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;
6662
7214
  }
6663
7215
  function getParagraphText(paragraph) {
6664
7216
  if (!paragraph) return "";
@@ -6814,9 +7366,9 @@ function resolveTargetParagraph(xmlDoc, options = {}) {
6814
7366
  const fuzzyMatch = findParagraphByBestTextMatch(xmlDoc, cleanTargetText, { onInfo });
6815
7367
  if (fuzzyMatch) return { paragraph: fuzzyMatch, resolvedBy: "fuzzy_text" };
6816
7368
  }
6817
- if (cleanTargetText) throw new Error(`Target paragraph not found: "${cleanTargetText}"`);
6818
- if (parsedRef) throw new Error(`Target paragraph reference not found: [P${parsedRef}]`);
6819
- 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#]).');
6820
7372
  }
6821
7373
  function isParagraphInTable(paragraph) {
6822
7374
  return !!findContainingWordElement(paragraph, "tbl");
@@ -6903,7 +7455,9 @@ function resolveTargetParagraphWithSnapshot(xmlDoc, options = {}) {
6903
7455
  onInfo(`[Target] [P${parsedRef}] appears stale after prior edits; using strict text rematch for ${opType}.`);
6904
7456
  return { paragraph: bestCandidate.paragraph, resolvedBy: "strict_text_after_ref_drift" };
6905
7457
  }
6906
- return resolved;
7458
+ throw createTargetNotFoundError(
7459
+ `Target paragraph [P${parsedRef}] no longer matches its batch-start anchor.`
7460
+ );
6907
7461
  }
6908
7462
  function resolveParagraphRangeByRefs(xmlDoc, startRef, endRef, options = {}) {
6909
7463
  if (!xmlDoc || !startRef || !endRef) return null;
@@ -7029,6 +7583,9 @@ function shouldPromoteBulletInsertionsToChildDepth(parsedItems, normalizedTarget
7029
7583
  return isNormalizedTextEqual(firstItem.text, normalizedTargetText);
7030
7584
  }
7031
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;
7032
7589
  return isNormalizedTextEqual(firstItem.text, normalizedTargetText);
7033
7590
  }
7034
7591
  return false;
@@ -7471,10 +8028,7 @@ function enforceListBindingOnParagraphNodes(nodes, options = {}) {
7471
8028
  if (!ownerDoc) continue;
7472
8029
  let pPr = getDirectWordChild2(paragraph, "pPr");
7473
8030
  if (!pPr) {
7474
- pPr = ownerDoc.createElementNS(
7475
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
7476
- "w:pPr"
7477
- );
8031
+ pPr = createWordElement(ownerDoc, "w:pPr");
7478
8032
  paragraph.insertBefore(pPr, paragraph.firstChild);
7479
8033
  }
7480
8034
  if (clearParagraphPropertyChanges) {
@@ -7487,27 +8041,18 @@ function enforceListBindingOnParagraphNodes(nodes, options = {}) {
7487
8041
  }
7488
8042
  let numPr = getDirectWordChild2(pPr, "numPr");
7489
8043
  if (!numPr) {
7490
- numPr = ownerDoc.createElementNS(
7491
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
7492
- "w:numPr"
7493
- );
8044
+ numPr = createWordElement(ownerDoc, "w:numPr");
7494
8045
  pPr.appendChild(numPr);
7495
8046
  }
7496
8047
  let ilvlEl = getDirectWordChild2(numPr, "ilvl");
7497
8048
  if (!ilvlEl) {
7498
- ilvlEl = ownerDoc.createElementNS(
7499
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
7500
- "w:ilvl"
7501
- );
8049
+ ilvlEl = createWordElement(ownerDoc, "w:ilvl");
7502
8050
  numPr.appendChild(ilvlEl);
7503
8051
  }
7504
8052
  ilvlEl.setAttribute("w:val", String(ilvl));
7505
8053
  let numIdEl = getDirectWordChild2(numPr, "numId");
7506
8054
  if (!numIdEl) {
7507
- numIdEl = ownerDoc.createElementNS(
7508
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
7509
- "w:numId"
7510
- );
8055
+ numIdEl = createWordElement(ownerDoc, "w:numId");
7511
8056
  numPr.appendChild(numIdEl);
7512
8057
  }
7513
8058
  numIdEl.setAttribute("w:val", String(numId));
@@ -7516,8 +8061,8 @@ function enforceListBindingOnParagraphNodes(nodes, options = {}) {
7516
8061
  return updated;
7517
8062
  }
7518
8063
  function getFirstParagraphFromOxml(oxml) {
7519
- const parser = createParser();
7520
- const doc = parser.parseFromString(String(oxml || ""), "application/xml");
8064
+ const doc = parseOoxmlSafe(oxml, "application/xml").doc;
8065
+ if (!doc) return null;
7521
8066
  const parseError = getXmlParseError(doc);
7522
8067
  if (parseError) return null;
7523
8068
  const paragraphs = getDocumentParagraphNodes(doc);
@@ -7543,8 +8088,8 @@ function setElementVal(element, value) {
7543
8088
  element.setAttribute("w:val", String(value));
7544
8089
  }
7545
8090
  function extractFirstParagraphNumIdFromOxml(oxml) {
7546
- const parser = createParser();
7547
- const doc = parser.parseFromString(String(oxml || ""), "application/xml");
8091
+ const doc = parseOoxmlSafe(oxml, "application/xml").doc;
8092
+ if (!doc) return null;
7548
8093
  const parseError = getXmlParseError(doc);
7549
8094
  if (parseError) return null;
7550
8095
  const paragraphs = getDocumentParagraphNodes(doc);
@@ -7560,9 +8105,9 @@ function extractFirstParagraphNumIdFromOxml(oxml) {
7560
8105
  function applyStartOverrideToNumberingXml(numberingXml, targetNumId, startAt, options = {}) {
7561
8106
  if (!numberingXml || !targetNumId || !Number.isInteger(startAt) || startAt < 1) return numberingXml;
7562
8107
  const setAbstractStartOverride = options.setAbstractStartOverride !== false;
7563
- const parser = createParser();
7564
8108
  const serializer = createSerializer();
7565
- const numberingDoc = parser.parseFromString(String(numberingXml || ""), "application/xml");
8109
+ const numberingDoc = parseOoxmlSafe(numberingXml, "application/xml").doc;
8110
+ if (!numberingDoc) return numberingXml;
7566
8111
  const parseError = getXmlParseError(numberingDoc);
7567
8112
  if (parseError) return numberingXml;
7568
8113
  const nums = Array.from(numberingDoc.getElementsByTagNameNS("*", "num"));
@@ -7578,19 +8123,13 @@ function applyStartOverrideToNumberingXml(numberingXml, targetNumId, startAt, op
7578
8123
  return ilvl === 0;
7579
8124
  }) || null;
7580
8125
  if (!lvlOverride) {
7581
- lvlOverride = numberingDoc.createElementNS(
7582
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
7583
- "w:lvlOverride"
7584
- );
8126
+ lvlOverride = createWordElement(numberingDoc, "w:lvlOverride");
7585
8127
  lvlOverride.setAttribute("w:ilvl", "0");
7586
8128
  target.appendChild(lvlOverride);
7587
8129
  }
7588
8130
  let startOverride = Array.from(lvlOverride.getElementsByTagNameNS("*", "startOverride"))[0] || null;
7589
8131
  if (!startOverride) {
7590
- startOverride = numberingDoc.createElementNS(
7591
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
7592
- "w:startOverride"
7593
- );
8132
+ startOverride = createWordElement(numberingDoc, "w:startOverride");
7594
8133
  lvlOverride.appendChild(startOverride);
7595
8134
  }
7596
8135
  setElementVal(startOverride, startAt);
@@ -7606,19 +8145,13 @@ function applyStartOverrideToNumberingXml(numberingXml, targetNumId, startAt, op
7606
8145
  return ilvl === 0;
7607
8146
  }) || null;
7608
8147
  if (!lvl) {
7609
- lvl = numberingDoc.createElementNS(
7610
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
7611
- "w:lvl"
7612
- );
8148
+ lvl = createWordElement(numberingDoc, "w:lvl");
7613
8149
  lvl.setAttribute("w:ilvl", "0");
7614
8150
  abstractNum.appendChild(lvl);
7615
8151
  }
7616
8152
  let startNode = Array.from(lvl.getElementsByTagNameNS("*", "start"))[0] || null;
7617
8153
  if (!startNode) {
7618
- startNode = numberingDoc.createElementNS(
7619
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
7620
- "w:start"
7621
- );
8154
+ startNode = createWordElement(numberingDoc, "w:start");
7622
8155
  lvl.insertBefore(startNode, lvl.firstChild);
7623
8156
  }
7624
8157
  setElementVal(startNode, startAt);
@@ -7710,20 +8243,20 @@ async function executeSingleLineListStructuralFallback(plan, options = {}) {
7710
8243
  // core/redline-validation.js
7711
8244
  var REVISION_ID_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "rPrChange", "pPrChange"]);
7712
8245
  var REVISION_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T/;
7713
- function localNameOf(node) {
8246
+ function localNameOf2(node) {
7714
8247
  return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
7715
8248
  }
7716
8249
  function elementsByLocalName(root, name) {
7717
- return Array.from(root.getElementsByTagName("*")).filter((el) => localNameOf(el) === name);
8250
+ return Array.from(root.getElementsByTagName("*")).filter((el) => localNameOf2(el) === name);
7718
8251
  }
7719
- function wordAttribute(node, name) {
8252
+ function wordAttribute2(node, name) {
7720
8253
  return node.getAttribute(`w:${name}`) || node.getAttribute(name) || "";
7721
8254
  }
7722
8255
  function xmlSpaceAttribute(node) {
7723
8256
  return node.getAttribute("xml:space") || node.getAttribute("space") || node.getAttributeNS?.("http://www.w3.org/XML/1998/namespace", "space") || "";
7724
8257
  }
7725
8258
  function isParagraphMarkRevision(node) {
7726
- return localNameOf(node.parentNode) === "rPr";
8259
+ return localNameOf2(node.parentNode) === "rPr";
7727
8260
  }
7728
8261
  function parseOoxmlForValidation(oxml) {
7729
8262
  const attempt = (xml) => {
@@ -7759,13 +8292,28 @@ function validateRedlineOoxml(oxml) {
7759
8292
  const insElements = elementsByLocalName(doc, "ins");
7760
8293
  const delElements = elementsByLocalName(doc, "del");
7761
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
+ }
7762
8310
  for (const revision of revisions) {
7763
- 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)));
7764
8312
  if (nested.length > 0) {
7765
8313
  addIssue(
7766
8314
  "NESTED_REVISION",
7767
8315
  "error",
7768
- `<${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}>.`
7769
8317
  );
7770
8318
  }
7771
8319
  }
@@ -7775,15 +8323,15 @@ function validateRedlineOoxml(oxml) {
7775
8323
  addIssue(
7776
8324
  "DEL_CONTAINS_T",
7777
8325
  "error",
7778
- `<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>.`
7779
8327
  );
7780
8328
  }
7781
8329
  }
7782
8330
  for (const revision of revisions) {
7783
8331
  const missing = [];
7784
- if (!wordAttribute(revision, "id")) missing.push("w:id");
7785
- if (!wordAttribute(revision, "author")) missing.push("w:author");
7786
- 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");
7787
8335
  if (missing.length > 0) {
7788
8336
  addIssue(
7789
8337
  "MISSING_REVISION_METADATA",
@@ -7794,8 +8342,8 @@ function validateRedlineOoxml(oxml) {
7794
8342
  }
7795
8343
  const seenIds = /* @__PURE__ */ new Set();
7796
8344
  for (const node of Array.from(doc.getElementsByTagName("*"))) {
7797
- if (!REVISION_ID_ELEMENTS.has(localNameOf(node))) continue;
7798
- const id = wordAttribute(node, "id");
8345
+ if (!REVISION_ID_ELEMENTS.has(localNameOf2(node))) continue;
8346
+ const id = wordAttribute2(node, "id");
7799
8347
  if (!id) continue;
7800
8348
  if (seenIds.has(id)) {
7801
8349
  addIssue("DUPLICATE_REVISION_ID", "error", `Revision id ${id} appears more than once.`);
@@ -7823,7 +8371,7 @@ function validateRedlineOoxml(oxml) {
7823
8371
  addIssue(
7824
8372
  "EMPTY_REVISION_WRAPPER",
7825
8373
  "warning",
7826
- `<${revision.nodeName}> (w:id="${wordAttribute(revision, "id")}") wraps no content.`
8374
+ `<${revision.nodeName}> (w:id="${wordAttribute2(revision, "id")}") wraps no content.`
7827
8375
  );
7828
8376
  }
7829
8377
  }
@@ -8262,20 +8810,9 @@ function hasParserError(doc) {
8262
8810
  return !!getXmlParseError(doc);
8263
8811
  }
8264
8812
  function parseWordOoxml(ooxml) {
8265
- const source = typeof ooxml === "string" ? ooxml : String(ooxml || "");
8266
- if (!source.trim()) {
8267
- return null;
8268
- }
8269
- try {
8270
- const parser = createParser();
8271
- const doc = parser.parseFromString(source, "application/xml");
8272
- if (hasParserError(doc)) {
8273
- return null;
8274
- }
8275
- return doc;
8276
- } catch {
8277
- return null;
8278
- }
8813
+ const parsed = parseOoxmlSafe(ooxml, "application/xml");
8814
+ if (parsed.error || hasParserError(parsed.doc)) return { ...parsed, doc: null };
8815
+ return parsed;
8279
8816
  }
8280
8817
  function getWordParagraphs(doc) {
8281
8818
  if (!doc) return [];
@@ -8430,25 +8967,37 @@ function paragraphToMarkdown(paragraph) {
8430
8967
  return normalizedInline;
8431
8968
  }
8432
8969
  function ingestWordOoxmlToPlainText(ooxml) {
8433
- const doc = parseWordOoxml(ooxml);
8434
- 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;
8435
8978
  const paragraphs = getWordParagraphs(doc);
8436
8979
  if (paragraphs.length === 0) {
8437
8980
  const fallback = normalizeInlineWhitespace(doc.documentElement?.textContent || "");
8438
- return fallback;
8981
+ return { text: fallback, status: "ok", warnings: parsed.warnings };
8439
8982
  }
8440
8983
  const lines = paragraphs.map(paragraphToPlainText);
8441
- return lines.join("\n\n").trim();
8984
+ return { text: lines.join("\n\n").trim(), status: "ok", warnings: parsed.warnings };
8442
8985
  }
8443
8986
  function ingestWordOoxmlToMarkdown(ooxml) {
8444
- const doc = parseWordOoxml(ooxml);
8445
- 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;
8446
8995
  const paragraphs = getWordParagraphs(doc);
8447
8996
  if (paragraphs.length === 0) {
8448
- return "";
8997
+ return { text: "", status: "ok", warnings: parsed.warnings };
8449
8998
  }
8450
8999
  const lines = paragraphs.map(paragraphToMarkdown);
8451
- return lines.join("\n\n").trim();
9000
+ return { text: lines.join("\n\n").trim(), status: "ok", warnings: parsed.warnings };
8452
9001
  }
8453
9002
 
8454
9003
  // services/comment-builders.js
@@ -8620,12 +9169,12 @@ function injectMarkersIntoParagraph(xmlDoc, paragraph, textToFind, commentId, pa
8620
9169
  var PKG_NS = "http://schemas.microsoft.com/office/2006/xmlPackage";
8621
9170
  var RELS_NS = "http://schemas.openxmlformats.org/package/2006/relationships";
8622
9171
  function injectCommentsIntoPackage(packageOxml, commentsXml) {
8623
- const parser = createParser();
8624
9172
  const serializer = createSerializer();
8625
- const pkgDoc = parser.parseFromString(packageOxml, "text/xml");
8626
- const parseError = getXmlParseError(pkgDoc);
8627
- if (parseError) {
8628
- 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);
8629
9178
  return packageOxml;
8630
9179
  }
8631
9180
  const pkgPackage = pkgDoc.documentElement;
@@ -8633,7 +9182,8 @@ function injectCommentsIntoPackage(packageOxml, commentsXml) {
8633
9182
  commentsPart.setAttribute("pkg:name", "/word/comments.xml");
8634
9183
  commentsPart.setAttribute("pkg:contentType", "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml");
8635
9184
  const commentsXmlData = pkgDoc.createElementNS(PKG_NS, "pkg:xmlData");
8636
- const commentsDoc = parser.parseFromString(commentsXml, "text/xml");
9185
+ const commentsDoc = parseOoxmlSafe(commentsXml, "text/xml").doc;
9186
+ if (!commentsDoc) return packageOxml;
8637
9187
  commentsXmlData.appendChild(pkgDoc.importNode(commentsDoc.documentElement, true));
8638
9188
  commentsPart.appendChild(commentsXmlData);
8639
9189
  pkgPackage.appendChild(commentsPart);
@@ -8683,17 +9233,19 @@ function injectCommentsIntoPackage(packageOxml, commentsXml) {
8683
9233
  }
8684
9234
 
8685
9235
  // services/comment-engine.js
8686
- function parseDocumentOxml(oxml, parser, parseFailureWarning) {
8687
- try {
8688
- const xmlDoc = parser.parseFromString(oxml, "text/xml");
8689
- const parseError = getXmlParseError(xmlDoc);
8690
- if (parseError) {
8691
- return { xmlDoc: null, warning: parseFailureWarning(parseError.textContent || "parse error") };
8692
- }
8693
- return { xmlDoc, warning: null };
8694
- } catch (error2) {
8695
- 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
+ };
8696
9247
  }
9248
+ return { xmlDoc: parsed.doc, warning: null, warnings: parsed.warnings, error: null };
8697
9249
  }
8698
9250
  function injectCommentsIntoOoxml(oxml, comments, options = {}) {
8699
9251
  const author = options?.author || getDefaultAuthor();
@@ -8703,23 +9255,26 @@ function injectCommentsIntoOoxml(oxml, comments, options = {}) {
8703
9255
  if (!comments || comments.length === 0) {
8704
9256
  return {
8705
9257
  oxml,
9258
+ hasChanges: false,
8706
9259
  commentsApplied: 0,
8707
9260
  warnings: ["No comments to inject"]
8708
9261
  };
8709
9262
  }
8710
- const parser = createParser();
8711
9263
  const serializer = createSerializer();
8712
9264
  const parseResult = parseDocumentOxml(
8713
9265
  oxml,
8714
- parser,
8715
9266
  (warning) => `Failed to parse OXML: ${warning}`
8716
9267
  );
9268
+ warnings.push(...parseResult.warnings || []);
8717
9269
  if (!parseResult.xmlDoc) {
8718
9270
  error("[CommentEngine] Parse failure:", parseResult.warning);
8719
9271
  return {
8720
9272
  oxml,
9273
+ hasChanges: false,
8721
9274
  commentsApplied: 0,
8722
- warnings: [parseResult.warning]
9275
+ status: "error",
9276
+ error: parseResult.error,
9277
+ warnings: [...warnings, parseResult.warning]
8723
9278
  };
8724
9279
  }
8725
9280
  const xmlDoc = parseResult.xmlDoc;
@@ -8778,12 +9333,14 @@ function injectCommentsIntoOoxml(oxml, comments, options = {}) {
8778
9333
  if (placedComments.length === 0) {
8779
9334
  return {
8780
9335
  oxml,
9336
+ hasChanges: false,
8781
9337
  commentsApplied: 0,
8782
9338
  warnings
8783
9339
  };
8784
9340
  }
8785
9341
  return {
8786
9342
  oxml: serializer.serializeToString(xmlDoc),
9343
+ hasChanges: true,
8787
9344
  commentsXml: buildCommentsPartXml(placedComments),
8788
9345
  commentsApplied: placedComments.length,
8789
9346
  warnings
@@ -8853,6 +9410,7 @@ function removeFormattingFromRPr(rPr, formatTypes = ["all"]) {
8853
9410
  function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
8854
9411
  if (!targetText || !ooxmlString) return ooxmlString;
8855
9412
  const doc = parseOoxml(ooxmlString);
9413
+ if (!doc) return ooxmlString;
8856
9414
  const NS_W7 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
8857
9415
  const runs = doc.getElementsByTagNameNS(NS_W7, "r");
8858
9416
  const insertions = doc.getElementsByTagNameNS(NS_W7, "ins");
@@ -8926,7 +9484,7 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
8926
9484
  rPrElement.appendChild(highlightEl);
8927
9485
  if (generateRedlines && previousRPrState) {
8928
9486
  const rPrChange = createWordElement(doc, "w:rPrChange");
8929
- const metadata = createRevisionMetadata(author);
9487
+ const metadata = createRevisionMetadata(author, doc);
8930
9488
  rPrChange.setAttribute("w:id", String(metadata.id));
8931
9489
  rPrChange.setAttribute("w:author", metadata.author);
8932
9490
  rPrChange.setAttribute("w:date", metadata.date);
@@ -8940,64 +9498,70 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
8940
9498
  function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", options = {}) {
8941
9499
  if (!targetText || !ooxmlString) return ooxmlString;
8942
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
+ }
8943
9507
  const NS_W7 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
8944
9508
  const getRunText = (run) => {
8945
9509
  const textNodes = run.getElementsByTagNameNS(NS_W7, "t");
8946
9510
  return Array.from(textNodes).map((t) => t.textContent).join("");
8947
9511
  };
8948
9512
  const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W7, "r"));
8949
- for (let i = 0; i < allRuns.length; i++) {
8950
- 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) {
8951
9534
  const runText = getRunText(run);
8952
9535
  if (!runText) continue;
8953
- const matchIndex = runText.indexOf(targetText);
8954
- 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;
8955
9545
  const parent = run.parentNode;
8956
9546
  if (!parent) {
8957
9547
  console.warn("[Highlight] Run parent is null; skipping. Likely already processed.");
8958
9548
  continue;
8959
9549
  }
8960
- const prefixText = runText.substring(0, matchIndex);
8961
- const matchText = runText.substring(matchIndex, matchIndex + targetText.length);
8962
- const suffixText = runText.substring(matchIndex + targetText.length);
8963
9550
  const fragment = doc.createDocumentFragment();
8964
- if (prefixText.length > 0) {
8965
- const prefixRun = run.cloneNode(true);
8966
- const tNodes = prefixRun.getElementsByTagNameNS(NS_W7, "t");
8967
- Array.from(tNodes).forEach(removeNode2);
8968
- const newT = createWordElement(doc, "w:t");
8969
- newT.setAttribute("xml:space", "preserve");
8970
- newT.textContent = prefixText;
8971
- prefixRun.appendChild(newT);
8972
- fragment.appendChild(prefixRun);
8973
- }
8974
- if (matchText.length > 0) {
8975
- const matchRun = run.cloneNode(true);
8976
- const tNodes = matchRun.getElementsByTagNameNS(NS_W7, "t");
8977
- Array.from(tNodes).forEach(removeNode2);
8978
- const newT = createWordElement(doc, "w:t");
8979
- newT.setAttribute("xml:space", "preserve");
8980
- newT.textContent = matchText;
8981
- matchRun.appendChild(newT);
8982
- const rPrElements = matchRun.getElementsByTagNameNS(NS_W7, "rPr");
8983
- const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
8984
- const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
8985
- if (existingRPr) {
8986
- matchRun.replaceChild(newRPr, existingRPr);
8987
- } else {
8988
- 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));
8989
9555
  }
8990
- fragment.appendChild(matchRun);
9556
+ fragment.appendChild(cloneRunWithText2(
9557
+ run,
9558
+ runText.slice(matchIndex, matchIndex + targetText.length),
9559
+ true
9560
+ ));
9561
+ cursor = matchIndex + targetText.length;
8991
9562
  }
8992
- if (suffixText.length > 0) {
8993
- const suffixRun = run.cloneNode(true);
8994
- const tNodes = suffixRun.getElementsByTagNameNS(NS_W7, "t");
8995
- Array.from(tNodes).forEach(removeNode2);
8996
- const newT = createWordElement(doc, "w:t");
8997
- newT.setAttribute("xml:space", "preserve");
8998
- newT.textContent = suffixText;
8999
- suffixRun.appendChild(newT);
9000
- fragment.appendChild(suffixRun);
9563
+ if (cursor < runText.length) {
9564
+ fragment.appendChild(cloneRunWithText2(run, runText.slice(cursor), false));
9001
9565
  }
9002
9566
  parent.replaceChild(fragment, run);
9003
9567
  }
@@ -9018,13 +9582,13 @@ var COMMENTS_PATH = "word/comments.xml";
9018
9582
  var CONTENT_TYPES_PATH = "[Content_Types].xml";
9019
9583
  var DOCUMENT_RELS_PATH = "word/_rels/document.xml.rels";
9020
9584
  function parseXmlStrictStandalone(xmlText, label = "xml") {
9021
- const parser = createParser();
9022
- const xmlDoc = parser.parseFromString(xmlText, "application/xml");
9023
- const parseError = xmlDoc.getElementsByTagName("parsererror")[0];
9024
- if (parseError) {
9025
- 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;
9026
9590
  }
9027
- return xmlDoc;
9591
+ return parsed.doc;
9028
9592
  }
9029
9593
  function isSectionPropertiesElement(node) {
9030
9594
  return !!node && node.nodeType === 1 && node.namespaceURI === NS_W6 && node.localName === "sectPr";
@@ -9088,9 +9652,8 @@ function getPackagePartName(partElement) {
9088
9652
  return partElement.getAttribute("pkg:name") || partElement.getAttribute("name") || "";
9089
9653
  }
9090
9654
  function extractFromPackageXml(packageXml) {
9091
- const parser = createParser();
9092
9655
  const serializer = createSerializer();
9093
- const pkgDoc = parser.parseFromString(packageXml, "application/xml");
9656
+ const pkgDoc = parseXmlStrictStandalone(packageXml, "package OOXML");
9094
9657
  const parts = Array.from(pkgDoc.getElementsByTagNameNS("*", "part"));
9095
9658
  const documentPart = parts.find((part) => getPackagePartName(part) === "/word/document.xml");
9096
9659
  if (!documentPart) {
@@ -9123,23 +9686,37 @@ function extractFromPackageXml(packageXml) {
9123
9686
  }
9124
9687
  function extractReplacementNodesFromOoxml(outputOxml) {
9125
9688
  if (typeof outputOxml !== "string" || !outputOxml.trim()) {
9126
- throw new Error("Reconciliation engine returned no OOXML payload for this operation");
9127
- }
9128
- if (outputOxml.includes("<pkg:package")) {
9129
- 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
+ };
9130
9696
  }
9131
- if (outputOxml.includes("<w:document")) {
9132
- const parser2 = createParser();
9133
- const doc = parser2.parseFromString(outputOxml, "application/xml");
9134
- const body = doc.getElementsByTagNameNS("*", "body")[0];
9135
- const replacementNodes2 = body ? Array.from(body.childNodes || []).filter((node) => node.nodeType === 1 && !isSectionPropertiesElement(node)) : Array.from(doc.childNodes || []).filter((node) => node.nodeType === 1);
9136
- 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
+ };
9137
9719
  }
9138
- const wrapped = `<root xmlns:w="${NS_W6}">${outputOxml}</root>`;
9139
- const parser = createParser();
9140
- const fragmentDoc = parser.parseFromString(wrapped, "application/xml");
9141
- const replacementNodes = Array.from(fragmentDoc.documentElement.childNodes || []).filter((node) => node.nodeType === 1);
9142
- return { replacementNodes, numberingXml: null, sourceType: "fragment" };
9143
9720
  }
9144
9721
  function upsertContentTypeOverride(ctDoc, partName, contentType) {
9145
9722
  const overrides = Array.from(ctDoc.getElementsByTagNameNS("*", "Override"));
@@ -9199,18 +9776,17 @@ async function ensureNumberingArtifactsInZip(zip, numberingXmlList, options = {}
9199
9776
  onInfo("[Demo] Merging numbering.xml payload(s) into existing numbering definitions");
9200
9777
  }
9201
9778
  zip.file(NUMBERING_PATH, mergedNumberingXml);
9202
- const parser = createParser();
9203
9779
  const serializer = createSerializer();
9204
9780
  const ctText = await readZipText(zip, CONTENT_TYPES_PATH);
9205
9781
  if (ctText) {
9206
- const ctDoc = parser.parseFromString(ctText, "application/xml");
9782
+ const ctDoc = parseXmlStrictStandalone(ctText, CONTENT_TYPES_PATH);
9207
9783
  if (upsertContentTypeOverride(ctDoc, "/word/numbering.xml", NUMBERING_CONTENT_TYPE)) {
9208
9784
  zip.file(CONTENT_TYPES_PATH, serializer.serializeToString(ctDoc));
9209
9785
  }
9210
9786
  }
9211
9787
  const relsText = await readZipText(zip, DOCUMENT_RELS_PATH);
9212
9788
  if (relsText) {
9213
- const relsDoc = parser.parseFromString(relsText, "application/xml");
9789
+ const relsDoc = parseXmlStrictStandalone(relsText, DOCUMENT_RELS_PATH);
9214
9790
  if (upsertDocumentRelationship(relsDoc, NUMBERING_REL_TYPE, "numbering.xml")) {
9215
9791
  zip.file(DOCUMENT_RELS_PATH, serializer.serializeToString(relsDoc));
9216
9792
  }
@@ -9220,7 +9796,6 @@ async function ensureCommentsArtifactsInZip(zip, commentsXml, options = {}) {
9220
9796
  const onInfo = typeof options?.onInfo === "function" ? options.onInfo : () => {
9221
9797
  };
9222
9798
  if (!commentsXml) return;
9223
- const parser = createParser();
9224
9799
  const serializer = createSerializer();
9225
9800
  const existingText = await readZipText(zip, COMMENTS_PATH);
9226
9801
  if (!existingText) {
@@ -9244,14 +9819,14 @@ async function ensureCommentsArtifactsInZip(zip, commentsXml, options = {}) {
9244
9819
  }
9245
9820
  const ctText = await readZipText(zip, CONTENT_TYPES_PATH);
9246
9821
  if (ctText) {
9247
- const ctDoc = parser.parseFromString(ctText, "application/xml");
9822
+ const ctDoc = parseXmlStrictStandalone(ctText, CONTENT_TYPES_PATH);
9248
9823
  if (upsertContentTypeOverride(ctDoc, "/word/comments.xml", COMMENTS_CONTENT_TYPE)) {
9249
9824
  zip.file(CONTENT_TYPES_PATH, serializer.serializeToString(ctDoc));
9250
9825
  }
9251
9826
  }
9252
9827
  const relsText = await readZipText(zip, DOCUMENT_RELS_PATH);
9253
9828
  if (relsText) {
9254
- const relsDoc = parser.parseFromString(relsText, "application/xml");
9829
+ const relsDoc = parseXmlStrictStandalone(relsText, DOCUMENT_RELS_PATH);
9255
9830
  if (upsertDocumentRelationship(relsDoc, COMMENTS_REL_TYPE, "comments.xml")) {
9256
9831
  zip.file(DOCUMENT_RELS_PATH, serializer.serializeToString(relsDoc));
9257
9832
  }
@@ -9514,10 +10089,12 @@ async function reconcileMarkdownTableOoxml(oxml, originalText, markdownTable, op
9514
10089
  }
9515
10090
  async function applyRedlineToOxmlWithListFallback(oxml, originalText, modifiedText, options = {}) {
9516
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."] : [];
9517
10094
  const plan = buildSingleLineListStructuralFallbackPlan({
9518
10095
  oxml,
9519
10096
  originalText,
9520
- modifiedText,
10097
+ modifiedText: fallbackModifiedText,
9521
10098
  allowExistingList: allowExistingListForFallback
9522
10099
  });
9523
10100
  const preferListFallback = options.preferListStructuralFallback !== false;
@@ -9537,7 +10114,7 @@ async function applyRedlineToOxmlWithListFallback(oxml, originalText, modifiedTe
9537
10114
  return withOoxmlSourceType({
9538
10115
  oxml: wrappedOxml2,
9539
10116
  hasChanges: true,
9540
- warnings: fallbackWarnings2,
10117
+ warnings: [...sanitizationWarnings, ...fallbackWarnings2],
9541
10118
  listStructuralFallbackApplied: true,
9542
10119
  listStructuralFallbackKey: fallbackResult2.listStructuralFallbackKey || null,
9543
10120
  listStructuralFallbackNumberingXml: fallbackResult2.numberingXml || null
@@ -9656,7 +10233,9 @@ export {
9656
10233
  inferTableReplacementParagraphBlock,
9657
10234
  ingestOoxml,
9658
10235
  ingestWordOoxmlToMarkdown,
10236
+ ingestWordOoxmlToMarkdownResult,
9659
10237
  ingestWordOoxmlToPlainText,
10238
+ ingestWordOoxmlToPlainTextResult,
9660
10239
  injectCommentsIntoOoxml,
9661
10240
  injectCommentsIntoPackage2 as injectCommentsIntoPackage,
9662
10241
  insertBodyElementBeforeSectPr,
@@ -9670,8 +10249,8 @@ export {
9670
10249
  overwriteParagraphNumIds,
9671
10250
  parseMarkdownListContent,
9672
10251
  parseOoxml,
10252
+ parseOoxmlSafe,
9673
10253
  parseParagraphReference,
9674
- parseXmlStrictStandalone,
9675
10254
  planListInsertionOnlyEdit,
9676
10255
  preprocessMarkdown,
9677
10256
  reconcileMarkdownTableOoxml,