@adeu/core 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -76,6 +76,9 @@ function findAllDescendants(element, tagName) {
76
76
  return Array.from(element.getElementsByTagName(tagName));
77
77
  }
78
78
  function parseXml(xmlString) {
79
+ if (xmlString.startsWith("\uFEFF")) {
80
+ xmlString = xmlString.slice(1);
81
+ }
79
82
  return new import_xmldom.DOMParser().parseFromString(xmlString, "text/xml");
80
83
  }
81
84
  function serializeXml(node) {
@@ -499,6 +502,21 @@ var CommentsManager = class {
499
502
  return part;
500
503
  }
501
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
+ }
502
520
  }
503
521
  _getNextCommentId() {
504
522
  const ids = [0];
@@ -624,9 +642,9 @@ var CommentsManager = class {
624
642
  return commentId;
625
643
  }
626
644
  deleteComment(commentId) {
627
- if (!this._commentsPart) return;
645
+ if (!this.commentsPart) return;
628
646
  let commentEl = null;
629
- for (const c of findAllDescendants(this._commentsPart._element, "w:comment")) {
647
+ for (const c of findAllDescendants(this.commentsPart._element, "w:comment")) {
630
648
  if (c.getAttribute("w:id") === commentId) {
631
649
  commentEl = c;
632
650
  break;
@@ -650,7 +668,7 @@ var CommentsManager = class {
650
668
  if (child.getAttribute("w15:paraIdParent") === paraId) {
651
669
  const childParaId = child.getAttribute("w15:paraId");
652
670
  if (childParaId) {
653
- for (const c of findAllDescendants(this._commentsPart._element, "w:comment")) {
671
+ for (const c of findAllDescendants(this.commentsPart._element, "w:comment")) {
654
672
  for (const p of findAllDescendants(c, "w:p")) {
655
673
  if (p.getAttribute("w14:paraId") === childParaId) {
656
674
  const cid = c.getAttribute("w:id");
@@ -1884,6 +1902,29 @@ ${header}`;
1884
1902
 
1885
1903
  // src/diff.ts
1886
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
+ }
1887
1928
  function trim_common_context(target, new_val) {
1888
1929
  if (!target || !new_val) return [0, 0];
1889
1930
  const isSpace = (char) => /\s/.test(char);
@@ -1914,7 +1955,7 @@ function trim_common_context(target, new_val) {
1914
1955
  const left = target.substring(0, prefix_len);
1915
1956
  const b_count = (left.match(/\*\*/g) || []).length;
1916
1957
  const u2_count = (left.match(/__/g) || []).length;
1917
- const u1_count = (left.replace(/__/g, "").match(/_/g) || []).length;
1958
+ const u1_count = _count_standalone_underscores(left);
1918
1959
  if (b_count % 2 !== 0) {
1919
1960
  prefix_len = left.lastIndexOf("**");
1920
1961
  continue;
@@ -1925,10 +1966,14 @@ function trim_common_context(target, new_val) {
1925
1966
  }
1926
1967
  if (u1_count % 2 !== 0) {
1927
1968
  let idx = left.length - 1;
1969
+ const isAlnum = (char) => /[a-zA-Z0-9]/.test(char);
1928
1970
  while (idx >= 0) {
1929
1971
  if (left[idx] === "_" && (idx === 0 || left[idx - 1] !== "_") && (idx === left.length - 1 || left[idx + 1] !== "_")) {
1930
- prefix_len = idx;
1931
- 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
+ }
1932
1977
  }
1933
1978
  idx--;
1934
1979
  }
@@ -1988,7 +2033,7 @@ function trim_common_context(target, new_val) {
1988
2033
  const right = target.substring(target.length - suffix_len);
1989
2034
  const b_count = (right.match(/\*\*/g) || []).length;
1990
2035
  const u2_count = (right.match(/__/g) || []).length;
1991
- const u1_count = (right.replace(/__/g, "").match(/_/g) || []).length;
2036
+ const u1_count = _count_standalone_underscores(right);
1992
2037
  if (b_count % 2 !== 0) {
1993
2038
  suffix_len -= right.indexOf("**") + 2;
1994
2039
  continue;
@@ -1999,10 +2044,14 @@ function trim_common_context(target, new_val) {
1999
2044
  }
2000
2045
  if (u1_count % 2 !== 0) {
2001
2046
  let idx_in_right = 0;
2047
+ const isAlnum = (char) => /[a-zA-Z0-9]/.test(char);
2002
2048
  while (idx_in_right < right.length) {
2003
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] !== "_")) {
2004
- suffix_len -= idx_in_right + 1;
2005
- 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
+ }
2006
2055
  }
2007
2056
  idx_in_right++;
2008
2057
  }
@@ -2649,6 +2698,36 @@ var RedlineEngine = class {
2649
2698
  this.mapper = new DocumentMapper(this.doc);
2650
2699
  this.comments_manager = new CommentsManager(this.doc);
2651
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
+ }
2652
2731
  _scan_existing_ids() {
2653
2732
  let maxId = 0;
2654
2733
  for (const tag of ["w:ins", "w:del"]) {
@@ -2741,28 +2820,84 @@ var RedlineEngine = class {
2741
2820
  }
2742
2821
  }
2743
2822
  }
2744
- const comment_ids = /* @__PURE__ */ new Set();
2745
- for (const tag of [
2746
- "w:commentRangeStart",
2747
- "w:commentRangeEnd",
2748
- "w:commentReference"
2749
- ]) {
2750
- for (const node of findAllDescendants(this.doc.element, tag)) {
2751
- const cid = node.getAttribute("w:id");
2752
- 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
+ }
2753
2828
  }
2754
- }
2755
- const comments_part = this.doc.pkg.parts.find(
2756
- (p) => p.contentType === "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml"
2757
- );
2758
- if (comments_part) {
2759
- for (const c of findAllDescendants(comments_part._element, "w:comment")) {
2760
- const cid = c.getAttribute("w:id");
2761
- 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
+ }
2762
2846
  }
2763
2847
  }
2764
- for (const cid of comment_ids) {
2765
- 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
+ }
2766
2901
  }
2767
2902
  }
2768
2903
  _getNextId() {
@@ -2853,40 +2988,40 @@ var RedlineEngine = class {
2853
2988
  }
2854
2989
  }
2855
2990
  /**
2856
- * Inserts `text` as one or more tracked paragraphs anchored relative to
2857
- * either an existing run or a paragraph. Returns:
2858
- * { first_node, last_p, last_ins, used_block_mode }
2859
- * where:
2860
- * - first_node: the first <w:ins> (for inline mode) OR the first new <w:p>
2861
- * (for block mode). The caller uses this for splicing into the DOM and
2862
- * for anchoring comments.
2863
- * - last_p: the last new <w:p> created, if any. null when entirely inline.
2864
- * - last_ins: the last <w:ins> created (inside the last new <w:p>, or the
2865
- * sole inline ins). Used as the comment's end anchor.
2866
- * - used_block_mode: true when the first line carried a heading/list style
2867
- * marker and we created a new paragraph for it (rather than inlining it).
2868
- *
2869
- * Multi-paragraph rules (only when text contains '\n'):
2870
- * - Each additional line becomes a new <w:p>, inserted after the anchor
2871
- * paragraph in document order.
2872
- * - Each new <w:p> gets a copy of the anchor paragraph's <w:pPr> (so list
2873
- * numbering / indentation are preserved) unless the line itself starts
2874
- * with a markdown heading or list marker, which overrides the style.
2875
- * - Each new <w:p> carries a tracked paragraph-break marker
2876
- * (<w:pPr><w:rPr><w:ins/></w:rPr></w:pPr>) so Word natively tracks the
2877
- * paragraph break.
2878
- * - Each new <w:p>'s content is wrapped in a <w:ins>, with inline bold/
2879
- * italic markdown parsed via _parse_inline_markdown.
2880
- *
2881
- * The first line:
2882
- * - If it carries a heading / list marker AND we have a paragraph anchor,
2883
- * we drop into "block mode": no inline <w:ins>; the first line itself
2884
- * becomes the first new <w:p>.
2885
- * - Otherwise we emit a single inline <w:ins> for the first line (current
2886
- * behaviour) and treat the remaining lines as block extensions.
2887
- *
2888
- * Does NOT attach comments; callers handle that.
2889
- */
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
+ */
2890
3025
  _track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id) {
2891
3026
  if (!text) {
2892
3027
  return {
@@ -3026,7 +3161,15 @@ var RedlineEngine = class {
3026
3161
  const anchor_rPr = findChild(anchor_run._element, "w:rPr");
3027
3162
  if (anchor_rPr) {
3028
3163
  const clone = anchor_rPr.cloneNode(true);
3029
- 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
+ ]) {
3030
3173
  const found = findChild(clone, tag);
3031
3174
  if (found) clone.removeChild(found);
3032
3175
  }
@@ -3300,6 +3443,16 @@ var RedlineEngine = class {
3300
3443
  matches = this.clean_mapper.find_all_match_indices(edit.target_text);
3301
3444
  if (matches.length > 0) activeText = this.clean_mapper.full_text;
3302
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
+ }
3303
3456
  if (matches.length === 0) {
3304
3457
  errors.push(
3305
3458
  `- Edit ${i + 1} Failed: Target text not found in document:
@@ -3319,6 +3472,31 @@ var RedlineEngine = class {
3319
3472
  )
3320
3473
  );
3321
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
+ }
3322
3500
  for (const [start, length] of matches) {
3323
3501
  const spans = this.mapper.spans.filter(
3324
3502
  (s) => s.end > start && s.start < start + length
@@ -3382,7 +3560,33 @@ var RedlineEngine = class {
3382
3560
  }
3383
3561
  return errors;
3384
3562
  }
3385
- 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) {
3386
3590
  this.skipped_details = [];
3387
3591
  const actions = changes.filter(
3388
3592
  (c) => ["accept", "reject", "reply"].includes(c.type)
@@ -3390,38 +3594,129 @@ var RedlineEngine = class {
3390
3594
  const edits = changes.filter(
3391
3595
  (c) => !["accept", "reject", "reply"].includes(c.type)
3392
3596
  );
3393
- const all_errors = [];
3394
- if (actions.length > 0) {
3395
- all_errors.push(...this.validate_review_actions(actions));
3396
- }
3397
- if (edits.length > 0) {
3398
- all_errors.push(...this.validate_edits(edits));
3399
- }
3400
- if (all_errors.length > 0) {
3401
- 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
+ }
3402
3615
  }
3403
- let applied_actions = 0, skipped_actions = 0;
3616
+ let applied_actions = 0;
3617
+ let skipped_actions = 0;
3404
3618
  if (actions.length > 0) {
3405
3619
  const res = this.apply_review_actions(actions);
3406
3620
  applied_actions = res[0];
3407
3621
  skipped_actions = res[1];
3622
+ if (skipped_actions > 0) {
3623
+ throw new BatchValidationError(this.skipped_details);
3624
+ }
3408
3625
  if (applied_actions > 0) {
3409
3626
  this.mapper["_build_map"]();
3410
3627
  if (this.clean_mapper) this.clean_mapper["_build_map"]();
3411
3628
  }
3412
3629
  }
3413
- let applied_edits = 0, skipped_edits = 0;
3630
+ const edits_reports = [];
3631
+ let applied_edits = 0;
3632
+ let skipped_edits = 0;
3414
3633
  if (edits.length > 0) {
3415
- const res = this.apply_edits(edits);
3416
- applied_edits = res[0];
3417
- 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
+ }
3418
3710
  }
3419
3711
  return {
3420
3712
  actions_applied: applied_actions,
3421
3713
  actions_skipped: skipped_actions,
3422
3714
  edits_applied: applied_edits,
3423
3715
  edits_skipped: skipped_edits,
3424
- skipped_details: this.skipped_details
3716
+ skipped_details: this.skipped_details,
3717
+ edits: edits_reports,
3718
+ engine: "node",
3719
+ version: "1.9.0"
3425
3720
  };
3426
3721
  }
3427
3722
  apply_edits(edits) {
@@ -3429,50 +3724,90 @@ var RedlineEngine = class {
3429
3724
  let skipped = 0;
3430
3725
  const resolved_edits = [];
3431
3726
  for (const edit of edits) {
3432
- 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;
3433
3735
  resolved_edits.push([edit, edit.new_text || null]);
3434
3736
  } else if (edit.type === "insert_row" || edit.type === "delete_row") {
3435
- const [idx] = this.mapper.find_match_index(edit.target_text);
3436
- if (idx !== -1) {
3437
- 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];
3438
3746
  resolved_edits.push([edit, null]);
3439
3747
  } else {
3440
3748
  skipped++;
3441
- this.skipped_details.push(
3442
- `- Failed to locate row target: '${(edit.target_text || "").substring(0, 40)}...'`
3443
- );
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;
3444
3754
  }
3445
3755
  } else {
3446
3756
  const resolved = this._pre_resolve_heuristic_edit(edit);
3447
3757
  if (resolved) {
3448
3758
  if (Array.isArray(resolved)) {
3449
- 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
+ }
3450
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;
3451
3775
  resolved_edits.push([resolved, resolved.new_text]);
3452
3776
  }
3453
3777
  } else {
3454
3778
  skipped++;
3455
- this.skipped_details.push(
3456
- `- Failed to apply edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3457
- );
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;
3458
3785
  }
3459
3786
  }
3460
3787
  }
3461
3788
  resolved_edits.sort(
3462
- (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)
3463
3790
  );
3464
3791
  const occupied_ranges = [];
3465
3792
  for (const [edit, orig_new] of resolved_edits) {
3466
- const start = edit._match_start_index || 0;
3793
+ const start = edit._resolved_start_idx || 0;
3467
3794
  const end = start + (edit.target_text ? edit.target_text.length : 0);
3468
3795
  const overlaps = occupied_ranges.some(
3469
3796
  ([occ_start, occ_end]) => start < occ_end && end > occ_start
3470
3797
  );
3471
3798
  if (overlaps) {
3472
3799
  skipped++;
3473
- this.skipped_details.push(
3474
- `- Skipped overlapping edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3475
- );
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
+ }
3476
3811
  continue;
3477
3812
  }
3478
3813
  let success = false;
@@ -3484,11 +3819,26 @@ var RedlineEngine = class {
3484
3819
  if (success) {
3485
3820
  applied++;
3486
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
+ }
3487
3827
  } else {
3488
3828
  skipped++;
3489
- this.skipped_details.push(
3490
- `- Failed to apply edit targeting: '${(edit.target_text || "insertion").substring(0, 40)}...'`
3491
- );
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
+ }
3492
3842
  }
3493
3843
  }
3494
3844
  return [applied, skipped];
@@ -3581,7 +3931,7 @@ var RedlineEngine = class {
3581
3931
  return [applied, skipped];
3582
3932
  }
3583
3933
  _apply_table_edit(edit, rebuild_map) {
3584
- 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;
3585
3935
  const [anchor_run, anchor_para] = this.mapper.get_insertion_anchor(
3586
3936
  start_idx,
3587
3937
  rebuild_map
@@ -3625,9 +3975,31 @@ var RedlineEngine = class {
3625
3975
  }
3626
3976
  return false;
3627
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
+ }
3628
4000
  _pre_resolve_heuristic_edit(edit) {
3629
4001
  if (!edit.target_text) return null;
3630
- 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);
3631
4003
  let use_clean_map = false;
3632
4004
  if (start_idx === -1) {
3633
4005
  if (!this.clean_mapper)
@@ -3691,7 +4063,7 @@ var RedlineEngine = class {
3691
4063
  _apply_single_edit_indexed(edit, orig_new, rebuild_map) {
3692
4064
  let op = edit._internal_op;
3693
4065
  const active_mapper = edit._active_mapper_ref || this.mapper;
3694
- 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;
3695
4067
  const length = edit.target_text ? edit.target_text.length : 0;
3696
4068
  const del_id = ["DELETION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
3697
4069
  const ins_id = ["INSERTION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
@@ -3740,6 +4112,76 @@ var RedlineEngine = class {
3740
4112
  rebuild_map
3741
4113
  );
3742
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
+ }
3743
4185
  const result = this._track_insert_multiline(
3744
4186
  edit.new_text || "",
3745
4187
  anchor_run,
@@ -5449,15 +5891,88 @@ function remove_all_comments(doc) {
5449
5891
  lines.push(` ${status} "${_truncate(info.text || "", 60)}" (${info.author || "Unknown"})`);
5450
5892
  cm.deleteComment(cId);
5451
5893
  }
5452
- for (const tag of ["w:commentRangeStart", "w:commentRangeEnd", "w:commentReference"]) {
5894
+ for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
5453
5895
  for (const el of findAllDescendants(doc.element, tag)) {
5454
5896
  el.parentNode?.removeChild(el);
5455
5897
  }
5456
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
+ }
5457
5917
  const resolvedCount = Object.values(data).filter((c) => c.resolved).length;
5458
5918
  const openCount = Object.values(data).filter((c) => !c.resolved).length;
5459
5919
  return [`Comments removed: ${keys.length} (${resolvedCount} resolved, ${openCount} open)`].concat(lines);
5460
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
+ }
5461
5976
  function replace_comment_authors(doc, newAuthor) {
5462
5977
  const cm = new CommentsManager(doc);
5463
5978
  if (!cm.commentsPart) return [];
@@ -5646,6 +6161,7 @@ async function finalize_document(doc, options) {
5646
6161
  const commentsSummary = get_comments_summary(doc);
5647
6162
  report.comments_removed = commentsSummary.total;
5648
6163
  report.add_transform_lines(remove_all_comments(doc));
6164
+ eject_comment_parts(doc);
5649
6165
  } else if (options.sanitize_mode === "keep-markup") {
5650
6166
  const counts = count_tracked_changes(doc);
5651
6167
  report.tracked_changes_found = counts[0] + counts[1] + counts[2];