@adeu/core 1.8.0 → 1.10.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.
package/dist/index.js CHANGED
@@ -26,6 +26,9 @@ function findAllDescendants(element, tagName) {
26
26
  return Array.from(element.getElementsByTagName(tagName));
27
27
  }
28
28
  function parseXml(xmlString) {
29
+ if (xmlString.startsWith("\uFEFF")) {
30
+ xmlString = xmlString.slice(1);
31
+ }
29
32
  return new DOMParser().parseFromString(xmlString, "text/xml");
30
33
  }
31
34
  function serializeXml(node) {
@@ -449,6 +452,21 @@ var CommentsManager = class {
449
452
  return part;
450
453
  }
451
454
  _ensureNamespaces() {
455
+ const root = this._commentsPart?._element;
456
+ if (!root) return;
457
+ const required = [
458
+ ["xmlns:w", NS.w],
459
+ ["xmlns:w14", NS.w14],
460
+ ["xmlns:w15", NS.w15],
461
+ ["xmlns:w16cid", NS.w16cid],
462
+ ["xmlns:w16cex", NS.w16cex],
463
+ ["xmlns:mc", NS.mc]
464
+ ];
465
+ for (const [attr, uri] of required) {
466
+ if (!root.getAttribute(attr)) {
467
+ root.setAttribute(attr, uri);
468
+ }
469
+ }
452
470
  }
453
471
  _getNextCommentId() {
454
472
  const ids = [0];
@@ -574,9 +592,9 @@ var CommentsManager = class {
574
592
  return commentId;
575
593
  }
576
594
  deleteComment(commentId) {
577
- if (!this._commentsPart) return;
595
+ if (!this.commentsPart) return;
578
596
  let commentEl = null;
579
- for (const c of findAllDescendants(this._commentsPart._element, "w:comment")) {
597
+ for (const c of findAllDescendants(this.commentsPart._element, "w:comment")) {
580
598
  if (c.getAttribute("w:id") === commentId) {
581
599
  commentEl = c;
582
600
  break;
@@ -600,7 +618,7 @@ var CommentsManager = class {
600
618
  if (child.getAttribute("w15:paraIdParent") === paraId) {
601
619
  const childParaId = child.getAttribute("w15:paraId");
602
620
  if (childParaId) {
603
- for (const c of findAllDescendants(this._commentsPart._element, "w:comment")) {
621
+ for (const c of findAllDescendants(this.commentsPart._element, "w:comment")) {
604
622
  for (const p of findAllDescendants(c, "w:p")) {
605
623
  if (p.getAttribute("w14:paraId") === childParaId) {
606
624
  const cid = c.getAttribute("w:id");
@@ -1834,6 +1852,29 @@ ${header}`;
1834
1852
 
1835
1853
  // src/diff.ts
1836
1854
  import diff_match_patch from "diff-match-patch";
1855
+ function _count_standalone_underscores(s) {
1856
+ let count = 0;
1857
+ let i = 0;
1858
+ const n = s.length;
1859
+ const isAlnum = (char) => /[a-zA-Z0-9]/.test(char);
1860
+ while (i < n) {
1861
+ if (s[i] === "_") {
1862
+ let is_double = false;
1863
+ if (i > 0 && s[i - 1] === "_" || i < n - 1 && s[i + 1] === "_") {
1864
+ is_double = true;
1865
+ }
1866
+ let is_intra = false;
1867
+ if (i > 0 && isAlnum(s[i - 1]) && i < n - 1 && isAlnum(s[i + 1])) {
1868
+ is_intra = true;
1869
+ }
1870
+ if (!is_double && !is_intra) {
1871
+ count++;
1872
+ }
1873
+ }
1874
+ i++;
1875
+ }
1876
+ return count;
1877
+ }
1837
1878
  function trim_common_context(target, new_val) {
1838
1879
  if (!target || !new_val) return [0, 0];
1839
1880
  const isSpace = (char) => /\s/.test(char);
@@ -1864,7 +1905,7 @@ function trim_common_context(target, new_val) {
1864
1905
  const left = target.substring(0, prefix_len);
1865
1906
  const b_count = (left.match(/\*\*/g) || []).length;
1866
1907
  const u2_count = (left.match(/__/g) || []).length;
1867
- const u1_count = (left.replace(/__/g, "").match(/_/g) || []).length;
1908
+ const u1_count = _count_standalone_underscores(left);
1868
1909
  if (b_count % 2 !== 0) {
1869
1910
  prefix_len = left.lastIndexOf("**");
1870
1911
  continue;
@@ -1875,10 +1916,14 @@ function trim_common_context(target, new_val) {
1875
1916
  }
1876
1917
  if (u1_count % 2 !== 0) {
1877
1918
  let idx = left.length - 1;
1919
+ const isAlnum = (char) => /[a-zA-Z0-9]/.test(char);
1878
1920
  while (idx >= 0) {
1879
1921
  if (left[idx] === "_" && (idx === 0 || left[idx - 1] !== "_") && (idx === left.length - 1 || left[idx + 1] !== "_")) {
1880
- prefix_len = idx;
1881
- break;
1922
+ const is_intra = idx > 0 && isAlnum(left[idx - 1]) && idx < left.length - 1 && isAlnum(left[idx + 1]);
1923
+ if (!is_intra) {
1924
+ prefix_len = idx;
1925
+ break;
1926
+ }
1882
1927
  }
1883
1928
  idx--;
1884
1929
  }
@@ -1938,7 +1983,7 @@ function trim_common_context(target, new_val) {
1938
1983
  const right = target.substring(target.length - suffix_len);
1939
1984
  const b_count = (right.match(/\*\*/g) || []).length;
1940
1985
  const u2_count = (right.match(/__/g) || []).length;
1941
- const u1_count = (right.replace(/__/g, "").match(/_/g) || []).length;
1986
+ const u1_count = _count_standalone_underscores(right);
1942
1987
  if (b_count % 2 !== 0) {
1943
1988
  suffix_len -= right.indexOf("**") + 2;
1944
1989
  continue;
@@ -1949,10 +1994,14 @@ function trim_common_context(target, new_val) {
1949
1994
  }
1950
1995
  if (u1_count % 2 !== 0) {
1951
1996
  let idx_in_right = 0;
1997
+ const isAlnum = (char) => /[a-zA-Z0-9]/.test(char);
1952
1998
  while (idx_in_right < right.length) {
1953
1999
  if (right[idx_in_right] === "_" && (idx_in_right === 0 || right[idx_in_right - 1] !== "_") && (idx_in_right === right.length - 1 || right[idx_in_right + 1] !== "_")) {
1954
- suffix_len -= idx_in_right + 1;
1955
- break;
2000
+ const is_intra = idx_in_right > 0 && isAlnum(right[idx_in_right - 1]) && idx_in_right < right.length - 1 && isAlnum(right[idx_in_right + 1]);
2001
+ if (!is_intra) {
2002
+ suffix_len -= idx_in_right + 1;
2003
+ break;
2004
+ }
1956
2005
  }
1957
2006
  idx_in_right++;
1958
2007
  }
@@ -2599,6 +2648,36 @@ var RedlineEngine = class {
2599
2648
  this.mapper = new DocumentMapper(this.doc);
2600
2649
  this.comments_manager = new CommentsManager(this.doc);
2601
2650
  }
2651
+ _check_punctuation_warning(target_text) {
2652
+ if (!target_text) return null;
2653
+ if (target_text.includes("_") || target_text.includes("-")) {
2654
+ return `Warning: target_text '${target_text}' contains tokenization-splitting punctuation ('_' or '-'). This can trigger mid-word splits in the diff engine. Consider using a longer plain-prose anchor.`;
2655
+ }
2656
+ return null;
2657
+ }
2658
+ _build_edit_context_previews(edit) {
2659
+ if (edit.type !== "modify") return [null, null];
2660
+ if (edit._resolved_proxy_edit) {
2661
+ edit = edit._resolved_proxy_edit;
2662
+ }
2663
+ const start_idx = edit._resolved_start_idx;
2664
+ if (start_idx === void 0 || start_idx === null) return [null, null];
2665
+ const target_text = edit.target_text || "";
2666
+ const new_text = edit.new_text || "";
2667
+ const length = target_text.length;
2668
+ const active_mapper = edit._active_mapper_ref || this.mapper;
2669
+ const full_text = active_mapper.full_text;
2670
+ if (!full_text) return [null, null];
2671
+ const before_start = Math.max(0, start_idx - 30);
2672
+ const context_before = full_text.substring(before_start, start_idx);
2673
+ const context_after = full_text.substring(start_idx + length, start_idx + length + 30);
2674
+ const critic_markup = `${context_before}{--${target_text}--}{++${new_text}++}${context_after}`;
2675
+ let clean_text = critic_markup;
2676
+ clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
2677
+ clean_text = clean_text.replace(/\{--.*?--\}/gs, "");
2678
+ clean_text = clean_text.replace(/\{\+\+(.*?)\+\+\}/gs, "$1");
2679
+ return [critic_markup, clean_text];
2680
+ }
2602
2681
  _scan_existing_ids() {
2603
2682
  let maxId = 0;
2604
2683
  for (const tag of ["w:ins", "w:del"]) {
@@ -2691,28 +2770,84 @@ var RedlineEngine = class {
2691
2770
  }
2692
2771
  }
2693
2772
  }
2694
- const comment_ids = /* @__PURE__ */ new Set();
2695
- for (const tag of [
2696
- "w:commentRangeStart",
2697
- "w:commentRangeEnd",
2698
- "w:commentReference"
2699
- ]) {
2700
- for (const node of findAllDescendants(this.doc.element, tag)) {
2701
- const cid = node.getAttribute("w:id");
2702
- if (cid) comment_ids.add(cid);
2773
+ for (const root_element of parts_to_process) {
2774
+ for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
2775
+ for (const el of findAllDescendants(root_element, tag)) {
2776
+ el.parentNode?.removeChild(el);
2777
+ }
2703
2778
  }
2704
- }
2705
- const comments_part = this.doc.pkg.parts.find(
2706
- (p) => p.contentType === "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml"
2707
- );
2708
- if (comments_part) {
2709
- for (const c of findAllDescendants(comments_part._element, "w:comment")) {
2710
- const cid = c.getAttribute("w:id");
2711
- if (cid) comment_ids.add(cid);
2779
+ const refs = findAllDescendants(root_element, "w:commentReference");
2780
+ for (const ref of refs) {
2781
+ const parent = ref.parentNode;
2782
+ if (parent) {
2783
+ if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
2784
+ const nonRprChildren = Array.from(parent.childNodes).filter(
2785
+ (c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
2786
+ );
2787
+ if (nonRprChildren.length <= 1) {
2788
+ parent.parentNode?.removeChild(parent);
2789
+ } else {
2790
+ parent.removeChild(ref);
2791
+ }
2792
+ } else {
2793
+ parent.removeChild(ref);
2794
+ }
2795
+ }
2712
2796
  }
2713
2797
  }
2714
- for (const cid of comment_ids) {
2715
- this.comments_manager.deleteComment(cid);
2798
+ const pkg = this.doc.pkg;
2799
+ const comment_partnames = /* @__PURE__ */ new Set();
2800
+ for (const part of pkg.parts) {
2801
+ if (part.partname.toLowerCase().includes("comments")) {
2802
+ comment_partnames.add(part.partname);
2803
+ const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
2804
+ const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
2805
+ comment_partnames.add(withSlash);
2806
+ comment_partnames.add(withoutSlash);
2807
+ }
2808
+ }
2809
+ if (comment_partnames.size > 0) {
2810
+ for (const part of pkg.parts) {
2811
+ if (part.partname.endsWith(".rels")) {
2812
+ const rels = findAllDescendants(part._element, "Relationship");
2813
+ const toRemove = [];
2814
+ for (const rel of rels) {
2815
+ const target = rel.getAttribute("Target") || "";
2816
+ if (target.toLowerCase().includes("comments")) {
2817
+ toRemove.push(rel);
2818
+ const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
2819
+ const sourcePart = pkg.getPartByPath(sourcePath);
2820
+ if (sourcePart) {
2821
+ const relId = rel.getAttribute("Id");
2822
+ if (relId) sourcePart.rels.delete(relId);
2823
+ }
2824
+ }
2825
+ }
2826
+ for (const relEl of toRemove) {
2827
+ relEl.parentNode?.removeChild(relEl);
2828
+ }
2829
+ }
2830
+ }
2831
+ const ctPart = pkg.getPartByPath("[Content_Types].xml");
2832
+ if (ctPart) {
2833
+ const overrides = findAllDescendants(ctPart._element, "Override");
2834
+ const toRemove = [];
2835
+ for (const override of overrides) {
2836
+ const partName = override.getAttribute("PartName") || "";
2837
+ if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
2838
+ toRemove.push(override);
2839
+ }
2840
+ }
2841
+ for (const overrideEl of toRemove) {
2842
+ overrideEl.parentNode?.removeChild(overrideEl);
2843
+ }
2844
+ }
2845
+ pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
2846
+ for (const key of Object.keys(pkg.unzipped)) {
2847
+ if (key.toLowerCase().includes("comments")) {
2848
+ delete pkg.unzipped[key];
2849
+ }
2850
+ }
2716
2851
  }
2717
2852
  }
2718
2853
  _getNextId() {
@@ -2803,40 +2938,40 @@ var RedlineEngine = class {
2803
2938
  }
2804
2939
  }
2805
2940
  /**
2806
- * Inserts `text` as one or more tracked paragraphs anchored relative to
2807
- * either an existing run or a paragraph. Returns:
2808
- * { first_node, last_p, last_ins, used_block_mode }
2809
- * where:
2810
- * - first_node: the first <w:ins> (for inline mode) OR the first new <w:p>
2811
- * (for block mode). The caller uses this for splicing into the DOM and
2812
- * for anchoring comments.
2813
- * - last_p: the last new <w:p> created, if any. null when entirely inline.
2814
- * - last_ins: the last <w:ins> created (inside the last new <w:p>, or the
2815
- * sole inline ins). Used as the comment's end anchor.
2816
- * - used_block_mode: true when the first line carried a heading/list style
2817
- * marker and we created a new paragraph for it (rather than inlining it).
2818
- *
2819
- * Multi-paragraph rules (only when text contains '\n'):
2820
- * - Each additional line becomes a new <w:p>, inserted after the anchor
2821
- * paragraph in document order.
2822
- * - Each new <w:p> gets a copy of the anchor paragraph's <w:pPr> (so list
2823
- * numbering / indentation are preserved) unless the line itself starts
2824
- * with a markdown heading or list marker, which overrides the style.
2825
- * - Each new <w:p> carries a tracked paragraph-break marker
2826
- * (<w:pPr><w:rPr><w:ins/></w:rPr></w:pPr>) so Word natively tracks the
2827
- * paragraph break.
2828
- * - Each new <w:p>'s content is wrapped in a <w:ins>, with inline bold/
2829
- * italic markdown parsed via _parse_inline_markdown.
2830
- *
2831
- * The first line:
2832
- * - If it carries a heading / list marker AND we have a paragraph anchor,
2833
- * we drop into "block mode": no inline <w:ins>; the first line itself
2834
- * becomes the first new <w:p>.
2835
- * - Otherwise we emit a single inline <w:ins> for the first line (current
2836
- * behaviour) and treat the remaining lines as block extensions.
2837
- *
2838
- * Does NOT attach comments; callers handle that.
2839
- */
2941
+ * Inserts `text` as one or more tracked paragraphs anchored relative to
2942
+ * either an existing run or a paragraph. Returns:
2943
+ * { first_node, last_p, last_ins, used_block_mode }
2944
+ * where:
2945
+ * - first_node: the first <w:ins> (for inline mode) OR the first new <w:p>
2946
+ * (for block mode). The caller uses this for splicing into the DOM and
2947
+ * for anchoring comments.
2948
+ * - last_p: the last new <w:p> created, if any. null when entirely inline.
2949
+ * - last_ins: the last <w:ins> created (inside the last new <w:p>, or the
2950
+ * sole inline ins). Used as the comment's end anchor.
2951
+ * - used_block_mode: true when the first line carried a heading/list style
2952
+ * marker and we created a new paragraph for it (rather than inlining it).
2953
+ *
2954
+ * Multi-paragraph rules (only when text contains '\n'):
2955
+ * - Each additional line becomes a new <w:p>, inserted after the anchor
2956
+ * paragraph in document order.
2957
+ * - Each new <w:p> gets a copy of the anchor paragraph's <w:pPr> (so list
2958
+ * numbering / indentation are preserved) unless the line itself starts
2959
+ * with a markdown heading or list marker, which overrides the style.
2960
+ * - Each new <w:p> carries a tracked paragraph-break marker
2961
+ * (<w:pPr><w:rPr><w:ins/></w:rPr></w:pPr>) so Word natively tracks the
2962
+ * paragraph break.
2963
+ * - Each new <w:p>'s content is wrapped in a <w:ins>, with inline bold/
2964
+ * italic markdown parsed via _parse_inline_markdown.
2965
+ *
2966
+ * The first line:
2967
+ * - If it carries a heading / list marker AND we have a paragraph anchor,
2968
+ * we drop into "block mode": no inline <w:ins>; the first line itself
2969
+ * becomes the first new <w:p>.
2970
+ * - Otherwise we emit a single inline <w:ins> for the first line (current
2971
+ * behaviour) and treat the remaining lines as block extensions.
2972
+ *
2973
+ * Does NOT attach comments; callers handle that.
2974
+ */
2840
2975
  _track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id) {
2841
2976
  if (!text) {
2842
2977
  return {
@@ -2976,7 +3111,15 @@ var RedlineEngine = class {
2976
3111
  const anchor_rPr = findChild(anchor_run._element, "w:rPr");
2977
3112
  if (anchor_rPr) {
2978
3113
  const clone = anchor_rPr.cloneNode(true);
2979
- for (const tag of ["w:vanish", "w:strike", "w:dstrike"]) {
3114
+ for (const tag of [
3115
+ "w:vanish",
3116
+ "w:strike",
3117
+ "w:dstrike",
3118
+ "w:i",
3119
+ "w:iCs",
3120
+ "w:b",
3121
+ "w:bCs"
3122
+ ]) {
2980
3123
  const found = findChild(clone, tag);
2981
3124
  if (found) clone.removeChild(found);
2982
3125
  }
@@ -3250,6 +3393,16 @@ var RedlineEngine = class {
3250
3393
  matches = this.clean_mapper.find_all_match_indices(edit.target_text);
3251
3394
  if (matches.length > 0) activeText = this.clean_mapper.full_text;
3252
3395
  }
3396
+ if (activeText === this.mapper.full_text && matches.length > 1) {
3397
+ const liveMatches = matches.filter(([start, length]) => {
3398
+ const realSpans = this.mapper.spans.filter(
3399
+ (s) => s.run !== null && s.end > start && s.start < start + length
3400
+ );
3401
+ if (realSpans.length === 0) return true;
3402
+ return realSpans.some((s) => !s.del_id);
3403
+ });
3404
+ if (liveMatches.length > 0) matches = liveMatches;
3405
+ }
3253
3406
  if (matches.length === 0) {
3254
3407
  errors.push(
3255
3408
  `- Edit ${i + 1} Failed: Target text not found in document:
@@ -3269,6 +3422,31 @@ var RedlineEngine = class {
3269
3422
  )
3270
3423
  );
3271
3424
  }
3425
+ if (matches.length === 1) {
3426
+ const [m_start, m_len] = matches[0];
3427
+ const matched = activeText.substring(m_start, m_start + m_len);
3428
+ const [pfx, sfx] = trim_common_context(matched, edit.new_text || "");
3429
+ const t_end = matched.length - sfx;
3430
+ const final_target = matched.substring(pfx, t_end);
3431
+ const final_new = (edit.new_text || "").substring(pfx, (edit.new_text || "").length - sfx);
3432
+ if (final_target.includes("\n\n")) {
3433
+ if (final_new.includes("\n\n")) {
3434
+ const parts = matched.split("\n\n");
3435
+ if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3436
+ errors.push(
3437
+ `- Edit ${i + 1} Failed: target_text spans a paragraph boundary with body text on both sides. The paragraph break is a structural element, not literal text, so it cannot be replaced as a single span without corrupting the document. Split this into one edit per paragraph.`
3438
+ );
3439
+ }
3440
+ } else {
3441
+ const parts = final_target.split("\n\n");
3442
+ if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3443
+ errors.push(
3444
+ `- Edit ${i + 1} Failed: target_text spans a paragraph boundary with body text on both sides. The paragraph break is a structural element, not literal text, so it cannot be replaced as a single span without corrupting the document. Split this into one edit per paragraph.`
3445
+ );
3446
+ }
3447
+ }
3448
+ }
3449
+ }
3272
3450
  for (const [start, length] of matches) {
3273
3451
  const spans = this.mapper.spans.filter(
3274
3452
  (s) => s.end > start && s.start < start + length
@@ -3332,7 +3510,33 @@ var RedlineEngine = class {
3332
3510
  }
3333
3511
  return errors;
3334
3512
  }
3335
- process_batch(changes) {
3513
+ process_batch(changes, dry_run = false) {
3514
+ if (dry_run) {
3515
+ const baselines = /* @__PURE__ */ new Map();
3516
+ for (const part of this.doc.pkg.parts) {
3517
+ if (part._element) {
3518
+ baselines.set(part, part._element.cloneNode(true));
3519
+ }
3520
+ }
3521
+ try {
3522
+ return this._process_batch_internal(changes, true);
3523
+ } finally {
3524
+ for (const [part, originalEl] of baselines.entries()) {
3525
+ const doc = part._element.ownerDocument;
3526
+ if (doc && doc.documentElement) {
3527
+ doc.replaceChild(originalEl, doc.documentElement);
3528
+ }
3529
+ part._element = originalEl;
3530
+ }
3531
+ this.mapper = new DocumentMapper(this.doc);
3532
+ this.comments_manager = new CommentsManager(this.doc);
3533
+ this.clean_mapper = null;
3534
+ }
3535
+ } else {
3536
+ return this._process_batch_internal(changes, false);
3537
+ }
3538
+ }
3539
+ _process_batch_internal(changes, dry_run_mode = false) {
3336
3540
  this.skipped_details = [];
3337
3541
  const actions = changes.filter(
3338
3542
  (c) => ["accept", "reject", "reply"].includes(c.type)
@@ -3340,38 +3544,129 @@ var RedlineEngine = class {
3340
3544
  const edits = changes.filter(
3341
3545
  (c) => !["accept", "reject", "reply"].includes(c.type)
3342
3546
  );
3343
- const all_errors = [];
3344
- if (actions.length > 0) {
3345
- all_errors.push(...this.validate_review_actions(actions));
3346
- }
3347
- if (edits.length > 0) {
3348
- all_errors.push(...this.validate_edits(edits));
3349
- }
3350
- if (all_errors.length > 0) {
3351
- throw new BatchValidationError(all_errors);
3547
+ if (!dry_run_mode) {
3548
+ const all_errors = [];
3549
+ if (actions.length > 0) {
3550
+ all_errors.push(...this.validate_review_actions(actions));
3551
+ }
3552
+ if (edits.length > 0) {
3553
+ all_errors.push(...this.validate_edits(edits));
3554
+ }
3555
+ if (all_errors.length > 0) {
3556
+ throw new BatchValidationError(all_errors);
3557
+ }
3558
+ } else {
3559
+ if (actions.length > 0) {
3560
+ const action_errors = this.validate_review_actions(actions);
3561
+ if (action_errors.length > 0) {
3562
+ throw new BatchValidationError(action_errors);
3563
+ }
3564
+ }
3352
3565
  }
3353
- let applied_actions = 0, skipped_actions = 0;
3566
+ let applied_actions = 0;
3567
+ let skipped_actions = 0;
3354
3568
  if (actions.length > 0) {
3355
3569
  const res = this.apply_review_actions(actions);
3356
3570
  applied_actions = res[0];
3357
3571
  skipped_actions = res[1];
3572
+ if (skipped_actions > 0) {
3573
+ throw new BatchValidationError(this.skipped_details);
3574
+ }
3358
3575
  if (applied_actions > 0) {
3359
3576
  this.mapper["_build_map"]();
3360
3577
  if (this.clean_mapper) this.clean_mapper["_build_map"]();
3361
3578
  }
3362
3579
  }
3363
- let applied_edits = 0, skipped_edits = 0;
3580
+ const edits_reports = [];
3581
+ let applied_edits = 0;
3582
+ let skipped_edits = 0;
3364
3583
  if (edits.length > 0) {
3365
- const res = this.apply_edits(edits);
3366
- applied_edits = res[0];
3367
- skipped_edits = res[1];
3584
+ if (dry_run_mode) {
3585
+ for (const edit of edits) {
3586
+ const single_errors = this.validate_edits([edit]);
3587
+ const warning = this._check_punctuation_warning(edit.target_text || "");
3588
+ if (single_errors.length > 0) {
3589
+ skipped_edits++;
3590
+ edits_reports.push({
3591
+ status: "failed",
3592
+ target_text: edit.target_text || "",
3593
+ new_text: edit.new_text || "",
3594
+ warning,
3595
+ error: single_errors[0],
3596
+ critic_markup: null,
3597
+ clean_text: null
3598
+ });
3599
+ continue;
3600
+ }
3601
+ const res = this.apply_edits([edit]);
3602
+ const applied = res[0];
3603
+ if (applied > 0) {
3604
+ applied_edits++;
3605
+ const previews = this._build_edit_context_previews(edit);
3606
+ edits_reports.push({
3607
+ status: "applied",
3608
+ target_text: edit.target_text || "",
3609
+ new_text: edit.new_text || "",
3610
+ warning,
3611
+ error: null,
3612
+ critic_markup: previews[0],
3613
+ clean_text: previews[1]
3614
+ });
3615
+ } else {
3616
+ skipped_edits++;
3617
+ const error_msg = this.skipped_details.length > 0 ? this.skipped_details[this.skipped_details.length - 1] : "Failed to apply edit";
3618
+ edits_reports.push({
3619
+ status: "failed",
3620
+ target_text: edit.target_text || "",
3621
+ new_text: edit.new_text || "",
3622
+ warning,
3623
+ error: error_msg,
3624
+ critic_markup: null,
3625
+ clean_text: null
3626
+ });
3627
+ }
3628
+ }
3629
+ } else {
3630
+ const errors = this.validate_edits(edits);
3631
+ if (errors.length > 0) {
3632
+ throw new BatchValidationError(errors);
3633
+ }
3634
+ const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
3635
+ const res = this.apply_edits(cloned_edits);
3636
+ applied_edits = res[0];
3637
+ skipped_edits = res[1];
3638
+ for (const edit of cloned_edits) {
3639
+ const success = edit._applied_status || false;
3640
+ const error_msg = edit._error_msg || null;
3641
+ const warning = this._check_punctuation_warning(edit.target_text || "");
3642
+ let critic_markup = null;
3643
+ let clean_text = null;
3644
+ if (success) {
3645
+ const previews = this._build_edit_context_previews(edit);
3646
+ critic_markup = previews[0];
3647
+ clean_text = previews[1];
3648
+ }
3649
+ edits_reports.push({
3650
+ status: success ? "applied" : "failed",
3651
+ target_text: edit.target_text || "",
3652
+ new_text: edit.new_text || "",
3653
+ warning,
3654
+ error: error_msg,
3655
+ critic_markup,
3656
+ clean_text
3657
+ });
3658
+ }
3659
+ }
3368
3660
  }
3369
3661
  return {
3370
3662
  actions_applied: applied_actions,
3371
3663
  actions_skipped: skipped_actions,
3372
3664
  edits_applied: applied_edits,
3373
3665
  edits_skipped: skipped_edits,
3374
- skipped_details: this.skipped_details
3666
+ skipped_details: this.skipped_details,
3667
+ edits: edits_reports,
3668
+ engine: "node",
3669
+ version: "1.9.0"
3375
3670
  };
3376
3671
  }
3377
3672
  apply_edits(edits) {
@@ -3379,50 +3674,90 @@ var RedlineEngine = class {
3379
3674
  let skipped = 0;
3380
3675
  const resolved_edits = [];
3381
3676
  for (const edit of edits) {
3382
- if (edit._match_start_index !== void 0 && edit._match_start_index !== null) {
3677
+ edit._applied_status = false;
3678
+ edit._error_msg = null;
3679
+ }
3680
+ for (const edit of edits) {
3681
+ if (edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null) {
3682
+ resolved_edits.push([edit, edit.new_text || null]);
3683
+ } else if (edit._match_start_index !== void 0 && edit._match_start_index !== null) {
3684
+ edit._resolved_start_idx = edit._match_start_index;
3383
3685
  resolved_edits.push([edit, edit.new_text || null]);
3384
3686
  } else if (edit.type === "insert_row" || edit.type === "delete_row") {
3385
- const [idx] = this.mapper.find_match_index(edit.target_text);
3386
- if (idx !== -1) {
3387
- edit._match_start_index = idx;
3687
+ let matches = this.mapper.find_all_match_indices(edit.target_text);
3688
+ if (matches.length === 0) {
3689
+ if (!this.clean_mapper) {
3690
+ this.clean_mapper = new DocumentMapper(this.doc, true);
3691
+ }
3692
+ matches = this.clean_mapper.find_all_match_indices(edit.target_text);
3693
+ }
3694
+ if (matches.length > 0) {
3695
+ edit._resolved_start_idx = matches[0][0];
3388
3696
  resolved_edits.push([edit, null]);
3389
3697
  } else {
3390
3698
  skipped++;
3391
- this.skipped_details.push(
3392
- `- Failed to locate row target: '${(edit.target_text || "").substring(0, 40)}...'`
3393
- );
3699
+ edit._applied_status = false;
3700
+ const target_snippet = (edit.target_text || "").trim().substring(0, 40);
3701
+ const msg = `- Failed to locate row target: '${target_snippet}...'`;
3702
+ this.skipped_details.push(msg);
3703
+ edit._error_msg = msg;
3394
3704
  }
3395
3705
  } else {
3396
3706
  const resolved = this._pre_resolve_heuristic_edit(edit);
3397
3707
  if (resolved) {
3398
3708
  if (Array.isArray(resolved)) {
3399
- for (const r of resolved) resolved_edits.push([r, r.new_text]);
3709
+ for (const r of resolved) {
3710
+ r._resolved_start_idx = r._match_start_index;
3711
+ r._parent_edit_ref = edit;
3712
+ if (edit._resolved_start_idx === void 0 || edit._resolved_start_idx === null) {
3713
+ edit._resolved_start_idx = r._resolved_start_idx;
3714
+ }
3715
+ if (!edit._resolved_proxy_edit) {
3716
+ edit._resolved_proxy_edit = r;
3717
+ }
3718
+ resolved_edits.push([r, r.new_text]);
3719
+ }
3400
3720
  } else {
3721
+ resolved._resolved_start_idx = resolved._match_start_index;
3722
+ resolved._parent_edit_ref = edit;
3723
+ edit._resolved_start_idx = resolved._resolved_start_idx;
3724
+ edit._resolved_proxy_edit = resolved;
3401
3725
  resolved_edits.push([resolved, resolved.new_text]);
3402
3726
  }
3403
3727
  } else {
3404
3728
  skipped++;
3405
- this.skipped_details.push(
3406
- `- Failed to apply edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3407
- );
3729
+ edit._applied_status = false;
3730
+ const display_text = edit.target_text || "insertion";
3731
+ const target_snippet = display_text.trim().substring(0, 40);
3732
+ const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
3733
+ this.skipped_details.push(msg);
3734
+ edit._error_msg = msg;
3408
3735
  }
3409
3736
  }
3410
3737
  }
3411
3738
  resolved_edits.sort(
3412
- (a, b) => (b[0]._match_start_index || 0) - (a[0]._match_start_index || 0)
3739
+ (a, b) => (b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0)
3413
3740
  );
3414
3741
  const occupied_ranges = [];
3415
3742
  for (const [edit, orig_new] of resolved_edits) {
3416
- const start = edit._match_start_index || 0;
3743
+ const start = edit._resolved_start_idx || 0;
3417
3744
  const end = start + (edit.target_text ? edit.target_text.length : 0);
3418
3745
  const overlaps = occupied_ranges.some(
3419
3746
  ([occ_start, occ_end]) => start < occ_end && end > occ_start
3420
3747
  );
3421
3748
  if (overlaps) {
3422
3749
  skipped++;
3423
- this.skipped_details.push(
3424
- `- Skipped overlapping edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3425
- );
3750
+ const display_text = edit.target_text || "insertion";
3751
+ const target_snippet = display_text.trim().substring(0, 40);
3752
+ const msg = `- Skipped overlapping edit targeting: '${target_snippet}...'`;
3753
+ this.skipped_details.push(msg);
3754
+ edit._applied_status = false;
3755
+ edit._error_msg = msg;
3756
+ const parent = edit._parent_edit_ref;
3757
+ if (parent) {
3758
+ parent._applied_status = false;
3759
+ parent._error_msg = msg;
3760
+ }
3426
3761
  continue;
3427
3762
  }
3428
3763
  let success = false;
@@ -3434,11 +3769,26 @@ var RedlineEngine = class {
3434
3769
  if (success) {
3435
3770
  applied++;
3436
3771
  occupied_ranges.push([start, end]);
3772
+ edit._applied_status = true;
3773
+ const parent = edit._parent_edit_ref;
3774
+ if (parent) {
3775
+ parent._applied_status = true;
3776
+ }
3437
3777
  } else {
3438
3778
  skipped++;
3439
- this.skipped_details.push(
3440
- `- Failed to apply edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3441
- );
3779
+ const display_text = edit.target_text || "insertion";
3780
+ const target_snippet = display_text.trim().substring(0, 40);
3781
+ const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
3782
+ this.skipped_details.push(msg);
3783
+ edit._applied_status = false;
3784
+ edit._error_msg = msg;
3785
+ const parent = edit._parent_edit_ref;
3786
+ if (parent) {
3787
+ if (!parent._applied_status) {
3788
+ parent._applied_status = false;
3789
+ parent._error_msg = msg;
3790
+ }
3791
+ }
3442
3792
  }
3443
3793
  }
3444
3794
  return [applied, skipped];
@@ -3531,7 +3881,7 @@ var RedlineEngine = class {
3531
3881
  return [applied, skipped];
3532
3882
  }
3533
3883
  _apply_table_edit(edit, rebuild_map) {
3534
- const start_idx = edit._match_start_index || 0;
3884
+ const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
3535
3885
  const [anchor_run, anchor_para] = this.mapper.get_insertion_anchor(
3536
3886
  start_idx,
3537
3887
  rebuild_map
@@ -3575,9 +3925,31 @@ var RedlineEngine = class {
3575
3925
  }
3576
3926
  return false;
3577
3927
  }
3928
+ /**
3929
+ * Returns the first match of `target_text` in the raw mapper that is NOT
3930
+ * entirely contained within a tracked deletion (<w:del>). Tracked-deleted
3931
+ * copies are not live, editable text, so an edit must resolve to a live
3932
+ * occurrence even when a dead copy appears earlier in the document
3933
+ * (BUG-23-5). Falls back to the plain first match when no live copy is
3934
+ * found (e.g. fuzzy/normalized matches the span filter cannot align).
3935
+ */
3936
+ _first_live_match(target_text) {
3937
+ const all = this.mapper.find_all_match_indices(target_text);
3938
+ if (all.length <= 1) {
3939
+ return this.mapper.find_match_index(target_text);
3940
+ }
3941
+ for (const [start, length] of all) {
3942
+ const realSpans = this.mapper.spans.filter(
3943
+ (s) => s.run !== null && s.end > start && s.start < start + length
3944
+ );
3945
+ if (realSpans.length === 0) return [start, length];
3946
+ if (realSpans.some((s) => !s.del_id)) return [start, length];
3947
+ }
3948
+ return this.mapper.find_match_index(target_text);
3949
+ }
3578
3950
  _pre_resolve_heuristic_edit(edit) {
3579
3951
  if (!edit.target_text) return null;
3580
- let [start_idx, match_len] = this.mapper.find_match_index(edit.target_text);
3952
+ let [start_idx, match_len] = this._first_live_match(edit.target_text);
3581
3953
  let use_clean_map = false;
3582
3954
  if (start_idx === -1) {
3583
3955
  if (!this.clean_mapper)
@@ -3641,7 +4013,7 @@ var RedlineEngine = class {
3641
4013
  _apply_single_edit_indexed(edit, orig_new, rebuild_map) {
3642
4014
  let op = edit._internal_op;
3643
4015
  const active_mapper = edit._active_mapper_ref || this.mapper;
3644
- const start_idx = edit._match_start_index || 0;
4016
+ const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
3645
4017
  const length = edit.target_text ? edit.target_text.length : 0;
3646
4018
  const del_id = ["DELETION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
3647
4019
  const ins_id = ["INSERTION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
@@ -3690,6 +4062,76 @@ var RedlineEngine = class {
3690
4062
  rebuild_map
3691
4063
  );
3692
4064
  if (!anchor_run && !anchor_para) return false;
4065
+ const _bug233_new = edit.new_text || "";
4066
+ const _bug233_trailing_break = /\n\s*$/.test(_bug233_new);
4067
+ let _bug233_target_para = null;
4068
+ {
4069
+ const startingSpans = active_mapper.spans.filter(
4070
+ (s) => s.paragraph !== null && s.start === start_idx
4071
+ );
4072
+ if (startingSpans.length > 0 && startingSpans[0].paragraph) {
4073
+ _bug233_target_para = startingSpans[0].paragraph._element;
4074
+ }
4075
+ }
4076
+ if (_bug233_trailing_break && _bug233_target_para && _bug233_target_para.parentNode) {
4077
+ const body = _bug233_target_para.parentNode;
4078
+ const xmlDoc = this.doc.part._element.ownerDocument;
4079
+ const lines = _bug233_new.split(/[\r\n]+/).filter((l) => l !== "");
4080
+ let firstNew = null;
4081
+ let lastNew = null;
4082
+ let lastIns = null;
4083
+ for (const raw_line of lines) {
4084
+ const [clean_text, style_name] = this._parse_markdown_style(raw_line);
4085
+ const new_p = xmlDoc.createElement("w:p");
4086
+ if (style_name) {
4087
+ this._set_paragraph_style(new_p, style_name);
4088
+ } else {
4089
+ const existing_pPr = findChild(_bug233_target_para, "w:pPr");
4090
+ if (existing_pPr) new_p.appendChild(existing_pPr.cloneNode(true));
4091
+ }
4092
+ let pPr = findChild(new_p, "w:pPr");
4093
+ if (!pPr) {
4094
+ pPr = xmlDoc.createElement("w:pPr");
4095
+ new_p.insertBefore(pPr, new_p.firstChild);
4096
+ }
4097
+ let rPr = findChild(pPr, "w:rPr");
4098
+ if (!rPr) {
4099
+ rPr = xmlDoc.createElement("w:rPr");
4100
+ pPr.appendChild(rPr);
4101
+ }
4102
+ rPr.appendChild(this._create_track_change_tag("w:ins", "", ins_id));
4103
+ const content_ins = this._build_tracked_ins_for_line(
4104
+ clean_text,
4105
+ anchor_run,
4106
+ ins_id,
4107
+ xmlDoc
4108
+ );
4109
+ if (content_ins) new_p.appendChild(content_ins);
4110
+ body.insertBefore(new_p, _bug233_target_para);
4111
+ if (!firstNew) firstNew = new_p;
4112
+ lastNew = new_p;
4113
+ lastIns = content_ins;
4114
+ }
4115
+ if (firstNew) {
4116
+ if (edit.comment && lastNew && lastIns) {
4117
+ const ascend = (el, p) => {
4118
+ let cur = el;
4119
+ while (cur.parentNode && cur.parentNode !== p)
4120
+ cur = cur.parentNode;
4121
+ return cur;
4122
+ };
4123
+ const startIns = findAllDescendants(firstNew, "w:ins")[0] || firstNew;
4124
+ this._attach_comment_spanning(
4125
+ firstNew,
4126
+ ascend(startIns, firstNew),
4127
+ lastNew,
4128
+ ascend(lastIns, lastNew),
4129
+ edit.comment
4130
+ );
4131
+ }
4132
+ return true;
4133
+ }
4134
+ }
3693
4135
  const result = this._track_insert_multiline(
3694
4136
  edit.new_text || "",
3695
4137
  anchor_run,
@@ -5399,15 +5841,88 @@ function remove_all_comments(doc) {
5399
5841
  lines.push(` ${status} "${_truncate(info.text || "", 60)}" (${info.author || "Unknown"})`);
5400
5842
  cm.deleteComment(cId);
5401
5843
  }
5402
- for (const tag of ["w:commentRangeStart", "w:commentRangeEnd", "w:commentReference"]) {
5844
+ for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
5403
5845
  for (const el of findAllDescendants(doc.element, tag)) {
5404
5846
  el.parentNode?.removeChild(el);
5405
5847
  }
5406
5848
  }
5849
+ const refs = findAllDescendants(doc.element, "w:commentReference");
5850
+ for (const ref of refs) {
5851
+ const parent = ref.parentNode;
5852
+ if (parent) {
5853
+ if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
5854
+ const nonRprChildren = Array.from(parent.childNodes).filter(
5855
+ (c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
5856
+ );
5857
+ if (nonRprChildren.length <= 1) {
5858
+ parent.parentNode?.removeChild(parent);
5859
+ } else {
5860
+ parent.removeChild(ref);
5861
+ }
5862
+ } else {
5863
+ parent.removeChild(ref);
5864
+ }
5865
+ }
5866
+ }
5407
5867
  const resolvedCount = Object.values(data).filter((c) => c.resolved).length;
5408
5868
  const openCount = Object.values(data).filter((c) => !c.resolved).length;
5409
5869
  return [`Comments removed: ${keys.length} (${resolvedCount} resolved, ${openCount} open)`].concat(lines);
5410
5870
  }
5871
+ function eject_comment_parts(doc) {
5872
+ const pkg = doc.pkg;
5873
+ const comment_partnames = /* @__PURE__ */ new Set();
5874
+ for (const part of pkg.parts) {
5875
+ if (part.partname.toLowerCase().includes("comments")) {
5876
+ comment_partnames.add(part.partname);
5877
+ const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
5878
+ const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
5879
+ comment_partnames.add(withSlash);
5880
+ comment_partnames.add(withoutSlash);
5881
+ }
5882
+ }
5883
+ if (comment_partnames.size === 0) return;
5884
+ for (const part of pkg.parts) {
5885
+ if (part.partname.endsWith(".rels")) {
5886
+ const rels = findAllDescendants(part._element, "Relationship");
5887
+ const toRemove = [];
5888
+ for (const rel of rels) {
5889
+ const target = rel.getAttribute("Target") || "";
5890
+ if (target.toLowerCase().includes("comments")) {
5891
+ toRemove.push(rel);
5892
+ const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
5893
+ const sourcePart = pkg.getPartByPath(sourcePath);
5894
+ if (sourcePart) {
5895
+ const relId = rel.getAttribute("Id");
5896
+ if (relId) sourcePart.rels.delete(relId);
5897
+ }
5898
+ }
5899
+ }
5900
+ for (const relEl of toRemove) {
5901
+ relEl.parentNode?.removeChild(relEl);
5902
+ }
5903
+ }
5904
+ }
5905
+ const ctPart = pkg.getPartByPath("[Content_Types].xml");
5906
+ if (ctPart) {
5907
+ const overrides = findAllDescendants(ctPart._element, "Override");
5908
+ const toRemove = [];
5909
+ for (const override of overrides) {
5910
+ const partName = override.getAttribute("PartName") || "";
5911
+ if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
5912
+ toRemove.push(override);
5913
+ }
5914
+ }
5915
+ for (const overrideEl of toRemove) {
5916
+ overrideEl.parentNode?.removeChild(overrideEl);
5917
+ }
5918
+ }
5919
+ pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
5920
+ for (const key of Object.keys(pkg.unzipped)) {
5921
+ if (key.toLowerCase().includes("comments")) {
5922
+ delete pkg.unzipped[key];
5923
+ }
5924
+ }
5925
+ }
5411
5926
  function replace_comment_authors(doc, newAuthor) {
5412
5927
  const cm = new CommentsManager(doc);
5413
5928
  if (!cm.commentsPart) return [];
@@ -5596,6 +6111,7 @@ async function finalize_document(doc, options) {
5596
6111
  const commentsSummary = get_comments_summary(doc);
5597
6112
  report.comments_removed = commentsSummary.total;
5598
6113
  report.add_transform_lines(remove_all_comments(doc));
6114
+ eject_comment_parts(doc);
5599
6115
  } else if (options.sanitize_mode === "keep-markup") {
5600
6116
  const counts = count_tracked_changes(doc);
5601
6117
  report.tracked_changes_found = counts[0] + counts[1] + counts[2];