@adeu/core 1.9.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
@@ -452,6 +452,21 @@ var CommentsManager = class {
452
452
  return part;
453
453
  }
454
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
+ }
455
470
  }
456
471
  _getNextCommentId() {
457
472
  const ids = [0];
@@ -577,9 +592,9 @@ var CommentsManager = class {
577
592
  return commentId;
578
593
  }
579
594
  deleteComment(commentId) {
580
- if (!this._commentsPart) return;
595
+ if (!this.commentsPart) return;
581
596
  let commentEl = null;
582
- for (const c of findAllDescendants(this._commentsPart._element, "w:comment")) {
597
+ for (const c of findAllDescendants(this.commentsPart._element, "w:comment")) {
583
598
  if (c.getAttribute("w:id") === commentId) {
584
599
  commentEl = c;
585
600
  break;
@@ -603,7 +618,7 @@ var CommentsManager = class {
603
618
  if (child.getAttribute("w15:paraIdParent") === paraId) {
604
619
  const childParaId = child.getAttribute("w15:paraId");
605
620
  if (childParaId) {
606
- for (const c of findAllDescendants(this._commentsPart._element, "w:comment")) {
621
+ for (const c of findAllDescendants(this.commentsPart._element, "w:comment")) {
607
622
  for (const p of findAllDescendants(c, "w:p")) {
608
623
  if (p.getAttribute("w14:paraId") === childParaId) {
609
624
  const cid = c.getAttribute("w:id");
@@ -1837,6 +1852,29 @@ ${header}`;
1837
1852
 
1838
1853
  // src/diff.ts
1839
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
+ }
1840
1878
  function trim_common_context(target, new_val) {
1841
1879
  if (!target || !new_val) return [0, 0];
1842
1880
  const isSpace = (char) => /\s/.test(char);
@@ -1867,7 +1905,7 @@ function trim_common_context(target, new_val) {
1867
1905
  const left = target.substring(0, prefix_len);
1868
1906
  const b_count = (left.match(/\*\*/g) || []).length;
1869
1907
  const u2_count = (left.match(/__/g) || []).length;
1870
- const u1_count = (left.replace(/__/g, "").match(/_/g) || []).length;
1908
+ const u1_count = _count_standalone_underscores(left);
1871
1909
  if (b_count % 2 !== 0) {
1872
1910
  prefix_len = left.lastIndexOf("**");
1873
1911
  continue;
@@ -1878,10 +1916,14 @@ function trim_common_context(target, new_val) {
1878
1916
  }
1879
1917
  if (u1_count % 2 !== 0) {
1880
1918
  let idx = left.length - 1;
1919
+ const isAlnum = (char) => /[a-zA-Z0-9]/.test(char);
1881
1920
  while (idx >= 0) {
1882
1921
  if (left[idx] === "_" && (idx === 0 || left[idx - 1] !== "_") && (idx === left.length - 1 || left[idx + 1] !== "_")) {
1883
- prefix_len = idx;
1884
- 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
+ }
1885
1927
  }
1886
1928
  idx--;
1887
1929
  }
@@ -1941,7 +1983,7 @@ function trim_common_context(target, new_val) {
1941
1983
  const right = target.substring(target.length - suffix_len);
1942
1984
  const b_count = (right.match(/\*\*/g) || []).length;
1943
1985
  const u2_count = (right.match(/__/g) || []).length;
1944
- const u1_count = (right.replace(/__/g, "").match(/_/g) || []).length;
1986
+ const u1_count = _count_standalone_underscores(right);
1945
1987
  if (b_count % 2 !== 0) {
1946
1988
  suffix_len -= right.indexOf("**") + 2;
1947
1989
  continue;
@@ -1952,10 +1994,14 @@ function trim_common_context(target, new_val) {
1952
1994
  }
1953
1995
  if (u1_count % 2 !== 0) {
1954
1996
  let idx_in_right = 0;
1997
+ const isAlnum = (char) => /[a-zA-Z0-9]/.test(char);
1955
1998
  while (idx_in_right < right.length) {
1956
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] !== "_")) {
1957
- suffix_len -= idx_in_right + 1;
1958
- 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
+ }
1959
2005
  }
1960
2006
  idx_in_right++;
1961
2007
  }
@@ -2602,6 +2648,36 @@ var RedlineEngine = class {
2602
2648
  this.mapper = new DocumentMapper(this.doc);
2603
2649
  this.comments_manager = new CommentsManager(this.doc);
2604
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
+ }
2605
2681
  _scan_existing_ids() {
2606
2682
  let maxId = 0;
2607
2683
  for (const tag of ["w:ins", "w:del"]) {
@@ -2694,28 +2770,84 @@ var RedlineEngine = class {
2694
2770
  }
2695
2771
  }
2696
2772
  }
2697
- const comment_ids = /* @__PURE__ */ new Set();
2698
- for (const tag of [
2699
- "w:commentRangeStart",
2700
- "w:commentRangeEnd",
2701
- "w:commentReference"
2702
- ]) {
2703
- for (const node of findAllDescendants(this.doc.element, tag)) {
2704
- const cid = node.getAttribute("w:id");
2705
- 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
+ }
2706
2778
  }
2707
- }
2708
- const comments_part = this.doc.pkg.parts.find(
2709
- (p) => p.contentType === "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml"
2710
- );
2711
- if (comments_part) {
2712
- for (const c of findAllDescendants(comments_part._element, "w:comment")) {
2713
- const cid = c.getAttribute("w:id");
2714
- 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
+ }
2715
2796
  }
2716
2797
  }
2717
- for (const cid of comment_ids) {
2718
- 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
+ }
2719
2851
  }
2720
2852
  }
2721
2853
  _getNextId() {
@@ -2806,40 +2938,40 @@ var RedlineEngine = class {
2806
2938
  }
2807
2939
  }
2808
2940
  /**
2809
- * Inserts `text` as one or more tracked paragraphs anchored relative to
2810
- * either an existing run or a paragraph. Returns:
2811
- * { first_node, last_p, last_ins, used_block_mode }
2812
- * where:
2813
- * - first_node: the first <w:ins> (for inline mode) OR the first new <w:p>
2814
- * (for block mode). The caller uses this for splicing into the DOM and
2815
- * for anchoring comments.
2816
- * - last_p: the last new <w:p> created, if any. null when entirely inline.
2817
- * - last_ins: the last <w:ins> created (inside the last new <w:p>, or the
2818
- * sole inline ins). Used as the comment's end anchor.
2819
- * - used_block_mode: true when the first line carried a heading/list style
2820
- * marker and we created a new paragraph for it (rather than inlining it).
2821
- *
2822
- * Multi-paragraph rules (only when text contains '\n'):
2823
- * - Each additional line becomes a new <w:p>, inserted after the anchor
2824
- * paragraph in document order.
2825
- * - Each new <w:p> gets a copy of the anchor paragraph's <w:pPr> (so list
2826
- * numbering / indentation are preserved) unless the line itself starts
2827
- * with a markdown heading or list marker, which overrides the style.
2828
- * - Each new <w:p> carries a tracked paragraph-break marker
2829
- * (<w:pPr><w:rPr><w:ins/></w:rPr></w:pPr>) so Word natively tracks the
2830
- * paragraph break.
2831
- * - Each new <w:p>'s content is wrapped in a <w:ins>, with inline bold/
2832
- * italic markdown parsed via _parse_inline_markdown.
2833
- *
2834
- * The first line:
2835
- * - If it carries a heading / list marker AND we have a paragraph anchor,
2836
- * we drop into "block mode": no inline <w:ins>; the first line itself
2837
- * becomes the first new <w:p>.
2838
- * - Otherwise we emit a single inline <w:ins> for the first line (current
2839
- * behaviour) and treat the remaining lines as block extensions.
2840
- *
2841
- * Does NOT attach comments; callers handle that.
2842
- */
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
+ */
2843
2975
  _track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id) {
2844
2976
  if (!text) {
2845
2977
  return {
@@ -2979,7 +3111,15 @@ var RedlineEngine = class {
2979
3111
  const anchor_rPr = findChild(anchor_run._element, "w:rPr");
2980
3112
  if (anchor_rPr) {
2981
3113
  const clone = anchor_rPr.cloneNode(true);
2982
- 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
+ ]) {
2983
3123
  const found = findChild(clone, tag);
2984
3124
  if (found) clone.removeChild(found);
2985
3125
  }
@@ -3253,6 +3393,16 @@ var RedlineEngine = class {
3253
3393
  matches = this.clean_mapper.find_all_match_indices(edit.target_text);
3254
3394
  if (matches.length > 0) activeText = this.clean_mapper.full_text;
3255
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
+ }
3256
3406
  if (matches.length === 0) {
3257
3407
  errors.push(
3258
3408
  `- Edit ${i + 1} Failed: Target text not found in document:
@@ -3272,6 +3422,31 @@ var RedlineEngine = class {
3272
3422
  )
3273
3423
  );
3274
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
+ }
3275
3450
  for (const [start, length] of matches) {
3276
3451
  const spans = this.mapper.spans.filter(
3277
3452
  (s) => s.end > start && s.start < start + length
@@ -3335,7 +3510,33 @@ var RedlineEngine = class {
3335
3510
  }
3336
3511
  return errors;
3337
3512
  }
3338
- 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) {
3339
3540
  this.skipped_details = [];
3340
3541
  const actions = changes.filter(
3341
3542
  (c) => ["accept", "reject", "reply"].includes(c.type)
@@ -3343,38 +3544,129 @@ var RedlineEngine = class {
3343
3544
  const edits = changes.filter(
3344
3545
  (c) => !["accept", "reject", "reply"].includes(c.type)
3345
3546
  );
3346
- const all_errors = [];
3347
- if (actions.length > 0) {
3348
- all_errors.push(...this.validate_review_actions(actions));
3349
- }
3350
- if (edits.length > 0) {
3351
- all_errors.push(...this.validate_edits(edits));
3352
- }
3353
- if (all_errors.length > 0) {
3354
- 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
+ }
3355
3565
  }
3356
- let applied_actions = 0, skipped_actions = 0;
3566
+ let applied_actions = 0;
3567
+ let skipped_actions = 0;
3357
3568
  if (actions.length > 0) {
3358
3569
  const res = this.apply_review_actions(actions);
3359
3570
  applied_actions = res[0];
3360
3571
  skipped_actions = res[1];
3572
+ if (skipped_actions > 0) {
3573
+ throw new BatchValidationError(this.skipped_details);
3574
+ }
3361
3575
  if (applied_actions > 0) {
3362
3576
  this.mapper["_build_map"]();
3363
3577
  if (this.clean_mapper) this.clean_mapper["_build_map"]();
3364
3578
  }
3365
3579
  }
3366
- let applied_edits = 0, skipped_edits = 0;
3580
+ const edits_reports = [];
3581
+ let applied_edits = 0;
3582
+ let skipped_edits = 0;
3367
3583
  if (edits.length > 0) {
3368
- const res = this.apply_edits(edits);
3369
- applied_edits = res[0];
3370
- 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
+ }
3371
3660
  }
3372
3661
  return {
3373
3662
  actions_applied: applied_actions,
3374
3663
  actions_skipped: skipped_actions,
3375
3664
  edits_applied: applied_edits,
3376
3665
  edits_skipped: skipped_edits,
3377
- skipped_details: this.skipped_details
3666
+ skipped_details: this.skipped_details,
3667
+ edits: edits_reports,
3668
+ engine: "node",
3669
+ version: "1.9.0"
3378
3670
  };
3379
3671
  }
3380
3672
  apply_edits(edits) {
@@ -3382,50 +3674,90 @@ var RedlineEngine = class {
3382
3674
  let skipped = 0;
3383
3675
  const resolved_edits = [];
3384
3676
  for (const edit of edits) {
3385
- 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;
3386
3685
  resolved_edits.push([edit, edit.new_text || null]);
3387
3686
  } else if (edit.type === "insert_row" || edit.type === "delete_row") {
3388
- const [idx] = this.mapper.find_match_index(edit.target_text);
3389
- if (idx !== -1) {
3390
- 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];
3391
3696
  resolved_edits.push([edit, null]);
3392
3697
  } else {
3393
3698
  skipped++;
3394
- this.skipped_details.push(
3395
- `- Failed to locate row target: '${(edit.target_text || "").substring(0, 40)}...'`
3396
- );
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;
3397
3704
  }
3398
3705
  } else {
3399
3706
  const resolved = this._pre_resolve_heuristic_edit(edit);
3400
3707
  if (resolved) {
3401
3708
  if (Array.isArray(resolved)) {
3402
- 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
+ }
3403
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;
3404
3725
  resolved_edits.push([resolved, resolved.new_text]);
3405
3726
  }
3406
3727
  } else {
3407
3728
  skipped++;
3408
- this.skipped_details.push(
3409
- `- Failed to apply edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3410
- );
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;
3411
3735
  }
3412
3736
  }
3413
3737
  }
3414
3738
  resolved_edits.sort(
3415
- (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)
3416
3740
  );
3417
3741
  const occupied_ranges = [];
3418
3742
  for (const [edit, orig_new] of resolved_edits) {
3419
- const start = edit._match_start_index || 0;
3743
+ const start = edit._resolved_start_idx || 0;
3420
3744
  const end = start + (edit.target_text ? edit.target_text.length : 0);
3421
3745
  const overlaps = occupied_ranges.some(
3422
3746
  ([occ_start, occ_end]) => start < occ_end && end > occ_start
3423
3747
  );
3424
3748
  if (overlaps) {
3425
3749
  skipped++;
3426
- this.skipped_details.push(
3427
- `- Skipped overlapping edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3428
- );
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
+ }
3429
3761
  continue;
3430
3762
  }
3431
3763
  let success = false;
@@ -3437,11 +3769,26 @@ var RedlineEngine = class {
3437
3769
  if (success) {
3438
3770
  applied++;
3439
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
+ }
3440
3777
  } else {
3441
3778
  skipped++;
3442
- this.skipped_details.push(
3443
- `- Failed to apply edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3444
- );
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
+ }
3445
3792
  }
3446
3793
  }
3447
3794
  return [applied, skipped];
@@ -3534,7 +3881,7 @@ var RedlineEngine = class {
3534
3881
  return [applied, skipped];
3535
3882
  }
3536
3883
  _apply_table_edit(edit, rebuild_map) {
3537
- 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;
3538
3885
  const [anchor_run, anchor_para] = this.mapper.get_insertion_anchor(
3539
3886
  start_idx,
3540
3887
  rebuild_map
@@ -3578,9 +3925,31 @@ var RedlineEngine = class {
3578
3925
  }
3579
3926
  return false;
3580
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
+ }
3581
3950
  _pre_resolve_heuristic_edit(edit) {
3582
3951
  if (!edit.target_text) return null;
3583
- 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);
3584
3953
  let use_clean_map = false;
3585
3954
  if (start_idx === -1) {
3586
3955
  if (!this.clean_mapper)
@@ -3644,7 +4013,7 @@ var RedlineEngine = class {
3644
4013
  _apply_single_edit_indexed(edit, orig_new, rebuild_map) {
3645
4014
  let op = edit._internal_op;
3646
4015
  const active_mapper = edit._active_mapper_ref || this.mapper;
3647
- 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;
3648
4017
  const length = edit.target_text ? edit.target_text.length : 0;
3649
4018
  const del_id = ["DELETION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
3650
4019
  const ins_id = ["INSERTION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
@@ -3693,6 +4062,76 @@ var RedlineEngine = class {
3693
4062
  rebuild_map
3694
4063
  );
3695
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
+ }
3696
4135
  const result = this._track_insert_multiline(
3697
4136
  edit.new_text || "",
3698
4137
  anchor_run,
@@ -5402,15 +5841,88 @@ function remove_all_comments(doc) {
5402
5841
  lines.push(` ${status} "${_truncate(info.text || "", 60)}" (${info.author || "Unknown"})`);
5403
5842
  cm.deleteComment(cId);
5404
5843
  }
5405
- for (const tag of ["w:commentRangeStart", "w:commentRangeEnd", "w:commentReference"]) {
5844
+ for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
5406
5845
  for (const el of findAllDescendants(doc.element, tag)) {
5407
5846
  el.parentNode?.removeChild(el);
5408
5847
  }
5409
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
+ }
5410
5867
  const resolvedCount = Object.values(data).filter((c) => c.resolved).length;
5411
5868
  const openCount = Object.values(data).filter((c) => !c.resolved).length;
5412
5869
  return [`Comments removed: ${keys.length} (${resolvedCount} resolved, ${openCount} open)`].concat(lines);
5413
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
+ }
5414
5926
  function replace_comment_authors(doc, newAuthor) {
5415
5927
  const cm = new CommentsManager(doc);
5416
5928
  if (!cm.commentsPart) return [];
@@ -5599,6 +6111,7 @@ async function finalize_document(doc, options) {
5599
6111
  const commentsSummary = get_comments_summary(doc);
5600
6112
  report.comments_removed = commentsSummary.total;
5601
6113
  report.add_transform_lines(remove_all_comments(doc));
6114
+ eject_comment_parts(doc);
5602
6115
  } else if (options.sanitize_mode === "keep-markup") {
5603
6116
  const counts = count_tracked_changes(doc);
5604
6117
  report.tracked_changes_found = counts[0] + counts[1] + counts[2];