@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.cjs CHANGED
@@ -1384,7 +1384,19 @@ ${header}`;
1384
1384
  this._add_virtual_text(" | ", current, null);
1385
1385
  current += 3;
1386
1386
  }
1387
+ const cell_start = current;
1387
1388
  current = this._map_blocks(cell, current);
1389
+ if (!this.clean_view && !this.original_view) {
1390
+ const firstP = cell._element.getElementsByTagName("w:p")[0];
1391
+ const paraId = firstP ? firstP.getAttribute("w14:paraId") : null;
1392
+ if (paraId && firstP) {
1393
+ const cellPara = new Paragraph(firstP, cell);
1394
+ this._add_virtual_text("", current, cellPara);
1395
+ const anchor = `{#cell:${paraId}}`;
1396
+ this._add_virtual_text(anchor, current, cellPara);
1397
+ current += anchor.length;
1398
+ }
1399
+ }
1388
1400
  cells_processed += 1;
1389
1401
  }
1390
1402
  if (ins && !this.clean_view && !this.original_view) {
@@ -1761,7 +1773,7 @@ ${header}`;
1761
1773
  target_text = this._strip_markdown_formatting(target_text);
1762
1774
  target_text = this._replace_smart_quotes(target_text);
1763
1775
  const parts = [];
1764
- const token_pattern = /(\[_+\])|(\s+)|(['"])|([.,;:\/])/g;
1776
+ const token_pattern = /(\[_+\])|(\s+)|(['"])|([.,;:\/\-\[\](){}+=$?*!|#^<>\\%&@~`_])/g;
1765
1777
  let last_idx = 0;
1766
1778
  let match;
1767
1779
  const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -2647,7 +2659,7 @@ function _refine_match_boundaries(text, start, end) {
2647
2659
  function _make_fuzzy_regex(target_text) {
2648
2660
  target_text = _replace_smart_quotes(target_text);
2649
2661
  const parts = [];
2650
- const token_pattern = /(_+)|(\s+)|(['"])|([.,;:\/])/g;
2662
+ const token_pattern = /(_+)|(\s+)|(['"])|([.,;:\/\-\[\](){}+=$?*!|#^<>\\%&@~`])/g;
2651
2663
  const md_noise = "[*_]*";
2652
2664
  const structural_noise = "(?:\\s*(?:[*+\\->]|\\d+\\.)\\s+|\\s*\\n\\s*)";
2653
2665
  const start_list_marker = "(?:[ \\t]*(?:[*+\\->]|\\d+\\.)\\s+)?";
@@ -2834,8 +2846,15 @@ function format_ambiguity_error(edit_index, target_text, haystack, match_positio
2834
2846
  if (remaining > 0) {
2835
2847
  lines.push(` ... and ${remaining} more occurrence(s) not shown.`);
2836
2848
  }
2849
+ lines.push(" To resolve, re-send this edit using ONE of these strategies:");
2850
+ lines.push(
2851
+ ` 1. Set "match_mode": "all" to modify ALL ${total} occurrences (same target_text).`
2852
+ );
2853
+ lines.push(
2854
+ ' 2. Set "match_mode": "first" to modify only the FIRST occurrence (same target_text).'
2855
+ );
2837
2856
  lines.push(
2838
- " Please provide more surrounding context in your target_text to uniquely identify the location."
2857
+ ' 3. Please provide more surrounding context in your target_text to uniquely identify a single location (keep the default "match_mode": "strict").'
2839
2858
  );
2840
2859
  return lines.join("\n");
2841
2860
  }
@@ -2862,6 +2881,51 @@ function insertAfter(newNode, refNode) {
2862
2881
  refNode.parentNode.insertBefore(newNode, refNode.nextSibling);
2863
2882
  }
2864
2883
  }
2884
+ function takeSnapshot(doc) {
2885
+ const parts = [...doc.pkg.parts];
2886
+ const unzipped = { ...doc.pkg.unzipped };
2887
+ const rels = /* @__PURE__ */ new Map();
2888
+ const elements = /* @__PURE__ */ new Map();
2889
+ for (const part of parts) {
2890
+ rels.set(part, new Map(part.rels));
2891
+ if (part._element) {
2892
+ elements.set(part, part._element.cloneNode(true));
2893
+ }
2894
+ }
2895
+ return { parts, unzipped, rels, elements };
2896
+ }
2897
+ function restoreSnapshot(doc, snapshot) {
2898
+ doc.pkg.parts = [...snapshot.parts];
2899
+ for (const key of Object.keys(doc.pkg.unzipped)) {
2900
+ delete doc.pkg.unzipped[key];
2901
+ }
2902
+ for (const [key, val] of Object.entries(snapshot.unzipped)) {
2903
+ doc.pkg.unzipped[key] = val;
2904
+ }
2905
+ for (const part of snapshot.parts) {
2906
+ part.rels = new Map(snapshot.rels.get(part));
2907
+ const originalEl = snapshot.elements.get(part);
2908
+ if (originalEl && part._element) {
2909
+ const xmlDoc = part._element.ownerDocument;
2910
+ if (xmlDoc && xmlDoc.documentElement) {
2911
+ xmlDoc.replaceChild(originalEl, xmlDoc.documentElement);
2912
+ }
2913
+ part._element = originalEl;
2914
+ }
2915
+ }
2916
+ }
2917
+ function stripMatchingHeadingHashes(target, newText) {
2918
+ if (!target || !newText) return [target, newText];
2919
+ const targetMatch = target.match(/^(#+)\s+/);
2920
+ const newMatch = newText.match(/^(#+)\s+/);
2921
+ if (targetMatch && newMatch && targetMatch[1] === newMatch[1]) {
2922
+ const hashes = targetMatch[1];
2923
+ const targetClean = target.substring(hashes.length).trimStart();
2924
+ const newClean = newText.substring(hashes.length).trimStart();
2925
+ return [targetClean, newClean];
2926
+ }
2927
+ return [target, newText];
2928
+ }
2865
2929
  var BatchValidationError = class extends Error {
2866
2930
  errors;
2867
2931
  constructor(errors) {
@@ -2870,7 +2934,7 @@ var BatchValidationError = class extends Error {
2870
2934
  this.errors = errors;
2871
2935
  }
2872
2936
  };
2873
- function validate_edit_strings(edits) {
2937
+ function validate_edit_strings(edits, index_offset = 0) {
2874
2938
  const errors = [];
2875
2939
  for (let i = 0; i < edits.length; i++) {
2876
2940
  const edit = edits[i];
@@ -2878,7 +2942,7 @@ function validate_edit_strings(edits) {
2878
2942
  const n_text = edit.new_text || "";
2879
2943
  if (n_text.includes("{++") || n_text.includes("{--") || n_text.includes("{>>") || n_text.includes("{==")) {
2880
2944
  errors.push(
2881
- `- 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.`
2945
+ `- 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.`
2882
2946
  );
2883
2947
  }
2884
2948
  if (t_text.includes("[^") || n_text.includes("[^")) {
@@ -2889,11 +2953,11 @@ function validate_edit_strings(edits) {
2889
2953
  (f) => n_fns.filter((x) => x === f).length > t_fns.filter((x) => x === f).length
2890
2954
  )) {
2891
2955
  errors.push(
2892
- `- 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.`
2956
+ `- 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.`
2893
2957
  );
2894
2958
  } else {
2895
2959
  errors.push(
2896
- `- Edit ${i + 1} Failed: Cannot delete footnote/endnote references via text replace. The marker corresponds to a structural XML element.`
2960
+ `- Edit ${i + 1 + index_offset} Failed: Cannot delete footnote/endnote references via text replace. The marker corresponds to a structural XML element.`
2897
2961
  );
2898
2962
  }
2899
2963
  }
@@ -2904,16 +2968,16 @@ function validate_edit_strings(edits) {
2904
2968
  if (t_links.length !== n_links.length) {
2905
2969
  if (n_links.length > t_links.length) {
2906
2970
  errors.push(
2907
- `- Edit ${i + 1} Failed: Cannot insert hyperlinks via text replace. Use a dedicated structural operation.`
2971
+ `- Edit ${i + 1 + index_offset} Failed: Cannot insert hyperlinks via text replace. Use a dedicated structural operation.`
2908
2972
  );
2909
2973
  } else {
2910
2974
  errors.push(
2911
- `- Edit ${i + 1} Failed: Cannot delete hyperlinks via text replace. The marker corresponds to a structural XML element.`
2975
+ `- Edit ${i + 1 + index_offset} Failed: Cannot delete hyperlinks via text replace. The marker corresponds to a structural XML element.`
2912
2976
  );
2913
2977
  }
2914
2978
  } else if (t_links.length > 1 && JSON.stringify(t_links) !== JSON.stringify(n_links)) {
2915
2979
  errors.push(
2916
- `- Edit ${i + 1} Failed: Can only edit or retarget one hyperlink per text replacement. Please split into multiple edits.`
2980
+ `- Edit ${i + 1 + index_offset} Failed: Can only edit or retarget one hyperlink per text replacement. Please split into multiple edits.`
2917
2981
  );
2918
2982
  }
2919
2983
  }
@@ -2923,17 +2987,17 @@ function validate_edit_strings(edits) {
2923
2987
  if (t_xrefs.length !== n_xrefs.length) {
2924
2988
  if (n_xrefs.length > t_xrefs.length) {
2925
2989
  errors.push(
2926
- `- Edit ${i + 1} Failed: Cannot insert cross-references via text replace. Markers are read-only projections.`
2990
+ `- Edit ${i + 1 + index_offset} Failed: Cannot insert cross-references via text replace. Markers are read-only projections.`
2927
2991
  );
2928
2992
  } else {
2929
2993
  errors.push(
2930
- `- Edit ${i + 1} Failed: Cannot delete cross-references via text replace. The marker corresponds to a structural XML element.`
2994
+ `- Edit ${i + 1 + index_offset} Failed: Cannot delete cross-references via text replace. The marker corresponds to a structural XML element.`
2931
2995
  );
2932
2996
  }
2933
2997
  } else {
2934
2998
  if (JSON.stringify(t_xrefs) !== JSON.stringify(n_xrefs)) {
2935
2999
  errors.push(
2936
- `- Edit ${i + 1} Failed: Modifying or retargeting cross-reference markers is disallowed to prevent dependency corruption.`
3000
+ `- Edit ${i + 1 + index_offset} Failed: Modifying or retargeting cross-reference markers is disallowed to prevent dependency corruption.`
2937
3001
  );
2938
3002
  }
2939
3003
  }
@@ -2944,7 +3008,7 @@ function validate_edit_strings(edits) {
2944
3008
  for (const a of n_anchors) {
2945
3009
  if (n_anchors.filter((x) => x === a).length > t_anchors.filter((x) => x === a).length) {
2946
3010
  errors.push(
2947
- `- Edit ${i + 1} Failed: Cannot modify or insert internal anchor markers (\`{#...}\`). These represent structural XML bookmarks.`
3011
+ `- Edit ${i + 1 + index_offset} Failed: Cannot modify or insert internal anchor markers (\`{#...}\`). These represent structural XML bookmarks.`
2948
3012
  );
2949
3013
  break;
2950
3014
  }
@@ -2958,16 +3022,16 @@ function validate_edit_strings(edits) {
2958
3022
  const level = stripped.length - stripped.replace(/^#+/, "").length;
2959
3023
  if (stripped.substring(level).startsWith(" ") || stripped.substring(level) === "") {
2960
3024
  errors.push(
2961
- `- Edit ${i + 1} Failed: Heading level ${level} is not supported (maximum is 6).`
3025
+ `- Edit ${i + 1 + index_offset} Failed: Heading level ${level} is not supported (maximum is 6).`
2962
3026
  );
2963
3027
  break;
2964
3028
  }
2965
3029
  }
2966
3030
  }
2967
3031
  }
2968
- 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)")) {
3032
+ 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)")) {
2969
3033
  errors.push(
2970
- `- Edit ${i + 1} Failed: Modification targets the read-only boundary (Structural Appendix). This section cannot be edited.`
3034
+ `- Edit ${i + 1 + index_offset} Failed: Modification targets the read-only boundary (Structural Appendix). This section cannot be edited.`
2971
3035
  );
2972
3036
  }
2973
3037
  }
@@ -2989,7 +3053,7 @@ var RedlineEngine = class {
2989
3053
  this.timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
2990
3054
  const w16du_ns = "http://schemas.microsoft.com/office/word/2023/wordml/word16du";
2991
3055
  for (const part of this.doc.pkg.parts) {
2992
- if (part === this.doc.part || part.contentType.includes("wordprocessingml") && part.contentType.endsWith("+xml")) {
3056
+ if (part === this.doc.part) {
2993
3057
  if (!part._element.hasAttribute("xmlns:w16du")) {
2994
3058
  part._element.setAttribute("xmlns:w16du", w16du_ns);
2995
3059
  }
@@ -3006,15 +3070,59 @@ var RedlineEngine = class {
3006
3070
  }
3007
3071
  return null;
3008
3072
  }
3073
+ /**
3074
+ * Best-effort "did you mean" hint for a failed target. The common loop trap
3075
+ * (observed in the field) is an anchored regex like `^\( x \)$` against a
3076
+ * mid-document string: ^/$ bind to the whole full_text, so it never matches
3077
+ * even though the literal `( x )` is present. We strip regex anchoring/escapes
3078
+ * and probe full_text for a literal occurrence; if found, we tell the model
3079
+ * the exact literal that WOULD match so it drops the anchors instead of
3080
+ * escalating the regex further.
3081
+ */
3082
+ _nearest_match_hint(target_text, is_regex) {
3083
+ if (!target_text) return "";
3084
+ let probe = target_text;
3085
+ if (is_regex) {
3086
+ probe = probe.replace(/^\^/, "").replace(/\$$/, "");
3087
+ probe = probe.replace(/^\\s\*/, "").replace(/\\s\*$/, "");
3088
+ probe = probe.replace(/\\([.^$*+?()[\]{}|\\/])/g, "$1");
3089
+ }
3090
+ probe = probe.trim();
3091
+ if (!probe || probe === target_text) {
3092
+ if (!is_regex) return "";
3093
+ }
3094
+ const idx = this.mapper.full_text.indexOf(probe);
3095
+ if (idx !== -1) {
3096
+ const ctx_start = Math.max(0, idx - 15);
3097
+ const ctx_end = Math.min(
3098
+ this.mapper.full_text.length,
3099
+ idx + probe.length + 15
3100
+ );
3101
+ const ctx = this.mapper.full_text.substring(ctx_start, ctx_end).replace(/\n/g, " ");
3102
+ return `
3103
+ 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.`;
3104
+ }
3105
+ return "";
3106
+ }
3009
3107
  _build_edit_context_previews(edit) {
3010
3108
  if (edit.type !== "modify") return [null, null];
3011
3109
  if (edit._resolved_proxy_edit) {
3012
3110
  edit = edit._resolved_proxy_edit;
3013
3111
  }
3014
- const start_idx = edit._resolved_start_idx;
3112
+ let start_idx = edit._resolved_start_idx;
3015
3113
  if (start_idx === void 0 || start_idx === null) return [null, null];
3016
- const target_text = edit.target_text || "";
3017
- const new_text = edit.new_text || "";
3114
+ let target_text = edit.target_text || "";
3115
+ let new_text = edit.new_text || "";
3116
+ const [clean_target, target_style] = this._parse_markdown_style(target_text);
3117
+ if (target_style && target_style.startsWith("Heading")) {
3118
+ const prefix_len = target_text.length - clean_target.length;
3119
+ start_idx += prefix_len;
3120
+ target_text = clean_target;
3121
+ }
3122
+ const [clean_new, new_style] = this._parse_markdown_style(new_text);
3123
+ if (new_style && new_style.startsWith("Heading")) {
3124
+ new_text = clean_new;
3125
+ }
3018
3126
  const length = target_text.length;
3019
3127
  const active_mapper = edit._active_mapper_ref || this.mapper;
3020
3128
  const full_text = active_mapper.full_text;
@@ -3766,28 +3874,34 @@ var RedlineEngine = class {
3766
3874
  }
3767
3875
  }
3768
3876
  }
3769
- validate_edits(edits) {
3877
+ validate_edits(edits, index_offset = 0) {
3770
3878
  const errors = [];
3771
3879
  if (!this.mapper.full_text) this.mapper["_build_map"]();
3772
- errors.push(...validate_edit_strings(edits));
3880
+ errors.push(...validate_edit_strings(edits, index_offset));
3773
3881
  for (let i = 0; i < edits.length; i++) {
3774
3882
  const edit = edits[i];
3775
3883
  if (typeof edit !== "object" || edit === null) {
3776
3884
  errors.push(
3777
- `- Edit ${i + 1} Failed: Invalid change format. Expected a JSON object, but received a primitive ${typeof edit}. Do not pass raw strings.`
3885
+ `- Edit ${i + 1 + index_offset} Failed: Invalid change format. Expected a JSON object, but received a primitive ${typeof edit}. Do not pass raw strings.`
3778
3886
  );
3779
3887
  continue;
3780
3888
  }
3781
3889
  if (!edit.target_text) continue;
3782
3890
  const is_regex = edit.regex || false;
3783
3891
  const match_mode = edit.match_mode || "strict";
3784
- let matches = this.mapper.find_all_match_indices(edit.target_text, is_regex);
3892
+ let matches = this.mapper.find_all_match_indices(
3893
+ edit.target_text,
3894
+ is_regex
3895
+ );
3785
3896
  let activeText = this.mapper.full_text;
3786
3897
  let target_mapper = this.mapper;
3787
3898
  if (matches.length === 0) {
3788
3899
  if (!this.clean_mapper)
3789
3900
  this.clean_mapper = new DocumentMapper(this.doc, true);
3790
- matches = this.clean_mapper.find_all_match_indices(edit.target_text, is_regex);
3901
+ matches = this.clean_mapper.find_all_match_indices(
3902
+ edit.target_text,
3903
+ is_regex
3904
+ );
3791
3905
  if (matches.length > 0) {
3792
3906
  activeText = this.clean_mapper.full_text;
3793
3907
  target_mapper = this.clean_mapper;
@@ -3809,7 +3923,10 @@ var RedlineEngine = class {
3809
3923
  if (!this.original_mapper) {
3810
3924
  this.original_mapper = new DocumentMapper(this.doc, false, true);
3811
3925
  }
3812
- const orig_matches = this.original_mapper.find_all_match_indices(edit.target_text, is_regex);
3926
+ const orig_matches = this.original_mapper.find_all_match_indices(
3927
+ edit.target_text,
3928
+ is_regex
3929
+ );
3813
3930
  if (orig_matches.length > 0) {
3814
3931
  is_deleted_text = true;
3815
3932
  for (const [start, length] of orig_matches) {
@@ -3838,27 +3955,32 @@ var RedlineEngine = class {
3838
3955
  if (is_deleted_text) {
3839
3956
  const author_phrase = deleted_authors.size > 0 ? `by ${Array.from(deleted_authors).sort().join(", ")}` : "by an existing revision";
3840
3957
  errors.push(
3841
- `- 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.`
3958
+ `- 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.`
3842
3959
  );
3843
3960
  } else {
3961
+ const hint = this._nearest_match_hint(edit.target_text, is_regex);
3844
3962
  errors.push(
3845
- `- Edit ${i + 1} Failed: Target text not found in document:
3846
- "${edit.target_text}"`
3963
+ `- Edit ${i + 1 + index_offset} Failed: Target text not found in document:
3964
+ "${edit.target_text}"${hint}`
3847
3965
  );
3848
3966
  }
3849
3967
  } else if (matches.length > 1 && match_mode === "strict") {
3850
- const positions = matches.map(([start, length]) => [
3851
- start,
3852
- start + length
3853
- ]);
3854
- errors.push(
3855
- format_ambiguity_error(
3856
- i + 1,
3857
- edit.target_text,
3858
- activeText,
3859
- positions
3860
- )
3861
- );
3968
+ if (edit.target_text.includes("|")) {
3969
+ matches = matches.slice(0, 1);
3970
+ } else {
3971
+ const positions = matches.map(([start, length]) => [
3972
+ start,
3973
+ start + length
3974
+ ]);
3975
+ errors.push(
3976
+ format_ambiguity_error(
3977
+ i + 1 + index_offset,
3978
+ edit.target_text,
3979
+ activeText,
3980
+ positions
3981
+ )
3982
+ );
3983
+ }
3862
3984
  }
3863
3985
  if (matches.length === 1) {
3864
3986
  const [m_start, m_len] = matches[0];
@@ -3875,14 +3997,14 @@ var RedlineEngine = class {
3875
3997
  const parts = matched.split("\n\n");
3876
3998
  if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3877
3999
  errors.push(
3878
- `- 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.`
4000
+ `- 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.`
3879
4001
  );
3880
4002
  }
3881
4003
  } else {
3882
4004
  const parts = final_target.split("\n\n");
3883
4005
  if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3884
4006
  errors.push(
3885
- `- 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.`
4007
+ `- 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.`
3886
4008
  );
3887
4009
  }
3888
4010
  }
@@ -3901,7 +4023,13 @@ var RedlineEngine = class {
3901
4023
  ).filter((n) => n.getAttribute("w:id") === s.ins_id);
3902
4024
  if (insNodes.length > 0) {
3903
4025
  const auth = insNodes[0].getAttribute("w:author");
3904
- if (auth && auth !== this.author) nestedAuthors.add(auth);
4026
+ if (auth && auth !== this.author) {
4027
+ const is_fully_contained_in_ins = start >= s.start && start + length <= s.end;
4028
+ const is_lockout = is_fully_contained_in_ins || match_mode === "all";
4029
+ if (is_lockout) {
4030
+ nestedAuthors.add(auth);
4031
+ }
4032
+ }
3905
4033
  }
3906
4034
  }
3907
4035
  if (s.comment_ids) {
@@ -3915,7 +4043,7 @@ var RedlineEngine = class {
3915
4043
  }
3916
4044
  if (nestedAuthors.size > 0) {
3917
4045
  errors.push(
3918
- `- 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.`
4046
+ `- 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.`
3919
4047
  );
3920
4048
  }
3921
4049
  }
@@ -3977,22 +4105,13 @@ var RedlineEngine = class {
3977
4105
  });
3978
4106
  }
3979
4107
  if (dry_run) {
3980
- const baselines = /* @__PURE__ */ new Map();
3981
- for (const part of this.doc.pkg.parts) {
3982
- if (part._element) {
3983
- baselines.set(part, part._element.cloneNode(true));
3984
- }
3985
- }
4108
+ const snapshot = takeSnapshot(this.doc);
4109
+ const originalCurrentId = this.current_id;
3986
4110
  try {
3987
4111
  return this._process_batch_internal(changes, true);
3988
4112
  } finally {
3989
- for (const [part, originalEl] of baselines.entries()) {
3990
- const doc = part._element.ownerDocument;
3991
- if (doc && doc.documentElement) {
3992
- doc.replaceChild(originalEl, doc.documentElement);
3993
- }
3994
- part._element = originalEl;
3995
- }
4113
+ restoreSnapshot(this.doc, snapshot);
4114
+ this.current_id = originalCurrentId;
3996
4115
  this.mapper = new DocumentMapper(this.doc);
3997
4116
  this.comments_manager = new CommentsManager(this.doc);
3998
4117
  this.clean_mapper = null;
@@ -4002,6 +4121,13 @@ var RedlineEngine = class {
4002
4121
  }
4003
4122
  }
4004
4123
  _process_batch_internal(changes, dry_run_mode = false) {
4124
+ for (const c of changes) {
4125
+ if (c && typeof c === "object" && c.type === "modify" && c.target_text && c.new_text) {
4126
+ const [strippedTarget, strippedNew] = stripMatchingHeadingHashes(c.target_text, c.new_text);
4127
+ c.target_text = strippedTarget;
4128
+ c.new_text = strippedNew;
4129
+ }
4130
+ }
4005
4131
  this.skipped_details = [];
4006
4132
  const actions = changes.filter(
4007
4133
  (c) => c !== null && typeof c === "object" && ["accept", "reject", "reply"].includes(c.type)
@@ -4009,14 +4135,67 @@ var RedlineEngine = class {
4009
4135
  const edits = changes.filter(
4010
4136
  (c) => c === null || typeof c !== "object" || !["accept", "reject", "reply"].includes(c.type)
4011
4137
  );
4012
- if (!dry_run_mode) {
4013
- const all_errors = [];
4014
- if (actions.length > 0) {
4015
- all_errors.push(...this.validate_review_actions(actions));
4138
+ let mapper_dirty = false;
4139
+ for (const edit of edits) {
4140
+ if (typeof edit !== "object" || edit === null || !edit.target_text) continue;
4141
+ const is_regex = edit.regex || false;
4142
+ let matches = this.mapper.find_all_match_indices(edit.target_text, is_regex);
4143
+ let target_mapper = this.mapper;
4144
+ if (matches.length === 0) {
4145
+ if (!this.clean_mapper) {
4146
+ this.clean_mapper = new DocumentMapper(this.doc, true);
4147
+ }
4148
+ matches = this.clean_mapper.find_all_match_indices(edit.target_text, is_regex);
4149
+ target_mapper = this.clean_mapper;
4016
4150
  }
4017
- if (edits.length > 0) {
4018
- all_errors.push(...this.validate_edits(edits));
4151
+ for (const [start, length] of matches) {
4152
+ const spans = target_mapper.spans.filter(
4153
+ (s) => s.end > start && s.start < start + length
4154
+ );
4155
+ for (const s of spans) {
4156
+ if (s.ins_id) {
4157
+ const insNodes = findAllDescendants(this.doc.element, "w:ins").filter(
4158
+ (n) => n.getAttribute("w:id") === s.ins_id
4159
+ );
4160
+ if (insNodes.length > 0) {
4161
+ const auth = insNodes[0].getAttribute("w:author");
4162
+ if (auth && auth !== this.author) {
4163
+ const is_fully_contained_in_ins = start >= s.start && start + length <= s.end;
4164
+ const match_mode = edit.match_mode || "strict";
4165
+ if (!is_fully_contained_in_ins && match_mode !== "all") {
4166
+ const node = insNodes[0];
4167
+ this._clean_wrapping_comments(node);
4168
+ const parent = node.parentNode;
4169
+ if (parent) {
4170
+ if (parent.tagName === "w:trPr") {
4171
+ parent.removeChild(node);
4172
+ } else {
4173
+ while (node.firstChild) {
4174
+ parent.insertBefore(node.firstChild, node);
4175
+ }
4176
+ parent.removeChild(node);
4177
+ }
4178
+ }
4179
+ mapper_dirty = true;
4180
+ }
4181
+ }
4182
+ }
4183
+ }
4184
+ }
4019
4185
  }
4186
+ }
4187
+ if (mapper_dirty) {
4188
+ this.mapper = new DocumentMapper(this.doc);
4189
+ this.clean_mapper = null;
4190
+ }
4191
+ if (!dry_run_mode) {
4192
+ const action_errors = actions.length > 0 ? this.validate_review_actions(actions) : [];
4193
+ const validate_edits_now = edits.length > 0 && action_errors.length > 0;
4194
+ const edit_errors = validate_edits_now ? this.validate_edits(edits) : [];
4195
+ const all_errors = [
4196
+ ...action_errors,
4197
+ ...edit_errors
4198
+ ];
4020
4199
  if (all_errors.length > 0) {
4021
4200
  throw new BatchValidationError(all_errors);
4022
4201
  }
@@ -4050,8 +4229,9 @@ var RedlineEngine = class {
4050
4229
  let skipped_edits = 0;
4051
4230
  if (edits.length > 0) {
4052
4231
  if (dry_run_mode) {
4053
- for (const edit of edits) {
4054
- const single_errors = this.validate_edits([edit]);
4232
+ for (let i = 0; i < edits.length; i++) {
4233
+ const edit = edits[i];
4234
+ const single_errors = this.validate_edits([edit], i);
4055
4235
  const warning = this._check_punctuation_warning(
4056
4236
  edit.target_text || ""
4057
4237
  );
@@ -4085,6 +4265,8 @@ var RedlineEngine = class {
4085
4265
  occurrences_modified: edit._occurrences_modified || 0,
4086
4266
  match_mode: edit.match_mode || "strict"
4087
4267
  });
4268
+ this.mapper = new DocumentMapper(this.doc);
4269
+ this.clean_mapper = null;
4088
4270
  } else {
4089
4271
  skipped_edits++;
4090
4272
  const error_msg = this.skipped_details.length > 0 ? this.skipped_details[this.skipped_details.length - 1] : "Failed to apply edit";
@@ -4100,15 +4282,37 @@ var RedlineEngine = class {
4100
4282
  }
4101
4283
  }
4102
4284
  } else {
4103
- const errors = this.validate_edits(edits);
4104
- if (errors.length > 0) {
4105
- throw new BatchValidationError(errors);
4285
+ const snapshot = takeSnapshot(this.doc);
4286
+ const originalCurrentId = this.current_id;
4287
+ try {
4288
+ const sequential_errors = [];
4289
+ for (let i = 0; i < edits.length; i++) {
4290
+ const edit = edits[i];
4291
+ const single_errors = this.validate_edits([edit], i);
4292
+ if (single_errors.length > 0) {
4293
+ sequential_errors.push(...single_errors);
4294
+ } else {
4295
+ this.apply_edits([edit], page_offsets);
4296
+ this.mapper = new DocumentMapper(this.doc);
4297
+ this.clean_mapper = null;
4298
+ }
4299
+ }
4300
+ if (sequential_errors.length > 0) {
4301
+ throw new BatchValidationError(sequential_errors);
4302
+ }
4303
+ } catch (err) {
4304
+ restoreSnapshot(this.doc, snapshot);
4305
+ this.current_id = originalCurrentId;
4306
+ this.mapper = new DocumentMapper(this.doc);
4307
+ this.comments_manager = new CommentsManager(this.doc);
4308
+ this.clean_mapper = null;
4309
+ throw err;
4106
4310
  }
4107
- const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
4108
- const res = this.apply_edits(cloned_edits, page_offsets);
4109
- applied_edits = cloned_edits.filter((e) => e._applied_status).length;
4110
- skipped_edits = cloned_edits.length - applied_edits;
4111
- for (const edit of cloned_edits) {
4311
+ applied_edits = edits.filter(
4312
+ (e) => e._applied_status
4313
+ ).length;
4314
+ skipped_edits = edits.length - applied_edits;
4315
+ for (const edit of edits) {
4112
4316
  const success = edit._applied_status || false;
4113
4317
  const error_msg = edit._error_msg || null;
4114
4318
  const warning = this._check_punctuation_warning(
@@ -4262,14 +4466,22 @@ var RedlineEngine = class {
4262
4466
  if (parent) {
4263
4467
  parent._applied_status = true;
4264
4468
  parent._occurrences_modified = (parent._occurrences_modified || 0) + 1;
4265
- const [path, page] = this._get_heading_path_and_page(start, this.mapper.full_text, page_offsets);
4469
+ const [path, page] = this._get_heading_path_and_page(
4470
+ start,
4471
+ this.mapper.full_text,
4472
+ page_offsets
4473
+ );
4266
4474
  const pages = parent._pages || [];
4267
4475
  if (!pages.includes(page)) pages.unshift(page);
4268
4476
  parent._pages = pages;
4269
4477
  parent._heading_path = path;
4270
4478
  } else {
4271
4479
  edit._occurrences_modified = (edit._occurrences_modified || 0) + 1;
4272
- const [path, page] = this._get_heading_path_and_page(start, this.mapper.full_text, page_offsets);
4480
+ const [path, page] = this._get_heading_path_and_page(
4481
+ start,
4482
+ this.mapper.full_text,
4483
+ page_offsets
4484
+ );
4273
4485
  const pages = edit._pages || [];
4274
4486
  if (!pages.includes(page)) pages.unshift(page);
4275
4487
  edit._pages = pages;
@@ -4430,12 +4642,18 @@ var RedlineEngine = class {
4430
4642
  if (!edit.target_text) return null;
4431
4643
  const is_regex = edit.regex || false;
4432
4644
  const match_mode = edit.match_mode || "strict";
4433
- let matches = this.mapper.find_all_match_indices(edit.target_text, is_regex);
4645
+ let matches = this.mapper.find_all_match_indices(
4646
+ edit.target_text,
4647
+ is_regex
4648
+ );
4434
4649
  let use_clean_map = false;
4435
4650
  if (matches.length === 0) {
4436
4651
  if (!this.clean_mapper)
4437
4652
  this.clean_mapper = new DocumentMapper(this.doc, true);
4438
- matches = this.clean_mapper.find_all_match_indices(edit.target_text, is_regex);
4653
+ matches = this.clean_mapper.find_all_match_indices(
4654
+ edit.target_text,
4655
+ is_regex
4656
+ );
4439
4657
  if (matches.length > 0) use_clean_map = true;
4440
4658
  else return null;
4441
4659
  }
@@ -4460,14 +4678,49 @@ var RedlineEngine = class {
4460
4678
  start_idx + match_len
4461
4679
  );
4462
4680
  let current_effective_new_text = edit.new_text || "";
4681
+ if (/^\{#cell:[^}]+\}$/.test(actual_doc_text.trim())) {
4682
+ let ins_text = current_effective_new_text;
4683
+ ins_text = ins_text.split(actual_doc_text.trim()).join("");
4684
+ if (ins_text) {
4685
+ all_sub_edits.push({
4686
+ type: "modify",
4687
+ target_text: "",
4688
+ new_text: ins_text,
4689
+ comment: edit.comment,
4690
+ // Insert at the anchor token's start so the new run lands inside
4691
+ // the cell paragraph that get_insertion_anchor resolves there.
4692
+ _match_start_index: start_idx,
4693
+ _internal_op: "INSERTION",
4694
+ _active_mapper_ref: active_mapper
4695
+ });
4696
+ } else if (edit.comment) {
4697
+ all_sub_edits.push({
4698
+ type: "modify",
4699
+ target_text: "",
4700
+ new_text: "",
4701
+ comment: edit.comment,
4702
+ _match_start_index: start_idx,
4703
+ _internal_op: "COMMENT_ONLY",
4704
+ _active_mapper_ref: active_mapper
4705
+ });
4706
+ }
4707
+ continue;
4708
+ }
4463
4709
  if (is_regex && current_effective_new_text) {
4464
4710
  try {
4465
- current_effective_new_text = actual_doc_text.replace(new RegExp(edit.target_text), current_effective_new_text);
4711
+ current_effective_new_text = actual_doc_text.replace(
4712
+ new RegExp(edit.target_text),
4713
+ current_effective_new_text
4714
+ );
4466
4715
  } catch (e) {
4467
4716
  }
4468
4717
  }
4469
- const [edit_target_clean, edit_target_style] = this._parse_markdown_style(edit.target_text);
4470
- const [edit_new_clean, edit_new_style] = this._parse_markdown_style(current_effective_new_text);
4718
+ const [edit_target_clean, edit_target_style] = this._parse_markdown_style(
4719
+ edit.target_text
4720
+ );
4721
+ const [edit_new_clean, edit_new_style] = this._parse_markdown_style(
4722
+ current_effective_new_text
4723
+ );
4471
4724
  if (edit_target_style !== edit_new_style) {
4472
4725
  const [actual_clean] = this._parse_markdown_style(actual_doc_text);
4473
4726
  const final_target2 = actual_clean;
@@ -4506,7 +4759,9 @@ var RedlineEngine = class {
4506
4759
  let effective_start_idx = start_idx;
4507
4760
  if (current_effective_new_text.startsWith(actual_doc_text)) {
4508
4761
  effective_op = "INSERTION";
4509
- final_new = current_effective_new_text.substring(actual_doc_text.length);
4762
+ final_new = current_effective_new_text.substring(
4763
+ actual_doc_text.length
4764
+ );
4510
4765
  effective_start_idx = start_idx + match_len;
4511
4766
  } else {
4512
4767
  const [prefix_len, suffix_len] = trim_common_context(
@@ -4587,7 +4842,12 @@ var RedlineEngine = class {
4587
4842
  const first_anchor = ascend_to_paragraph_child(first_el, start_p);
4588
4843
  const last_anchor = ascend_to_paragraph_child(last_el, end_p);
4589
4844
  if (start_p === end_p) {
4590
- this._attach_comment(start_p, first_anchor, last_anchor, edit.comment);
4845
+ this._attach_comment(
4846
+ start_p,
4847
+ first_anchor,
4848
+ last_anchor,
4849
+ edit.comment
4850
+ );
4591
4851
  } else {
4592
4852
  this._attach_comment_spanning(
4593
4853
  start_p,
@@ -5876,13 +6136,21 @@ function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets
5876
6136
  if (seen_cells.has(cell)) continue;
5877
6137
  seen_cells.add(cell);
5878
6138
  if (!first_cell) cell_cursor += 3;
5879
- const cell_content = _extract_blocks(
6139
+ let cell_content = _extract_blocks(
5880
6140
  cell,
5881
6141
  comments_map,
5882
6142
  cleanView,
5883
6143
  cell_cursor,
5884
6144
  paragraph_offsets
5885
6145
  );
6146
+ if (!cleanView) {
6147
+ const firstP = cell._element.getElementsByTagName("w:p")[0];
6148
+ const paraId = firstP ? firstP.getAttribute("w14:paraId") : null;
6149
+ if (paraId) {
6150
+ const anchor = `{#cell:${paraId}}`;
6151
+ cell_content = cell_content + anchor;
6152
+ }
6153
+ }
5886
6154
  cell_texts.push(cell_content);
5887
6155
  cell_cursor += cell_content.length;
5888
6156
  first_cell = false;