@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.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,36 @@ 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(start_idx + length, start_idx + length + 30);
2724
+ const critic_markup = `${context_before}{--${target_text}--}{++${new_text}++}${context_after}`;
2725
+ let clean_text = critic_markup;
2726
+ clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
2727
+ clean_text = clean_text.replace(/\{--.*?--\}/gs, "");
2728
+ clean_text = clean_text.replace(/\{\+\+(.*?)\+\+\}/gs, "$1");
2729
+ return [critic_markup, clean_text];
2730
+ }
2655
2731
  _scan_existing_ids() {
2656
2732
  let maxId = 0;
2657
2733
  for (const tag of ["w:ins", "w:del"]) {
@@ -2744,28 +2820,84 @@ var RedlineEngine = class {
2744
2820
  }
2745
2821
  }
2746
2822
  }
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);
2823
+ for (const root_element of parts_to_process) {
2824
+ for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
2825
+ for (const el of findAllDescendants(root_element, tag)) {
2826
+ el.parentNode?.removeChild(el);
2827
+ }
2756
2828
  }
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);
2829
+ const refs = findAllDescendants(root_element, "w:commentReference");
2830
+ for (const ref of refs) {
2831
+ const parent = ref.parentNode;
2832
+ if (parent) {
2833
+ if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
2834
+ const nonRprChildren = Array.from(parent.childNodes).filter(
2835
+ (c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
2836
+ );
2837
+ if (nonRprChildren.length <= 1) {
2838
+ parent.parentNode?.removeChild(parent);
2839
+ } else {
2840
+ parent.removeChild(ref);
2841
+ }
2842
+ } else {
2843
+ parent.removeChild(ref);
2844
+ }
2845
+ }
2765
2846
  }
2766
2847
  }
2767
- for (const cid of comment_ids) {
2768
- this.comments_manager.deleteComment(cid);
2848
+ const pkg = this.doc.pkg;
2849
+ const comment_partnames = /* @__PURE__ */ new Set();
2850
+ for (const part of pkg.parts) {
2851
+ if (part.partname.toLowerCase().includes("comments")) {
2852
+ comment_partnames.add(part.partname);
2853
+ const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
2854
+ const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
2855
+ comment_partnames.add(withSlash);
2856
+ comment_partnames.add(withoutSlash);
2857
+ }
2858
+ }
2859
+ if (comment_partnames.size > 0) {
2860
+ for (const part of pkg.parts) {
2861
+ if (part.partname.endsWith(".rels")) {
2862
+ const rels = findAllDescendants(part._element, "Relationship");
2863
+ const toRemove = [];
2864
+ for (const rel of rels) {
2865
+ const target = rel.getAttribute("Target") || "";
2866
+ if (target.toLowerCase().includes("comments")) {
2867
+ toRemove.push(rel);
2868
+ const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
2869
+ const sourcePart = pkg.getPartByPath(sourcePath);
2870
+ if (sourcePart) {
2871
+ const relId = rel.getAttribute("Id");
2872
+ if (relId) sourcePart.rels.delete(relId);
2873
+ }
2874
+ }
2875
+ }
2876
+ for (const relEl of toRemove) {
2877
+ relEl.parentNode?.removeChild(relEl);
2878
+ }
2879
+ }
2880
+ }
2881
+ const ctPart = pkg.getPartByPath("[Content_Types].xml");
2882
+ if (ctPart) {
2883
+ const overrides = findAllDescendants(ctPart._element, "Override");
2884
+ const toRemove = [];
2885
+ for (const override of overrides) {
2886
+ const partName = override.getAttribute("PartName") || "";
2887
+ if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
2888
+ toRemove.push(override);
2889
+ }
2890
+ }
2891
+ for (const overrideEl of toRemove) {
2892
+ overrideEl.parentNode?.removeChild(overrideEl);
2893
+ }
2894
+ }
2895
+ pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
2896
+ for (const key of Object.keys(pkg.unzipped)) {
2897
+ if (key.toLowerCase().includes("comments")) {
2898
+ delete pkg.unzipped[key];
2899
+ }
2900
+ }
2769
2901
  }
2770
2902
  }
2771
2903
  _getNextId() {
@@ -2856,40 +2988,40 @@ var RedlineEngine = class {
2856
2988
  }
2857
2989
  }
2858
2990
  /**
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
- */
2991
+ * Inserts `text` as one or more tracked paragraphs anchored relative to
2992
+ * either an existing run or a paragraph. Returns:
2993
+ * { first_node, last_p, last_ins, used_block_mode }
2994
+ * where:
2995
+ * - first_node: the first <w:ins> (for inline mode) OR the first new <w:p>
2996
+ * (for block mode). The caller uses this for splicing into the DOM and
2997
+ * for anchoring comments.
2998
+ * - last_p: the last new <w:p> created, if any. null when entirely inline.
2999
+ * - last_ins: the last <w:ins> created (inside the last new <w:p>, or the
3000
+ * sole inline ins). Used as the comment's end anchor.
3001
+ * - used_block_mode: true when the first line carried a heading/list style
3002
+ * marker and we created a new paragraph for it (rather than inlining it).
3003
+ *
3004
+ * Multi-paragraph rules (only when text contains '\n'):
3005
+ * - Each additional line becomes a new <w:p>, inserted after the anchor
3006
+ * paragraph in document order.
3007
+ * - Each new <w:p> gets a copy of the anchor paragraph's <w:pPr> (so list
3008
+ * numbering / indentation are preserved) unless the line itself starts
3009
+ * with a markdown heading or list marker, which overrides the style.
3010
+ * - Each new <w:p> carries a tracked paragraph-break marker
3011
+ * (<w:pPr><w:rPr><w:ins/></w:rPr></w:pPr>) so Word natively tracks the
3012
+ * paragraph break.
3013
+ * - Each new <w:p>'s content is wrapped in a <w:ins>, with inline bold/
3014
+ * italic markdown parsed via _parse_inline_markdown.
3015
+ *
3016
+ * The first line:
3017
+ * - If it carries a heading / list marker AND we have a paragraph anchor,
3018
+ * we drop into "block mode": no inline <w:ins>; the first line itself
3019
+ * becomes the first new <w:p>.
3020
+ * - Otherwise we emit a single inline <w:ins> for the first line (current
3021
+ * behaviour) and treat the remaining lines as block extensions.
3022
+ *
3023
+ * Does NOT attach comments; callers handle that.
3024
+ */
2893
3025
  _track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id) {
2894
3026
  if (!text) {
2895
3027
  return {
@@ -3029,7 +3161,15 @@ var RedlineEngine = class {
3029
3161
  const anchor_rPr = findChild(anchor_run._element, "w:rPr");
3030
3162
  if (anchor_rPr) {
3031
3163
  const clone = anchor_rPr.cloneNode(true);
3032
- for (const tag of ["w:vanish", "w:strike", "w:dstrike"]) {
3164
+ for (const tag of [
3165
+ "w:vanish",
3166
+ "w:strike",
3167
+ "w:dstrike",
3168
+ "w:i",
3169
+ "w:iCs",
3170
+ "w:b",
3171
+ "w:bCs"
3172
+ ]) {
3033
3173
  const found = findChild(clone, tag);
3034
3174
  if (found) clone.removeChild(found);
3035
3175
  }
@@ -3303,6 +3443,16 @@ var RedlineEngine = class {
3303
3443
  matches = this.clean_mapper.find_all_match_indices(edit.target_text);
3304
3444
  if (matches.length > 0) activeText = this.clean_mapper.full_text;
3305
3445
  }
3446
+ if (activeText === this.mapper.full_text && matches.length > 1) {
3447
+ const liveMatches = matches.filter(([start, length]) => {
3448
+ const realSpans = this.mapper.spans.filter(
3449
+ (s) => s.run !== null && s.end > start && s.start < start + length
3450
+ );
3451
+ if (realSpans.length === 0) return true;
3452
+ return realSpans.some((s) => !s.del_id);
3453
+ });
3454
+ if (liveMatches.length > 0) matches = liveMatches;
3455
+ }
3306
3456
  if (matches.length === 0) {
3307
3457
  errors.push(
3308
3458
  `- Edit ${i + 1} Failed: Target text not found in document:
@@ -3322,6 +3472,31 @@ var RedlineEngine = class {
3322
3472
  )
3323
3473
  );
3324
3474
  }
3475
+ if (matches.length === 1) {
3476
+ const [m_start, m_len] = matches[0];
3477
+ const matched = activeText.substring(m_start, m_start + m_len);
3478
+ const [pfx, sfx] = trim_common_context(matched, edit.new_text || "");
3479
+ const t_end = matched.length - sfx;
3480
+ const final_target = matched.substring(pfx, t_end);
3481
+ const final_new = (edit.new_text || "").substring(pfx, (edit.new_text || "").length - sfx);
3482
+ if (final_target.includes("\n\n")) {
3483
+ if (final_new.includes("\n\n")) {
3484
+ const parts = matched.split("\n\n");
3485
+ if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3486
+ errors.push(
3487
+ `- 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.`
3488
+ );
3489
+ }
3490
+ } else {
3491
+ const parts = final_target.split("\n\n");
3492
+ if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3493
+ errors.push(
3494
+ `- 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.`
3495
+ );
3496
+ }
3497
+ }
3498
+ }
3499
+ }
3325
3500
  for (const [start, length] of matches) {
3326
3501
  const spans = this.mapper.spans.filter(
3327
3502
  (s) => s.end > start && s.start < start + length
@@ -3385,7 +3560,33 @@ var RedlineEngine = class {
3385
3560
  }
3386
3561
  return errors;
3387
3562
  }
3388
- process_batch(changes) {
3563
+ process_batch(changes, dry_run = false) {
3564
+ if (dry_run) {
3565
+ const baselines = /* @__PURE__ */ new Map();
3566
+ for (const part of this.doc.pkg.parts) {
3567
+ if (part._element) {
3568
+ baselines.set(part, part._element.cloneNode(true));
3569
+ }
3570
+ }
3571
+ try {
3572
+ return this._process_batch_internal(changes, true);
3573
+ } finally {
3574
+ for (const [part, originalEl] of baselines.entries()) {
3575
+ const doc = part._element.ownerDocument;
3576
+ if (doc && doc.documentElement) {
3577
+ doc.replaceChild(originalEl, doc.documentElement);
3578
+ }
3579
+ part._element = originalEl;
3580
+ }
3581
+ this.mapper = new DocumentMapper(this.doc);
3582
+ this.comments_manager = new CommentsManager(this.doc);
3583
+ this.clean_mapper = null;
3584
+ }
3585
+ } else {
3586
+ return this._process_batch_internal(changes, false);
3587
+ }
3588
+ }
3589
+ _process_batch_internal(changes, dry_run_mode = false) {
3389
3590
  this.skipped_details = [];
3390
3591
  const actions = changes.filter(
3391
3592
  (c) => ["accept", "reject", "reply"].includes(c.type)
@@ -3393,38 +3594,129 @@ var RedlineEngine = class {
3393
3594
  const edits = changes.filter(
3394
3595
  (c) => !["accept", "reject", "reply"].includes(c.type)
3395
3596
  );
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);
3597
+ if (!dry_run_mode) {
3598
+ const all_errors = [];
3599
+ if (actions.length > 0) {
3600
+ all_errors.push(...this.validate_review_actions(actions));
3601
+ }
3602
+ if (edits.length > 0) {
3603
+ all_errors.push(...this.validate_edits(edits));
3604
+ }
3605
+ if (all_errors.length > 0) {
3606
+ throw new BatchValidationError(all_errors);
3607
+ }
3608
+ } else {
3609
+ if (actions.length > 0) {
3610
+ const action_errors = this.validate_review_actions(actions);
3611
+ if (action_errors.length > 0) {
3612
+ throw new BatchValidationError(action_errors);
3613
+ }
3614
+ }
3405
3615
  }
3406
- let applied_actions = 0, skipped_actions = 0;
3616
+ let applied_actions = 0;
3617
+ let skipped_actions = 0;
3407
3618
  if (actions.length > 0) {
3408
3619
  const res = this.apply_review_actions(actions);
3409
3620
  applied_actions = res[0];
3410
3621
  skipped_actions = res[1];
3622
+ if (skipped_actions > 0) {
3623
+ throw new BatchValidationError(this.skipped_details);
3624
+ }
3411
3625
  if (applied_actions > 0) {
3412
3626
  this.mapper["_build_map"]();
3413
3627
  if (this.clean_mapper) this.clean_mapper["_build_map"]();
3414
3628
  }
3415
3629
  }
3416
- let applied_edits = 0, skipped_edits = 0;
3630
+ const edits_reports = [];
3631
+ let applied_edits = 0;
3632
+ let skipped_edits = 0;
3417
3633
  if (edits.length > 0) {
3418
- const res = this.apply_edits(edits);
3419
- applied_edits = res[0];
3420
- skipped_edits = res[1];
3634
+ if (dry_run_mode) {
3635
+ for (const edit of edits) {
3636
+ const single_errors = this.validate_edits([edit]);
3637
+ const warning = this._check_punctuation_warning(edit.target_text || "");
3638
+ if (single_errors.length > 0) {
3639
+ skipped_edits++;
3640
+ edits_reports.push({
3641
+ status: "failed",
3642
+ target_text: edit.target_text || "",
3643
+ new_text: edit.new_text || "",
3644
+ warning,
3645
+ error: single_errors[0],
3646
+ critic_markup: null,
3647
+ clean_text: null
3648
+ });
3649
+ continue;
3650
+ }
3651
+ const res = this.apply_edits([edit]);
3652
+ const applied = res[0];
3653
+ if (applied > 0) {
3654
+ applied_edits++;
3655
+ const previews = this._build_edit_context_previews(edit);
3656
+ edits_reports.push({
3657
+ status: "applied",
3658
+ target_text: edit.target_text || "",
3659
+ new_text: edit.new_text || "",
3660
+ warning,
3661
+ error: null,
3662
+ critic_markup: previews[0],
3663
+ clean_text: previews[1]
3664
+ });
3665
+ } else {
3666
+ skipped_edits++;
3667
+ const error_msg = this.skipped_details.length > 0 ? this.skipped_details[this.skipped_details.length - 1] : "Failed to apply edit";
3668
+ edits_reports.push({
3669
+ status: "failed",
3670
+ target_text: edit.target_text || "",
3671
+ new_text: edit.new_text || "",
3672
+ warning,
3673
+ error: error_msg,
3674
+ critic_markup: null,
3675
+ clean_text: null
3676
+ });
3677
+ }
3678
+ }
3679
+ } else {
3680
+ const errors = this.validate_edits(edits);
3681
+ if (errors.length > 0) {
3682
+ throw new BatchValidationError(errors);
3683
+ }
3684
+ const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
3685
+ const res = this.apply_edits(cloned_edits);
3686
+ applied_edits = res[0];
3687
+ skipped_edits = res[1];
3688
+ for (const edit of cloned_edits) {
3689
+ const success = edit._applied_status || false;
3690
+ const error_msg = edit._error_msg || null;
3691
+ const warning = this._check_punctuation_warning(edit.target_text || "");
3692
+ let critic_markup = null;
3693
+ let clean_text = null;
3694
+ if (success) {
3695
+ const previews = this._build_edit_context_previews(edit);
3696
+ critic_markup = previews[0];
3697
+ clean_text = previews[1];
3698
+ }
3699
+ edits_reports.push({
3700
+ status: success ? "applied" : "failed",
3701
+ target_text: edit.target_text || "",
3702
+ new_text: edit.new_text || "",
3703
+ warning,
3704
+ error: error_msg,
3705
+ critic_markup,
3706
+ clean_text
3707
+ });
3708
+ }
3709
+ }
3421
3710
  }
3422
3711
  return {
3423
3712
  actions_applied: applied_actions,
3424
3713
  actions_skipped: skipped_actions,
3425
3714
  edits_applied: applied_edits,
3426
3715
  edits_skipped: skipped_edits,
3427
- skipped_details: this.skipped_details
3716
+ skipped_details: this.skipped_details,
3717
+ edits: edits_reports,
3718
+ engine: "node",
3719
+ version: "1.9.0"
3428
3720
  };
3429
3721
  }
3430
3722
  apply_edits(edits) {
@@ -3432,50 +3724,90 @@ var RedlineEngine = class {
3432
3724
  let skipped = 0;
3433
3725
  const resolved_edits = [];
3434
3726
  for (const edit of edits) {
3435
- if (edit._match_start_index !== void 0 && edit._match_start_index !== null) {
3727
+ edit._applied_status = false;
3728
+ edit._error_msg = null;
3729
+ }
3730
+ for (const edit of edits) {
3731
+ if (edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null) {
3732
+ resolved_edits.push([edit, edit.new_text || null]);
3733
+ } else if (edit._match_start_index !== void 0 && edit._match_start_index !== null) {
3734
+ edit._resolved_start_idx = edit._match_start_index;
3436
3735
  resolved_edits.push([edit, edit.new_text || null]);
3437
3736
  } 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;
3737
+ let matches = this.mapper.find_all_match_indices(edit.target_text);
3738
+ if (matches.length === 0) {
3739
+ if (!this.clean_mapper) {
3740
+ this.clean_mapper = new DocumentMapper(this.doc, true);
3741
+ }
3742
+ matches = this.clean_mapper.find_all_match_indices(edit.target_text);
3743
+ }
3744
+ if (matches.length > 0) {
3745
+ edit._resolved_start_idx = matches[0][0];
3441
3746
  resolved_edits.push([edit, null]);
3442
3747
  } else {
3443
3748
  skipped++;
3444
- this.skipped_details.push(
3445
- `- Failed to locate row target: '${(edit.target_text || "").substring(0, 40)}...'`
3446
- );
3749
+ edit._applied_status = false;
3750
+ const target_snippet = (edit.target_text || "").trim().substring(0, 40);
3751
+ const msg = `- Failed to locate row target: '${target_snippet}...'`;
3752
+ this.skipped_details.push(msg);
3753
+ edit._error_msg = msg;
3447
3754
  }
3448
3755
  } else {
3449
3756
  const resolved = this._pre_resolve_heuristic_edit(edit);
3450
3757
  if (resolved) {
3451
3758
  if (Array.isArray(resolved)) {
3452
- for (const r of resolved) resolved_edits.push([r, r.new_text]);
3759
+ for (const r of resolved) {
3760
+ r._resolved_start_idx = r._match_start_index;
3761
+ r._parent_edit_ref = edit;
3762
+ if (edit._resolved_start_idx === void 0 || edit._resolved_start_idx === null) {
3763
+ edit._resolved_start_idx = r._resolved_start_idx;
3764
+ }
3765
+ if (!edit._resolved_proxy_edit) {
3766
+ edit._resolved_proxy_edit = r;
3767
+ }
3768
+ resolved_edits.push([r, r.new_text]);
3769
+ }
3453
3770
  } else {
3771
+ resolved._resolved_start_idx = resolved._match_start_index;
3772
+ resolved._parent_edit_ref = edit;
3773
+ edit._resolved_start_idx = resolved._resolved_start_idx;
3774
+ edit._resolved_proxy_edit = resolved;
3454
3775
  resolved_edits.push([resolved, resolved.new_text]);
3455
3776
  }
3456
3777
  } else {
3457
3778
  skipped++;
3458
- this.skipped_details.push(
3459
- `- Failed to apply edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3460
- );
3779
+ edit._applied_status = false;
3780
+ const display_text = edit.target_text || "insertion";
3781
+ const target_snippet = display_text.trim().substring(0, 40);
3782
+ const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
3783
+ this.skipped_details.push(msg);
3784
+ edit._error_msg = msg;
3461
3785
  }
3462
3786
  }
3463
3787
  }
3464
3788
  resolved_edits.sort(
3465
- (a, b) => (b[0]._match_start_index || 0) - (a[0]._match_start_index || 0)
3789
+ (a, b) => (b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0)
3466
3790
  );
3467
3791
  const occupied_ranges = [];
3468
3792
  for (const [edit, orig_new] of resolved_edits) {
3469
- const start = edit._match_start_index || 0;
3793
+ const start = edit._resolved_start_idx || 0;
3470
3794
  const end = start + (edit.target_text ? edit.target_text.length : 0);
3471
3795
  const overlaps = occupied_ranges.some(
3472
3796
  ([occ_start, occ_end]) => start < occ_end && end > occ_start
3473
3797
  );
3474
3798
  if (overlaps) {
3475
3799
  skipped++;
3476
- this.skipped_details.push(
3477
- `- Skipped overlapping edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3478
- );
3800
+ const display_text = edit.target_text || "insertion";
3801
+ const target_snippet = display_text.trim().substring(0, 40);
3802
+ const msg = `- Skipped overlapping edit targeting: '${target_snippet}...'`;
3803
+ this.skipped_details.push(msg);
3804
+ edit._applied_status = false;
3805
+ edit._error_msg = msg;
3806
+ const parent = edit._parent_edit_ref;
3807
+ if (parent) {
3808
+ parent._applied_status = false;
3809
+ parent._error_msg = msg;
3810
+ }
3479
3811
  continue;
3480
3812
  }
3481
3813
  let success = false;
@@ -3487,11 +3819,26 @@ var RedlineEngine = class {
3487
3819
  if (success) {
3488
3820
  applied++;
3489
3821
  occupied_ranges.push([start, end]);
3822
+ edit._applied_status = true;
3823
+ const parent = edit._parent_edit_ref;
3824
+ if (parent) {
3825
+ parent._applied_status = true;
3826
+ }
3490
3827
  } else {
3491
3828
  skipped++;
3492
- this.skipped_details.push(
3493
- `- Failed to apply edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3494
- );
3829
+ const display_text = edit.target_text || "insertion";
3830
+ const target_snippet = display_text.trim().substring(0, 40);
3831
+ const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
3832
+ this.skipped_details.push(msg);
3833
+ edit._applied_status = false;
3834
+ edit._error_msg = msg;
3835
+ const parent = edit._parent_edit_ref;
3836
+ if (parent) {
3837
+ if (!parent._applied_status) {
3838
+ parent._applied_status = false;
3839
+ parent._error_msg = msg;
3840
+ }
3841
+ }
3495
3842
  }
3496
3843
  }
3497
3844
  return [applied, skipped];
@@ -3584,7 +3931,7 @@ var RedlineEngine = class {
3584
3931
  return [applied, skipped];
3585
3932
  }
3586
3933
  _apply_table_edit(edit, rebuild_map) {
3587
- const start_idx = edit._match_start_index || 0;
3934
+ const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
3588
3935
  const [anchor_run, anchor_para] = this.mapper.get_insertion_anchor(
3589
3936
  start_idx,
3590
3937
  rebuild_map
@@ -3628,9 +3975,31 @@ var RedlineEngine = class {
3628
3975
  }
3629
3976
  return false;
3630
3977
  }
3978
+ /**
3979
+ * Returns the first match of `target_text` in the raw mapper that is NOT
3980
+ * entirely contained within a tracked deletion (<w:del>). Tracked-deleted
3981
+ * copies are not live, editable text, so an edit must resolve to a live
3982
+ * occurrence even when a dead copy appears earlier in the document
3983
+ * (BUG-23-5). Falls back to the plain first match when no live copy is
3984
+ * found (e.g. fuzzy/normalized matches the span filter cannot align).
3985
+ */
3986
+ _first_live_match(target_text) {
3987
+ const all = this.mapper.find_all_match_indices(target_text);
3988
+ if (all.length <= 1) {
3989
+ return this.mapper.find_match_index(target_text);
3990
+ }
3991
+ for (const [start, length] of all) {
3992
+ const realSpans = this.mapper.spans.filter(
3993
+ (s) => s.run !== null && s.end > start && s.start < start + length
3994
+ );
3995
+ if (realSpans.length === 0) return [start, length];
3996
+ if (realSpans.some((s) => !s.del_id)) return [start, length];
3997
+ }
3998
+ return this.mapper.find_match_index(target_text);
3999
+ }
3631
4000
  _pre_resolve_heuristic_edit(edit) {
3632
4001
  if (!edit.target_text) return null;
3633
- let [start_idx, match_len] = this.mapper.find_match_index(edit.target_text);
4002
+ let [start_idx, match_len] = this._first_live_match(edit.target_text);
3634
4003
  let use_clean_map = false;
3635
4004
  if (start_idx === -1) {
3636
4005
  if (!this.clean_mapper)
@@ -3694,7 +4063,7 @@ var RedlineEngine = class {
3694
4063
  _apply_single_edit_indexed(edit, orig_new, rebuild_map) {
3695
4064
  let op = edit._internal_op;
3696
4065
  const active_mapper = edit._active_mapper_ref || this.mapper;
3697
- const start_idx = edit._match_start_index || 0;
4066
+ const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
3698
4067
  const length = edit.target_text ? edit.target_text.length : 0;
3699
4068
  const del_id = ["DELETION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
3700
4069
  const ins_id = ["INSERTION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
@@ -3743,6 +4112,76 @@ var RedlineEngine = class {
3743
4112
  rebuild_map
3744
4113
  );
3745
4114
  if (!anchor_run && !anchor_para) return false;
4115
+ const _bug233_new = edit.new_text || "";
4116
+ const _bug233_trailing_break = /\n\s*$/.test(_bug233_new);
4117
+ let _bug233_target_para = null;
4118
+ {
4119
+ const startingSpans = active_mapper.spans.filter(
4120
+ (s) => s.paragraph !== null && s.start === start_idx
4121
+ );
4122
+ if (startingSpans.length > 0 && startingSpans[0].paragraph) {
4123
+ _bug233_target_para = startingSpans[0].paragraph._element;
4124
+ }
4125
+ }
4126
+ if (_bug233_trailing_break && _bug233_target_para && _bug233_target_para.parentNode) {
4127
+ const body = _bug233_target_para.parentNode;
4128
+ const xmlDoc = this.doc.part._element.ownerDocument;
4129
+ const lines = _bug233_new.split(/[\r\n]+/).filter((l) => l !== "");
4130
+ let firstNew = null;
4131
+ let lastNew = null;
4132
+ let lastIns = null;
4133
+ for (const raw_line of lines) {
4134
+ const [clean_text, style_name] = this._parse_markdown_style(raw_line);
4135
+ const new_p = xmlDoc.createElement("w:p");
4136
+ if (style_name) {
4137
+ this._set_paragraph_style(new_p, style_name);
4138
+ } else {
4139
+ const existing_pPr = findChild(_bug233_target_para, "w:pPr");
4140
+ if (existing_pPr) new_p.appendChild(existing_pPr.cloneNode(true));
4141
+ }
4142
+ let pPr = findChild(new_p, "w:pPr");
4143
+ if (!pPr) {
4144
+ pPr = xmlDoc.createElement("w:pPr");
4145
+ new_p.insertBefore(pPr, new_p.firstChild);
4146
+ }
4147
+ let rPr = findChild(pPr, "w:rPr");
4148
+ if (!rPr) {
4149
+ rPr = xmlDoc.createElement("w:rPr");
4150
+ pPr.appendChild(rPr);
4151
+ }
4152
+ rPr.appendChild(this._create_track_change_tag("w:ins", "", ins_id));
4153
+ const content_ins = this._build_tracked_ins_for_line(
4154
+ clean_text,
4155
+ anchor_run,
4156
+ ins_id,
4157
+ xmlDoc
4158
+ );
4159
+ if (content_ins) new_p.appendChild(content_ins);
4160
+ body.insertBefore(new_p, _bug233_target_para);
4161
+ if (!firstNew) firstNew = new_p;
4162
+ lastNew = new_p;
4163
+ lastIns = content_ins;
4164
+ }
4165
+ if (firstNew) {
4166
+ if (edit.comment && lastNew && lastIns) {
4167
+ const ascend = (el, p) => {
4168
+ let cur = el;
4169
+ while (cur.parentNode && cur.parentNode !== p)
4170
+ cur = cur.parentNode;
4171
+ return cur;
4172
+ };
4173
+ const startIns = findAllDescendants(firstNew, "w:ins")[0] || firstNew;
4174
+ this._attach_comment_spanning(
4175
+ firstNew,
4176
+ ascend(startIns, firstNew),
4177
+ lastNew,
4178
+ ascend(lastIns, lastNew),
4179
+ edit.comment
4180
+ );
4181
+ }
4182
+ return true;
4183
+ }
4184
+ }
3746
4185
  const result = this._track_insert_multiline(
3747
4186
  edit.new_text || "",
3748
4187
  anchor_run,
@@ -5452,15 +5891,88 @@ function remove_all_comments(doc) {
5452
5891
  lines.push(` ${status} "${_truncate(info.text || "", 60)}" (${info.author || "Unknown"})`);
5453
5892
  cm.deleteComment(cId);
5454
5893
  }
5455
- for (const tag of ["w:commentRangeStart", "w:commentRangeEnd", "w:commentReference"]) {
5894
+ for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
5456
5895
  for (const el of findAllDescendants(doc.element, tag)) {
5457
5896
  el.parentNode?.removeChild(el);
5458
5897
  }
5459
5898
  }
5899
+ const refs = findAllDescendants(doc.element, "w:commentReference");
5900
+ for (const ref of refs) {
5901
+ const parent = ref.parentNode;
5902
+ if (parent) {
5903
+ if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
5904
+ const nonRprChildren = Array.from(parent.childNodes).filter(
5905
+ (c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
5906
+ );
5907
+ if (nonRprChildren.length <= 1) {
5908
+ parent.parentNode?.removeChild(parent);
5909
+ } else {
5910
+ parent.removeChild(ref);
5911
+ }
5912
+ } else {
5913
+ parent.removeChild(ref);
5914
+ }
5915
+ }
5916
+ }
5460
5917
  const resolvedCount = Object.values(data).filter((c) => c.resolved).length;
5461
5918
  const openCount = Object.values(data).filter((c) => !c.resolved).length;
5462
5919
  return [`Comments removed: ${keys.length} (${resolvedCount} resolved, ${openCount} open)`].concat(lines);
5463
5920
  }
5921
+ function eject_comment_parts(doc) {
5922
+ const pkg = doc.pkg;
5923
+ const comment_partnames = /* @__PURE__ */ new Set();
5924
+ for (const part of pkg.parts) {
5925
+ if (part.partname.toLowerCase().includes("comments")) {
5926
+ comment_partnames.add(part.partname);
5927
+ const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
5928
+ const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
5929
+ comment_partnames.add(withSlash);
5930
+ comment_partnames.add(withoutSlash);
5931
+ }
5932
+ }
5933
+ if (comment_partnames.size === 0) return;
5934
+ for (const part of pkg.parts) {
5935
+ if (part.partname.endsWith(".rels")) {
5936
+ const rels = findAllDescendants(part._element, "Relationship");
5937
+ const toRemove = [];
5938
+ for (const rel of rels) {
5939
+ const target = rel.getAttribute("Target") || "";
5940
+ if (target.toLowerCase().includes("comments")) {
5941
+ toRemove.push(rel);
5942
+ const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
5943
+ const sourcePart = pkg.getPartByPath(sourcePath);
5944
+ if (sourcePart) {
5945
+ const relId = rel.getAttribute("Id");
5946
+ if (relId) sourcePart.rels.delete(relId);
5947
+ }
5948
+ }
5949
+ }
5950
+ for (const relEl of toRemove) {
5951
+ relEl.parentNode?.removeChild(relEl);
5952
+ }
5953
+ }
5954
+ }
5955
+ const ctPart = pkg.getPartByPath("[Content_Types].xml");
5956
+ if (ctPart) {
5957
+ const overrides = findAllDescendants(ctPart._element, "Override");
5958
+ const toRemove = [];
5959
+ for (const override of overrides) {
5960
+ const partName = override.getAttribute("PartName") || "";
5961
+ if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
5962
+ toRemove.push(override);
5963
+ }
5964
+ }
5965
+ for (const overrideEl of toRemove) {
5966
+ overrideEl.parentNode?.removeChild(overrideEl);
5967
+ }
5968
+ }
5969
+ pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
5970
+ for (const key of Object.keys(pkg.unzipped)) {
5971
+ if (key.toLowerCase().includes("comments")) {
5972
+ delete pkg.unzipped[key];
5973
+ }
5974
+ }
5975
+ }
5464
5976
  function replace_comment_authors(doc, newAuthor) {
5465
5977
  const cm = new CommentsManager(doc);
5466
5978
  if (!cm.commentsPart) return [];
@@ -5649,6 +6161,7 @@ async function finalize_document(doc, options) {
5649
6161
  const commentsSummary = get_comments_summary(doc);
5650
6162
  report.comments_removed = commentsSummary.total;
5651
6163
  report.add_transform_lines(remove_all_comments(doc));
6164
+ eject_comment_parts(doc);
5652
6165
  } else if (options.sanitize_mode === "keep-markup") {
5653
6166
  const counts = count_tracked_changes(doc);
5654
6167
  report.tracked_changes_found = counts[0] + counts[1] + counts[2];