@adeu/core 1.17.1 → 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+)?";
@@ -2818,6 +2830,51 @@ function insertAfter(newNode, refNode) {
2818
2830
  refNode.parentNode.insertBefore(newNode, refNode.nextSibling);
2819
2831
  }
2820
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
+ }
2821
2878
  var BatchValidationError = class extends Error {
2822
2879
  errors;
2823
2880
  constructor(errors) {
@@ -2826,7 +2883,7 @@ var BatchValidationError = class extends Error {
2826
2883
  this.errors = errors;
2827
2884
  }
2828
2885
  };
2829
- function validate_edit_strings(edits) {
2886
+ function validate_edit_strings(edits, index_offset = 0) {
2830
2887
  const errors = [];
2831
2888
  for (let i = 0; i < edits.length; i++) {
2832
2889
  const edit = edits[i];
@@ -2834,7 +2891,7 @@ function validate_edit_strings(edits) {
2834
2891
  const n_text = edit.new_text || "";
2835
2892
  if (n_text.includes("{++") || n_text.includes("{--") || n_text.includes("{>>") || n_text.includes("{==")) {
2836
2893
  errors.push(
2837
- `- 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.`
2838
2895
  );
2839
2896
  }
2840
2897
  if (t_text.includes("[^") || n_text.includes("[^")) {
@@ -2845,11 +2902,11 @@ function validate_edit_strings(edits) {
2845
2902
  (f) => n_fns.filter((x) => x === f).length > t_fns.filter((x) => x === f).length
2846
2903
  )) {
2847
2904
  errors.push(
2848
- `- 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.`
2849
2906
  );
2850
2907
  } else {
2851
2908
  errors.push(
2852
- `- 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.`
2853
2910
  );
2854
2911
  }
2855
2912
  }
@@ -2860,16 +2917,16 @@ function validate_edit_strings(edits) {
2860
2917
  if (t_links.length !== n_links.length) {
2861
2918
  if (n_links.length > t_links.length) {
2862
2919
  errors.push(
2863
- `- 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.`
2864
2921
  );
2865
2922
  } else {
2866
2923
  errors.push(
2867
- `- 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.`
2868
2925
  );
2869
2926
  }
2870
2927
  } else if (t_links.length > 1 && JSON.stringify(t_links) !== JSON.stringify(n_links)) {
2871
2928
  errors.push(
2872
- `- 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.`
2873
2930
  );
2874
2931
  }
2875
2932
  }
@@ -2879,17 +2936,17 @@ function validate_edit_strings(edits) {
2879
2936
  if (t_xrefs.length !== n_xrefs.length) {
2880
2937
  if (n_xrefs.length > t_xrefs.length) {
2881
2938
  errors.push(
2882
- `- 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.`
2883
2940
  );
2884
2941
  } else {
2885
2942
  errors.push(
2886
- `- 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.`
2887
2944
  );
2888
2945
  }
2889
2946
  } else {
2890
2947
  if (JSON.stringify(t_xrefs) !== JSON.stringify(n_xrefs)) {
2891
2948
  errors.push(
2892
- `- 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.`
2893
2950
  );
2894
2951
  }
2895
2952
  }
@@ -2900,7 +2957,7 @@ function validate_edit_strings(edits) {
2900
2957
  for (const a of n_anchors) {
2901
2958
  if (n_anchors.filter((x) => x === a).length > t_anchors.filter((x) => x === a).length) {
2902
2959
  errors.push(
2903
- `- 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.`
2904
2961
  );
2905
2962
  break;
2906
2963
  }
@@ -2914,16 +2971,16 @@ function validate_edit_strings(edits) {
2914
2971
  const level = stripped.length - stripped.replace(/^#+/, "").length;
2915
2972
  if (stripped.substring(level).startsWith(" ") || stripped.substring(level) === "") {
2916
2973
  errors.push(
2917
- `- 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).`
2918
2975
  );
2919
2976
  break;
2920
2977
  }
2921
2978
  }
2922
2979
  }
2923
2980
  }
2924
- 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)")) {
2925
2982
  errors.push(
2926
- `- 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.`
2927
2984
  );
2928
2985
  }
2929
2986
  }
@@ -2945,7 +3002,7 @@ var RedlineEngine = class {
2945
3002
  this.timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
2946
3003
  const w16du_ns = "http://schemas.microsoft.com/office/word/2023/wordml/word16du";
2947
3004
  for (const part of this.doc.pkg.parts) {
2948
- if (part === this.doc.part || part.contentType.includes("wordprocessingml") && part.contentType.endsWith("+xml")) {
3005
+ if (part === this.doc.part) {
2949
3006
  if (!part._element.hasAttribute("xmlns:w16du")) {
2950
3007
  part._element.setAttribute("xmlns:w16du", w16du_ns);
2951
3008
  }
@@ -2962,15 +3019,59 @@ var RedlineEngine = class {
2962
3019
  }
2963
3020
  return null;
2964
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
+ }
2965
3056
  _build_edit_context_previews(edit) {
2966
3057
  if (edit.type !== "modify") return [null, null];
2967
3058
  if (edit._resolved_proxy_edit) {
2968
3059
  edit = edit._resolved_proxy_edit;
2969
3060
  }
2970
- const start_idx = edit._resolved_start_idx;
3061
+ let start_idx = edit._resolved_start_idx;
2971
3062
  if (start_idx === void 0 || start_idx === null) return [null, null];
2972
- const target_text = edit.target_text || "";
2973
- 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
+ }
2974
3075
  const length = target_text.length;
2975
3076
  const active_mapper = edit._active_mapper_ref || this.mapper;
2976
3077
  const full_text = active_mapper.full_text;
@@ -3722,28 +3823,34 @@ var RedlineEngine = class {
3722
3823
  }
3723
3824
  }
3724
3825
  }
3725
- validate_edits(edits) {
3826
+ validate_edits(edits, index_offset = 0) {
3726
3827
  const errors = [];
3727
3828
  if (!this.mapper.full_text) this.mapper["_build_map"]();
3728
- errors.push(...validate_edit_strings(edits));
3829
+ errors.push(...validate_edit_strings(edits, index_offset));
3729
3830
  for (let i = 0; i < edits.length; i++) {
3730
3831
  const edit = edits[i];
3731
3832
  if (typeof edit !== "object" || edit === null) {
3732
3833
  errors.push(
3733
- `- 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.`
3734
3835
  );
3735
3836
  continue;
3736
3837
  }
3737
3838
  if (!edit.target_text) continue;
3738
3839
  const is_regex = edit.regex || false;
3739
3840
  const match_mode = edit.match_mode || "strict";
3740
- 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
+ );
3741
3845
  let activeText = this.mapper.full_text;
3742
3846
  let target_mapper = this.mapper;
3743
3847
  if (matches.length === 0) {
3744
3848
  if (!this.clean_mapper)
3745
3849
  this.clean_mapper = new DocumentMapper(this.doc, true);
3746
- 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
+ );
3747
3854
  if (matches.length > 0) {
3748
3855
  activeText = this.clean_mapper.full_text;
3749
3856
  target_mapper = this.clean_mapper;
@@ -3765,7 +3872,10 @@ var RedlineEngine = class {
3765
3872
  if (!this.original_mapper) {
3766
3873
  this.original_mapper = new DocumentMapper(this.doc, false, true);
3767
3874
  }
3768
- 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
+ );
3769
3879
  if (orig_matches.length > 0) {
3770
3880
  is_deleted_text = true;
3771
3881
  for (const [start, length] of orig_matches) {
@@ -3794,27 +3904,32 @@ var RedlineEngine = class {
3794
3904
  if (is_deleted_text) {
3795
3905
  const author_phrase = deleted_authors.size > 0 ? `by ${Array.from(deleted_authors).sort().join(", ")}` : "by an existing revision";
3796
3906
  errors.push(
3797
- `- 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.`
3798
3908
  );
3799
3909
  } else {
3910
+ const hint = this._nearest_match_hint(edit.target_text, is_regex);
3800
3911
  errors.push(
3801
- `- Edit ${i + 1} Failed: Target text not found in document:
3802
- "${edit.target_text}"`
3912
+ `- Edit ${i + 1 + index_offset} Failed: Target text not found in document:
3913
+ "${edit.target_text}"${hint}`
3803
3914
  );
3804
3915
  }
3805
3916
  } else if (matches.length > 1 && match_mode === "strict") {
3806
- const positions = matches.map(([start, length]) => [
3807
- start,
3808
- start + length
3809
- ]);
3810
- errors.push(
3811
- format_ambiguity_error(
3812
- i + 1,
3813
- edit.target_text,
3814
- activeText,
3815
- positions
3816
- )
3817
- );
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
+ }
3818
3933
  }
3819
3934
  if (matches.length === 1) {
3820
3935
  const [m_start, m_len] = matches[0];
@@ -3831,14 +3946,14 @@ var RedlineEngine = class {
3831
3946
  const parts = matched.split("\n\n");
3832
3947
  if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3833
3948
  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.`
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.`
3835
3950
  );
3836
3951
  }
3837
3952
  } else {
3838
3953
  const parts = final_target.split("\n\n");
3839
3954
  if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3840
3955
  errors.push(
3841
- `- 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.`
3842
3957
  );
3843
3958
  }
3844
3959
  }
@@ -3857,7 +3972,13 @@ var RedlineEngine = class {
3857
3972
  ).filter((n) => n.getAttribute("w:id") === s.ins_id);
3858
3973
  if (insNodes.length > 0) {
3859
3974
  const auth = insNodes[0].getAttribute("w:author");
3860
- 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
+ }
3861
3982
  }
3862
3983
  }
3863
3984
  if (s.comment_ids) {
@@ -3871,7 +3992,7 @@ var RedlineEngine = class {
3871
3992
  }
3872
3993
  if (nestedAuthors.size > 0) {
3873
3994
  errors.push(
3874
- `- 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.`
3875
3996
  );
3876
3997
  }
3877
3998
  }
@@ -3933,22 +4054,13 @@ var RedlineEngine = class {
3933
4054
  });
3934
4055
  }
3935
4056
  if (dry_run) {
3936
- const baselines = /* @__PURE__ */ new Map();
3937
- for (const part of this.doc.pkg.parts) {
3938
- if (part._element) {
3939
- baselines.set(part, part._element.cloneNode(true));
3940
- }
3941
- }
4057
+ const snapshot = takeSnapshot(this.doc);
4058
+ const originalCurrentId = this.current_id;
3942
4059
  try {
3943
4060
  return this._process_batch_internal(changes, true);
3944
4061
  } finally {
3945
- for (const [part, originalEl] of baselines.entries()) {
3946
- const doc = part._element.ownerDocument;
3947
- if (doc && doc.documentElement) {
3948
- doc.replaceChild(originalEl, doc.documentElement);
3949
- }
3950
- part._element = originalEl;
3951
- }
4062
+ restoreSnapshot(this.doc, snapshot);
4063
+ this.current_id = originalCurrentId;
3952
4064
  this.mapper = new DocumentMapper(this.doc);
3953
4065
  this.comments_manager = new CommentsManager(this.doc);
3954
4066
  this.clean_mapper = null;
@@ -3958,6 +4070,13 @@ var RedlineEngine = class {
3958
4070
  }
3959
4071
  }
3960
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
+ }
3961
4080
  this.skipped_details = [];
3962
4081
  const actions = changes.filter(
3963
4082
  (c) => c !== null && typeof c === "object" && ["accept", "reject", "reply"].includes(c.type)
@@ -3965,14 +4084,67 @@ var RedlineEngine = class {
3965
4084
  const edits = changes.filter(
3966
4085
  (c) => c === null || typeof c !== "object" || !["accept", "reject", "reply"].includes(c.type)
3967
4086
  );
3968
- if (!dry_run_mode) {
3969
- const all_errors = [];
3970
- if (actions.length > 0) {
3971
- 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;
3972
4099
  }
3973
- if (edits.length > 0) {
3974
- 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
+ }
3975
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
+ ];
3976
4148
  if (all_errors.length > 0) {
3977
4149
  throw new BatchValidationError(all_errors);
3978
4150
  }
@@ -4006,8 +4178,9 @@ var RedlineEngine = class {
4006
4178
  let skipped_edits = 0;
4007
4179
  if (edits.length > 0) {
4008
4180
  if (dry_run_mode) {
4009
- for (const edit of edits) {
4010
- 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);
4011
4184
  const warning = this._check_punctuation_warning(
4012
4185
  edit.target_text || ""
4013
4186
  );
@@ -4041,6 +4214,8 @@ var RedlineEngine = class {
4041
4214
  occurrences_modified: edit._occurrences_modified || 0,
4042
4215
  match_mode: edit.match_mode || "strict"
4043
4216
  });
4217
+ this.mapper = new DocumentMapper(this.doc);
4218
+ this.clean_mapper = null;
4044
4219
  } else {
4045
4220
  skipped_edits++;
4046
4221
  const error_msg = this.skipped_details.length > 0 ? this.skipped_details[this.skipped_details.length - 1] : "Failed to apply edit";
@@ -4056,15 +4231,37 @@ var RedlineEngine = class {
4056
4231
  }
4057
4232
  }
4058
4233
  } else {
4059
- const errors = this.validate_edits(edits);
4060
- if (errors.length > 0) {
4061
- 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;
4062
4259
  }
4063
- const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
4064
- const res = this.apply_edits(cloned_edits, page_offsets);
4065
- applied_edits = cloned_edits.filter((e) => e._applied_status).length;
4066
- skipped_edits = cloned_edits.length - applied_edits;
4067
- 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) {
4068
4265
  const success = edit._applied_status || false;
4069
4266
  const error_msg = edit._error_msg || null;
4070
4267
  const warning = this._check_punctuation_warning(
@@ -4218,14 +4415,22 @@ var RedlineEngine = class {
4218
4415
  if (parent) {
4219
4416
  parent._applied_status = true;
4220
4417
  parent._occurrences_modified = (parent._occurrences_modified || 0) + 1;
4221
- 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
+ );
4222
4423
  const pages = parent._pages || [];
4223
4424
  if (!pages.includes(page)) pages.unshift(page);
4224
4425
  parent._pages = pages;
4225
4426
  parent._heading_path = path;
4226
4427
  } else {
4227
4428
  edit._occurrences_modified = (edit._occurrences_modified || 0) + 1;
4228
- 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
+ );
4229
4434
  const pages = edit._pages || [];
4230
4435
  if (!pages.includes(page)) pages.unshift(page);
4231
4436
  edit._pages = pages;
@@ -4386,12 +4591,18 @@ var RedlineEngine = class {
4386
4591
  if (!edit.target_text) return null;
4387
4592
  const is_regex = edit.regex || false;
4388
4593
  const match_mode = edit.match_mode || "strict";
4389
- 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
+ );
4390
4598
  let use_clean_map = false;
4391
4599
  if (matches.length === 0) {
4392
4600
  if (!this.clean_mapper)
4393
4601
  this.clean_mapper = new DocumentMapper(this.doc, true);
4394
- 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
+ );
4395
4606
  if (matches.length > 0) use_clean_map = true;
4396
4607
  else return null;
4397
4608
  }
@@ -4416,14 +4627,49 @@ var RedlineEngine = class {
4416
4627
  start_idx + match_len
4417
4628
  );
4418
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
+ }
4419
4658
  if (is_regex && current_effective_new_text) {
4420
4659
  try {
4421
- 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
+ );
4422
4664
  } catch (e) {
4423
4665
  }
4424
4666
  }
4425
- const [edit_target_clean, edit_target_style] = this._parse_markdown_style(edit.target_text);
4426
- 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
+ );
4427
4673
  if (edit_target_style !== edit_new_style) {
4428
4674
  const [actual_clean] = this._parse_markdown_style(actual_doc_text);
4429
4675
  const final_target2 = actual_clean;
@@ -4462,7 +4708,9 @@ var RedlineEngine = class {
4462
4708
  let effective_start_idx = start_idx;
4463
4709
  if (current_effective_new_text.startsWith(actual_doc_text)) {
4464
4710
  effective_op = "INSERTION";
4465
- 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
+ );
4466
4714
  effective_start_idx = start_idx + match_len;
4467
4715
  } else {
4468
4716
  const [prefix_len, suffix_len] = trim_common_context(
@@ -4543,7 +4791,12 @@ var RedlineEngine = class {
4543
4791
  const first_anchor = ascend_to_paragraph_child(first_el, start_p);
4544
4792
  const last_anchor = ascend_to_paragraph_child(last_el, end_p);
4545
4793
  if (start_p === end_p) {
4546
- 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
+ );
4547
4800
  } else {
4548
4801
  this._attach_comment_spanning(
4549
4802
  start_p,
@@ -5832,13 +6085,21 @@ function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets
5832
6085
  if (seen_cells.has(cell)) continue;
5833
6086
  seen_cells.add(cell);
5834
6087
  if (!first_cell) cell_cursor += 3;
5835
- const cell_content = _extract_blocks(
6088
+ let cell_content = _extract_blocks(
5836
6089
  cell,
5837
6090
  comments_map,
5838
6091
  cleanView,
5839
6092
  cell_cursor,
5840
6093
  paragraph_offsets
5841
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
+ }
5842
6103
  cell_texts.push(cell_content);
5843
6104
  cell_cursor += cell_content.length;
5844
6105
  first_cell = false;