@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.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+)?";
@@ -2869,6 +2881,51 @@ function insertAfter(newNode, refNode) {
2869
2881
  refNode.parentNode.insertBefore(newNode, refNode.nextSibling);
2870
2882
  }
2871
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
+ }
2872
2929
  var BatchValidationError = class extends Error {
2873
2930
  errors;
2874
2931
  constructor(errors) {
@@ -2877,7 +2934,7 @@ var BatchValidationError = class extends Error {
2877
2934
  this.errors = errors;
2878
2935
  }
2879
2936
  };
2880
- function validate_edit_strings(edits) {
2937
+ function validate_edit_strings(edits, index_offset = 0) {
2881
2938
  const errors = [];
2882
2939
  for (let i = 0; i < edits.length; i++) {
2883
2940
  const edit = edits[i];
@@ -2885,7 +2942,7 @@ function validate_edit_strings(edits) {
2885
2942
  const n_text = edit.new_text || "";
2886
2943
  if (n_text.includes("{++") || n_text.includes("{--") || n_text.includes("{>>") || n_text.includes("{==")) {
2887
2944
  errors.push(
2888
- `- 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.`
2889
2946
  );
2890
2947
  }
2891
2948
  if (t_text.includes("[^") || n_text.includes("[^")) {
@@ -2896,11 +2953,11 @@ function validate_edit_strings(edits) {
2896
2953
  (f) => n_fns.filter((x) => x === f).length > t_fns.filter((x) => x === f).length
2897
2954
  )) {
2898
2955
  errors.push(
2899
- `- 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.`
2900
2957
  );
2901
2958
  } else {
2902
2959
  errors.push(
2903
- `- 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.`
2904
2961
  );
2905
2962
  }
2906
2963
  }
@@ -2911,16 +2968,16 @@ function validate_edit_strings(edits) {
2911
2968
  if (t_links.length !== n_links.length) {
2912
2969
  if (n_links.length > t_links.length) {
2913
2970
  errors.push(
2914
- `- 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.`
2915
2972
  );
2916
2973
  } else {
2917
2974
  errors.push(
2918
- `- 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.`
2919
2976
  );
2920
2977
  }
2921
2978
  } else if (t_links.length > 1 && JSON.stringify(t_links) !== JSON.stringify(n_links)) {
2922
2979
  errors.push(
2923
- `- 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.`
2924
2981
  );
2925
2982
  }
2926
2983
  }
@@ -2930,17 +2987,17 @@ function validate_edit_strings(edits) {
2930
2987
  if (t_xrefs.length !== n_xrefs.length) {
2931
2988
  if (n_xrefs.length > t_xrefs.length) {
2932
2989
  errors.push(
2933
- `- 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.`
2934
2991
  );
2935
2992
  } else {
2936
2993
  errors.push(
2937
- `- 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.`
2938
2995
  );
2939
2996
  }
2940
2997
  } else {
2941
2998
  if (JSON.stringify(t_xrefs) !== JSON.stringify(n_xrefs)) {
2942
2999
  errors.push(
2943
- `- 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.`
2944
3001
  );
2945
3002
  }
2946
3003
  }
@@ -2951,7 +3008,7 @@ function validate_edit_strings(edits) {
2951
3008
  for (const a of n_anchors) {
2952
3009
  if (n_anchors.filter((x) => x === a).length > t_anchors.filter((x) => x === a).length) {
2953
3010
  errors.push(
2954
- `- 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.`
2955
3012
  );
2956
3013
  break;
2957
3014
  }
@@ -2965,16 +3022,16 @@ function validate_edit_strings(edits) {
2965
3022
  const level = stripped.length - stripped.replace(/^#+/, "").length;
2966
3023
  if (stripped.substring(level).startsWith(" ") || stripped.substring(level) === "") {
2967
3024
  errors.push(
2968
- `- 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).`
2969
3026
  );
2970
3027
  break;
2971
3028
  }
2972
3029
  }
2973
3030
  }
2974
3031
  }
2975
- 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)")) {
2976
3033
  errors.push(
2977
- `- 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.`
2978
3035
  );
2979
3036
  }
2980
3037
  }
@@ -2996,7 +3053,7 @@ var RedlineEngine = class {
2996
3053
  this.timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
2997
3054
  const w16du_ns = "http://schemas.microsoft.com/office/word/2023/wordml/word16du";
2998
3055
  for (const part of this.doc.pkg.parts) {
2999
- if (part === this.doc.part || part.contentType.includes("wordprocessingml") && part.contentType.endsWith("+xml")) {
3056
+ if (part === this.doc.part) {
3000
3057
  if (!part._element.hasAttribute("xmlns:w16du")) {
3001
3058
  part._element.setAttribute("xmlns:w16du", w16du_ns);
3002
3059
  }
@@ -3013,15 +3070,59 @@ var RedlineEngine = class {
3013
3070
  }
3014
3071
  return null;
3015
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
+ }
3016
3107
  _build_edit_context_previews(edit) {
3017
3108
  if (edit.type !== "modify") return [null, null];
3018
3109
  if (edit._resolved_proxy_edit) {
3019
3110
  edit = edit._resolved_proxy_edit;
3020
3111
  }
3021
- const start_idx = edit._resolved_start_idx;
3112
+ let start_idx = edit._resolved_start_idx;
3022
3113
  if (start_idx === void 0 || start_idx === null) return [null, null];
3023
- const target_text = edit.target_text || "";
3024
- 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
+ }
3025
3126
  const length = target_text.length;
3026
3127
  const active_mapper = edit._active_mapper_ref || this.mapper;
3027
3128
  const full_text = active_mapper.full_text;
@@ -3773,28 +3874,34 @@ var RedlineEngine = class {
3773
3874
  }
3774
3875
  }
3775
3876
  }
3776
- validate_edits(edits) {
3877
+ validate_edits(edits, index_offset = 0) {
3777
3878
  const errors = [];
3778
3879
  if (!this.mapper.full_text) this.mapper["_build_map"]();
3779
- errors.push(...validate_edit_strings(edits));
3880
+ errors.push(...validate_edit_strings(edits, index_offset));
3780
3881
  for (let i = 0; i < edits.length; i++) {
3781
3882
  const edit = edits[i];
3782
3883
  if (typeof edit !== "object" || edit === null) {
3783
3884
  errors.push(
3784
- `- 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.`
3785
3886
  );
3786
3887
  continue;
3787
3888
  }
3788
3889
  if (!edit.target_text) continue;
3789
3890
  const is_regex = edit.regex || false;
3790
3891
  const match_mode = edit.match_mode || "strict";
3791
- 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
+ );
3792
3896
  let activeText = this.mapper.full_text;
3793
3897
  let target_mapper = this.mapper;
3794
3898
  if (matches.length === 0) {
3795
3899
  if (!this.clean_mapper)
3796
3900
  this.clean_mapper = new DocumentMapper(this.doc, true);
3797
- 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
+ );
3798
3905
  if (matches.length > 0) {
3799
3906
  activeText = this.clean_mapper.full_text;
3800
3907
  target_mapper = this.clean_mapper;
@@ -3816,7 +3923,10 @@ var RedlineEngine = class {
3816
3923
  if (!this.original_mapper) {
3817
3924
  this.original_mapper = new DocumentMapper(this.doc, false, true);
3818
3925
  }
3819
- 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
+ );
3820
3930
  if (orig_matches.length > 0) {
3821
3931
  is_deleted_text = true;
3822
3932
  for (const [start, length] of orig_matches) {
@@ -3845,27 +3955,32 @@ var RedlineEngine = class {
3845
3955
  if (is_deleted_text) {
3846
3956
  const author_phrase = deleted_authors.size > 0 ? `by ${Array.from(deleted_authors).sort().join(", ")}` : "by an existing revision";
3847
3957
  errors.push(
3848
- `- 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.`
3849
3959
  );
3850
3960
  } else {
3961
+ const hint = this._nearest_match_hint(edit.target_text, is_regex);
3851
3962
  errors.push(
3852
- `- Edit ${i + 1} Failed: Target text not found in document:
3853
- "${edit.target_text}"`
3963
+ `- Edit ${i + 1 + index_offset} Failed: Target text not found in document:
3964
+ "${edit.target_text}"${hint}`
3854
3965
  );
3855
3966
  }
3856
3967
  } else if (matches.length > 1 && match_mode === "strict") {
3857
- const positions = matches.map(([start, length]) => [
3858
- start,
3859
- start + length
3860
- ]);
3861
- errors.push(
3862
- format_ambiguity_error(
3863
- i + 1,
3864
- edit.target_text,
3865
- activeText,
3866
- positions
3867
- )
3868
- );
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
+ }
3869
3984
  }
3870
3985
  if (matches.length === 1) {
3871
3986
  const [m_start, m_len] = matches[0];
@@ -3882,14 +3997,14 @@ var RedlineEngine = class {
3882
3997
  const parts = matched.split("\n\n");
3883
3998
  if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3884
3999
  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.`
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.`
3886
4001
  );
3887
4002
  }
3888
4003
  } else {
3889
4004
  const parts = final_target.split("\n\n");
3890
4005
  if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3891
4006
  errors.push(
3892
- `- 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.`
3893
4008
  );
3894
4009
  }
3895
4010
  }
@@ -3908,7 +4023,13 @@ var RedlineEngine = class {
3908
4023
  ).filter((n) => n.getAttribute("w:id") === s.ins_id);
3909
4024
  if (insNodes.length > 0) {
3910
4025
  const auth = insNodes[0].getAttribute("w:author");
3911
- 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
+ }
3912
4033
  }
3913
4034
  }
3914
4035
  if (s.comment_ids) {
@@ -3922,7 +4043,7 @@ var RedlineEngine = class {
3922
4043
  }
3923
4044
  if (nestedAuthors.size > 0) {
3924
4045
  errors.push(
3925
- `- 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.`
3926
4047
  );
3927
4048
  }
3928
4049
  }
@@ -3984,22 +4105,13 @@ var RedlineEngine = class {
3984
4105
  });
3985
4106
  }
3986
4107
  if (dry_run) {
3987
- const baselines = /* @__PURE__ */ new Map();
3988
- for (const part of this.doc.pkg.parts) {
3989
- if (part._element) {
3990
- baselines.set(part, part._element.cloneNode(true));
3991
- }
3992
- }
4108
+ const snapshot = takeSnapshot(this.doc);
4109
+ const originalCurrentId = this.current_id;
3993
4110
  try {
3994
4111
  return this._process_batch_internal(changes, true);
3995
4112
  } finally {
3996
- for (const [part, originalEl] of baselines.entries()) {
3997
- const doc = part._element.ownerDocument;
3998
- if (doc && doc.documentElement) {
3999
- doc.replaceChild(originalEl, doc.documentElement);
4000
- }
4001
- part._element = originalEl;
4002
- }
4113
+ restoreSnapshot(this.doc, snapshot);
4114
+ this.current_id = originalCurrentId;
4003
4115
  this.mapper = new DocumentMapper(this.doc);
4004
4116
  this.comments_manager = new CommentsManager(this.doc);
4005
4117
  this.clean_mapper = null;
@@ -4009,6 +4121,13 @@ var RedlineEngine = class {
4009
4121
  }
4010
4122
  }
4011
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
+ }
4012
4131
  this.skipped_details = [];
4013
4132
  const actions = changes.filter(
4014
4133
  (c) => c !== null && typeof c === "object" && ["accept", "reject", "reply"].includes(c.type)
@@ -4016,14 +4135,67 @@ var RedlineEngine = class {
4016
4135
  const edits = changes.filter(
4017
4136
  (c) => c === null || typeof c !== "object" || !["accept", "reject", "reply"].includes(c.type)
4018
4137
  );
4019
- if (!dry_run_mode) {
4020
- const all_errors = [];
4021
- if (actions.length > 0) {
4022
- 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;
4023
4150
  }
4024
- if (edits.length > 0) {
4025
- 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
+ }
4026
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
+ ];
4027
4199
  if (all_errors.length > 0) {
4028
4200
  throw new BatchValidationError(all_errors);
4029
4201
  }
@@ -4057,8 +4229,9 @@ var RedlineEngine = class {
4057
4229
  let skipped_edits = 0;
4058
4230
  if (edits.length > 0) {
4059
4231
  if (dry_run_mode) {
4060
- for (const edit of edits) {
4061
- 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);
4062
4235
  const warning = this._check_punctuation_warning(
4063
4236
  edit.target_text || ""
4064
4237
  );
@@ -4092,6 +4265,8 @@ var RedlineEngine = class {
4092
4265
  occurrences_modified: edit._occurrences_modified || 0,
4093
4266
  match_mode: edit.match_mode || "strict"
4094
4267
  });
4268
+ this.mapper = new DocumentMapper(this.doc);
4269
+ this.clean_mapper = null;
4095
4270
  } else {
4096
4271
  skipped_edits++;
4097
4272
  const error_msg = this.skipped_details.length > 0 ? this.skipped_details[this.skipped_details.length - 1] : "Failed to apply edit";
@@ -4107,15 +4282,37 @@ var RedlineEngine = class {
4107
4282
  }
4108
4283
  }
4109
4284
  } else {
4110
- const errors = this.validate_edits(edits);
4111
- if (errors.length > 0) {
4112
- 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;
4113
4310
  }
4114
- const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
4115
- const res = this.apply_edits(cloned_edits, page_offsets);
4116
- applied_edits = cloned_edits.filter((e) => e._applied_status).length;
4117
- skipped_edits = cloned_edits.length - applied_edits;
4118
- 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) {
4119
4316
  const success = edit._applied_status || false;
4120
4317
  const error_msg = edit._error_msg || null;
4121
4318
  const warning = this._check_punctuation_warning(
@@ -4269,14 +4466,22 @@ var RedlineEngine = class {
4269
4466
  if (parent) {
4270
4467
  parent._applied_status = true;
4271
4468
  parent._occurrences_modified = (parent._occurrences_modified || 0) + 1;
4272
- 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
+ );
4273
4474
  const pages = parent._pages || [];
4274
4475
  if (!pages.includes(page)) pages.unshift(page);
4275
4476
  parent._pages = pages;
4276
4477
  parent._heading_path = path;
4277
4478
  } else {
4278
4479
  edit._occurrences_modified = (edit._occurrences_modified || 0) + 1;
4279
- 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
+ );
4280
4485
  const pages = edit._pages || [];
4281
4486
  if (!pages.includes(page)) pages.unshift(page);
4282
4487
  edit._pages = pages;
@@ -4437,12 +4642,18 @@ var RedlineEngine = class {
4437
4642
  if (!edit.target_text) return null;
4438
4643
  const is_regex = edit.regex || false;
4439
4644
  const match_mode = edit.match_mode || "strict";
4440
- 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
+ );
4441
4649
  let use_clean_map = false;
4442
4650
  if (matches.length === 0) {
4443
4651
  if (!this.clean_mapper)
4444
4652
  this.clean_mapper = new DocumentMapper(this.doc, true);
4445
- 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
+ );
4446
4657
  if (matches.length > 0) use_clean_map = true;
4447
4658
  else return null;
4448
4659
  }
@@ -4467,14 +4678,49 @@ var RedlineEngine = class {
4467
4678
  start_idx + match_len
4468
4679
  );
4469
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
+ }
4470
4709
  if (is_regex && current_effective_new_text) {
4471
4710
  try {
4472
- 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
+ );
4473
4715
  } catch (e) {
4474
4716
  }
4475
4717
  }
4476
- const [edit_target_clean, edit_target_style] = this._parse_markdown_style(edit.target_text);
4477
- 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
+ );
4478
4724
  if (edit_target_style !== edit_new_style) {
4479
4725
  const [actual_clean] = this._parse_markdown_style(actual_doc_text);
4480
4726
  const final_target2 = actual_clean;
@@ -4513,7 +4759,9 @@ var RedlineEngine = class {
4513
4759
  let effective_start_idx = start_idx;
4514
4760
  if (current_effective_new_text.startsWith(actual_doc_text)) {
4515
4761
  effective_op = "INSERTION";
4516
- 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
+ );
4517
4765
  effective_start_idx = start_idx + match_len;
4518
4766
  } else {
4519
4767
  const [prefix_len, suffix_len] = trim_common_context(
@@ -4594,7 +4842,12 @@ var RedlineEngine = class {
4594
4842
  const first_anchor = ascend_to_paragraph_child(first_el, start_p);
4595
4843
  const last_anchor = ascend_to_paragraph_child(last_el, end_p);
4596
4844
  if (start_p === end_p) {
4597
- 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
+ );
4598
4851
  } else {
4599
4852
  this._attach_comment_spanning(
4600
4853
  start_p,
@@ -5883,13 +6136,21 @@ function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets
5883
6136
  if (seen_cells.has(cell)) continue;
5884
6137
  seen_cells.add(cell);
5885
6138
  if (!first_cell) cell_cursor += 3;
5886
- const cell_content = _extract_blocks(
6139
+ let cell_content = _extract_blocks(
5887
6140
  cell,
5888
6141
  comments_map,
5889
6142
  cleanView,
5890
6143
  cell_cursor,
5891
6144
  paragraph_offsets
5892
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
+ }
5893
6154
  cell_texts.push(cell_content);
5894
6155
  cell_cursor += cell_content.length;
5895
6156
  first_cell = false;