@adeu/core 1.9.0 → 1.10.1

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.cjs CHANGED
@@ -502,6 +502,21 @@ var CommentsManager = class {
502
502
  return part;
503
503
  }
504
504
  _ensureNamespaces() {
505
+ const root = this._commentsPart?._element;
506
+ if (!root) return;
507
+ const required = [
508
+ ["xmlns:w", NS.w],
509
+ ["xmlns:w14", NS.w14],
510
+ ["xmlns:w15", NS.w15],
511
+ ["xmlns:w16cid", NS.w16cid],
512
+ ["xmlns:w16cex", NS.w16cex],
513
+ ["xmlns:mc", NS.mc]
514
+ ];
515
+ for (const [attr, uri] of required) {
516
+ if (!root.getAttribute(attr)) {
517
+ root.setAttribute(attr, uri);
518
+ }
519
+ }
505
520
  }
506
521
  _getNextCommentId() {
507
522
  const ids = [0];
@@ -627,9 +642,9 @@ var CommentsManager = class {
627
642
  return commentId;
628
643
  }
629
644
  deleteComment(commentId) {
630
- if (!this._commentsPart) return;
645
+ if (!this.commentsPart) return;
631
646
  let commentEl = null;
632
- for (const c of findAllDescendants(this._commentsPart._element, "w:comment")) {
647
+ for (const c of findAllDescendants(this.commentsPart._element, "w:comment")) {
633
648
  if (c.getAttribute("w:id") === commentId) {
634
649
  commentEl = c;
635
650
  break;
@@ -653,7 +668,7 @@ var CommentsManager = class {
653
668
  if (child.getAttribute("w15:paraIdParent") === paraId) {
654
669
  const childParaId = child.getAttribute("w15:paraId");
655
670
  if (childParaId) {
656
- for (const c of findAllDescendants(this._commentsPart._element, "w:comment")) {
671
+ for (const c of findAllDescendants(this.commentsPart._element, "w:comment")) {
657
672
  for (const p of findAllDescendants(c, "w:p")) {
658
673
  if (p.getAttribute("w14:paraId") === childParaId) {
659
674
  const cid = c.getAttribute("w:id");
@@ -1887,6 +1902,29 @@ ${header}`;
1887
1902
 
1888
1903
  // src/diff.ts
1889
1904
  var import_diff_match_patch = __toESM(require("diff-match-patch"), 1);
1905
+ function _count_standalone_underscores(s) {
1906
+ let count = 0;
1907
+ let i = 0;
1908
+ const n = s.length;
1909
+ const isAlnum = (char) => /[a-zA-Z0-9]/.test(char);
1910
+ while (i < n) {
1911
+ if (s[i] === "_") {
1912
+ let is_double = false;
1913
+ if (i > 0 && s[i - 1] === "_" || i < n - 1 && s[i + 1] === "_") {
1914
+ is_double = true;
1915
+ }
1916
+ let is_intra = false;
1917
+ if (i > 0 && isAlnum(s[i - 1]) && i < n - 1 && isAlnum(s[i + 1])) {
1918
+ is_intra = true;
1919
+ }
1920
+ if (!is_double && !is_intra) {
1921
+ count++;
1922
+ }
1923
+ }
1924
+ i++;
1925
+ }
1926
+ return count;
1927
+ }
1890
1928
  function trim_common_context(target, new_val) {
1891
1929
  if (!target || !new_val) return [0, 0];
1892
1930
  const isSpace = (char) => /\s/.test(char);
@@ -1917,7 +1955,7 @@ function trim_common_context(target, new_val) {
1917
1955
  const left = target.substring(0, prefix_len);
1918
1956
  const b_count = (left.match(/\*\*/g) || []).length;
1919
1957
  const u2_count = (left.match(/__/g) || []).length;
1920
- const u1_count = (left.replace(/__/g, "").match(/_/g) || []).length;
1958
+ const u1_count = _count_standalone_underscores(left);
1921
1959
  if (b_count % 2 !== 0) {
1922
1960
  prefix_len = left.lastIndexOf("**");
1923
1961
  continue;
@@ -1928,10 +1966,14 @@ function trim_common_context(target, new_val) {
1928
1966
  }
1929
1967
  if (u1_count % 2 !== 0) {
1930
1968
  let idx = left.length - 1;
1969
+ const isAlnum = (char) => /[a-zA-Z0-9]/.test(char);
1931
1970
  while (idx >= 0) {
1932
1971
  if (left[idx] === "_" && (idx === 0 || left[idx - 1] !== "_") && (idx === left.length - 1 || left[idx + 1] !== "_")) {
1933
- prefix_len = idx;
1934
- break;
1972
+ const is_intra = idx > 0 && isAlnum(left[idx - 1]) && idx < left.length - 1 && isAlnum(left[idx + 1]);
1973
+ if (!is_intra) {
1974
+ prefix_len = idx;
1975
+ break;
1976
+ }
1935
1977
  }
1936
1978
  idx--;
1937
1979
  }
@@ -1991,7 +2033,7 @@ function trim_common_context(target, new_val) {
1991
2033
  const right = target.substring(target.length - suffix_len);
1992
2034
  const b_count = (right.match(/\*\*/g) || []).length;
1993
2035
  const u2_count = (right.match(/__/g) || []).length;
1994
- const u1_count = (right.replace(/__/g, "").match(/_/g) || []).length;
2036
+ const u1_count = _count_standalone_underscores(right);
1995
2037
  if (b_count % 2 !== 0) {
1996
2038
  suffix_len -= right.indexOf("**") + 2;
1997
2039
  continue;
@@ -2002,10 +2044,14 @@ function trim_common_context(target, new_val) {
2002
2044
  }
2003
2045
  if (u1_count % 2 !== 0) {
2004
2046
  let idx_in_right = 0;
2047
+ const isAlnum = (char) => /[a-zA-Z0-9]/.test(char);
2005
2048
  while (idx_in_right < right.length) {
2006
2049
  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] !== "_")) {
2007
- suffix_len -= idx_in_right + 1;
2008
- break;
2050
+ 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]);
2051
+ if (!is_intra) {
2052
+ suffix_len -= idx_in_right + 1;
2053
+ break;
2054
+ }
2009
2055
  }
2010
2056
  idx_in_right++;
2011
2057
  }
@@ -2652,6 +2698,39 @@ var RedlineEngine = class {
2652
2698
  this.mapper = new DocumentMapper(this.doc);
2653
2699
  this.comments_manager = new CommentsManager(this.doc);
2654
2700
  }
2701
+ _check_punctuation_warning(target_text) {
2702
+ if (!target_text) return null;
2703
+ if (target_text.includes("_") || target_text.includes("-")) {
2704
+ 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.`;
2705
+ }
2706
+ return null;
2707
+ }
2708
+ _build_edit_context_previews(edit) {
2709
+ if (edit.type !== "modify") return [null, null];
2710
+ if (edit._resolved_proxy_edit) {
2711
+ edit = edit._resolved_proxy_edit;
2712
+ }
2713
+ const start_idx = edit._resolved_start_idx;
2714
+ if (start_idx === void 0 || start_idx === null) return [null, null];
2715
+ const target_text = edit.target_text || "";
2716
+ const new_text = edit.new_text || "";
2717
+ const length = target_text.length;
2718
+ const active_mapper = edit._active_mapper_ref || this.mapper;
2719
+ const full_text = active_mapper.full_text;
2720
+ if (!full_text) return [null, null];
2721
+ const before_start = Math.max(0, start_idx - 30);
2722
+ const context_before = full_text.substring(before_start, start_idx);
2723
+ const context_after = full_text.substring(
2724
+ start_idx + length,
2725
+ start_idx + length + 30
2726
+ );
2727
+ const critic_markup = `${context_before}{--${target_text}--}{++${new_text}++}${context_after}`;
2728
+ let clean_text = critic_markup;
2729
+ clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
2730
+ clean_text = clean_text.replace(/\{--.*?--\}/gs, "");
2731
+ clean_text = clean_text.replace(/\{\+\+(.*?)\+\+\}/gs, "$1");
2732
+ return [critic_markup, clean_text];
2733
+ }
2655
2734
  _scan_existing_ids() {
2656
2735
  let maxId = 0;
2657
2736
  for (const tag of ["w:ins", "w:del"]) {
@@ -2744,28 +2823,86 @@ var RedlineEngine = class {
2744
2823
  }
2745
2824
  }
2746
2825
  }
2747
- const comment_ids = /* @__PURE__ */ new Set();
2748
- for (const tag of [
2749
- "w:commentRangeStart",
2750
- "w:commentRangeEnd",
2751
- "w:commentReference"
2752
- ]) {
2753
- for (const node of findAllDescendants(this.doc.element, tag)) {
2754
- const cid = node.getAttribute("w:id");
2755
- if (cid) comment_ids.add(cid);
2826
+ for (const root_element of parts_to_process) {
2827
+ for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
2828
+ for (const el of findAllDescendants(root_element, tag)) {
2829
+ el.parentNode?.removeChild(el);
2830
+ }
2756
2831
  }
2757
- }
2758
- const comments_part = this.doc.pkg.parts.find(
2759
- (p) => p.contentType === "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml"
2760
- );
2761
- if (comments_part) {
2762
- for (const c of findAllDescendants(comments_part._element, "w:comment")) {
2763
- const cid = c.getAttribute("w:id");
2764
- if (cid) comment_ids.add(cid);
2832
+ const refs = findAllDescendants(root_element, "w:commentReference");
2833
+ for (const ref of refs) {
2834
+ const parent = ref.parentNode;
2835
+ if (parent) {
2836
+ if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
2837
+ const nonRprChildren = Array.from(parent.childNodes).filter(
2838
+ (c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
2839
+ );
2840
+ if (nonRprChildren.length <= 1) {
2841
+ parent.parentNode?.removeChild(parent);
2842
+ } else {
2843
+ parent.removeChild(ref);
2844
+ }
2845
+ } else {
2846
+ parent.removeChild(ref);
2847
+ }
2848
+ }
2765
2849
  }
2766
2850
  }
2767
- for (const cid of comment_ids) {
2768
- this.comments_manager.deleteComment(cid);
2851
+ const pkg = this.doc.pkg;
2852
+ const comment_partnames = /* @__PURE__ */ new Set();
2853
+ for (const part of pkg.parts) {
2854
+ if (part.partname.toLowerCase().includes("comments")) {
2855
+ comment_partnames.add(part.partname);
2856
+ const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
2857
+ const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
2858
+ comment_partnames.add(withSlash);
2859
+ comment_partnames.add(withoutSlash);
2860
+ }
2861
+ }
2862
+ if (comment_partnames.size > 0) {
2863
+ for (const part of pkg.parts) {
2864
+ if (part.partname.endsWith(".rels")) {
2865
+ const rels = findAllDescendants(part._element, "Relationship");
2866
+ const toRemove = [];
2867
+ for (const rel of rels) {
2868
+ const target = rel.getAttribute("Target") || "";
2869
+ if (target.toLowerCase().includes("comments")) {
2870
+ toRemove.push(rel);
2871
+ const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
2872
+ const sourcePart = pkg.getPartByPath(sourcePath);
2873
+ if (sourcePart) {
2874
+ const relId = rel.getAttribute("Id");
2875
+ if (relId) sourcePart.rels.delete(relId);
2876
+ }
2877
+ }
2878
+ }
2879
+ for (const relEl of toRemove) {
2880
+ relEl.parentNode?.removeChild(relEl);
2881
+ }
2882
+ }
2883
+ }
2884
+ const ctPart = pkg.getPartByPath("[Content_Types].xml");
2885
+ if (ctPart) {
2886
+ const overrides = findAllDescendants(ctPart._element, "Override");
2887
+ const toRemove = [];
2888
+ for (const override of overrides) {
2889
+ const partName = override.getAttribute("PartName") || "";
2890
+ if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
2891
+ toRemove.push(override);
2892
+ }
2893
+ }
2894
+ for (const overrideEl of toRemove) {
2895
+ overrideEl.parentNode?.removeChild(overrideEl);
2896
+ }
2897
+ }
2898
+ pkg.parts = pkg.parts.filter(
2899
+ (p) => !p.partname.toLowerCase().includes("comments")
2900
+ );
2901
+ for (const key of Object.keys(pkg.unzipped)) {
2902
+ if (key.toLowerCase().includes("comments")) {
2903
+ delete pkg.unzipped[key];
2904
+ }
2905
+ }
2769
2906
  }
2770
2907
  }
2771
2908
  _getNextId() {
@@ -2856,40 +2993,40 @@ var RedlineEngine = class {
2856
2993
  }
2857
2994
  }
2858
2995
  /**
2859
- * Inserts `text` as one or more tracked paragraphs anchored relative to
2860
- * either an existing run or a paragraph. Returns:
2861
- * { first_node, last_p, last_ins, used_block_mode }
2862
- * where:
2863
- * - first_node: the first <w:ins> (for inline mode) OR the first new <w:p>
2864
- * (for block mode). The caller uses this for splicing into the DOM and
2865
- * for anchoring comments.
2866
- * - last_p: the last new <w:p> created, if any. null when entirely inline.
2867
- * - last_ins: the last <w:ins> created (inside the last new <w:p>, or the
2868
- * sole inline ins). Used as the comment's end anchor.
2869
- * - used_block_mode: true when the first line carried a heading/list style
2870
- * marker and we created a new paragraph for it (rather than inlining it).
2871
- *
2872
- * Multi-paragraph rules (only when text contains '\n'):
2873
- * - Each additional line becomes a new <w:p>, inserted after the anchor
2874
- * paragraph in document order.
2875
- * - Each new <w:p> gets a copy of the anchor paragraph's <w:pPr> (so list
2876
- * numbering / indentation are preserved) unless the line itself starts
2877
- * with a markdown heading or list marker, which overrides the style.
2878
- * - Each new <w:p> carries a tracked paragraph-break marker
2879
- * (<w:pPr><w:rPr><w:ins/></w:rPr></w:pPr>) so Word natively tracks the
2880
- * paragraph break.
2881
- * - Each new <w:p>'s content is wrapped in a <w:ins>, with inline bold/
2882
- * italic markdown parsed via _parse_inline_markdown.
2883
- *
2884
- * The first line:
2885
- * - If it carries a heading / list marker AND we have a paragraph anchor,
2886
- * we drop into "block mode": no inline <w:ins>; the first line itself
2887
- * becomes the first new <w:p>.
2888
- * - Otherwise we emit a single inline <w:ins> for the first line (current
2889
- * behaviour) and treat the remaining lines as block extensions.
2890
- *
2891
- * Does NOT attach comments; callers handle that.
2892
- */
2996
+ * Inserts `text` as one or more tracked paragraphs anchored relative to
2997
+ * either an existing run or a paragraph. Returns:
2998
+ * { first_node, last_p, last_ins, used_block_mode }
2999
+ * where:
3000
+ * - first_node: the first <w:ins> (for inline mode) OR the first new <w:p>
3001
+ * (for block mode). The caller uses this for splicing into the DOM and
3002
+ * for anchoring comments.
3003
+ * - last_p: the last new <w:p> created, if any. null when entirely inline.
3004
+ * - last_ins: the last <w:ins> created (inside the last new <w:p>, or the
3005
+ * sole inline ins). Used as the comment's end anchor.
3006
+ * - used_block_mode: true when the first line carried a heading/list style
3007
+ * marker and we created a new paragraph for it (rather than inlining it).
3008
+ *
3009
+ * Multi-paragraph rules (only when text contains '\n'):
3010
+ * - Each additional line becomes a new <w:p>, inserted after the anchor
3011
+ * paragraph in document order.
3012
+ * - Each new <w:p> gets a copy of the anchor paragraph's <w:pPr> (so list
3013
+ * numbering / indentation are preserved) unless the line itself starts
3014
+ * with a markdown heading or list marker, which overrides the style.
3015
+ * - Each new <w:p> carries a tracked paragraph-break marker
3016
+ * (<w:pPr><w:rPr><w:ins/></w:rPr></w:pPr>) so Word natively tracks the
3017
+ * paragraph break.
3018
+ * - Each new <w:p>'s content is wrapped in a <w:ins>, with inline bold/
3019
+ * italic markdown parsed via _parse_inline_markdown.
3020
+ *
3021
+ * The first line:
3022
+ * - If it carries a heading / list marker AND we have a paragraph anchor,
3023
+ * we drop into "block mode": no inline <w:ins>; the first line itself
3024
+ * becomes the first new <w:p>.
3025
+ * - Otherwise we emit a single inline <w:ins> for the first line (current
3026
+ * behaviour) and treat the remaining lines as block extensions.
3027
+ *
3028
+ * Does NOT attach comments; callers handle that.
3029
+ */
2893
3030
  _track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id) {
2894
3031
  if (!text) {
2895
3032
  return {
@@ -3029,7 +3166,15 @@ var RedlineEngine = class {
3029
3166
  const anchor_rPr = findChild(anchor_run._element, "w:rPr");
3030
3167
  if (anchor_rPr) {
3031
3168
  const clone = anchor_rPr.cloneNode(true);
3032
- for (const tag of ["w:vanish", "w:strike", "w:dstrike"]) {
3169
+ for (const tag of [
3170
+ "w:vanish",
3171
+ "w:strike",
3172
+ "w:dstrike",
3173
+ "w:i",
3174
+ "w:iCs",
3175
+ "w:b",
3176
+ "w:bCs"
3177
+ ]) {
3033
3178
  const found = findChild(clone, tag);
3034
3179
  if (found) clone.removeChild(found);
3035
3180
  }
@@ -3303,6 +3448,16 @@ var RedlineEngine = class {
3303
3448
  matches = this.clean_mapper.find_all_match_indices(edit.target_text);
3304
3449
  if (matches.length > 0) activeText = this.clean_mapper.full_text;
3305
3450
  }
3451
+ if (activeText === this.mapper.full_text && matches.length > 1) {
3452
+ const liveMatches = matches.filter(([start, length]) => {
3453
+ const realSpans = this.mapper.spans.filter(
3454
+ (s) => s.run !== null && s.end > start && s.start < start + length
3455
+ );
3456
+ if (realSpans.length === 0) return true;
3457
+ return realSpans.some((s) => !s.del_id);
3458
+ });
3459
+ if (liveMatches.length > 0) matches = liveMatches;
3460
+ }
3306
3461
  if (matches.length === 0) {
3307
3462
  errors.push(
3308
3463
  `- Edit ${i + 1} Failed: Target text not found in document:
@@ -3322,6 +3477,34 @@ var RedlineEngine = class {
3322
3477
  )
3323
3478
  );
3324
3479
  }
3480
+ if (matches.length === 1) {
3481
+ const [m_start, m_len] = matches[0];
3482
+ const matched = activeText.substring(m_start, m_start + m_len);
3483
+ const [pfx, sfx] = trim_common_context(matched, edit.new_text || "");
3484
+ const t_end = matched.length - sfx;
3485
+ const final_target = matched.substring(pfx, t_end);
3486
+ const final_new = (edit.new_text || "").substring(
3487
+ pfx,
3488
+ (edit.new_text || "").length - sfx
3489
+ );
3490
+ if (final_target.includes("\n\n")) {
3491
+ if (final_new.includes("\n\n")) {
3492
+ const parts = matched.split("\n\n");
3493
+ if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3494
+ errors.push(
3495
+ `- 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.`
3496
+ );
3497
+ }
3498
+ } else {
3499
+ const parts = final_target.split("\n\n");
3500
+ if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3501
+ errors.push(
3502
+ `- 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.`
3503
+ );
3504
+ }
3505
+ }
3506
+ }
3507
+ }
3325
3508
  for (const [start, length] of matches) {
3326
3509
  const spans = this.mapper.spans.filter(
3327
3510
  (s) => s.end > start && s.start < start + length
@@ -3385,7 +3568,33 @@ var RedlineEngine = class {
3385
3568
  }
3386
3569
  return errors;
3387
3570
  }
3388
- process_batch(changes) {
3571
+ process_batch(changes, dry_run = false) {
3572
+ if (dry_run) {
3573
+ const baselines = /* @__PURE__ */ new Map();
3574
+ for (const part of this.doc.pkg.parts) {
3575
+ if (part._element) {
3576
+ baselines.set(part, part._element.cloneNode(true));
3577
+ }
3578
+ }
3579
+ try {
3580
+ return this._process_batch_internal(changes, true);
3581
+ } finally {
3582
+ for (const [part, originalEl] of baselines.entries()) {
3583
+ const doc = part._element.ownerDocument;
3584
+ if (doc && doc.documentElement) {
3585
+ doc.replaceChild(originalEl, doc.documentElement);
3586
+ }
3587
+ part._element = originalEl;
3588
+ }
3589
+ this.mapper = new DocumentMapper(this.doc);
3590
+ this.comments_manager = new CommentsManager(this.doc);
3591
+ this.clean_mapper = null;
3592
+ }
3593
+ } else {
3594
+ return this._process_batch_internal(changes, false);
3595
+ }
3596
+ }
3597
+ _process_batch_internal(changes, dry_run_mode = false) {
3389
3598
  this.skipped_details = [];
3390
3599
  const actions = changes.filter(
3391
3600
  (c) => ["accept", "reject", "reply"].includes(c.type)
@@ -3393,38 +3602,133 @@ var RedlineEngine = class {
3393
3602
  const edits = changes.filter(
3394
3603
  (c) => !["accept", "reject", "reply"].includes(c.type)
3395
3604
  );
3396
- const all_errors = [];
3397
- if (actions.length > 0) {
3398
- all_errors.push(...this.validate_review_actions(actions));
3399
- }
3400
- if (edits.length > 0) {
3401
- all_errors.push(...this.validate_edits(edits));
3402
- }
3403
- if (all_errors.length > 0) {
3404
- throw new BatchValidationError(all_errors);
3605
+ if (!dry_run_mode) {
3606
+ const all_errors = [];
3607
+ if (actions.length > 0) {
3608
+ all_errors.push(...this.validate_review_actions(actions));
3609
+ }
3610
+ if (edits.length > 0) {
3611
+ all_errors.push(...this.validate_edits(edits));
3612
+ }
3613
+ if (all_errors.length > 0) {
3614
+ throw new BatchValidationError(all_errors);
3615
+ }
3616
+ } else {
3617
+ if (actions.length > 0) {
3618
+ const action_errors = this.validate_review_actions(actions);
3619
+ if (action_errors.length > 0) {
3620
+ throw new BatchValidationError(action_errors);
3621
+ }
3622
+ }
3405
3623
  }
3406
- let applied_actions = 0, skipped_actions = 0;
3624
+ let applied_actions = 0;
3625
+ let skipped_actions = 0;
3407
3626
  if (actions.length > 0) {
3408
3627
  const res = this.apply_review_actions(actions);
3409
3628
  applied_actions = res[0];
3410
3629
  skipped_actions = res[1];
3630
+ if (skipped_actions > 0) {
3631
+ throw new BatchValidationError(this.skipped_details);
3632
+ }
3411
3633
  if (applied_actions > 0) {
3412
3634
  this.mapper["_build_map"]();
3413
3635
  if (this.clean_mapper) this.clean_mapper["_build_map"]();
3414
3636
  }
3415
3637
  }
3416
- let applied_edits = 0, skipped_edits = 0;
3638
+ const edits_reports = [];
3639
+ let applied_edits = 0;
3640
+ let skipped_edits = 0;
3417
3641
  if (edits.length > 0) {
3418
- const res = this.apply_edits(edits);
3419
- applied_edits = res[0];
3420
- skipped_edits = res[1];
3642
+ if (dry_run_mode) {
3643
+ for (const edit of edits) {
3644
+ const single_errors = this.validate_edits([edit]);
3645
+ const warning = this._check_punctuation_warning(
3646
+ edit.target_text || ""
3647
+ );
3648
+ if (single_errors.length > 0) {
3649
+ skipped_edits++;
3650
+ edits_reports.push({
3651
+ status: "failed",
3652
+ target_text: edit.target_text || "",
3653
+ new_text: edit.new_text || "",
3654
+ warning,
3655
+ error: single_errors[0],
3656
+ critic_markup: null,
3657
+ clean_text: null
3658
+ });
3659
+ continue;
3660
+ }
3661
+ const res = this.apply_edits([edit]);
3662
+ const applied = res[0];
3663
+ if (applied > 0) {
3664
+ applied_edits++;
3665
+ const previews = this._build_edit_context_previews(edit);
3666
+ edits_reports.push({
3667
+ status: "applied",
3668
+ target_text: edit.target_text || "",
3669
+ new_text: edit.new_text || "",
3670
+ warning,
3671
+ error: null,
3672
+ critic_markup: previews[0],
3673
+ clean_text: previews[1]
3674
+ });
3675
+ } else {
3676
+ skipped_edits++;
3677
+ const error_msg = this.skipped_details.length > 0 ? this.skipped_details[this.skipped_details.length - 1] : "Failed to apply edit";
3678
+ edits_reports.push({
3679
+ status: "failed",
3680
+ target_text: edit.target_text || "",
3681
+ new_text: edit.new_text || "",
3682
+ warning,
3683
+ error: error_msg,
3684
+ critic_markup: null,
3685
+ clean_text: null
3686
+ });
3687
+ }
3688
+ }
3689
+ } else {
3690
+ const errors = this.validate_edits(edits);
3691
+ if (errors.length > 0) {
3692
+ throw new BatchValidationError(errors);
3693
+ }
3694
+ const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
3695
+ const res = this.apply_edits(cloned_edits);
3696
+ applied_edits = res[0];
3697
+ skipped_edits = res[1];
3698
+ for (const edit of cloned_edits) {
3699
+ const success = edit._applied_status || false;
3700
+ const error_msg = edit._error_msg || null;
3701
+ const warning = this._check_punctuation_warning(
3702
+ edit.target_text || ""
3703
+ );
3704
+ let critic_markup = null;
3705
+ let clean_text = null;
3706
+ if (success) {
3707
+ const previews = this._build_edit_context_previews(edit);
3708
+ critic_markup = previews[0];
3709
+ clean_text = previews[1];
3710
+ }
3711
+ edits_reports.push({
3712
+ status: success ? "applied" : "failed",
3713
+ target_text: edit.target_text || "",
3714
+ new_text: edit.new_text || "",
3715
+ warning,
3716
+ error: error_msg,
3717
+ critic_markup,
3718
+ clean_text
3719
+ });
3720
+ }
3721
+ }
3421
3722
  }
3422
3723
  return {
3423
3724
  actions_applied: applied_actions,
3424
3725
  actions_skipped: skipped_actions,
3425
3726
  edits_applied: applied_edits,
3426
3727
  edits_skipped: skipped_edits,
3427
- skipped_details: this.skipped_details
3728
+ skipped_details: this.skipped_details,
3729
+ edits: edits_reports,
3730
+ engine: "node",
3731
+ version: "1.10.0"
3428
3732
  };
3429
3733
  }
3430
3734
  apply_edits(edits) {
@@ -3432,50 +3736,90 @@ var RedlineEngine = class {
3432
3736
  let skipped = 0;
3433
3737
  const resolved_edits = [];
3434
3738
  for (const edit of edits) {
3435
- if (edit._match_start_index !== void 0 && edit._match_start_index !== null) {
3739
+ edit._applied_status = false;
3740
+ edit._error_msg = null;
3741
+ }
3742
+ for (const edit of edits) {
3743
+ if (edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null) {
3744
+ resolved_edits.push([edit, edit.new_text || null]);
3745
+ } else if (edit._match_start_index !== void 0 && edit._match_start_index !== null) {
3746
+ edit._resolved_start_idx = edit._match_start_index;
3436
3747
  resolved_edits.push([edit, edit.new_text || null]);
3437
3748
  } else if (edit.type === "insert_row" || edit.type === "delete_row") {
3438
- const [idx] = this.mapper.find_match_index(edit.target_text);
3439
- if (idx !== -1) {
3440
- edit._match_start_index = idx;
3749
+ let matches = this.mapper.find_all_match_indices(edit.target_text);
3750
+ if (matches.length === 0) {
3751
+ if (!this.clean_mapper) {
3752
+ this.clean_mapper = new DocumentMapper(this.doc, true);
3753
+ }
3754
+ matches = this.clean_mapper.find_all_match_indices(edit.target_text);
3755
+ }
3756
+ if (matches.length > 0) {
3757
+ edit._resolved_start_idx = matches[0][0];
3441
3758
  resolved_edits.push([edit, null]);
3442
3759
  } else {
3443
3760
  skipped++;
3444
- this.skipped_details.push(
3445
- `- Failed to locate row target: '${(edit.target_text || "").substring(0, 40)}...'`
3446
- );
3761
+ edit._applied_status = false;
3762
+ const target_snippet = (edit.target_text || "").trim().substring(0, 40);
3763
+ const msg = `- Failed to locate row target: '${target_snippet}...'`;
3764
+ this.skipped_details.push(msg);
3765
+ edit._error_msg = msg;
3447
3766
  }
3448
3767
  } else {
3449
3768
  const resolved = this._pre_resolve_heuristic_edit(edit);
3450
3769
  if (resolved) {
3451
3770
  if (Array.isArray(resolved)) {
3452
- for (const r of resolved) resolved_edits.push([r, r.new_text]);
3771
+ for (const r of resolved) {
3772
+ r._resolved_start_idx = r._match_start_index;
3773
+ r._parent_edit_ref = edit;
3774
+ if (edit._resolved_start_idx === void 0 || edit._resolved_start_idx === null) {
3775
+ edit._resolved_start_idx = r._resolved_start_idx;
3776
+ }
3777
+ if (!edit._resolved_proxy_edit) {
3778
+ edit._resolved_proxy_edit = r;
3779
+ }
3780
+ resolved_edits.push([r, r.new_text]);
3781
+ }
3453
3782
  } else {
3783
+ resolved._resolved_start_idx = resolved._match_start_index;
3784
+ resolved._parent_edit_ref = edit;
3785
+ edit._resolved_start_idx = resolved._resolved_start_idx;
3786
+ edit._resolved_proxy_edit = resolved;
3454
3787
  resolved_edits.push([resolved, resolved.new_text]);
3455
3788
  }
3456
3789
  } else {
3457
3790
  skipped++;
3458
- this.skipped_details.push(
3459
- `- Failed to apply edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3460
- );
3791
+ edit._applied_status = false;
3792
+ const display_text = edit.target_text || "insertion";
3793
+ const target_snippet = display_text.trim().substring(0, 40);
3794
+ const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
3795
+ this.skipped_details.push(msg);
3796
+ edit._error_msg = msg;
3461
3797
  }
3462
3798
  }
3463
3799
  }
3464
3800
  resolved_edits.sort(
3465
- (a, b) => (b[0]._match_start_index || 0) - (a[0]._match_start_index || 0)
3801
+ (a, b) => (b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0)
3466
3802
  );
3467
3803
  const occupied_ranges = [];
3468
3804
  for (const [edit, orig_new] of resolved_edits) {
3469
- const start = edit._match_start_index || 0;
3805
+ const start = edit._resolved_start_idx || 0;
3470
3806
  const end = start + (edit.target_text ? edit.target_text.length : 0);
3471
3807
  const overlaps = occupied_ranges.some(
3472
3808
  ([occ_start, occ_end]) => start < occ_end && end > occ_start
3473
3809
  );
3474
3810
  if (overlaps) {
3475
3811
  skipped++;
3476
- this.skipped_details.push(
3477
- `- Skipped overlapping edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3478
- );
3812
+ const display_text = edit.target_text || "insertion";
3813
+ const target_snippet = display_text.trim().substring(0, 40);
3814
+ const msg = `- Skipped overlapping edit targeting: '${target_snippet}...'`;
3815
+ this.skipped_details.push(msg);
3816
+ edit._applied_status = false;
3817
+ edit._error_msg = msg;
3818
+ const parent = edit._parent_edit_ref;
3819
+ if (parent) {
3820
+ parent._applied_status = false;
3821
+ parent._error_msg = msg;
3822
+ }
3479
3823
  continue;
3480
3824
  }
3481
3825
  let success = false;
@@ -3487,11 +3831,26 @@ var RedlineEngine = class {
3487
3831
  if (success) {
3488
3832
  applied++;
3489
3833
  occupied_ranges.push([start, end]);
3834
+ edit._applied_status = true;
3835
+ const parent = edit._parent_edit_ref;
3836
+ if (parent) {
3837
+ parent._applied_status = true;
3838
+ }
3490
3839
  } else {
3491
3840
  skipped++;
3492
- this.skipped_details.push(
3493
- `- Failed to apply edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3494
- );
3841
+ const display_text = edit.target_text || "insertion";
3842
+ const target_snippet = display_text.trim().substring(0, 40);
3843
+ const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
3844
+ this.skipped_details.push(msg);
3845
+ edit._applied_status = false;
3846
+ edit._error_msg = msg;
3847
+ const parent = edit._parent_edit_ref;
3848
+ if (parent) {
3849
+ if (!parent._applied_status) {
3850
+ parent._applied_status = false;
3851
+ parent._error_msg = msg;
3852
+ }
3853
+ }
3495
3854
  }
3496
3855
  }
3497
3856
  return [applied, skipped];
@@ -3584,7 +3943,7 @@ var RedlineEngine = class {
3584
3943
  return [applied, skipped];
3585
3944
  }
3586
3945
  _apply_table_edit(edit, rebuild_map) {
3587
- const start_idx = edit._match_start_index || 0;
3946
+ const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
3588
3947
  const [anchor_run, anchor_para] = this.mapper.get_insertion_anchor(
3589
3948
  start_idx,
3590
3949
  rebuild_map
@@ -3628,9 +3987,31 @@ var RedlineEngine = class {
3628
3987
  }
3629
3988
  return false;
3630
3989
  }
3990
+ /**
3991
+ * Returns the first match of `target_text` in the raw mapper that is NOT
3992
+ * entirely contained within a tracked deletion (<w:del>). Tracked-deleted
3993
+ * copies are not live, editable text, so an edit must resolve to a live
3994
+ * occurrence even when a dead copy appears earlier in the document
3995
+ * (BUG-23-5). Falls back to the plain first match when no live copy is
3996
+ * found (e.g. fuzzy/normalized matches the span filter cannot align).
3997
+ */
3998
+ _first_live_match(target_text) {
3999
+ const all = this.mapper.find_all_match_indices(target_text);
4000
+ if (all.length <= 1) {
4001
+ return this.mapper.find_match_index(target_text);
4002
+ }
4003
+ for (const [start, length] of all) {
4004
+ const realSpans = this.mapper.spans.filter(
4005
+ (s) => s.run !== null && s.end > start && s.start < start + length
4006
+ );
4007
+ if (realSpans.length === 0) return [start, length];
4008
+ if (realSpans.some((s) => !s.del_id)) return [start, length];
4009
+ }
4010
+ return this.mapper.find_match_index(target_text);
4011
+ }
3631
4012
  _pre_resolve_heuristic_edit(edit) {
3632
4013
  if (!edit.target_text) return null;
3633
- let [start_idx, match_len] = this.mapper.find_match_index(edit.target_text);
4014
+ let [start_idx, match_len] = this._first_live_match(edit.target_text);
3634
4015
  let use_clean_map = false;
3635
4016
  if (start_idx === -1) {
3636
4017
  if (!this.clean_mapper)
@@ -3694,7 +4075,7 @@ var RedlineEngine = class {
3694
4075
  _apply_single_edit_indexed(edit, orig_new, rebuild_map) {
3695
4076
  let op = edit._internal_op;
3696
4077
  const active_mapper = edit._active_mapper_ref || this.mapper;
3697
- const start_idx = edit._match_start_index || 0;
4078
+ const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
3698
4079
  const length = edit.target_text ? edit.target_text.length : 0;
3699
4080
  const del_id = ["DELETION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
3700
4081
  const ins_id = ["INSERTION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
@@ -3743,6 +4124,76 @@ var RedlineEngine = class {
3743
4124
  rebuild_map
3744
4125
  );
3745
4126
  if (!anchor_run && !anchor_para) return false;
4127
+ const _bug233_new = edit.new_text || "";
4128
+ const _bug233_trailing_break = /\n\s*$/.test(_bug233_new);
4129
+ let _bug233_target_para = null;
4130
+ {
4131
+ const startingSpans = active_mapper.spans.filter(
4132
+ (s) => s.paragraph !== null && s.start === start_idx
4133
+ );
4134
+ if (startingSpans.length > 0 && startingSpans[0].paragraph) {
4135
+ _bug233_target_para = startingSpans[0].paragraph._element;
4136
+ }
4137
+ }
4138
+ if (_bug233_trailing_break && _bug233_target_para && _bug233_target_para.parentNode) {
4139
+ const body = _bug233_target_para.parentNode;
4140
+ const xmlDoc = this.doc.part._element.ownerDocument;
4141
+ const lines = _bug233_new.split(/[\r\n]+/).filter((l) => l !== "");
4142
+ let firstNew = null;
4143
+ let lastNew = null;
4144
+ let lastIns = null;
4145
+ for (const raw_line of lines) {
4146
+ const [clean_text, style_name] = this._parse_markdown_style(raw_line);
4147
+ const new_p = xmlDoc.createElement("w:p");
4148
+ if (style_name) {
4149
+ this._set_paragraph_style(new_p, style_name);
4150
+ } else {
4151
+ const existing_pPr = findChild(_bug233_target_para, "w:pPr");
4152
+ if (existing_pPr) new_p.appendChild(existing_pPr.cloneNode(true));
4153
+ }
4154
+ let pPr = findChild(new_p, "w:pPr");
4155
+ if (!pPr) {
4156
+ pPr = xmlDoc.createElement("w:pPr");
4157
+ new_p.insertBefore(pPr, new_p.firstChild);
4158
+ }
4159
+ let rPr = findChild(pPr, "w:rPr");
4160
+ if (!rPr) {
4161
+ rPr = xmlDoc.createElement("w:rPr");
4162
+ pPr.appendChild(rPr);
4163
+ }
4164
+ rPr.appendChild(this._create_track_change_tag("w:ins", "", ins_id));
4165
+ const content_ins = this._build_tracked_ins_for_line(
4166
+ clean_text,
4167
+ anchor_run,
4168
+ ins_id,
4169
+ xmlDoc
4170
+ );
4171
+ if (content_ins) new_p.appendChild(content_ins);
4172
+ body.insertBefore(new_p, _bug233_target_para);
4173
+ if (!firstNew) firstNew = new_p;
4174
+ lastNew = new_p;
4175
+ lastIns = content_ins;
4176
+ }
4177
+ if (firstNew) {
4178
+ if (edit.comment && lastNew && lastIns) {
4179
+ const ascend = (el, p) => {
4180
+ let cur = el;
4181
+ while (cur.parentNode && cur.parentNode !== p)
4182
+ cur = cur.parentNode;
4183
+ return cur;
4184
+ };
4185
+ const startIns = findAllDescendants(firstNew, "w:ins")[0] || firstNew;
4186
+ this._attach_comment_spanning(
4187
+ firstNew,
4188
+ ascend(startIns, firstNew),
4189
+ lastNew,
4190
+ ascend(lastIns, lastNew),
4191
+ edit.comment
4192
+ );
4193
+ }
4194
+ return true;
4195
+ }
4196
+ }
3746
4197
  const result = this._track_insert_multiline(
3747
4198
  edit.new_text || "",
3748
4199
  anchor_run,
@@ -3800,7 +4251,10 @@ var RedlineEngine = class {
3800
4251
  if (result.first_node.tagName === "w:p") {
3801
4252
  first_anchor_target = findAllDescendants(result.first_node, "w:ins")[0] || result.first_node;
3802
4253
  }
3803
- const anchor = ascend_to_paragraph_child(first_anchor_target, host_p);
4254
+ const anchor = ascend_to_paragraph_child(
4255
+ first_anchor_target,
4256
+ host_p
4257
+ );
3804
4258
  this._attach_comment(host_p, anchor, anchor, edit.comment);
3805
4259
  }
3806
4260
  }
@@ -3812,7 +4266,10 @@ var RedlineEngine = class {
3812
4266
  length,
3813
4267
  rebuild_map
3814
4268
  );
3815
- const virtual_spans = active_mapper.get_virtual_spans_in_range(start_idx, length);
4269
+ const virtual_spans = active_mapper.get_virtual_spans_in_range(
4270
+ start_idx,
4271
+ length
4272
+ );
3816
4273
  if (target_runs.length === 0 && virtual_spans.length === 0) return false;
3817
4274
  const affected_ps = /* @__PURE__ */ new Set();
3818
4275
  for (const run of target_runs) {
@@ -3895,7 +4352,10 @@ var RedlineEngine = class {
3895
4352
  let pPr = findChild(p1_element, "w:pPr");
3896
4353
  if (!pPr) {
3897
4354
  pPr = p1_element.ownerDocument.createElement("w:pPr");
3898
- p1_element.insertBefore(pPr, p1_element.firstChild);
4355
+ p1_element.insertBefore(
4356
+ pPr,
4357
+ p1_element.firstChild
4358
+ );
3899
4359
  }
3900
4360
  let rPr = findChild(pPr, "w:rPr");
3901
4361
  if (!rPr) {
@@ -5452,15 +5912,88 @@ function remove_all_comments(doc) {
5452
5912
  lines.push(` ${status} "${_truncate(info.text || "", 60)}" (${info.author || "Unknown"})`);
5453
5913
  cm.deleteComment(cId);
5454
5914
  }
5455
- for (const tag of ["w:commentRangeStart", "w:commentRangeEnd", "w:commentReference"]) {
5915
+ for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
5456
5916
  for (const el of findAllDescendants(doc.element, tag)) {
5457
5917
  el.parentNode?.removeChild(el);
5458
5918
  }
5459
5919
  }
5920
+ const refs = findAllDescendants(doc.element, "w:commentReference");
5921
+ for (const ref of refs) {
5922
+ const parent = ref.parentNode;
5923
+ if (parent) {
5924
+ if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
5925
+ const nonRprChildren = Array.from(parent.childNodes).filter(
5926
+ (c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
5927
+ );
5928
+ if (nonRprChildren.length <= 1) {
5929
+ parent.parentNode?.removeChild(parent);
5930
+ } else {
5931
+ parent.removeChild(ref);
5932
+ }
5933
+ } else {
5934
+ parent.removeChild(ref);
5935
+ }
5936
+ }
5937
+ }
5460
5938
  const resolvedCount = Object.values(data).filter((c) => c.resolved).length;
5461
5939
  const openCount = Object.values(data).filter((c) => !c.resolved).length;
5462
5940
  return [`Comments removed: ${keys.length} (${resolvedCount} resolved, ${openCount} open)`].concat(lines);
5463
5941
  }
5942
+ function eject_comment_parts(doc) {
5943
+ const pkg = doc.pkg;
5944
+ const comment_partnames = /* @__PURE__ */ new Set();
5945
+ for (const part of pkg.parts) {
5946
+ if (part.partname.toLowerCase().includes("comments")) {
5947
+ comment_partnames.add(part.partname);
5948
+ const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
5949
+ const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
5950
+ comment_partnames.add(withSlash);
5951
+ comment_partnames.add(withoutSlash);
5952
+ }
5953
+ }
5954
+ if (comment_partnames.size === 0) return;
5955
+ for (const part of pkg.parts) {
5956
+ if (part.partname.endsWith(".rels")) {
5957
+ const rels = findAllDescendants(part._element, "Relationship");
5958
+ const toRemove = [];
5959
+ for (const rel of rels) {
5960
+ const target = rel.getAttribute("Target") || "";
5961
+ if (target.toLowerCase().includes("comments")) {
5962
+ toRemove.push(rel);
5963
+ const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
5964
+ const sourcePart = pkg.getPartByPath(sourcePath);
5965
+ if (sourcePart) {
5966
+ const relId = rel.getAttribute("Id");
5967
+ if (relId) sourcePart.rels.delete(relId);
5968
+ }
5969
+ }
5970
+ }
5971
+ for (const relEl of toRemove) {
5972
+ relEl.parentNode?.removeChild(relEl);
5973
+ }
5974
+ }
5975
+ }
5976
+ const ctPart = pkg.getPartByPath("[Content_Types].xml");
5977
+ if (ctPart) {
5978
+ const overrides = findAllDescendants(ctPart._element, "Override");
5979
+ const toRemove = [];
5980
+ for (const override of overrides) {
5981
+ const partName = override.getAttribute("PartName") || "";
5982
+ if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
5983
+ toRemove.push(override);
5984
+ }
5985
+ }
5986
+ for (const overrideEl of toRemove) {
5987
+ overrideEl.parentNode?.removeChild(overrideEl);
5988
+ }
5989
+ }
5990
+ pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
5991
+ for (const key of Object.keys(pkg.unzipped)) {
5992
+ if (key.toLowerCase().includes("comments")) {
5993
+ delete pkg.unzipped[key];
5994
+ }
5995
+ }
5996
+ }
5464
5997
  function replace_comment_authors(doc, newAuthor) {
5465
5998
  const cm = new CommentsManager(doc);
5466
5999
  if (!cm.commentsPart) return [];
@@ -5649,6 +6182,7 @@ async function finalize_document(doc, options) {
5649
6182
  const commentsSummary = get_comments_summary(doc);
5650
6183
  report.comments_removed = commentsSummary.total;
5651
6184
  report.add_transform_lines(remove_all_comments(doc));
6185
+ eject_comment_parts(doc);
5652
6186
  } else if (options.sanitize_mode === "keep-markup") {
5653
6187
  const counts = count_tracked_changes(doc);
5654
6188
  report.tracked_changes_found = counts[0] + counts[1] + counts[2];