@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.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,39 @@ 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(
2674
+ start_idx + length,
2675
+ start_idx + length + 30
2676
+ );
2677
+ const critic_markup = `${context_before}{--${target_text}--}{++${new_text}++}${context_after}`;
2678
+ let clean_text = critic_markup;
2679
+ clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
2680
+ clean_text = clean_text.replace(/\{--.*?--\}/gs, "");
2681
+ clean_text = clean_text.replace(/\{\+\+(.*?)\+\+\}/gs, "$1");
2682
+ return [critic_markup, clean_text];
2683
+ }
2605
2684
  _scan_existing_ids() {
2606
2685
  let maxId = 0;
2607
2686
  for (const tag of ["w:ins", "w:del"]) {
@@ -2694,28 +2773,86 @@ var RedlineEngine = class {
2694
2773
  }
2695
2774
  }
2696
2775
  }
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);
2776
+ for (const root_element of parts_to_process) {
2777
+ for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
2778
+ for (const el of findAllDescendants(root_element, tag)) {
2779
+ el.parentNode?.removeChild(el);
2780
+ }
2706
2781
  }
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);
2782
+ const refs = findAllDescendants(root_element, "w:commentReference");
2783
+ for (const ref of refs) {
2784
+ const parent = ref.parentNode;
2785
+ if (parent) {
2786
+ if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
2787
+ const nonRprChildren = Array.from(parent.childNodes).filter(
2788
+ (c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
2789
+ );
2790
+ if (nonRprChildren.length <= 1) {
2791
+ parent.parentNode?.removeChild(parent);
2792
+ } else {
2793
+ parent.removeChild(ref);
2794
+ }
2795
+ } else {
2796
+ parent.removeChild(ref);
2797
+ }
2798
+ }
2715
2799
  }
2716
2800
  }
2717
- for (const cid of comment_ids) {
2718
- this.comments_manager.deleteComment(cid);
2801
+ const pkg = this.doc.pkg;
2802
+ const comment_partnames = /* @__PURE__ */ new Set();
2803
+ for (const part of pkg.parts) {
2804
+ if (part.partname.toLowerCase().includes("comments")) {
2805
+ comment_partnames.add(part.partname);
2806
+ const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
2807
+ const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
2808
+ comment_partnames.add(withSlash);
2809
+ comment_partnames.add(withoutSlash);
2810
+ }
2811
+ }
2812
+ if (comment_partnames.size > 0) {
2813
+ for (const part of pkg.parts) {
2814
+ if (part.partname.endsWith(".rels")) {
2815
+ const rels = findAllDescendants(part._element, "Relationship");
2816
+ const toRemove = [];
2817
+ for (const rel of rels) {
2818
+ const target = rel.getAttribute("Target") || "";
2819
+ if (target.toLowerCase().includes("comments")) {
2820
+ toRemove.push(rel);
2821
+ const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
2822
+ const sourcePart = pkg.getPartByPath(sourcePath);
2823
+ if (sourcePart) {
2824
+ const relId = rel.getAttribute("Id");
2825
+ if (relId) sourcePart.rels.delete(relId);
2826
+ }
2827
+ }
2828
+ }
2829
+ for (const relEl of toRemove) {
2830
+ relEl.parentNode?.removeChild(relEl);
2831
+ }
2832
+ }
2833
+ }
2834
+ const ctPart = pkg.getPartByPath("[Content_Types].xml");
2835
+ if (ctPart) {
2836
+ const overrides = findAllDescendants(ctPart._element, "Override");
2837
+ const toRemove = [];
2838
+ for (const override of overrides) {
2839
+ const partName = override.getAttribute("PartName") || "";
2840
+ if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
2841
+ toRemove.push(override);
2842
+ }
2843
+ }
2844
+ for (const overrideEl of toRemove) {
2845
+ overrideEl.parentNode?.removeChild(overrideEl);
2846
+ }
2847
+ }
2848
+ pkg.parts = pkg.parts.filter(
2849
+ (p) => !p.partname.toLowerCase().includes("comments")
2850
+ );
2851
+ for (const key of Object.keys(pkg.unzipped)) {
2852
+ if (key.toLowerCase().includes("comments")) {
2853
+ delete pkg.unzipped[key];
2854
+ }
2855
+ }
2719
2856
  }
2720
2857
  }
2721
2858
  _getNextId() {
@@ -2806,40 +2943,40 @@ var RedlineEngine = class {
2806
2943
  }
2807
2944
  }
2808
2945
  /**
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
- */
2946
+ * Inserts `text` as one or more tracked paragraphs anchored relative to
2947
+ * either an existing run or a paragraph. Returns:
2948
+ * { first_node, last_p, last_ins, used_block_mode }
2949
+ * where:
2950
+ * - first_node: the first <w:ins> (for inline mode) OR the first new <w:p>
2951
+ * (for block mode). The caller uses this for splicing into the DOM and
2952
+ * for anchoring comments.
2953
+ * - last_p: the last new <w:p> created, if any. null when entirely inline.
2954
+ * - last_ins: the last <w:ins> created (inside the last new <w:p>, or the
2955
+ * sole inline ins). Used as the comment's end anchor.
2956
+ * - used_block_mode: true when the first line carried a heading/list style
2957
+ * marker and we created a new paragraph for it (rather than inlining it).
2958
+ *
2959
+ * Multi-paragraph rules (only when text contains '\n'):
2960
+ * - Each additional line becomes a new <w:p>, inserted after the anchor
2961
+ * paragraph in document order.
2962
+ * - Each new <w:p> gets a copy of the anchor paragraph's <w:pPr> (so list
2963
+ * numbering / indentation are preserved) unless the line itself starts
2964
+ * with a markdown heading or list marker, which overrides the style.
2965
+ * - Each new <w:p> carries a tracked paragraph-break marker
2966
+ * (<w:pPr><w:rPr><w:ins/></w:rPr></w:pPr>) so Word natively tracks the
2967
+ * paragraph break.
2968
+ * - Each new <w:p>'s content is wrapped in a <w:ins>, with inline bold/
2969
+ * italic markdown parsed via _parse_inline_markdown.
2970
+ *
2971
+ * The first line:
2972
+ * - If it carries a heading / list marker AND we have a paragraph anchor,
2973
+ * we drop into "block mode": no inline <w:ins>; the first line itself
2974
+ * becomes the first new <w:p>.
2975
+ * - Otherwise we emit a single inline <w:ins> for the first line (current
2976
+ * behaviour) and treat the remaining lines as block extensions.
2977
+ *
2978
+ * Does NOT attach comments; callers handle that.
2979
+ */
2843
2980
  _track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id) {
2844
2981
  if (!text) {
2845
2982
  return {
@@ -2979,7 +3116,15 @@ var RedlineEngine = class {
2979
3116
  const anchor_rPr = findChild(anchor_run._element, "w:rPr");
2980
3117
  if (anchor_rPr) {
2981
3118
  const clone = anchor_rPr.cloneNode(true);
2982
- for (const tag of ["w:vanish", "w:strike", "w:dstrike"]) {
3119
+ for (const tag of [
3120
+ "w:vanish",
3121
+ "w:strike",
3122
+ "w:dstrike",
3123
+ "w:i",
3124
+ "w:iCs",
3125
+ "w:b",
3126
+ "w:bCs"
3127
+ ]) {
2983
3128
  const found = findChild(clone, tag);
2984
3129
  if (found) clone.removeChild(found);
2985
3130
  }
@@ -3253,6 +3398,16 @@ var RedlineEngine = class {
3253
3398
  matches = this.clean_mapper.find_all_match_indices(edit.target_text);
3254
3399
  if (matches.length > 0) activeText = this.clean_mapper.full_text;
3255
3400
  }
3401
+ if (activeText === this.mapper.full_text && matches.length > 1) {
3402
+ const liveMatches = matches.filter(([start, length]) => {
3403
+ const realSpans = this.mapper.spans.filter(
3404
+ (s) => s.run !== null && s.end > start && s.start < start + length
3405
+ );
3406
+ if (realSpans.length === 0) return true;
3407
+ return realSpans.some((s) => !s.del_id);
3408
+ });
3409
+ if (liveMatches.length > 0) matches = liveMatches;
3410
+ }
3256
3411
  if (matches.length === 0) {
3257
3412
  errors.push(
3258
3413
  `- Edit ${i + 1} Failed: Target text not found in document:
@@ -3272,6 +3427,34 @@ var RedlineEngine = class {
3272
3427
  )
3273
3428
  );
3274
3429
  }
3430
+ if (matches.length === 1) {
3431
+ const [m_start, m_len] = matches[0];
3432
+ const matched = activeText.substring(m_start, m_start + m_len);
3433
+ const [pfx, sfx] = trim_common_context(matched, edit.new_text || "");
3434
+ const t_end = matched.length - sfx;
3435
+ const final_target = matched.substring(pfx, t_end);
3436
+ const final_new = (edit.new_text || "").substring(
3437
+ pfx,
3438
+ (edit.new_text || "").length - sfx
3439
+ );
3440
+ if (final_target.includes("\n\n")) {
3441
+ if (final_new.includes("\n\n")) {
3442
+ const parts = matched.split("\n\n");
3443
+ if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3444
+ errors.push(
3445
+ `- 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.`
3446
+ );
3447
+ }
3448
+ } else {
3449
+ const parts = final_target.split("\n\n");
3450
+ if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3451
+ errors.push(
3452
+ `- 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.`
3453
+ );
3454
+ }
3455
+ }
3456
+ }
3457
+ }
3275
3458
  for (const [start, length] of matches) {
3276
3459
  const spans = this.mapper.spans.filter(
3277
3460
  (s) => s.end > start && s.start < start + length
@@ -3335,7 +3518,33 @@ var RedlineEngine = class {
3335
3518
  }
3336
3519
  return errors;
3337
3520
  }
3338
- process_batch(changes) {
3521
+ process_batch(changes, dry_run = false) {
3522
+ if (dry_run) {
3523
+ const baselines = /* @__PURE__ */ new Map();
3524
+ for (const part of this.doc.pkg.parts) {
3525
+ if (part._element) {
3526
+ baselines.set(part, part._element.cloneNode(true));
3527
+ }
3528
+ }
3529
+ try {
3530
+ return this._process_batch_internal(changes, true);
3531
+ } finally {
3532
+ for (const [part, originalEl] of baselines.entries()) {
3533
+ const doc = part._element.ownerDocument;
3534
+ if (doc && doc.documentElement) {
3535
+ doc.replaceChild(originalEl, doc.documentElement);
3536
+ }
3537
+ part._element = originalEl;
3538
+ }
3539
+ this.mapper = new DocumentMapper(this.doc);
3540
+ this.comments_manager = new CommentsManager(this.doc);
3541
+ this.clean_mapper = null;
3542
+ }
3543
+ } else {
3544
+ return this._process_batch_internal(changes, false);
3545
+ }
3546
+ }
3547
+ _process_batch_internal(changes, dry_run_mode = false) {
3339
3548
  this.skipped_details = [];
3340
3549
  const actions = changes.filter(
3341
3550
  (c) => ["accept", "reject", "reply"].includes(c.type)
@@ -3343,38 +3552,133 @@ var RedlineEngine = class {
3343
3552
  const edits = changes.filter(
3344
3553
  (c) => !["accept", "reject", "reply"].includes(c.type)
3345
3554
  );
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);
3555
+ if (!dry_run_mode) {
3556
+ const all_errors = [];
3557
+ if (actions.length > 0) {
3558
+ all_errors.push(...this.validate_review_actions(actions));
3559
+ }
3560
+ if (edits.length > 0) {
3561
+ all_errors.push(...this.validate_edits(edits));
3562
+ }
3563
+ if (all_errors.length > 0) {
3564
+ throw new BatchValidationError(all_errors);
3565
+ }
3566
+ } else {
3567
+ if (actions.length > 0) {
3568
+ const action_errors = this.validate_review_actions(actions);
3569
+ if (action_errors.length > 0) {
3570
+ throw new BatchValidationError(action_errors);
3571
+ }
3572
+ }
3355
3573
  }
3356
- let applied_actions = 0, skipped_actions = 0;
3574
+ let applied_actions = 0;
3575
+ let skipped_actions = 0;
3357
3576
  if (actions.length > 0) {
3358
3577
  const res = this.apply_review_actions(actions);
3359
3578
  applied_actions = res[0];
3360
3579
  skipped_actions = res[1];
3580
+ if (skipped_actions > 0) {
3581
+ throw new BatchValidationError(this.skipped_details);
3582
+ }
3361
3583
  if (applied_actions > 0) {
3362
3584
  this.mapper["_build_map"]();
3363
3585
  if (this.clean_mapper) this.clean_mapper["_build_map"]();
3364
3586
  }
3365
3587
  }
3366
- let applied_edits = 0, skipped_edits = 0;
3588
+ const edits_reports = [];
3589
+ let applied_edits = 0;
3590
+ let skipped_edits = 0;
3367
3591
  if (edits.length > 0) {
3368
- const res = this.apply_edits(edits);
3369
- applied_edits = res[0];
3370
- skipped_edits = res[1];
3592
+ if (dry_run_mode) {
3593
+ for (const edit of edits) {
3594
+ const single_errors = this.validate_edits([edit]);
3595
+ const warning = this._check_punctuation_warning(
3596
+ edit.target_text || ""
3597
+ );
3598
+ if (single_errors.length > 0) {
3599
+ skipped_edits++;
3600
+ edits_reports.push({
3601
+ status: "failed",
3602
+ target_text: edit.target_text || "",
3603
+ new_text: edit.new_text || "",
3604
+ warning,
3605
+ error: single_errors[0],
3606
+ critic_markup: null,
3607
+ clean_text: null
3608
+ });
3609
+ continue;
3610
+ }
3611
+ const res = this.apply_edits([edit]);
3612
+ const applied = res[0];
3613
+ if (applied > 0) {
3614
+ applied_edits++;
3615
+ const previews = this._build_edit_context_previews(edit);
3616
+ edits_reports.push({
3617
+ status: "applied",
3618
+ target_text: edit.target_text || "",
3619
+ new_text: edit.new_text || "",
3620
+ warning,
3621
+ error: null,
3622
+ critic_markup: previews[0],
3623
+ clean_text: previews[1]
3624
+ });
3625
+ } else {
3626
+ skipped_edits++;
3627
+ const error_msg = this.skipped_details.length > 0 ? this.skipped_details[this.skipped_details.length - 1] : "Failed to apply edit";
3628
+ edits_reports.push({
3629
+ status: "failed",
3630
+ target_text: edit.target_text || "",
3631
+ new_text: edit.new_text || "",
3632
+ warning,
3633
+ error: error_msg,
3634
+ critic_markup: null,
3635
+ clean_text: null
3636
+ });
3637
+ }
3638
+ }
3639
+ } else {
3640
+ const errors = this.validate_edits(edits);
3641
+ if (errors.length > 0) {
3642
+ throw new BatchValidationError(errors);
3643
+ }
3644
+ const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
3645
+ const res = this.apply_edits(cloned_edits);
3646
+ applied_edits = res[0];
3647
+ skipped_edits = res[1];
3648
+ for (const edit of cloned_edits) {
3649
+ const success = edit._applied_status || false;
3650
+ const error_msg = edit._error_msg || null;
3651
+ const warning = this._check_punctuation_warning(
3652
+ edit.target_text || ""
3653
+ );
3654
+ let critic_markup = null;
3655
+ let clean_text = null;
3656
+ if (success) {
3657
+ const previews = this._build_edit_context_previews(edit);
3658
+ critic_markup = previews[0];
3659
+ clean_text = previews[1];
3660
+ }
3661
+ edits_reports.push({
3662
+ status: success ? "applied" : "failed",
3663
+ target_text: edit.target_text || "",
3664
+ new_text: edit.new_text || "",
3665
+ warning,
3666
+ error: error_msg,
3667
+ critic_markup,
3668
+ clean_text
3669
+ });
3670
+ }
3671
+ }
3371
3672
  }
3372
3673
  return {
3373
3674
  actions_applied: applied_actions,
3374
3675
  actions_skipped: skipped_actions,
3375
3676
  edits_applied: applied_edits,
3376
3677
  edits_skipped: skipped_edits,
3377
- skipped_details: this.skipped_details
3678
+ skipped_details: this.skipped_details,
3679
+ edits: edits_reports,
3680
+ engine: "node",
3681
+ version: "1.10.0"
3378
3682
  };
3379
3683
  }
3380
3684
  apply_edits(edits) {
@@ -3382,50 +3686,90 @@ var RedlineEngine = class {
3382
3686
  let skipped = 0;
3383
3687
  const resolved_edits = [];
3384
3688
  for (const edit of edits) {
3385
- if (edit._match_start_index !== void 0 && edit._match_start_index !== null) {
3689
+ edit._applied_status = false;
3690
+ edit._error_msg = null;
3691
+ }
3692
+ for (const edit of edits) {
3693
+ if (edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null) {
3694
+ resolved_edits.push([edit, edit.new_text || null]);
3695
+ } else if (edit._match_start_index !== void 0 && edit._match_start_index !== null) {
3696
+ edit._resolved_start_idx = edit._match_start_index;
3386
3697
  resolved_edits.push([edit, edit.new_text || null]);
3387
3698
  } 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;
3699
+ let matches = this.mapper.find_all_match_indices(edit.target_text);
3700
+ if (matches.length === 0) {
3701
+ if (!this.clean_mapper) {
3702
+ this.clean_mapper = new DocumentMapper(this.doc, true);
3703
+ }
3704
+ matches = this.clean_mapper.find_all_match_indices(edit.target_text);
3705
+ }
3706
+ if (matches.length > 0) {
3707
+ edit._resolved_start_idx = matches[0][0];
3391
3708
  resolved_edits.push([edit, null]);
3392
3709
  } else {
3393
3710
  skipped++;
3394
- this.skipped_details.push(
3395
- `- Failed to locate row target: '${(edit.target_text || "").substring(0, 40)}...'`
3396
- );
3711
+ edit._applied_status = false;
3712
+ const target_snippet = (edit.target_text || "").trim().substring(0, 40);
3713
+ const msg = `- Failed to locate row target: '${target_snippet}...'`;
3714
+ this.skipped_details.push(msg);
3715
+ edit._error_msg = msg;
3397
3716
  }
3398
3717
  } else {
3399
3718
  const resolved = this._pre_resolve_heuristic_edit(edit);
3400
3719
  if (resolved) {
3401
3720
  if (Array.isArray(resolved)) {
3402
- for (const r of resolved) resolved_edits.push([r, r.new_text]);
3721
+ for (const r of resolved) {
3722
+ r._resolved_start_idx = r._match_start_index;
3723
+ r._parent_edit_ref = edit;
3724
+ if (edit._resolved_start_idx === void 0 || edit._resolved_start_idx === null) {
3725
+ edit._resolved_start_idx = r._resolved_start_idx;
3726
+ }
3727
+ if (!edit._resolved_proxy_edit) {
3728
+ edit._resolved_proxy_edit = r;
3729
+ }
3730
+ resolved_edits.push([r, r.new_text]);
3731
+ }
3403
3732
  } else {
3733
+ resolved._resolved_start_idx = resolved._match_start_index;
3734
+ resolved._parent_edit_ref = edit;
3735
+ edit._resolved_start_idx = resolved._resolved_start_idx;
3736
+ edit._resolved_proxy_edit = resolved;
3404
3737
  resolved_edits.push([resolved, resolved.new_text]);
3405
3738
  }
3406
3739
  } else {
3407
3740
  skipped++;
3408
- this.skipped_details.push(
3409
- `- Failed to apply edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3410
- );
3741
+ edit._applied_status = false;
3742
+ const display_text = edit.target_text || "insertion";
3743
+ const target_snippet = display_text.trim().substring(0, 40);
3744
+ const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
3745
+ this.skipped_details.push(msg);
3746
+ edit._error_msg = msg;
3411
3747
  }
3412
3748
  }
3413
3749
  }
3414
3750
  resolved_edits.sort(
3415
- (a, b) => (b[0]._match_start_index || 0) - (a[0]._match_start_index || 0)
3751
+ (a, b) => (b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0)
3416
3752
  );
3417
3753
  const occupied_ranges = [];
3418
3754
  for (const [edit, orig_new] of resolved_edits) {
3419
- const start = edit._match_start_index || 0;
3755
+ const start = edit._resolved_start_idx || 0;
3420
3756
  const end = start + (edit.target_text ? edit.target_text.length : 0);
3421
3757
  const overlaps = occupied_ranges.some(
3422
3758
  ([occ_start, occ_end]) => start < occ_end && end > occ_start
3423
3759
  );
3424
3760
  if (overlaps) {
3425
3761
  skipped++;
3426
- this.skipped_details.push(
3427
- `- Skipped overlapping edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3428
- );
3762
+ const display_text = edit.target_text || "insertion";
3763
+ const target_snippet = display_text.trim().substring(0, 40);
3764
+ const msg = `- Skipped overlapping edit targeting: '${target_snippet}...'`;
3765
+ this.skipped_details.push(msg);
3766
+ edit._applied_status = false;
3767
+ edit._error_msg = msg;
3768
+ const parent = edit._parent_edit_ref;
3769
+ if (parent) {
3770
+ parent._applied_status = false;
3771
+ parent._error_msg = msg;
3772
+ }
3429
3773
  continue;
3430
3774
  }
3431
3775
  let success = false;
@@ -3437,11 +3781,26 @@ var RedlineEngine = class {
3437
3781
  if (success) {
3438
3782
  applied++;
3439
3783
  occupied_ranges.push([start, end]);
3784
+ edit._applied_status = true;
3785
+ const parent = edit._parent_edit_ref;
3786
+ if (parent) {
3787
+ parent._applied_status = true;
3788
+ }
3440
3789
  } else {
3441
3790
  skipped++;
3442
- this.skipped_details.push(
3443
- `- Failed to apply edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3444
- );
3791
+ const display_text = edit.target_text || "insertion";
3792
+ const target_snippet = display_text.trim().substring(0, 40);
3793
+ const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
3794
+ this.skipped_details.push(msg);
3795
+ edit._applied_status = false;
3796
+ edit._error_msg = msg;
3797
+ const parent = edit._parent_edit_ref;
3798
+ if (parent) {
3799
+ if (!parent._applied_status) {
3800
+ parent._applied_status = false;
3801
+ parent._error_msg = msg;
3802
+ }
3803
+ }
3445
3804
  }
3446
3805
  }
3447
3806
  return [applied, skipped];
@@ -3534,7 +3893,7 @@ var RedlineEngine = class {
3534
3893
  return [applied, skipped];
3535
3894
  }
3536
3895
  _apply_table_edit(edit, rebuild_map) {
3537
- const start_idx = edit._match_start_index || 0;
3896
+ const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
3538
3897
  const [anchor_run, anchor_para] = this.mapper.get_insertion_anchor(
3539
3898
  start_idx,
3540
3899
  rebuild_map
@@ -3578,9 +3937,31 @@ var RedlineEngine = class {
3578
3937
  }
3579
3938
  return false;
3580
3939
  }
3940
+ /**
3941
+ * Returns the first match of `target_text` in the raw mapper that is NOT
3942
+ * entirely contained within a tracked deletion (<w:del>). Tracked-deleted
3943
+ * copies are not live, editable text, so an edit must resolve to a live
3944
+ * occurrence even when a dead copy appears earlier in the document
3945
+ * (BUG-23-5). Falls back to the plain first match when no live copy is
3946
+ * found (e.g. fuzzy/normalized matches the span filter cannot align).
3947
+ */
3948
+ _first_live_match(target_text) {
3949
+ const all = this.mapper.find_all_match_indices(target_text);
3950
+ if (all.length <= 1) {
3951
+ return this.mapper.find_match_index(target_text);
3952
+ }
3953
+ for (const [start, length] of all) {
3954
+ const realSpans = this.mapper.spans.filter(
3955
+ (s) => s.run !== null && s.end > start && s.start < start + length
3956
+ );
3957
+ if (realSpans.length === 0) return [start, length];
3958
+ if (realSpans.some((s) => !s.del_id)) return [start, length];
3959
+ }
3960
+ return this.mapper.find_match_index(target_text);
3961
+ }
3581
3962
  _pre_resolve_heuristic_edit(edit) {
3582
3963
  if (!edit.target_text) return null;
3583
- let [start_idx, match_len] = this.mapper.find_match_index(edit.target_text);
3964
+ let [start_idx, match_len] = this._first_live_match(edit.target_text);
3584
3965
  let use_clean_map = false;
3585
3966
  if (start_idx === -1) {
3586
3967
  if (!this.clean_mapper)
@@ -3644,7 +4025,7 @@ var RedlineEngine = class {
3644
4025
  _apply_single_edit_indexed(edit, orig_new, rebuild_map) {
3645
4026
  let op = edit._internal_op;
3646
4027
  const active_mapper = edit._active_mapper_ref || this.mapper;
3647
- const start_idx = edit._match_start_index || 0;
4028
+ const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
3648
4029
  const length = edit.target_text ? edit.target_text.length : 0;
3649
4030
  const del_id = ["DELETION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
3650
4031
  const ins_id = ["INSERTION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
@@ -3693,6 +4074,76 @@ var RedlineEngine = class {
3693
4074
  rebuild_map
3694
4075
  );
3695
4076
  if (!anchor_run && !anchor_para) return false;
4077
+ const _bug233_new = edit.new_text || "";
4078
+ const _bug233_trailing_break = /\n\s*$/.test(_bug233_new);
4079
+ let _bug233_target_para = null;
4080
+ {
4081
+ const startingSpans = active_mapper.spans.filter(
4082
+ (s) => s.paragraph !== null && s.start === start_idx
4083
+ );
4084
+ if (startingSpans.length > 0 && startingSpans[0].paragraph) {
4085
+ _bug233_target_para = startingSpans[0].paragraph._element;
4086
+ }
4087
+ }
4088
+ if (_bug233_trailing_break && _bug233_target_para && _bug233_target_para.parentNode) {
4089
+ const body = _bug233_target_para.parentNode;
4090
+ const xmlDoc = this.doc.part._element.ownerDocument;
4091
+ const lines = _bug233_new.split(/[\r\n]+/).filter((l) => l !== "");
4092
+ let firstNew = null;
4093
+ let lastNew = null;
4094
+ let lastIns = null;
4095
+ for (const raw_line of lines) {
4096
+ const [clean_text, style_name] = this._parse_markdown_style(raw_line);
4097
+ const new_p = xmlDoc.createElement("w:p");
4098
+ if (style_name) {
4099
+ this._set_paragraph_style(new_p, style_name);
4100
+ } else {
4101
+ const existing_pPr = findChild(_bug233_target_para, "w:pPr");
4102
+ if (existing_pPr) new_p.appendChild(existing_pPr.cloneNode(true));
4103
+ }
4104
+ let pPr = findChild(new_p, "w:pPr");
4105
+ if (!pPr) {
4106
+ pPr = xmlDoc.createElement("w:pPr");
4107
+ new_p.insertBefore(pPr, new_p.firstChild);
4108
+ }
4109
+ let rPr = findChild(pPr, "w:rPr");
4110
+ if (!rPr) {
4111
+ rPr = xmlDoc.createElement("w:rPr");
4112
+ pPr.appendChild(rPr);
4113
+ }
4114
+ rPr.appendChild(this._create_track_change_tag("w:ins", "", ins_id));
4115
+ const content_ins = this._build_tracked_ins_for_line(
4116
+ clean_text,
4117
+ anchor_run,
4118
+ ins_id,
4119
+ xmlDoc
4120
+ );
4121
+ if (content_ins) new_p.appendChild(content_ins);
4122
+ body.insertBefore(new_p, _bug233_target_para);
4123
+ if (!firstNew) firstNew = new_p;
4124
+ lastNew = new_p;
4125
+ lastIns = content_ins;
4126
+ }
4127
+ if (firstNew) {
4128
+ if (edit.comment && lastNew && lastIns) {
4129
+ const ascend = (el, p) => {
4130
+ let cur = el;
4131
+ while (cur.parentNode && cur.parentNode !== p)
4132
+ cur = cur.parentNode;
4133
+ return cur;
4134
+ };
4135
+ const startIns = findAllDescendants(firstNew, "w:ins")[0] || firstNew;
4136
+ this._attach_comment_spanning(
4137
+ firstNew,
4138
+ ascend(startIns, firstNew),
4139
+ lastNew,
4140
+ ascend(lastIns, lastNew),
4141
+ edit.comment
4142
+ );
4143
+ }
4144
+ return true;
4145
+ }
4146
+ }
3696
4147
  const result = this._track_insert_multiline(
3697
4148
  edit.new_text || "",
3698
4149
  anchor_run,
@@ -3750,7 +4201,10 @@ var RedlineEngine = class {
3750
4201
  if (result.first_node.tagName === "w:p") {
3751
4202
  first_anchor_target = findAllDescendants(result.first_node, "w:ins")[0] || result.first_node;
3752
4203
  }
3753
- const anchor = ascend_to_paragraph_child(first_anchor_target, host_p);
4204
+ const anchor = ascend_to_paragraph_child(
4205
+ first_anchor_target,
4206
+ host_p
4207
+ );
3754
4208
  this._attach_comment(host_p, anchor, anchor, edit.comment);
3755
4209
  }
3756
4210
  }
@@ -3762,7 +4216,10 @@ var RedlineEngine = class {
3762
4216
  length,
3763
4217
  rebuild_map
3764
4218
  );
3765
- const virtual_spans = active_mapper.get_virtual_spans_in_range(start_idx, length);
4219
+ const virtual_spans = active_mapper.get_virtual_spans_in_range(
4220
+ start_idx,
4221
+ length
4222
+ );
3766
4223
  if (target_runs.length === 0 && virtual_spans.length === 0) return false;
3767
4224
  const affected_ps = /* @__PURE__ */ new Set();
3768
4225
  for (const run of target_runs) {
@@ -3845,7 +4302,10 @@ var RedlineEngine = class {
3845
4302
  let pPr = findChild(p1_element, "w:pPr");
3846
4303
  if (!pPr) {
3847
4304
  pPr = p1_element.ownerDocument.createElement("w:pPr");
3848
- p1_element.insertBefore(pPr, p1_element.firstChild);
4305
+ p1_element.insertBefore(
4306
+ pPr,
4307
+ p1_element.firstChild
4308
+ );
3849
4309
  }
3850
4310
  let rPr = findChild(pPr, "w:rPr");
3851
4311
  if (!rPr) {
@@ -5402,15 +5862,88 @@ function remove_all_comments(doc) {
5402
5862
  lines.push(` ${status} "${_truncate(info.text || "", 60)}" (${info.author || "Unknown"})`);
5403
5863
  cm.deleteComment(cId);
5404
5864
  }
5405
- for (const tag of ["w:commentRangeStart", "w:commentRangeEnd", "w:commentReference"]) {
5865
+ for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
5406
5866
  for (const el of findAllDescendants(doc.element, tag)) {
5407
5867
  el.parentNode?.removeChild(el);
5408
5868
  }
5409
5869
  }
5870
+ const refs = findAllDescendants(doc.element, "w:commentReference");
5871
+ for (const ref of refs) {
5872
+ const parent = ref.parentNode;
5873
+ if (parent) {
5874
+ if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
5875
+ const nonRprChildren = Array.from(parent.childNodes).filter(
5876
+ (c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
5877
+ );
5878
+ if (nonRprChildren.length <= 1) {
5879
+ parent.parentNode?.removeChild(parent);
5880
+ } else {
5881
+ parent.removeChild(ref);
5882
+ }
5883
+ } else {
5884
+ parent.removeChild(ref);
5885
+ }
5886
+ }
5887
+ }
5410
5888
  const resolvedCount = Object.values(data).filter((c) => c.resolved).length;
5411
5889
  const openCount = Object.values(data).filter((c) => !c.resolved).length;
5412
5890
  return [`Comments removed: ${keys.length} (${resolvedCount} resolved, ${openCount} open)`].concat(lines);
5413
5891
  }
5892
+ function eject_comment_parts(doc) {
5893
+ const pkg = doc.pkg;
5894
+ const comment_partnames = /* @__PURE__ */ new Set();
5895
+ for (const part of pkg.parts) {
5896
+ if (part.partname.toLowerCase().includes("comments")) {
5897
+ comment_partnames.add(part.partname);
5898
+ const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
5899
+ const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
5900
+ comment_partnames.add(withSlash);
5901
+ comment_partnames.add(withoutSlash);
5902
+ }
5903
+ }
5904
+ if (comment_partnames.size === 0) return;
5905
+ for (const part of pkg.parts) {
5906
+ if (part.partname.endsWith(".rels")) {
5907
+ const rels = findAllDescendants(part._element, "Relationship");
5908
+ const toRemove = [];
5909
+ for (const rel of rels) {
5910
+ const target = rel.getAttribute("Target") || "";
5911
+ if (target.toLowerCase().includes("comments")) {
5912
+ toRemove.push(rel);
5913
+ const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
5914
+ const sourcePart = pkg.getPartByPath(sourcePath);
5915
+ if (sourcePart) {
5916
+ const relId = rel.getAttribute("Id");
5917
+ if (relId) sourcePart.rels.delete(relId);
5918
+ }
5919
+ }
5920
+ }
5921
+ for (const relEl of toRemove) {
5922
+ relEl.parentNode?.removeChild(relEl);
5923
+ }
5924
+ }
5925
+ }
5926
+ const ctPart = pkg.getPartByPath("[Content_Types].xml");
5927
+ if (ctPart) {
5928
+ const overrides = findAllDescendants(ctPart._element, "Override");
5929
+ const toRemove = [];
5930
+ for (const override of overrides) {
5931
+ const partName = override.getAttribute("PartName") || "";
5932
+ if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
5933
+ toRemove.push(override);
5934
+ }
5935
+ }
5936
+ for (const overrideEl of toRemove) {
5937
+ overrideEl.parentNode?.removeChild(overrideEl);
5938
+ }
5939
+ }
5940
+ pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
5941
+ for (const key of Object.keys(pkg.unzipped)) {
5942
+ if (key.toLowerCase().includes("comments")) {
5943
+ delete pkg.unzipped[key];
5944
+ }
5945
+ }
5946
+ }
5414
5947
  function replace_comment_authors(doc, newAuthor) {
5415
5948
  const cm = new CommentsManager(doc);
5416
5949
  if (!cm.commentsPart) return [];
@@ -5599,6 +6132,7 @@ async function finalize_document(doc, options) {
5599
6132
  const commentsSummary = get_comments_summary(doc);
5600
6133
  report.comments_removed = commentsSummary.total;
5601
6134
  report.add_transform_lines(remove_all_comments(doc));
6135
+ eject_comment_parts(doc);
5602
6136
  } else if (options.sanitize_mode === "keep-markup") {
5603
6137
  const counts = count_tracked_changes(doc);
5604
6138
  report.tracked_changes_found = counts[0] + counts[1] + counts[2];