@adeu/core 1.17.1 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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;
@@ -3194,6 +3295,93 @@ var RedlineEngine = class {
3194
3295
  }
3195
3296
  }
3196
3297
  }
3298
+ /**
3299
+ * Revert every tracked change, returning the document to the state it had
3300
+ * before any revision was proposed. The exact inverse of
3301
+ * accept_all_revisions:
3302
+ *
3303
+ * - <w:ins> -> removed together with all of its content (the proposed
3304
+ * insertion never existed); an inserted row (<w:ins> in
3305
+ * <w:trPr>) drops the whole row.
3306
+ * - <w:del> -> unwrapped, restoring the original text (<w:delText> becomes
3307
+ * <w:t> again); a row-deletion mark in <w:trPr> is removed so
3308
+ * the row survives.
3309
+ * - paragraph-mark <w:del> in pPr/rPr -> removed, undoing a proposed merge.
3310
+ *
3311
+ * Comments are annotations, not revisions, so standalone comments are left in
3312
+ * place; only anchors stranded inside a rejected insertion are cleaned up.
3313
+ *
3314
+ * Insertions are reverted before deletions are restored so a deletion nested
3315
+ * inside a foreign author's insertion is removed wholesale with the insertion
3316
+ * — the contingent text disappears rather than being promoted to committed
3317
+ * body text.
3318
+ *
3319
+ * Known limitation: tracked paragraph STRUCTURE changes (a split recorded as a
3320
+ * pilcrow <w:ins>, or a merge recorded as a pilcrow <w:del>) are reverted only
3321
+ * to the extent of dropping/keeping the mark; the original paragraph boundary
3322
+ * is not reconstructed, because the merge protocol coalesces paragraphs
3323
+ * destructively at edit time. Reverting run-level insertions/deletions (the
3324
+ * common case) is exact. This limitation is shared with the Python engine.
3325
+ */
3326
+ reject_all_revisions() {
3327
+ const parts_to_process = [this.doc.element];
3328
+ for (const part of this.doc.pkg.parts) {
3329
+ if (part === this.doc.part) continue;
3330
+ if (part.contentType.includes("wordprocessingml") && part.contentType.endsWith("+xml")) {
3331
+ parts_to_process.push(part._element);
3332
+ }
3333
+ }
3334
+ for (const root_element of parts_to_process) {
3335
+ const insNodes = findAllDescendants(root_element, "w:ins");
3336
+ for (const ins of insNodes) {
3337
+ const parent = ins.parentNode;
3338
+ if (!parent) continue;
3339
+ this._clean_wrapping_comments(ins);
3340
+ this._delete_comments_in_element(ins);
3341
+ if (parent.tagName === "w:trPr") {
3342
+ const row = parent.parentNode;
3343
+ if (row && row.parentNode) {
3344
+ row.parentNode.removeChild(row);
3345
+ }
3346
+ } else {
3347
+ parent.removeChild(ins);
3348
+ }
3349
+ }
3350
+ const pNodes = findAllDescendants(root_element, "w:p");
3351
+ for (const p of pNodes) {
3352
+ const pPr = findChild(p, "w:pPr");
3353
+ if (pPr) {
3354
+ const rPr = findChild(pPr, "w:rPr");
3355
+ const delMark = rPr ? findChild(rPr, "w:del") : null;
3356
+ if (rPr && delMark) {
3357
+ rPr.removeChild(delMark);
3358
+ }
3359
+ }
3360
+ }
3361
+ const delNodes = findAllDescendants(root_element, "w:del");
3362
+ for (const d of delNodes) {
3363
+ const parent = d.parentNode;
3364
+ if (!parent) continue;
3365
+ this._clean_wrapping_comments(d);
3366
+ if (parent.tagName === "w:trPr") {
3367
+ parent.removeChild(d);
3368
+ continue;
3369
+ }
3370
+ const delTexts = Array.from(d.getElementsByTagName("w:delText"));
3371
+ for (const dt of delTexts) {
3372
+ const t = d.ownerDocument.createElement("w:t");
3373
+ t.textContent = dt.textContent;
3374
+ if (dt.hasAttribute("xml:space"))
3375
+ t.setAttribute("xml:space", "preserve");
3376
+ dt.parentNode?.replaceChild(t, dt);
3377
+ }
3378
+ while (d.firstChild) {
3379
+ parent.insertBefore(d.firstChild, d);
3380
+ }
3381
+ parent.removeChild(d);
3382
+ }
3383
+ }
3384
+ }
3197
3385
  _getNextId() {
3198
3386
  this.current_id++;
3199
3387
  return this.current_id.toString();
@@ -3722,28 +3910,34 @@ var RedlineEngine = class {
3722
3910
  }
3723
3911
  }
3724
3912
  }
3725
- validate_edits(edits) {
3913
+ validate_edits(edits, index_offset = 0) {
3726
3914
  const errors = [];
3727
3915
  if (!this.mapper.full_text) this.mapper["_build_map"]();
3728
- errors.push(...validate_edit_strings(edits));
3916
+ errors.push(...validate_edit_strings(edits, index_offset));
3729
3917
  for (let i = 0; i < edits.length; i++) {
3730
3918
  const edit = edits[i];
3731
3919
  if (typeof edit !== "object" || edit === null) {
3732
3920
  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.`
3921
+ `- 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
3922
  );
3735
3923
  continue;
3736
3924
  }
3737
3925
  if (!edit.target_text) continue;
3738
3926
  const is_regex = edit.regex || false;
3739
3927
  const match_mode = edit.match_mode || "strict";
3740
- let matches = this.mapper.find_all_match_indices(edit.target_text, is_regex);
3928
+ let matches = this.mapper.find_all_match_indices(
3929
+ edit.target_text,
3930
+ is_regex
3931
+ );
3741
3932
  let activeText = this.mapper.full_text;
3742
3933
  let target_mapper = this.mapper;
3743
3934
  if (matches.length === 0) {
3744
3935
  if (!this.clean_mapper)
3745
3936
  this.clean_mapper = new DocumentMapper(this.doc, true);
3746
- matches = this.clean_mapper.find_all_match_indices(edit.target_text, is_regex);
3937
+ matches = this.clean_mapper.find_all_match_indices(
3938
+ edit.target_text,
3939
+ is_regex
3940
+ );
3747
3941
  if (matches.length > 0) {
3748
3942
  activeText = this.clean_mapper.full_text;
3749
3943
  target_mapper = this.clean_mapper;
@@ -3765,7 +3959,10 @@ var RedlineEngine = class {
3765
3959
  if (!this.original_mapper) {
3766
3960
  this.original_mapper = new DocumentMapper(this.doc, false, true);
3767
3961
  }
3768
- const orig_matches = this.original_mapper.find_all_match_indices(edit.target_text, is_regex);
3962
+ const orig_matches = this.original_mapper.find_all_match_indices(
3963
+ edit.target_text,
3964
+ is_regex
3965
+ );
3769
3966
  if (orig_matches.length > 0) {
3770
3967
  is_deleted_text = true;
3771
3968
  for (const [start, length] of orig_matches) {
@@ -3794,27 +3991,32 @@ var RedlineEngine = class {
3794
3991
  if (is_deleted_text) {
3795
3992
  const author_phrase = deleted_authors.size > 0 ? `by ${Array.from(deleted_authors).sort().join(", ")}` : "by an existing revision";
3796
3993
  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.`
3994
+ `- 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
3995
  );
3799
3996
  } else {
3997
+ const hint = this._nearest_match_hint(edit.target_text, is_regex);
3800
3998
  errors.push(
3801
- `- Edit ${i + 1} Failed: Target text not found in document:
3802
- "${edit.target_text}"`
3999
+ `- Edit ${i + 1 + index_offset} Failed: Target text not found in document:
4000
+ "${edit.target_text}"${hint}`
3803
4001
  );
3804
4002
  }
3805
4003
  } 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
- );
4004
+ if (edit.target_text.includes("|")) {
4005
+ matches = matches.slice(0, 1);
4006
+ } else {
4007
+ const positions = matches.map(([start, length]) => [
4008
+ start,
4009
+ start + length
4010
+ ]);
4011
+ errors.push(
4012
+ format_ambiguity_error(
4013
+ i + 1 + index_offset,
4014
+ edit.target_text,
4015
+ activeText,
4016
+ positions
4017
+ )
4018
+ );
4019
+ }
3818
4020
  }
3819
4021
  if (matches.length === 1) {
3820
4022
  const [m_start, m_len] = matches[0];
@@ -3827,29 +4029,36 @@ var RedlineEngine = class {
3827
4029
  (edit.new_text || "").length - sfx
3828
4030
  );
3829
4031
  if (final_target.includes("\n\n")) {
3830
- if (final_new.includes("\n\n")) {
3831
- const parts = matched.split("\n\n");
3832
- if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3833
- 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.`
3835
- );
3836
- }
3837
- } else {
3838
- const parts = final_target.split("\n\n");
3839
- if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3840
- 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.`
3842
- );
4032
+ const balanced = matched.split("\n\n").length === (edit.new_text || "").split("\n\n").length;
4033
+ if (!balanced) {
4034
+ if (final_new.includes("\n\n")) {
4035
+ const parts = matched.split("\n\n");
4036
+ if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
4037
+ errors.push(
4038
+ `- 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.`
4039
+ );
4040
+ }
4041
+ } else {
4042
+ const parts = final_target.split("\n\n");
4043
+ if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
4044
+ errors.push(
4045
+ `- 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.`
4046
+ );
4047
+ }
3843
4048
  }
3844
4049
  }
3845
4050
  }
3846
4051
  }
3847
4052
  for (const [start, length] of matches) {
3848
- const spans = this.mapper.spans.filter(
4053
+ const spans = target_mapper.spans.filter(
3849
4054
  (s) => s.end > start && s.start < start + length
3850
4055
  );
3851
- const nestedAuthors = /* @__PURE__ */ new Set();
4056
+ const insAuthors = /* @__PURE__ */ new Set();
4057
+ const commentAuthors = /* @__PURE__ */ new Set();
4058
+ let hasNonForeignRealText = false;
3852
4059
  for (const s of spans) {
4060
+ if (s.run === null) continue;
4061
+ let isForeignIns = false;
3853
4062
  if (s.ins_id) {
3854
4063
  const insNodes = findAllDescendants(
3855
4064
  this.doc.element,
@@ -3857,21 +4066,35 @@ var RedlineEngine = class {
3857
4066
  ).filter((n) => n.getAttribute("w:id") === s.ins_id);
3858
4067
  if (insNodes.length > 0) {
3859
4068
  const auth = insNodes[0].getAttribute("w:author");
3860
- if (auth && auth !== this.author) nestedAuthors.add(auth);
4069
+ if (auth && auth !== this.author) {
4070
+ insAuthors.add(auth);
4071
+ isForeignIns = true;
4072
+ }
3861
4073
  }
3862
4074
  }
4075
+ if (!isForeignIns) hasNonForeignRealText = true;
4076
+ }
4077
+ for (const s of spans) {
3863
4078
  if (s.comment_ids) {
3864
4079
  for (const cid of s.comment_ids) {
3865
4080
  const c_data = this.mapper.comments_map[cid];
3866
4081
  if (c_data && c_data.author && c_data.author !== this.author) {
3867
- nestedAuthors.add(c_data.author);
4082
+ commentAuthors.add(c_data.author);
3868
4083
  }
3869
4084
  }
3870
4085
  }
3871
4086
  }
3872
- if (nestedAuthors.size > 0) {
4087
+ if (insAuthors.size > 0 || commentAuthors.size > 0) {
4088
+ const fullyWithinForeignIns = insAuthors.size > 0 && !hasNonForeignRealText && commentAuthors.size === 0;
4089
+ if ((match_mode === "strict" || match_mode === "first") && fullyWithinForeignIns) {
4090
+ continue;
4091
+ }
4092
+ const nestedAuthors = /* @__PURE__ */ new Set([
4093
+ ...insAuthors,
4094
+ ...commentAuthors
4095
+ ]);
3873
4096
  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.`
4097
+ `- 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
4098
  );
3876
4099
  }
3877
4100
  }
@@ -3933,22 +4156,13 @@ var RedlineEngine = class {
3933
4156
  });
3934
4157
  }
3935
4158
  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
- }
4159
+ const snapshot = takeSnapshot(this.doc);
4160
+ const originalCurrentId = this.current_id;
3942
4161
  try {
3943
4162
  return this._process_batch_internal(changes, true);
3944
4163
  } 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
- }
4164
+ restoreSnapshot(this.doc, snapshot);
4165
+ this.current_id = originalCurrentId;
3952
4166
  this.mapper = new DocumentMapper(this.doc);
3953
4167
  this.comments_manager = new CommentsManager(this.doc);
3954
4168
  this.clean_mapper = null;
@@ -3958,6 +4172,13 @@ var RedlineEngine = class {
3958
4172
  }
3959
4173
  }
3960
4174
  _process_batch_internal(changes, dry_run_mode = false) {
4175
+ for (const c of changes) {
4176
+ if (c && typeof c === "object" && c.type === "modify" && c.target_text && c.new_text) {
4177
+ const [strippedTarget, strippedNew] = stripMatchingHeadingHashes(c.target_text, c.new_text);
4178
+ c.target_text = strippedTarget;
4179
+ c.new_text = strippedNew;
4180
+ }
4181
+ }
3961
4182
  this.skipped_details = [];
3962
4183
  const actions = changes.filter(
3963
4184
  (c) => c !== null && typeof c === "object" && ["accept", "reject", "reply"].includes(c.type)
@@ -3966,13 +4187,13 @@ var RedlineEngine = class {
3966
4187
  (c) => c === null || typeof c !== "object" || !["accept", "reject", "reply"].includes(c.type)
3967
4188
  );
3968
4189
  if (!dry_run_mode) {
3969
- const all_errors = [];
3970
- if (actions.length > 0) {
3971
- all_errors.push(...this.validate_review_actions(actions));
3972
- }
3973
- if (edits.length > 0) {
3974
- all_errors.push(...this.validate_edits(edits));
3975
- }
4190
+ const action_errors = actions.length > 0 ? this.validate_review_actions(actions) : [];
4191
+ const validate_edits_now = edits.length > 0 && action_errors.length > 0;
4192
+ const edit_errors = validate_edits_now ? this.validate_edits(edits) : [];
4193
+ const all_errors = [
4194
+ ...action_errors,
4195
+ ...edit_errors
4196
+ ];
3976
4197
  if (all_errors.length > 0) {
3977
4198
  throw new BatchValidationError(all_errors);
3978
4199
  }
@@ -4006,8 +4227,9 @@ var RedlineEngine = class {
4006
4227
  let skipped_edits = 0;
4007
4228
  if (edits.length > 0) {
4008
4229
  if (dry_run_mode) {
4009
- for (const edit of edits) {
4010
- const single_errors = this.validate_edits([edit]);
4230
+ for (let i = 0; i < edits.length; i++) {
4231
+ const edit = edits[i];
4232
+ const single_errors = this.validate_edits([edit], i);
4011
4233
  const warning = this._check_punctuation_warning(
4012
4234
  edit.target_text || ""
4013
4235
  );
@@ -4041,6 +4263,8 @@ var RedlineEngine = class {
4041
4263
  occurrences_modified: edit._occurrences_modified || 0,
4042
4264
  match_mode: edit.match_mode || "strict"
4043
4265
  });
4266
+ this.mapper = new DocumentMapper(this.doc);
4267
+ this.clean_mapper = null;
4044
4268
  } else {
4045
4269
  skipped_edits++;
4046
4270
  const error_msg = this.skipped_details.length > 0 ? this.skipped_details[this.skipped_details.length - 1] : "Failed to apply edit";
@@ -4056,15 +4280,37 @@ var RedlineEngine = class {
4056
4280
  }
4057
4281
  }
4058
4282
  } else {
4059
- const errors = this.validate_edits(edits);
4060
- if (errors.length > 0) {
4061
- throw new BatchValidationError(errors);
4062
- }
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) {
4283
+ const snapshot = takeSnapshot(this.doc);
4284
+ const originalCurrentId = this.current_id;
4285
+ try {
4286
+ const sequential_errors = [];
4287
+ for (let i = 0; i < edits.length; i++) {
4288
+ const edit = edits[i];
4289
+ const single_errors = this.validate_edits([edit], i);
4290
+ if (single_errors.length > 0) {
4291
+ sequential_errors.push(...single_errors);
4292
+ } else {
4293
+ this.apply_edits([edit], page_offsets);
4294
+ this.mapper = new DocumentMapper(this.doc);
4295
+ this.clean_mapper = null;
4296
+ }
4297
+ }
4298
+ if (sequential_errors.length > 0) {
4299
+ throw new BatchValidationError(sequential_errors);
4300
+ }
4301
+ } catch (err) {
4302
+ restoreSnapshot(this.doc, snapshot);
4303
+ this.current_id = originalCurrentId;
4304
+ this.mapper = new DocumentMapper(this.doc);
4305
+ this.comments_manager = new CommentsManager(this.doc);
4306
+ this.clean_mapper = null;
4307
+ throw err;
4308
+ }
4309
+ applied_edits = edits.filter(
4310
+ (e) => e._applied_status
4311
+ ).length;
4312
+ skipped_edits = edits.length - applied_edits;
4313
+ for (const edit of edits) {
4068
4314
  const success = edit._applied_status || false;
4069
4315
  const error_msg = edit._error_msg || null;
4070
4316
  const warning = this._check_punctuation_warning(
@@ -4183,6 +4429,7 @@ var RedlineEngine = class {
4183
4429
  (a, b) => (b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0)
4184
4430
  );
4185
4431
  const occupied_ranges = [];
4432
+ const counted_split_groups = /* @__PURE__ */ new Set();
4186
4433
  for (const [edit, orig_new] of resolved_edits) {
4187
4434
  const start = edit._resolved_start_idx || 0;
4188
4435
  const end = start + (edit.target_text ? edit.target_text.length : 0);
@@ -4211,21 +4458,38 @@ var RedlineEngine = class {
4211
4458
  success = this._apply_table_edit(edit, false);
4212
4459
  }
4213
4460
  if (success) {
4214
- applied++;
4461
+ const group_id = edit._split_group_id;
4462
+ const first_in_group = group_id === void 0 || group_id === null || !counted_split_groups.has(group_id);
4463
+ if (first_in_group && group_id !== void 0 && group_id !== null) {
4464
+ counted_split_groups.add(group_id);
4465
+ }
4466
+ if (first_in_group) applied++;
4215
4467
  occupied_ranges.push([start, end]);
4216
4468
  edit._applied_status = true;
4217
4469
  const parent = edit._parent_edit_ref;
4218
4470
  if (parent) {
4219
4471
  parent._applied_status = true;
4220
- 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);
4472
+ if (first_in_group) {
4473
+ parent._occurrences_modified = (parent._occurrences_modified || 0) + 1;
4474
+ }
4475
+ const [path, page] = this._get_heading_path_and_page(
4476
+ start,
4477
+ this.mapper.full_text,
4478
+ page_offsets
4479
+ );
4222
4480
  const pages = parent._pages || [];
4223
4481
  if (!pages.includes(page)) pages.unshift(page);
4224
4482
  parent._pages = pages;
4225
4483
  parent._heading_path = path;
4226
4484
  } else {
4227
- 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);
4485
+ if (first_in_group) {
4486
+ edit._occurrences_modified = (edit._occurrences_modified || 0) + 1;
4487
+ }
4488
+ const [path, page] = this._get_heading_path_and_page(
4489
+ start,
4490
+ this.mapper.full_text,
4491
+ page_offsets
4492
+ );
4229
4493
  const pages = edit._pages || [];
4230
4494
  if (!pages.includes(page)) pages.unshift(page);
4231
4495
  edit._pages = pages;
@@ -4386,12 +4650,18 @@ var RedlineEngine = class {
4386
4650
  if (!edit.target_text) return null;
4387
4651
  const is_regex = edit.regex || false;
4388
4652
  const match_mode = edit.match_mode || "strict";
4389
- let matches = this.mapper.find_all_match_indices(edit.target_text, is_regex);
4653
+ let matches = this.mapper.find_all_match_indices(
4654
+ edit.target_text,
4655
+ is_regex
4656
+ );
4390
4657
  let use_clean_map = false;
4391
4658
  if (matches.length === 0) {
4392
4659
  if (!this.clean_mapper)
4393
4660
  this.clean_mapper = new DocumentMapper(this.doc, true);
4394
- matches = this.clean_mapper.find_all_match_indices(edit.target_text, is_regex);
4661
+ matches = this.clean_mapper.find_all_match_indices(
4662
+ edit.target_text,
4663
+ is_regex
4664
+ );
4395
4665
  if (matches.length > 0) use_clean_map = true;
4396
4666
  else return null;
4397
4667
  }
@@ -4416,14 +4686,49 @@ var RedlineEngine = class {
4416
4686
  start_idx + match_len
4417
4687
  );
4418
4688
  let current_effective_new_text = edit.new_text || "";
4689
+ if (/^\{#cell:[^}]+\}$/.test(actual_doc_text.trim())) {
4690
+ let ins_text = current_effective_new_text;
4691
+ ins_text = ins_text.split(actual_doc_text.trim()).join("");
4692
+ if (ins_text) {
4693
+ all_sub_edits.push({
4694
+ type: "modify",
4695
+ target_text: "",
4696
+ new_text: ins_text,
4697
+ comment: edit.comment,
4698
+ // Insert at the anchor token's start so the new run lands inside
4699
+ // the cell paragraph that get_insertion_anchor resolves there.
4700
+ _match_start_index: start_idx,
4701
+ _internal_op: "INSERTION",
4702
+ _active_mapper_ref: active_mapper
4703
+ });
4704
+ } else if (edit.comment) {
4705
+ all_sub_edits.push({
4706
+ type: "modify",
4707
+ target_text: "",
4708
+ new_text: "",
4709
+ comment: edit.comment,
4710
+ _match_start_index: start_idx,
4711
+ _internal_op: "COMMENT_ONLY",
4712
+ _active_mapper_ref: active_mapper
4713
+ });
4714
+ }
4715
+ continue;
4716
+ }
4419
4717
  if (is_regex && current_effective_new_text) {
4420
4718
  try {
4421
- current_effective_new_text = actual_doc_text.replace(new RegExp(edit.target_text), current_effective_new_text);
4719
+ current_effective_new_text = actual_doc_text.replace(
4720
+ new RegExp(edit.target_text),
4721
+ current_effective_new_text
4722
+ );
4422
4723
  } catch (e) {
4423
4724
  }
4424
4725
  }
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);
4726
+ const [edit_target_clean, edit_target_style] = this._parse_markdown_style(
4727
+ edit.target_text
4728
+ );
4729
+ const [edit_new_clean, edit_new_style] = this._parse_markdown_style(
4730
+ current_effective_new_text
4731
+ );
4427
4732
  if (edit_target_style !== edit_new_style) {
4428
4733
  const [actual_clean] = this._parse_markdown_style(actual_doc_text);
4429
4734
  const final_target2 = actual_clean;
@@ -4462,7 +4767,9 @@ var RedlineEngine = class {
4462
4767
  let effective_start_idx = start_idx;
4463
4768
  if (current_effective_new_text.startsWith(actual_doc_text)) {
4464
4769
  effective_op = "INSERTION";
4465
- final_new = current_effective_new_text.substring(actual_doc_text.length);
4770
+ final_new = current_effective_new_text.substring(
4771
+ actual_doc_text.length
4772
+ );
4466
4773
  effective_start_idx = start_idx + match_len;
4467
4774
  } else {
4468
4775
  const [prefix_len, suffix_len] = trim_common_context(
@@ -4474,6 +4781,51 @@ var RedlineEngine = class {
4474
4781
  final_target = actual_doc_text.substring(prefix_len, t_end);
4475
4782
  final_new = current_effective_new_text.substring(prefix_len, n_end);
4476
4783
  effective_start_idx = start_idx + prefix_len;
4784
+ const target_segs = actual_doc_text.split("\n\n");
4785
+ const new_segs = current_effective_new_text.split("\n\n");
4786
+ if (actual_doc_text.includes("\n\n") && target_segs.length === new_segs.length) {
4787
+ const split_sub_edits = [];
4788
+ let seg_offset = start_idx;
4789
+ let comment_assigned = false;
4790
+ for (let k = 0; k < target_segs.length; k++) {
4791
+ const t_seg = target_segs[k];
4792
+ const n_seg = new_segs[k];
4793
+ if (t_seg !== n_seg) {
4794
+ const [seg_prefix, seg_suffix] = trim_common_context(t_seg, n_seg);
4795
+ const seg_target = t_seg.substring(
4796
+ seg_prefix,
4797
+ t_seg.length - seg_suffix
4798
+ );
4799
+ const seg_new = n_seg.substring(
4800
+ seg_prefix,
4801
+ n_seg.length - seg_suffix
4802
+ );
4803
+ const seg_start = seg_offset + seg_prefix;
4804
+ let seg_op;
4805
+ if (!seg_target && seg_new) seg_op = "INSERTION";
4806
+ else if (seg_target && !seg_new) seg_op = "DELETION";
4807
+ else if (seg_target && seg_new) seg_op = "MODIFICATION";
4808
+ else seg_op = "COMMENT_ONLY";
4809
+ const seg_comment = edit.comment && !comment_assigned ? edit.comment : null;
4810
+ if (seg_comment) comment_assigned = true;
4811
+ split_sub_edits.push({
4812
+ type: "modify",
4813
+ target_text: seg_target,
4814
+ new_text: seg_new,
4815
+ comment: seg_comment,
4816
+ _match_start_index: seg_start,
4817
+ _internal_op: seg_op,
4818
+ _active_mapper_ref: active_mapper,
4819
+ _split_group_id: start_idx
4820
+ });
4821
+ }
4822
+ seg_offset += t_seg.length + 2;
4823
+ }
4824
+ if (split_sub_edits.length > 0) {
4825
+ for (const sub of split_sub_edits) all_sub_edits.push(sub);
4826
+ continue;
4827
+ }
4828
+ }
4477
4829
  if (!final_target && final_new) effective_op = "INSERTION";
4478
4830
  else if (final_target && !final_new) effective_op = "DELETION";
4479
4831
  else if (final_target && final_new) effective_op = "MODIFICATION";
@@ -4493,6 +4845,35 @@ var RedlineEngine = class {
4493
4845
  if (match_mode === "all" || all_sub_edits.length > 1) return all_sub_edits;
4494
4846
  return all_sub_edits[0];
4495
4847
  }
4848
+ /**
4849
+ * Split a <w:ins> so that everything up to and INCLUDING split_after stays in
4850
+ * a left <w:ins>, new_elem is placed between, and the remainder moves to a
4851
+ * right <w:ins> — all at the grandparent level. Used when revising another
4852
+ * author's pending insertion: the <w:del> stays nested in their <w:ins> while
4853
+ * our replacement <w:ins> lands as a sibling, so we never nest <w:ins> in
4854
+ * <w:ins>.
4855
+ */
4856
+ _insert_and_split_ins(parent_ins, split_after, new_elem) {
4857
+ const grandparent = parent_ins.parentNode;
4858
+ if (!grandparent) return;
4859
+ const left = parent_ins.cloneNode(false);
4860
+ const right = parent_ins.cloneNode(false);
4861
+ let toRight = false;
4862
+ for (const kid of Array.from(parent_ins.childNodes)) {
4863
+ parent_ins.removeChild(kid);
4864
+ if (!toRight) {
4865
+ left.appendChild(kid);
4866
+ if (kid === split_after) toRight = true;
4867
+ } else {
4868
+ right.appendChild(kid);
4869
+ }
4870
+ }
4871
+ if (left.childNodes.length > 0) grandparent.insertBefore(left, parent_ins);
4872
+ grandparent.insertBefore(new_elem, parent_ins);
4873
+ if (right.childNodes.length > 0)
4874
+ grandparent.insertBefore(right, parent_ins);
4875
+ grandparent.removeChild(parent_ins);
4876
+ }
4496
4877
  _apply_single_edit_indexed(edit, orig_new, rebuild_map) {
4497
4878
  let op = edit._internal_op;
4498
4879
  const active_mapper = edit._active_mapper_ref || this.mapper;
@@ -4543,7 +4924,12 @@ var RedlineEngine = class {
4543
4924
  const first_anchor = ascend_to_paragraph_child(first_el, start_p);
4544
4925
  const last_anchor = ascend_to_paragraph_child(last_el, end_p);
4545
4926
  if (start_p === end_p) {
4546
- this._attach_comment(start_p, first_anchor, last_anchor, edit.comment);
4927
+ this._attach_comment(
4928
+ start_p,
4929
+ first_anchor,
4930
+ last_anchor,
4931
+ edit.comment
4932
+ );
4547
4933
  } else {
4548
4934
  this._attach_comment_spanning(
4549
4935
  start_p,
@@ -4695,7 +5081,16 @@ var RedlineEngine = class {
4695
5081
  const is_inline_first = result.first_node.tagName === "w:ins";
4696
5082
  if (is_inline_first) {
4697
5083
  if (anchor_run) {
4698
- insertAfter(result.first_node, anchor_run._element);
5084
+ const anchor_parent = anchor_run._element.parentNode;
5085
+ if (anchor_parent && anchor_parent.tagName === "w:ins") {
5086
+ this._insert_and_split_ins(
5087
+ anchor_parent,
5088
+ anchor_run._element,
5089
+ result.first_node
5090
+ );
5091
+ } else {
5092
+ insertAfter(result.first_node, anchor_run._element);
5093
+ }
4699
5094
  } else if (anchor_para) {
4700
5095
  anchor_para._element.appendChild(result.first_node);
4701
5096
  }
@@ -4805,7 +5200,12 @@ var RedlineEngine = class {
4805
5200
  if (result.first_node) {
4806
5201
  const is_inline_first = result.first_node.tagName === "w:ins";
4807
5202
  if (is_inline_first) {
4808
- insertAfter(result.first_node, last_del);
5203
+ const del_parent = last_del.parentNode;
5204
+ if (del_parent && del_parent.tagName === "w:ins") {
5205
+ this._insert_and_split_ins(del_parent, last_del, result.first_node);
5206
+ } else {
5207
+ insertAfter(result.first_node, last_del);
5208
+ }
4809
5209
  ins_elem = result.first_node;
4810
5210
  } else {
4811
5211
  ins_elem = result.last_ins;
@@ -5832,13 +6232,21 @@ function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets
5832
6232
  if (seen_cells.has(cell)) continue;
5833
6233
  seen_cells.add(cell);
5834
6234
  if (!first_cell) cell_cursor += 3;
5835
- const cell_content = _extract_blocks(
6235
+ let cell_content = _extract_blocks(
5836
6236
  cell,
5837
6237
  comments_map,
5838
6238
  cleanView,
5839
6239
  cell_cursor,
5840
6240
  paragraph_offsets
5841
6241
  );
6242
+ if (!cleanView) {
6243
+ const firstP = cell._element.getElementsByTagName("w:p")[0];
6244
+ const paraId = firstP ? firstP.getAttribute("w14:paraId") : null;
6245
+ if (paraId) {
6246
+ const anchor = `{#cell:${paraId}}`;
6247
+ cell_content = cell_content + anchor;
6248
+ }
6249
+ }
5842
6250
  cell_texts.push(cell_content);
5843
6251
  cell_cursor += cell_content.length;
5844
6252
  first_cell = false;