@adeu/core 1.17.0 → 1.17.2

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
@@ -1333,7 +1333,19 @@ ${header}`;
1333
1333
  this._add_virtual_text(" | ", current, null);
1334
1334
  current += 3;
1335
1335
  }
1336
+ const cell_start = current;
1336
1337
  current = this._map_blocks(cell, current);
1338
+ if (!this.clean_view && !this.original_view) {
1339
+ const firstP = cell._element.getElementsByTagName("w:p")[0];
1340
+ const paraId = firstP ? firstP.getAttribute("w14:paraId") : null;
1341
+ if (paraId && firstP) {
1342
+ const cellPara = new Paragraph(firstP, cell);
1343
+ this._add_virtual_text("", current, cellPara);
1344
+ const anchor = `{#cell:${paraId}}`;
1345
+ this._add_virtual_text(anchor, current, cellPara);
1346
+ current += anchor.length;
1347
+ }
1348
+ }
1337
1349
  cells_processed += 1;
1338
1350
  }
1339
1351
  if (ins && !this.clean_view && !this.original_view) {
@@ -1710,7 +1722,7 @@ ${header}`;
1710
1722
  target_text = this._strip_markdown_formatting(target_text);
1711
1723
  target_text = this._replace_smart_quotes(target_text);
1712
1724
  const parts = [];
1713
- const token_pattern = /(\[_+\])|(\s+)|(['"])|([.,;:\/])/g;
1725
+ const token_pattern = /(\[_+\])|(\s+)|(['"])|([.,;:\/\-\[\](){}+=$?*!|#^<>\\%&@~`_])/g;
1714
1726
  let last_idx = 0;
1715
1727
  let match;
1716
1728
  const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -2596,7 +2608,7 @@ function _refine_match_boundaries(text, start, end) {
2596
2608
  function _make_fuzzy_regex(target_text) {
2597
2609
  target_text = _replace_smart_quotes(target_text);
2598
2610
  const parts = [];
2599
- const token_pattern = /(_+)|(\s+)|(['"])|([.,;:\/])/g;
2611
+ const token_pattern = /(_+)|(\s+)|(['"])|([.,;:\/\-\[\](){}+=$?*!|#^<>\\%&@~`])/g;
2600
2612
  const md_noise = "[*_]*";
2601
2613
  const structural_noise = "(?:\\s*(?:[*+\\->]|\\d+\\.)\\s+|\\s*\\n\\s*)";
2602
2614
  const start_list_marker = "(?:[ \\t]*(?:[*+\\->]|\\d+\\.)\\s+)?";
@@ -2783,8 +2795,15 @@ function format_ambiguity_error(edit_index, target_text, haystack, match_positio
2783
2795
  if (remaining > 0) {
2784
2796
  lines.push(` ... and ${remaining} more occurrence(s) not shown.`);
2785
2797
  }
2798
+ lines.push(" To resolve, re-send this edit using ONE of these strategies:");
2799
+ lines.push(
2800
+ ` 1. Set "match_mode": "all" to modify ALL ${total} occurrences (same target_text).`
2801
+ );
2802
+ lines.push(
2803
+ ' 2. Set "match_mode": "first" to modify only the FIRST occurrence (same target_text).'
2804
+ );
2786
2805
  lines.push(
2787
- " Please provide more surrounding context in your target_text to uniquely identify the location."
2806
+ ' 3. Please provide more surrounding context in your target_text to uniquely identify a single location (keep the default "match_mode": "strict").'
2788
2807
  );
2789
2808
  return lines.join("\n");
2790
2809
  }
@@ -2811,6 +2830,51 @@ function insertAfter(newNode, refNode) {
2811
2830
  refNode.parentNode.insertBefore(newNode, refNode.nextSibling);
2812
2831
  }
2813
2832
  }
2833
+ function takeSnapshot(doc) {
2834
+ const parts = [...doc.pkg.parts];
2835
+ const unzipped = { ...doc.pkg.unzipped };
2836
+ const rels = /* @__PURE__ */ new Map();
2837
+ const elements = /* @__PURE__ */ new Map();
2838
+ for (const part of parts) {
2839
+ rels.set(part, new Map(part.rels));
2840
+ if (part._element) {
2841
+ elements.set(part, part._element.cloneNode(true));
2842
+ }
2843
+ }
2844
+ return { parts, unzipped, rels, elements };
2845
+ }
2846
+ function restoreSnapshot(doc, snapshot) {
2847
+ doc.pkg.parts = [...snapshot.parts];
2848
+ for (const key of Object.keys(doc.pkg.unzipped)) {
2849
+ delete doc.pkg.unzipped[key];
2850
+ }
2851
+ for (const [key, val] of Object.entries(snapshot.unzipped)) {
2852
+ doc.pkg.unzipped[key] = val;
2853
+ }
2854
+ for (const part of snapshot.parts) {
2855
+ part.rels = new Map(snapshot.rels.get(part));
2856
+ const originalEl = snapshot.elements.get(part);
2857
+ if (originalEl && part._element) {
2858
+ const xmlDoc = part._element.ownerDocument;
2859
+ if (xmlDoc && xmlDoc.documentElement) {
2860
+ xmlDoc.replaceChild(originalEl, xmlDoc.documentElement);
2861
+ }
2862
+ part._element = originalEl;
2863
+ }
2864
+ }
2865
+ }
2866
+ function stripMatchingHeadingHashes(target, newText) {
2867
+ if (!target || !newText) return [target, newText];
2868
+ const targetMatch = target.match(/^(#+)\s+/);
2869
+ const newMatch = newText.match(/^(#+)\s+/);
2870
+ if (targetMatch && newMatch && targetMatch[1] === newMatch[1]) {
2871
+ const hashes = targetMatch[1];
2872
+ const targetClean = target.substring(hashes.length).trimStart();
2873
+ const newClean = newText.substring(hashes.length).trimStart();
2874
+ return [targetClean, newClean];
2875
+ }
2876
+ return [target, newText];
2877
+ }
2814
2878
  var BatchValidationError = class extends Error {
2815
2879
  errors;
2816
2880
  constructor(errors) {
@@ -2819,7 +2883,7 @@ var BatchValidationError = class extends Error {
2819
2883
  this.errors = errors;
2820
2884
  }
2821
2885
  };
2822
- function validate_edit_strings(edits) {
2886
+ function validate_edit_strings(edits, index_offset = 0) {
2823
2887
  const errors = [];
2824
2888
  for (let i = 0; i < edits.length; i++) {
2825
2889
  const edit = edits[i];
@@ -2827,7 +2891,7 @@ function validate_edit_strings(edits) {
2827
2891
  const n_text = edit.new_text || "";
2828
2892
  if (n_text.includes("{++") || n_text.includes("{--") || n_text.includes("{>>") || n_text.includes("{==")) {
2829
2893
  errors.push(
2830
- `- Edit ${i + 1} Failed: Do not manually write CriticMarkup tags ({++, {--, {>>, {==) in \`new_text\`. The engine handles redlining automatically. To add a comment, use the \`comment\` parameter.`
2894
+ `- Edit ${i + 1 + index_offset} Failed: Do not manually write CriticMarkup tags ({++, {--, {>>, {==) in \`new_text\`. The engine handles redlining automatically. To add a comment, use the \`comment\` parameter.`
2831
2895
  );
2832
2896
  }
2833
2897
  if (t_text.includes("[^") || n_text.includes("[^")) {
@@ -2838,11 +2902,11 @@ function validate_edit_strings(edits) {
2838
2902
  (f) => n_fns.filter((x) => x === f).length > t_fns.filter((x) => x === f).length
2839
2903
  )) {
2840
2904
  errors.push(
2841
- `- Edit ${i + 1} Failed: Cannot insert footnote/endnote markers via text replace. Markers like \`[^fn-N]\` are read-only projections. Use Word's References menu.`
2905
+ `- Edit ${i + 1 + index_offset} Failed: Cannot insert footnote/endnote markers via text replace. Markers like \`[^fn-N]\` are read-only projections. Use Word's References menu.`
2842
2906
  );
2843
2907
  } else {
2844
2908
  errors.push(
2845
- `- Edit ${i + 1} Failed: Cannot delete footnote/endnote references via text replace. The marker corresponds to a structural XML element.`
2909
+ `- Edit ${i + 1 + index_offset} Failed: Cannot delete footnote/endnote references via text replace. The marker corresponds to a structural XML element.`
2846
2910
  );
2847
2911
  }
2848
2912
  }
@@ -2853,16 +2917,16 @@ function validate_edit_strings(edits) {
2853
2917
  if (t_links.length !== n_links.length) {
2854
2918
  if (n_links.length > t_links.length) {
2855
2919
  errors.push(
2856
- `- Edit ${i + 1} Failed: Cannot insert hyperlinks via text replace. Use a dedicated structural operation.`
2920
+ `- Edit ${i + 1 + index_offset} Failed: Cannot insert hyperlinks via text replace. Use a dedicated structural operation.`
2857
2921
  );
2858
2922
  } else {
2859
2923
  errors.push(
2860
- `- Edit ${i + 1} Failed: Cannot delete hyperlinks via text replace. The marker corresponds to a structural XML element.`
2924
+ `- Edit ${i + 1 + index_offset} Failed: Cannot delete hyperlinks via text replace. The marker corresponds to a structural XML element.`
2861
2925
  );
2862
2926
  }
2863
2927
  } else if (t_links.length > 1 && JSON.stringify(t_links) !== JSON.stringify(n_links)) {
2864
2928
  errors.push(
2865
- `- Edit ${i + 1} Failed: Can only edit or retarget one hyperlink per text replacement. Please split into multiple edits.`
2929
+ `- Edit ${i + 1 + index_offset} Failed: Can only edit or retarget one hyperlink per text replacement. Please split into multiple edits.`
2866
2930
  );
2867
2931
  }
2868
2932
  }
@@ -2872,17 +2936,17 @@ function validate_edit_strings(edits) {
2872
2936
  if (t_xrefs.length !== n_xrefs.length) {
2873
2937
  if (n_xrefs.length > t_xrefs.length) {
2874
2938
  errors.push(
2875
- `- Edit ${i + 1} Failed: Cannot insert cross-references via text replace. Markers are read-only projections.`
2939
+ `- Edit ${i + 1 + index_offset} Failed: Cannot insert cross-references via text replace. Markers are read-only projections.`
2876
2940
  );
2877
2941
  } else {
2878
2942
  errors.push(
2879
- `- Edit ${i + 1} Failed: Cannot delete cross-references via text replace. The marker corresponds to a structural XML element.`
2943
+ `- Edit ${i + 1 + index_offset} Failed: Cannot delete cross-references via text replace. The marker corresponds to a structural XML element.`
2880
2944
  );
2881
2945
  }
2882
2946
  } else {
2883
2947
  if (JSON.stringify(t_xrefs) !== JSON.stringify(n_xrefs)) {
2884
2948
  errors.push(
2885
- `- Edit ${i + 1} Failed: Modifying or retargeting cross-reference markers is disallowed to prevent dependency corruption.`
2949
+ `- Edit ${i + 1 + index_offset} Failed: Modifying or retargeting cross-reference markers is disallowed to prevent dependency corruption.`
2886
2950
  );
2887
2951
  }
2888
2952
  }
@@ -2893,7 +2957,7 @@ function validate_edit_strings(edits) {
2893
2957
  for (const a of n_anchors) {
2894
2958
  if (n_anchors.filter((x) => x === a).length > t_anchors.filter((x) => x === a).length) {
2895
2959
  errors.push(
2896
- `- Edit ${i + 1} Failed: Cannot modify or insert internal anchor markers (\`{#...}\`). These represent structural XML bookmarks.`
2960
+ `- Edit ${i + 1 + index_offset} Failed: Cannot modify or insert internal anchor markers (\`{#...}\`). These represent structural XML bookmarks.`
2897
2961
  );
2898
2962
  break;
2899
2963
  }
@@ -2907,16 +2971,16 @@ function validate_edit_strings(edits) {
2907
2971
  const level = stripped.length - stripped.replace(/^#+/, "").length;
2908
2972
  if (stripped.substring(level).startsWith(" ") || stripped.substring(level) === "") {
2909
2973
  errors.push(
2910
- `- Edit ${i + 1} Failed: Heading level ${level} is not supported (maximum is 6).`
2974
+ `- Edit ${i + 1 + index_offset} Failed: Heading level ${level} is not supported (maximum is 6).`
2911
2975
  );
2912
2976
  break;
2913
2977
  }
2914
2978
  }
2915
2979
  }
2916
2980
  }
2917
- if (t_text.includes("READONLY_BOUNDARY_START") || n_text.includes("READONLY_BOUNDARY_START") || t_text.includes("# Document Structure (Read-Only)") || n_text.includes("# Document Structure (Read-Only)")) {
2981
+ if (t_text.includes("READONLY_BOUNDARY_START") || n_text.includes("READONLY_BOUNDARY_START") || t_text.includes("# Document Structure (Read-Only)") || n_text.includes("# Document Structure (Read-Only)") || t_text.includes("Document Structure (Read-Only)") || n_text.includes("Document Structure (Read-Only)")) {
2918
2982
  errors.push(
2919
- `- Edit ${i + 1} Failed: Modification targets the read-only boundary (Structural Appendix). This section cannot be edited.`
2983
+ `- Edit ${i + 1 + index_offset} Failed: Modification targets the read-only boundary (Structural Appendix). This section cannot be edited.`
2920
2984
  );
2921
2985
  }
2922
2986
  }
@@ -2938,7 +3002,7 @@ var RedlineEngine = class {
2938
3002
  this.timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
2939
3003
  const w16du_ns = "http://schemas.microsoft.com/office/word/2023/wordml/word16du";
2940
3004
  for (const part of this.doc.pkg.parts) {
2941
- if (part === this.doc.part || part.contentType.includes("wordprocessingml") && part.contentType.endsWith("+xml")) {
3005
+ if (part === this.doc.part) {
2942
3006
  if (!part._element.hasAttribute("xmlns:w16du")) {
2943
3007
  part._element.setAttribute("xmlns:w16du", w16du_ns);
2944
3008
  }
@@ -2955,15 +3019,59 @@ var RedlineEngine = class {
2955
3019
  }
2956
3020
  return null;
2957
3021
  }
3022
+ /**
3023
+ * Best-effort "did you mean" hint for a failed target. The common loop trap
3024
+ * (observed in the field) is an anchored regex like `^\( x \)$` against a
3025
+ * mid-document string: ^/$ bind to the whole full_text, so it never matches
3026
+ * even though the literal `( x )` is present. We strip regex anchoring/escapes
3027
+ * and probe full_text for a literal occurrence; if found, we tell the model
3028
+ * the exact literal that WOULD match so it drops the anchors instead of
3029
+ * escalating the regex further.
3030
+ */
3031
+ _nearest_match_hint(target_text, is_regex) {
3032
+ if (!target_text) return "";
3033
+ let probe = target_text;
3034
+ if (is_regex) {
3035
+ probe = probe.replace(/^\^/, "").replace(/\$$/, "");
3036
+ probe = probe.replace(/^\\s\*/, "").replace(/\\s\*$/, "");
3037
+ probe = probe.replace(/\\([.^$*+?()[\]{}|\\/])/g, "$1");
3038
+ }
3039
+ probe = probe.trim();
3040
+ if (!probe || probe === target_text) {
3041
+ if (!is_regex) return "";
3042
+ }
3043
+ const idx = this.mapper.full_text.indexOf(probe);
3044
+ if (idx !== -1) {
3045
+ const ctx_start = Math.max(0, idx - 15);
3046
+ const ctx_end = Math.min(
3047
+ this.mapper.full_text.length,
3048
+ idx + probe.length + 15
3049
+ );
3050
+ const ctx = this.mapper.full_text.substring(ctx_start, ctx_end).replace(/\n/g, " ");
3051
+ return `
3052
+ Did you mean the literal "${probe}"? It appears in the document (\u2026${ctx}\u2026). If you used a regex, drop the ^/$ anchors \u2014 they match the start/end of the entire document, not a line.`;
3053
+ }
3054
+ return "";
3055
+ }
2958
3056
  _build_edit_context_previews(edit) {
2959
3057
  if (edit.type !== "modify") return [null, null];
2960
3058
  if (edit._resolved_proxy_edit) {
2961
3059
  edit = edit._resolved_proxy_edit;
2962
3060
  }
2963
- const start_idx = edit._resolved_start_idx;
3061
+ let start_idx = edit._resolved_start_idx;
2964
3062
  if (start_idx === void 0 || start_idx === null) return [null, null];
2965
- const target_text = edit.target_text || "";
2966
- const new_text = edit.new_text || "";
3063
+ let target_text = edit.target_text || "";
3064
+ let new_text = edit.new_text || "";
3065
+ const [clean_target, target_style] = this._parse_markdown_style(target_text);
3066
+ if (target_style && target_style.startsWith("Heading")) {
3067
+ const prefix_len = target_text.length - clean_target.length;
3068
+ start_idx += prefix_len;
3069
+ target_text = clean_target;
3070
+ }
3071
+ const [clean_new, new_style] = this._parse_markdown_style(new_text);
3072
+ if (new_style && new_style.startsWith("Heading")) {
3073
+ new_text = clean_new;
3074
+ }
2967
3075
  const length = target_text.length;
2968
3076
  const active_mapper = edit._active_mapper_ref || this.mapper;
2969
3077
  const full_text = active_mapper.full_text;
@@ -3715,28 +3823,34 @@ var RedlineEngine = class {
3715
3823
  }
3716
3824
  }
3717
3825
  }
3718
- validate_edits(edits) {
3826
+ validate_edits(edits, index_offset = 0) {
3719
3827
  const errors = [];
3720
3828
  if (!this.mapper.full_text) this.mapper["_build_map"]();
3721
- errors.push(...validate_edit_strings(edits));
3829
+ errors.push(...validate_edit_strings(edits, index_offset));
3722
3830
  for (let i = 0; i < edits.length; i++) {
3723
3831
  const edit = edits[i];
3724
3832
  if (typeof edit !== "object" || edit === null) {
3725
3833
  errors.push(
3726
- `- Edit ${i + 1} Failed: Invalid change format. Expected a JSON object, but received a primitive ${typeof edit}. Do not pass raw strings.`
3834
+ `- Edit ${i + 1 + index_offset} Failed: Invalid change format. Expected a JSON object, but received a primitive ${typeof edit}. Do not pass raw strings.`
3727
3835
  );
3728
3836
  continue;
3729
3837
  }
3730
3838
  if (!edit.target_text) continue;
3731
3839
  const is_regex = edit.regex || false;
3732
3840
  const match_mode = edit.match_mode || "strict";
3733
- let matches = this.mapper.find_all_match_indices(edit.target_text, is_regex);
3841
+ let matches = this.mapper.find_all_match_indices(
3842
+ edit.target_text,
3843
+ is_regex
3844
+ );
3734
3845
  let activeText = this.mapper.full_text;
3735
3846
  let target_mapper = this.mapper;
3736
3847
  if (matches.length === 0) {
3737
3848
  if (!this.clean_mapper)
3738
3849
  this.clean_mapper = new DocumentMapper(this.doc, true);
3739
- matches = this.clean_mapper.find_all_match_indices(edit.target_text, is_regex);
3850
+ matches = this.clean_mapper.find_all_match_indices(
3851
+ edit.target_text,
3852
+ is_regex
3853
+ );
3740
3854
  if (matches.length > 0) {
3741
3855
  activeText = this.clean_mapper.full_text;
3742
3856
  target_mapper = this.clean_mapper;
@@ -3758,7 +3872,10 @@ var RedlineEngine = class {
3758
3872
  if (!this.original_mapper) {
3759
3873
  this.original_mapper = new DocumentMapper(this.doc, false, true);
3760
3874
  }
3761
- const orig_matches = this.original_mapper.find_all_match_indices(edit.target_text, is_regex);
3875
+ const orig_matches = this.original_mapper.find_all_match_indices(
3876
+ edit.target_text,
3877
+ is_regex
3878
+ );
3762
3879
  if (orig_matches.length > 0) {
3763
3880
  is_deleted_text = true;
3764
3881
  for (const [start, length] of orig_matches) {
@@ -3787,27 +3904,32 @@ var RedlineEngine = class {
3787
3904
  if (is_deleted_text) {
3788
3905
  const author_phrase = deleted_authors.size > 0 ? `by ${Array.from(deleted_authors).sort().join(", ")}` : "by an existing revision";
3789
3906
  errors.push(
3790
- `- Edit ${i + 1} Failed: Target text matches text inside a tracked deletion ${author_phrase}. Reject/accept that change first or target the active replacement text instead.`
3907
+ `- Edit ${i + 1 + index_offset} Failed: Target text matches text inside a tracked deletion ${author_phrase}. Reject/accept that change first or target the active replacement text instead.`
3791
3908
  );
3792
3909
  } else {
3910
+ const hint = this._nearest_match_hint(edit.target_text, is_regex);
3793
3911
  errors.push(
3794
- `- Edit ${i + 1} Failed: Target text not found in document:
3795
- "${edit.target_text}"`
3912
+ `- Edit ${i + 1 + index_offset} Failed: Target text not found in document:
3913
+ "${edit.target_text}"${hint}`
3796
3914
  );
3797
3915
  }
3798
3916
  } else if (matches.length > 1 && match_mode === "strict") {
3799
- const positions = matches.map(([start, length]) => [
3800
- start,
3801
- start + length
3802
- ]);
3803
- errors.push(
3804
- format_ambiguity_error(
3805
- i + 1,
3806
- edit.target_text,
3807
- activeText,
3808
- positions
3809
- )
3810
- );
3917
+ if (edit.target_text.includes("|")) {
3918
+ matches = matches.slice(0, 1);
3919
+ } else {
3920
+ const positions = matches.map(([start, length]) => [
3921
+ start,
3922
+ start + length
3923
+ ]);
3924
+ errors.push(
3925
+ format_ambiguity_error(
3926
+ i + 1 + index_offset,
3927
+ edit.target_text,
3928
+ activeText,
3929
+ positions
3930
+ )
3931
+ );
3932
+ }
3811
3933
  }
3812
3934
  if (matches.length === 1) {
3813
3935
  const [m_start, m_len] = matches[0];
@@ -3824,14 +3946,14 @@ var RedlineEngine = class {
3824
3946
  const parts = matched.split("\n\n");
3825
3947
  if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3826
3948
  errors.push(
3827
- `- 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.`
3949
+ `- Edit ${i + 1 + index_offset} 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.`
3828
3950
  );
3829
3951
  }
3830
3952
  } else {
3831
3953
  const parts = final_target.split("\n\n");
3832
3954
  if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3833
3955
  errors.push(
3834
- `- 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.`
3956
+ `- Edit ${i + 1 + index_offset} 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.`
3835
3957
  );
3836
3958
  }
3837
3959
  }
@@ -3850,7 +3972,13 @@ var RedlineEngine = class {
3850
3972
  ).filter((n) => n.getAttribute("w:id") === s.ins_id);
3851
3973
  if (insNodes.length > 0) {
3852
3974
  const auth = insNodes[0].getAttribute("w:author");
3853
- if (auth && auth !== this.author) nestedAuthors.add(auth);
3975
+ if (auth && auth !== this.author) {
3976
+ const is_fully_contained_in_ins = start >= s.start && start + length <= s.end;
3977
+ const is_lockout = is_fully_contained_in_ins || match_mode === "all";
3978
+ if (is_lockout) {
3979
+ nestedAuthors.add(auth);
3980
+ }
3981
+ }
3854
3982
  }
3855
3983
  }
3856
3984
  if (s.comment_ids) {
@@ -3864,7 +3992,7 @@ var RedlineEngine = class {
3864
3992
  }
3865
3993
  if (nestedAuthors.size > 0) {
3866
3994
  errors.push(
3867
- `- Edit ${i + 1} Failed: Modification targets an active insertion from another author (${Array.from(nestedAuthors).join(", ")}). Accept that change first or scope your edit outside of it.`
3995
+ `- Edit ${i + 1 + index_offset} Failed: Modification targets an active insertion from another author (${Array.from(nestedAuthors).join(", ")}). Accept that change first or scope your edit outside of it.`
3868
3996
  );
3869
3997
  }
3870
3998
  }
@@ -3926,22 +4054,13 @@ var RedlineEngine = class {
3926
4054
  });
3927
4055
  }
3928
4056
  if (dry_run) {
3929
- const baselines = /* @__PURE__ */ new Map();
3930
- for (const part of this.doc.pkg.parts) {
3931
- if (part._element) {
3932
- baselines.set(part, part._element.cloneNode(true));
3933
- }
3934
- }
4057
+ const snapshot = takeSnapshot(this.doc);
4058
+ const originalCurrentId = this.current_id;
3935
4059
  try {
3936
4060
  return this._process_batch_internal(changes, true);
3937
4061
  } finally {
3938
- for (const [part, originalEl] of baselines.entries()) {
3939
- const doc = part._element.ownerDocument;
3940
- if (doc && doc.documentElement) {
3941
- doc.replaceChild(originalEl, doc.documentElement);
3942
- }
3943
- part._element = originalEl;
3944
- }
4062
+ restoreSnapshot(this.doc, snapshot);
4063
+ this.current_id = originalCurrentId;
3945
4064
  this.mapper = new DocumentMapper(this.doc);
3946
4065
  this.comments_manager = new CommentsManager(this.doc);
3947
4066
  this.clean_mapper = null;
@@ -3951,6 +4070,13 @@ var RedlineEngine = class {
3951
4070
  }
3952
4071
  }
3953
4072
  _process_batch_internal(changes, dry_run_mode = false) {
4073
+ for (const c of changes) {
4074
+ if (c && typeof c === "object" && c.type === "modify" && c.target_text && c.new_text) {
4075
+ const [strippedTarget, strippedNew] = stripMatchingHeadingHashes(c.target_text, c.new_text);
4076
+ c.target_text = strippedTarget;
4077
+ c.new_text = strippedNew;
4078
+ }
4079
+ }
3954
4080
  this.skipped_details = [];
3955
4081
  const actions = changes.filter(
3956
4082
  (c) => c !== null && typeof c === "object" && ["accept", "reject", "reply"].includes(c.type)
@@ -3958,14 +4084,67 @@ var RedlineEngine = class {
3958
4084
  const edits = changes.filter(
3959
4085
  (c) => c === null || typeof c !== "object" || !["accept", "reject", "reply"].includes(c.type)
3960
4086
  );
3961
- if (!dry_run_mode) {
3962
- const all_errors = [];
3963
- if (actions.length > 0) {
3964
- all_errors.push(...this.validate_review_actions(actions));
4087
+ let mapper_dirty = false;
4088
+ for (const edit of edits) {
4089
+ if (typeof edit !== "object" || edit === null || !edit.target_text) continue;
4090
+ const is_regex = edit.regex || false;
4091
+ let matches = this.mapper.find_all_match_indices(edit.target_text, is_regex);
4092
+ let target_mapper = this.mapper;
4093
+ if (matches.length === 0) {
4094
+ if (!this.clean_mapper) {
4095
+ this.clean_mapper = new DocumentMapper(this.doc, true);
4096
+ }
4097
+ matches = this.clean_mapper.find_all_match_indices(edit.target_text, is_regex);
4098
+ target_mapper = this.clean_mapper;
3965
4099
  }
3966
- if (edits.length > 0) {
3967
- all_errors.push(...this.validate_edits(edits));
4100
+ for (const [start, length] of matches) {
4101
+ const spans = target_mapper.spans.filter(
4102
+ (s) => s.end > start && s.start < start + length
4103
+ );
4104
+ for (const s of spans) {
4105
+ if (s.ins_id) {
4106
+ const insNodes = findAllDescendants(this.doc.element, "w:ins").filter(
4107
+ (n) => n.getAttribute("w:id") === s.ins_id
4108
+ );
4109
+ if (insNodes.length > 0) {
4110
+ const auth = insNodes[0].getAttribute("w:author");
4111
+ if (auth && auth !== this.author) {
4112
+ const is_fully_contained_in_ins = start >= s.start && start + length <= s.end;
4113
+ const match_mode = edit.match_mode || "strict";
4114
+ if (!is_fully_contained_in_ins && match_mode !== "all") {
4115
+ const node = insNodes[0];
4116
+ this._clean_wrapping_comments(node);
4117
+ const parent = node.parentNode;
4118
+ if (parent) {
4119
+ if (parent.tagName === "w:trPr") {
4120
+ parent.removeChild(node);
4121
+ } else {
4122
+ while (node.firstChild) {
4123
+ parent.insertBefore(node.firstChild, node);
4124
+ }
4125
+ parent.removeChild(node);
4126
+ }
4127
+ }
4128
+ mapper_dirty = true;
4129
+ }
4130
+ }
4131
+ }
4132
+ }
4133
+ }
3968
4134
  }
4135
+ }
4136
+ if (mapper_dirty) {
4137
+ this.mapper = new DocumentMapper(this.doc);
4138
+ this.clean_mapper = null;
4139
+ }
4140
+ if (!dry_run_mode) {
4141
+ const action_errors = actions.length > 0 ? this.validate_review_actions(actions) : [];
4142
+ const validate_edits_now = edits.length > 0 && action_errors.length > 0;
4143
+ const edit_errors = validate_edits_now ? this.validate_edits(edits) : [];
4144
+ const all_errors = [
4145
+ ...action_errors,
4146
+ ...edit_errors
4147
+ ];
3969
4148
  if (all_errors.length > 0) {
3970
4149
  throw new BatchValidationError(all_errors);
3971
4150
  }
@@ -3999,8 +4178,9 @@ var RedlineEngine = class {
3999
4178
  let skipped_edits = 0;
4000
4179
  if (edits.length > 0) {
4001
4180
  if (dry_run_mode) {
4002
- for (const edit of edits) {
4003
- const single_errors = this.validate_edits([edit]);
4181
+ for (let i = 0; i < edits.length; i++) {
4182
+ const edit = edits[i];
4183
+ const single_errors = this.validate_edits([edit], i);
4004
4184
  const warning = this._check_punctuation_warning(
4005
4185
  edit.target_text || ""
4006
4186
  );
@@ -4034,6 +4214,8 @@ var RedlineEngine = class {
4034
4214
  occurrences_modified: edit._occurrences_modified || 0,
4035
4215
  match_mode: edit.match_mode || "strict"
4036
4216
  });
4217
+ this.mapper = new DocumentMapper(this.doc);
4218
+ this.clean_mapper = null;
4037
4219
  } else {
4038
4220
  skipped_edits++;
4039
4221
  const error_msg = this.skipped_details.length > 0 ? this.skipped_details[this.skipped_details.length - 1] : "Failed to apply edit";
@@ -4049,15 +4231,37 @@ var RedlineEngine = class {
4049
4231
  }
4050
4232
  }
4051
4233
  } else {
4052
- const errors = this.validate_edits(edits);
4053
- if (errors.length > 0) {
4054
- throw new BatchValidationError(errors);
4234
+ const snapshot = takeSnapshot(this.doc);
4235
+ const originalCurrentId = this.current_id;
4236
+ try {
4237
+ const sequential_errors = [];
4238
+ for (let i = 0; i < edits.length; i++) {
4239
+ const edit = edits[i];
4240
+ const single_errors = this.validate_edits([edit], i);
4241
+ if (single_errors.length > 0) {
4242
+ sequential_errors.push(...single_errors);
4243
+ } else {
4244
+ this.apply_edits([edit], page_offsets);
4245
+ this.mapper = new DocumentMapper(this.doc);
4246
+ this.clean_mapper = null;
4247
+ }
4248
+ }
4249
+ if (sequential_errors.length > 0) {
4250
+ throw new BatchValidationError(sequential_errors);
4251
+ }
4252
+ } catch (err) {
4253
+ restoreSnapshot(this.doc, snapshot);
4254
+ this.current_id = originalCurrentId;
4255
+ this.mapper = new DocumentMapper(this.doc);
4256
+ this.comments_manager = new CommentsManager(this.doc);
4257
+ this.clean_mapper = null;
4258
+ throw err;
4055
4259
  }
4056
- const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
4057
- const res = this.apply_edits(cloned_edits, page_offsets);
4058
- applied_edits = cloned_edits.filter((e) => e._applied_status).length;
4059
- skipped_edits = cloned_edits.length - applied_edits;
4060
- for (const edit of cloned_edits) {
4260
+ applied_edits = edits.filter(
4261
+ (e) => e._applied_status
4262
+ ).length;
4263
+ skipped_edits = edits.length - applied_edits;
4264
+ for (const edit of edits) {
4061
4265
  const success = edit._applied_status || false;
4062
4266
  const error_msg = edit._error_msg || null;
4063
4267
  const warning = this._check_punctuation_warning(
@@ -4211,14 +4415,22 @@ var RedlineEngine = class {
4211
4415
  if (parent) {
4212
4416
  parent._applied_status = true;
4213
4417
  parent._occurrences_modified = (parent._occurrences_modified || 0) + 1;
4214
- const [path, page] = this._get_heading_path_and_page(start, this.mapper.full_text, page_offsets);
4418
+ const [path, page] = this._get_heading_path_and_page(
4419
+ start,
4420
+ this.mapper.full_text,
4421
+ page_offsets
4422
+ );
4215
4423
  const pages = parent._pages || [];
4216
4424
  if (!pages.includes(page)) pages.unshift(page);
4217
4425
  parent._pages = pages;
4218
4426
  parent._heading_path = path;
4219
4427
  } else {
4220
4428
  edit._occurrences_modified = (edit._occurrences_modified || 0) + 1;
4221
- const [path, page] = this._get_heading_path_and_page(start, this.mapper.full_text, page_offsets);
4429
+ const [path, page] = this._get_heading_path_and_page(
4430
+ start,
4431
+ this.mapper.full_text,
4432
+ page_offsets
4433
+ );
4222
4434
  const pages = edit._pages || [];
4223
4435
  if (!pages.includes(page)) pages.unshift(page);
4224
4436
  edit._pages = pages;
@@ -4379,12 +4591,18 @@ var RedlineEngine = class {
4379
4591
  if (!edit.target_text) return null;
4380
4592
  const is_regex = edit.regex || false;
4381
4593
  const match_mode = edit.match_mode || "strict";
4382
- let matches = this.mapper.find_all_match_indices(edit.target_text, is_regex);
4594
+ let matches = this.mapper.find_all_match_indices(
4595
+ edit.target_text,
4596
+ is_regex
4597
+ );
4383
4598
  let use_clean_map = false;
4384
4599
  if (matches.length === 0) {
4385
4600
  if (!this.clean_mapper)
4386
4601
  this.clean_mapper = new DocumentMapper(this.doc, true);
4387
- matches = this.clean_mapper.find_all_match_indices(edit.target_text, is_regex);
4602
+ matches = this.clean_mapper.find_all_match_indices(
4603
+ edit.target_text,
4604
+ is_regex
4605
+ );
4388
4606
  if (matches.length > 0) use_clean_map = true;
4389
4607
  else return null;
4390
4608
  }
@@ -4409,14 +4627,49 @@ var RedlineEngine = class {
4409
4627
  start_idx + match_len
4410
4628
  );
4411
4629
  let current_effective_new_text = edit.new_text || "";
4630
+ if (/^\{#cell:[^}]+\}$/.test(actual_doc_text.trim())) {
4631
+ let ins_text = current_effective_new_text;
4632
+ ins_text = ins_text.split(actual_doc_text.trim()).join("");
4633
+ if (ins_text) {
4634
+ all_sub_edits.push({
4635
+ type: "modify",
4636
+ target_text: "",
4637
+ new_text: ins_text,
4638
+ comment: edit.comment,
4639
+ // Insert at the anchor token's start so the new run lands inside
4640
+ // the cell paragraph that get_insertion_anchor resolves there.
4641
+ _match_start_index: start_idx,
4642
+ _internal_op: "INSERTION",
4643
+ _active_mapper_ref: active_mapper
4644
+ });
4645
+ } else if (edit.comment) {
4646
+ all_sub_edits.push({
4647
+ type: "modify",
4648
+ target_text: "",
4649
+ new_text: "",
4650
+ comment: edit.comment,
4651
+ _match_start_index: start_idx,
4652
+ _internal_op: "COMMENT_ONLY",
4653
+ _active_mapper_ref: active_mapper
4654
+ });
4655
+ }
4656
+ continue;
4657
+ }
4412
4658
  if (is_regex && current_effective_new_text) {
4413
4659
  try {
4414
- current_effective_new_text = actual_doc_text.replace(new RegExp(edit.target_text), current_effective_new_text);
4660
+ current_effective_new_text = actual_doc_text.replace(
4661
+ new RegExp(edit.target_text),
4662
+ current_effective_new_text
4663
+ );
4415
4664
  } catch (e) {
4416
4665
  }
4417
4666
  }
4418
- const [edit_target_clean, edit_target_style] = this._parse_markdown_style(edit.target_text);
4419
- const [edit_new_clean, edit_new_style] = this._parse_markdown_style(current_effective_new_text);
4667
+ const [edit_target_clean, edit_target_style] = this._parse_markdown_style(
4668
+ edit.target_text
4669
+ );
4670
+ const [edit_new_clean, edit_new_style] = this._parse_markdown_style(
4671
+ current_effective_new_text
4672
+ );
4420
4673
  if (edit_target_style !== edit_new_style) {
4421
4674
  const [actual_clean] = this._parse_markdown_style(actual_doc_text);
4422
4675
  const final_target2 = actual_clean;
@@ -4455,7 +4708,9 @@ var RedlineEngine = class {
4455
4708
  let effective_start_idx = start_idx;
4456
4709
  if (current_effective_new_text.startsWith(actual_doc_text)) {
4457
4710
  effective_op = "INSERTION";
4458
- final_new = current_effective_new_text.substring(actual_doc_text.length);
4711
+ final_new = current_effective_new_text.substring(
4712
+ actual_doc_text.length
4713
+ );
4459
4714
  effective_start_idx = start_idx + match_len;
4460
4715
  } else {
4461
4716
  const [prefix_len, suffix_len] = trim_common_context(
@@ -4536,7 +4791,12 @@ var RedlineEngine = class {
4536
4791
  const first_anchor = ascend_to_paragraph_child(first_el, start_p);
4537
4792
  const last_anchor = ascend_to_paragraph_child(last_el, end_p);
4538
4793
  if (start_p === end_p) {
4539
- this._attach_comment(start_p, first_anchor, last_anchor, edit.comment);
4794
+ this._attach_comment(
4795
+ start_p,
4796
+ first_anchor,
4797
+ last_anchor,
4798
+ edit.comment
4799
+ );
4540
4800
  } else {
4541
4801
  this._attach_comment_spanning(
4542
4802
  start_p,
@@ -5825,13 +6085,21 @@ function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets
5825
6085
  if (seen_cells.has(cell)) continue;
5826
6086
  seen_cells.add(cell);
5827
6087
  if (!first_cell) cell_cursor += 3;
5828
- const cell_content = _extract_blocks(
6088
+ let cell_content = _extract_blocks(
5829
6089
  cell,
5830
6090
  comments_map,
5831
6091
  cleanView,
5832
6092
  cell_cursor,
5833
6093
  paragraph_offsets
5834
6094
  );
6095
+ if (!cleanView) {
6096
+ const firstP = cell._element.getElementsByTagName("w:p")[0];
6097
+ const paraId = firstP ? firstP.getAttribute("w14:paraId") : null;
6098
+ if (paraId) {
6099
+ const anchor = `{#cell:${paraId}}`;
6100
+ cell_content = cell_content + anchor;
6101
+ }
6102
+ }
5835
6103
  cell_texts.push(cell_content);
5836
6104
  cell_cursor += cell_content.length;
5837
6105
  first_cell = false;