@adeu/core 1.25.0 → 1.27.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
@@ -1228,12 +1228,27 @@ function get_run_style_markers(run, is_heading = null) {
1228
1228
  }
1229
1229
  return [prefix, suffix];
1230
1230
  }
1231
+ function split_boundary_whitespace(text) {
1232
+ const core = text.trim();
1233
+ if (!core) return ["", "", text];
1234
+ const lead_len = text.length - text.trimStart().length;
1235
+ return [
1236
+ text.substring(0, lead_len),
1237
+ text.substring(lead_len, lead_len + core.length),
1238
+ text.substring(lead_len + core.length)
1239
+ ];
1240
+ }
1231
1241
  function apply_formatting_to_segments(text, prefix, suffix) {
1232
1242
  if (!prefix && !suffix) return text;
1233
1243
  if (!text) return "";
1234
- if (!text.includes("\n")) return `${prefix}${text}${suffix}`;
1244
+ const wrap = (segment) => {
1245
+ const [lead, core, trail] = split_boundary_whitespace(segment);
1246
+ if (!core) return segment;
1247
+ return `${lead}${prefix}${core}${suffix}${trail}`;
1248
+ };
1249
+ if (!text.includes("\n")) return wrap(text);
1235
1250
  const parts = text.split("\n");
1236
- return parts.map((p) => p ? `${prefix}${p}${suffix}` : "").join("\n");
1251
+ return parts.map((p) => p ? wrap(p) : "").join("\n");
1237
1252
  }
1238
1253
  function get_run_text(run) {
1239
1254
  let text = "";
@@ -1686,7 +1701,7 @@ ${header}`;
1686
1701
  this._add_virtual_text(s_tok, current, paragraph);
1687
1702
  current += s_tok.length;
1688
1703
  }
1689
- for (const [kind, txt, r_obj, i_id, d_id, c_ids] of pending_runs) {
1704
+ for (const [kind, txt, r_obj, r_off, i_id, d_id, c_ids] of pending_runs) {
1690
1705
  if (kind === "virtual") {
1691
1706
  this._add_virtual_text(txt, current, paragraph, active_hyperlink_id);
1692
1707
  } else {
@@ -1700,7 +1715,8 @@ ${header}`;
1700
1715
  del_id: d_id || void 0,
1701
1716
  hyperlink_id: active_hyperlink_id || void 0,
1702
1717
  comment_ids: c_ids.length > 0 ? c_ids : void 0,
1703
- part_index: this._current_part_index
1718
+ part_index: this._current_part_index,
1719
+ run_offset: r_off
1704
1720
  };
1705
1721
  this.spans.push(s);
1706
1722
  this._text_chunks.push(txt);
@@ -1735,20 +1751,42 @@ ${header}`;
1735
1751
  if (text === "" || /^\s*$/.test(text)) continue;
1736
1752
  leading_strip_active = false;
1737
1753
  }
1754
+ let run_local = 0;
1755
+ const append_wrapped = (segment) => {
1756
+ const [lead, core, trail] = split_boundary_whitespace(segment);
1757
+ if (!core) {
1758
+ run_parts.push(["real", segment, item, run_local]);
1759
+ run_local += segment.length;
1760
+ return;
1761
+ }
1762
+ if (lead) {
1763
+ run_parts.push(["real", lead, item, run_local]);
1764
+ run_local += lead.length;
1765
+ }
1766
+ if (prefix) run_parts.push(["virtual", prefix, null, 0]);
1767
+ run_parts.push(["real", core, item, run_local]);
1768
+ run_local += core.length;
1769
+ if (suffix) run_parts.push(["virtual", suffix, null, 0]);
1770
+ if (trail) {
1771
+ run_parts.push(["real", trail, item, run_local]);
1772
+ run_local += trail.length;
1773
+ }
1774
+ };
1738
1775
  if (text.includes("\n") && (prefix || suffix)) {
1739
1776
  const parts = text.split("\n");
1740
1777
  for (let idx = 0; idx < parts.length; idx++) {
1741
- if (idx > 0) run_parts.push(["real", "\n", item]);
1742
- if (parts[idx]) {
1743
- if (prefix) run_parts.push(["virtual", prefix, null]);
1744
- run_parts.push(["real", parts[idx], item]);
1745
- if (suffix) run_parts.push(["virtual", suffix, null]);
1778
+ if (idx > 0) {
1779
+ run_parts.push(["real", "\n", item, run_local]);
1780
+ run_local += 1;
1746
1781
  }
1782
+ if (parts[idx]) append_wrapped(parts[idx]);
1747
1783
  }
1784
+ } else if ((prefix || suffix) && text) {
1785
+ append_wrapped(text);
1748
1786
  } else {
1749
- if (prefix) run_parts.push(["virtual", prefix, null]);
1750
- if (text) run_parts.push(["real", text, item]);
1751
- if (suffix) run_parts.push(["virtual", suffix, null]);
1787
+ if (prefix) run_parts.push(["virtual", prefix, null, 0]);
1788
+ if (text) run_parts.push(["real", text, item, 0]);
1789
+ if (suffix) run_parts.push(["virtual", suffix, null, 0]);
1752
1790
  }
1753
1791
  if (this.clean_view && Object.keys(active_del).length > 0) {
1754
1792
  }
@@ -1772,7 +1810,7 @@ ${header}`;
1772
1810
  skip_leading_prefix = true;
1773
1811
  }
1774
1812
  const curr_comment_ids = Array.from(active_ids);
1775
- for (const [kind, txt, r_obj] of run_parts) {
1813
+ for (const [kind, txt, r_obj, r_off] of run_parts) {
1776
1814
  if (skip_leading_prefix && kind === "virtual" && txt === new_style[0]) {
1777
1815
  skip_leading_prefix = false;
1778
1816
  continue;
@@ -1781,6 +1819,7 @@ ${header}`;
1781
1819
  kind,
1782
1820
  txt,
1783
1821
  r_obj,
1822
+ r_off,
1784
1823
  curr_ins_id,
1785
1824
  curr_del_id,
1786
1825
  curr_comment_ids
@@ -1792,11 +1831,12 @@ ${header}`;
1792
1831
  current_wrappers = new_wrappers;
1793
1832
  current_style = new_style;
1794
1833
  const curr_comment_ids = Array.from(active_ids);
1795
- for (const [kind, txt, r_obj] of run_parts) {
1834
+ for (const [kind, txt, r_obj, r_off] of run_parts) {
1796
1835
  pending_runs.push([
1797
1836
  kind,
1798
1837
  txt,
1799
1838
  r_obj,
1839
+ r_off,
1800
1840
  curr_ins_id,
1801
1841
  curr_del_id,
1802
1842
  curr_comment_ids
@@ -2208,39 +2248,43 @@ ${header}`;
2208
2248
  (s) => s.end > start_idx && s.start < end_idx
2209
2249
  );
2210
2250
  if (affected_spans.length === 0) return [];
2211
- const working_runs = affected_spans.filter((s) => s.run !== null).map((s) => s.run);
2212
- if (working_runs.length === 0) return [];
2213
- let dom_modified = false;
2214
- const first_real_span = affected_spans.find((s) => s.run !== null);
2215
- let start_split_adjustment = 0;
2216
- if (first_real_span) {
2217
- const local_start = start_idx - first_real_span.start;
2218
- if (local_start > 0) {
2219
- const idx_in_working = 0;
2220
- const [, right_run] = this._split_run_at_index(
2221
- working_runs[idx_in_working],
2222
- local_start
2223
- );
2224
- working_runs[idx_in_working] = right_run;
2225
- dom_modified = true;
2226
- start_split_adjustment = local_start;
2251
+ const real_spans = affected_spans.filter((s) => s.run !== null);
2252
+ if (real_spans.length === 0) return [];
2253
+ const working_runs = [];
2254
+ for (const s of real_spans) {
2255
+ if (!working_runs.some((r) => r === s.run)) {
2256
+ working_runs.push(s.run);
2227
2257
  }
2228
2258
  }
2229
- const last_real_span = [...affected_spans].reverse().find((s) => s.run !== null);
2230
- if (last_real_span) {
2231
- const is_same_run = first_real_span === last_real_span;
2232
- const run_to_split = working_runs[working_runs.length - 1];
2233
- let overlap_end = Math.min(last_real_span.end, end_idx);
2234
- let local_end = overlap_end - last_real_span.start;
2235
- if (is_same_run && start_split_adjustment > 0) {
2236
- local_end -= start_split_adjustment;
2237
- }
2238
- const run_text = get_run_text(run_to_split);
2239
- if (local_end > 0 && local_end < run_text.length) {
2240
- const [left_run] = this._split_run_at_index(run_to_split, local_end);
2241
- working_runs[working_runs.length - 1] = left_run;
2242
- dom_modified = true;
2243
- }
2259
+ let dom_modified = false;
2260
+ const first_real_span = real_spans[0];
2261
+ let start_split_adjustment = 0;
2262
+ const local_start = Math.max(start_idx, first_real_span.start) - first_real_span.start + (first_real_span.run_offset || 0);
2263
+ if (local_start > 0) {
2264
+ const split_source = working_runs[0];
2265
+ const [, right_run] = this._split_run_at_index(
2266
+ split_source,
2267
+ local_start
2268
+ );
2269
+ for (let w = 0; w < working_runs.length; w++) {
2270
+ if (working_runs[w] === split_source) working_runs[w] = right_run;
2271
+ }
2272
+ dom_modified = true;
2273
+ start_split_adjustment = local_start;
2274
+ }
2275
+ const last_real_span = real_spans[real_spans.length - 1];
2276
+ const is_same_run = first_real_span.run === last_real_span.run;
2277
+ const run_to_split = working_runs[working_runs.length - 1];
2278
+ const overlap_end = Math.min(last_real_span.end, end_idx);
2279
+ let local_end = overlap_end - last_real_span.start + (last_real_span.run_offset || 0);
2280
+ if (is_same_run && start_split_adjustment > 0) {
2281
+ local_end -= start_split_adjustment;
2282
+ }
2283
+ const run_text = get_run_text(run_to_split);
2284
+ if (local_end > 0 && local_end < run_text.length) {
2285
+ const [left_run] = this._split_run_at_index(run_to_split, local_end);
2286
+ working_runs[working_runs.length - 1] = left_run;
2287
+ dom_modified = true;
2244
2288
  }
2245
2289
  if (dom_modified && rebuild_map) {
2246
2290
  this._build_map();
@@ -2277,7 +2321,7 @@ ${header}`;
2277
2321
  }
2278
2322
  return [null, span.paragraph];
2279
2323
  } else {
2280
- const offset = index - span.start;
2324
+ const offset = index - span.start + (span.run_offset || 0);
2281
2325
  const [left] = this._split_run_at_index(span.run, offset);
2282
2326
  if (rebuild_map) this._build_map();
2283
2327
  return [left, span.paragraph];
@@ -2348,6 +2392,7 @@ ${header}`;
2348
2392
  };
2349
2393
 
2350
2394
  // src/diff.ts
2395
+ import { unzipSync as unzipSync2 } from "fflate";
2351
2396
  import diff_match_patch from "diff-match-patch";
2352
2397
  function _count_standalone_underscores(s) {
2353
2398
  let count = 0;
@@ -2685,6 +2730,37 @@ function _drop_image_marker_hunks(edits, warnings) {
2685
2730
  }
2686
2731
  return kept;
2687
2732
  }
2733
+ function _drop_marker_interior_hunks(edits, text_orig, warnings) {
2734
+ const marker_spans = [];
2735
+ for (const m of text_orig.matchAll(_IMAGE_MARKER_RE)) {
2736
+ marker_spans.push([m.index, m.index + m[0].length]);
2737
+ }
2738
+ if (marker_spans.length === 0) return edits;
2739
+ const kept = [];
2740
+ let warned = false;
2741
+ for (const e of edits) {
2742
+ const idx = e._match_start_index;
2743
+ if (idx === void 0 || idx === null) {
2744
+ kept.push(e);
2745
+ continue;
2746
+ }
2747
+ const end = idx + (e.target_text || "").length;
2748
+ const cuts_into_marker = marker_spans.some(
2749
+ ([m_start, m_end]) => m_start < end && idx < m_end && !(idx <= m_start && m_end <= end)
2750
+ );
2751
+ if (!cuts_into_marker) {
2752
+ kept.push(e);
2753
+ continue;
2754
+ }
2755
+ if (!warned) {
2756
+ warnings.push(
2757
+ "An image's alternative text differs between the documents. Image markers (![alt](docx-image:N)) are read-only projections, so this difference cannot be expressed as a text edit \u2014 it was skipped; update the image or its alt text manually in Word."
2758
+ );
2759
+ warned = true;
2760
+ }
2761
+ }
2762
+ return kept;
2763
+ }
2688
2764
  function _rows_are_plain(text, table) {
2689
2765
  for (const row of table.rows) {
2690
2766
  if (text.substring(row.start, row.end) !== row.cells.join(" | ")) {
@@ -2826,8 +2902,12 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
2826
2902
  warnings.push(
2827
2903
  `The documents have different part layouts (${kinds_o.join(" + ") || "none"} vs ${kinds_m.join(" + ") || "none"}); comparing flattened text instead. Header/footer additions or removals cannot be expressed as text edits.`
2828
2904
  );
2829
- const flat = _drop_image_marker_hunks(
2830
- generate_edits_from_text(text_orig, text_mod),
2905
+ const flat = _drop_marker_interior_hunks(
2906
+ _drop_image_marker_hunks(
2907
+ generate_edits_from_text(text_orig, text_mod),
2908
+ warnings
2909
+ ),
2910
+ text_orig,
2831
2911
  warnings
2832
2912
  );
2833
2913
  return { edits: [...flat], warnings };
@@ -2931,12 +3011,63 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
2931
3011
  const modify_edits = edits.filter(
2932
3012
  (e) => e.type === "modify"
2933
3013
  );
2934
- const kept_modifies = new Set(_drop_image_marker_hunks(modify_edits, warnings));
3014
+ const kept_modifies = new Set(
3015
+ _drop_marker_interior_hunks(
3016
+ _drop_image_marker_hunks(modify_edits, warnings),
3017
+ text_orig,
3018
+ warnings
3019
+ )
3020
+ );
2935
3021
  const final_edits = edits.filter(
2936
3022
  (e) => e.type !== "modify" || kept_modifies.has(e)
2937
3023
  );
2938
3024
  return { edits: final_edits, warnings };
2939
3025
  }
3026
+ function collect_media_difference_warnings(original_docx, modified_docx) {
3027
+ const media_hashes = (data) => {
3028
+ const hashes = /* @__PURE__ */ new Map();
3029
+ try {
3030
+ const unzipped = unzipSync2(data);
3031
+ for (const [name, bytes] of Object.entries(unzipped)) {
3032
+ if (!name.startsWith("word/media/")) continue;
3033
+ let h1 = 2166136261;
3034
+ let h2 = 3421674724;
3035
+ for (let i = 0; i < bytes.length; i++) {
3036
+ h1 = Math.imul(h1 ^ bytes[i], 16777619) >>> 0;
3037
+ h2 = Math.imul(h2 ^ bytes[bytes.length - 1 - i], 16777619) >>> 0;
3038
+ }
3039
+ hashes.set(name, `${bytes.length}:${h1.toString(16)}:${h2.toString(16)}`);
3040
+ }
3041
+ } catch {
3042
+ return /* @__PURE__ */ new Map();
3043
+ }
3044
+ return hashes;
3045
+ };
3046
+ const hashes_orig = media_hashes(original_docx);
3047
+ const hashes_mod = media_hashes(modified_docx);
3048
+ const changed = [];
3049
+ const added = [];
3050
+ const removed = [];
3051
+ for (const [name, hash] of hashes_orig) {
3052
+ if (!hashes_mod.has(name)) removed.push(name);
3053
+ else if (hashes_mod.get(name) !== hash) changed.push(name);
3054
+ }
3055
+ for (const name of hashes_mod.keys()) {
3056
+ if (!hashes_orig.has(name)) added.push(name);
3057
+ }
3058
+ changed.sort();
3059
+ added.sort();
3060
+ removed.sort();
3061
+ if (changed.length + added.length + removed.length === 0) return [];
3062
+ const parts = [];
3063
+ if (changed.length) parts.push(`${changed.length} changed`);
3064
+ if (added.length) parts.push(`${added.length} added`);
3065
+ if (removed.length) parts.push(`${removed.length} removed`);
3066
+ const names = [...changed, ...added, ...removed].slice(0, 5).join(", ");
3067
+ return [
3068
+ `The documents' embedded media differ (${parts.join(", ")}: ${names}). This diff compares TEXT only \u2014 an empty edit list does not mean the documents are visually identical. Image changes must be applied manually in Word.`
3069
+ ];
3070
+ }
2940
3071
  function create_unified_diff(original_text, modified_text, context_lines = 3) {
2941
3072
  const dmp = new diff_match_patch.diff_match_patch();
2942
3073
  dmp.Diff_Timeout = 2;
@@ -3891,6 +4022,13 @@ var RedlineEngine = class _RedlineEngine {
3891
4022
  console.error("generate_edits_from_text failed, falling back to wholesale edit", e);
3892
4023
  raw_sub_edits = [];
3893
4024
  }
4025
+ const _marker_only = (text) => {
4026
+ const stripped = text.trim();
4027
+ return stripped.length > 0 && /^[*_]+$/.test(stripped);
4028
+ };
4029
+ raw_sub_edits = raw_sub_edits.filter(
4030
+ (e) => !((!e.target_text || _marker_only(e.target_text)) && (!e.new_text || _marker_only(e.new_text)) && (e.target_text || e.new_text))
4031
+ );
3894
4032
  if (!raw_sub_edits || raw_sub_edits.length === 0) {
3895
4033
  const fallback_edit = {
3896
4034
  type: "modify",
@@ -4671,7 +4809,7 @@ var RedlineEngine = class _RedlineEngine {
4671
4809
  *
4672
4810
  * Does NOT attach comments; callers handle that.
4673
4811
  */
4674
- _track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id, positional_anchor_el = null) {
4812
+ _track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id, positional_anchor_el = null, suppress_emphasis = false, insert_before = false) {
4675
4813
  if (!text) {
4676
4814
  return {
4677
4815
  first_node: null,
@@ -4693,7 +4831,11 @@ var RedlineEngine = class _RedlineEngine {
4693
4831
  current_p = walker;
4694
4832
  }
4695
4833
  const suffix_nodes = [];
4696
- const pos_source = positional_anchor_el && positional_anchor_el.parentNode ? positional_anchor_el : anchor_run !== null && anchor_run._element.parentNode ? anchor_run._element : null;
4834
+ const relocatable = /* @__PURE__ */ new Set(["w:r", "w:ins", "w:del"]);
4835
+ const pos_from_positional = positional_anchor_el && positional_anchor_el.parentNode ? positional_anchor_el : null;
4836
+ const pos_from_anchor_run = anchor_run !== null && anchor_run._element.parentNode ? anchor_run._element : null;
4837
+ const pos_source = pos_from_positional ?? pos_from_anchor_run;
4838
+ const suffix_includes_anchor = insert_before && pos_from_positional === null && pos_from_anchor_run !== null;
4697
4839
  if (current_p !== null && pos_source !== null) {
4698
4840
  let pos_anchor = pos_source;
4699
4841
  while (pos_anchor && pos_anchor.parentNode !== current_p) {
@@ -4704,8 +4846,7 @@ var RedlineEngine = class _RedlineEngine {
4704
4846
  }
4705
4847
  }
4706
4848
  if (pos_anchor) {
4707
- const relocatable = /* @__PURE__ */ new Set(["w:r", "w:ins", "w:del"]);
4708
- let nxt = pos_anchor.nextSibling;
4849
+ let nxt = suffix_includes_anchor ? pos_anchor : pos_anchor.nextSibling;
4709
4850
  while (nxt) {
4710
4851
  if (nxt.nodeType === 1 && relocatable.has(nxt.tagName)) {
4711
4852
  suffix_nodes.push(nxt);
@@ -4713,6 +4854,14 @@ var RedlineEngine = class _RedlineEngine {
4713
4854
  nxt = nxt.nextSibling;
4714
4855
  }
4715
4856
  }
4857
+ } else if (current_p !== null && insert_before) {
4858
+ let child = current_p.firstChild;
4859
+ while (child) {
4860
+ if (child.nodeType === 1 && relocatable.has(child.tagName)) {
4861
+ suffix_nodes.push(child);
4862
+ }
4863
+ child = child.nextSibling;
4864
+ }
4716
4865
  }
4717
4866
  while (lines.length > 1 && lines[lines.length - 1] === "" && suffix_nodes.length === 0) {
4718
4867
  lines.pop();
@@ -4735,7 +4884,8 @@ var RedlineEngine = class _RedlineEngine {
4735
4884
  first_clean === lines[0] ? lines[0] : lines[0],
4736
4885
  anchor_run,
4737
4886
  reuse_id,
4738
- xmlDoc
4887
+ xmlDoc,
4888
+ suppress_emphasis
4739
4889
  );
4740
4890
  first_node = inline_ins;
4741
4891
  }
@@ -4799,7 +4949,8 @@ var RedlineEngine = class _RedlineEngine {
4799
4949
  clean_text,
4800
4950
  anchor_run,
4801
4951
  reuse_id,
4802
- xmlDoc
4952
+ xmlDoc,
4953
+ suppress_emphasis
4803
4954
  );
4804
4955
  if (content_ins) {
4805
4956
  new_p.appendChild(content_ins);
@@ -4825,7 +4976,7 @@ var RedlineEngine = class _RedlineEngine {
4825
4976
  * <w:r> elements representing the inline markdown segments of `line_text`.
4826
4977
  * Returns null if line_text is empty.
4827
4978
  */
4828
- _build_tracked_ins_for_line(line_text, anchor_run, reuse_id, xmlDoc) {
4979
+ _build_tracked_ins_for_line(line_text, anchor_run, reuse_id, xmlDoc, suppress_emphasis = false) {
4829
4980
  if (!line_text && line_text !== "") return null;
4830
4981
  const ins = this._create_track_change_tag("w:ins", "", reuse_id);
4831
4982
  const segments = this._parse_inline_markdown(line_text);
@@ -4838,15 +4989,11 @@ var RedlineEngine = class _RedlineEngine {
4838
4989
  const anchor_rPr = findChild(anchor_run._element, "w:rPr");
4839
4990
  if (anchor_rPr) {
4840
4991
  const clone = anchor_rPr.cloneNode(true);
4841
- for (const tag of [
4842
- "w:vanish",
4843
- "w:strike",
4844
- "w:dstrike",
4845
- "w:i",
4846
- "w:iCs",
4847
- "w:b",
4848
- "w:bCs"
4849
- ]) {
4992
+ const strip_tags = ["w:vanish", "w:strike", "w:dstrike", "w:i", "w:iCs"];
4993
+ if (suppress_emphasis) {
4994
+ strip_tags.push("w:b", "w:bCs");
4995
+ }
4996
+ for (const tag of strip_tags) {
4850
4997
  const found = findChild(clone, tag);
4851
4998
  if (found) clone.removeChild(found);
4852
4999
  }
@@ -4881,6 +5028,27 @@ var RedlineEngine = class _RedlineEngine {
4881
5028
  }
4882
5029
  return [text, null];
4883
5030
  }
5031
+ /**
5032
+ * True when this edit's target or replacement text carries explicit
5033
+ * bold/italic markers, making the markers AUTHORITATIVE for the inserted
5034
+ * runs' formatting. Replacing `**X**` with `_X_` must yield italic-only
5035
+ * text, and replacing `**X**` with `X` must yield plain text — inheriting
5036
+ * the replaced span's run properties on top of (or instead of) the
5037
+ * requested markers silently produces the wrong document while the report
5038
+ * claims success (QA 2026-07-19 F-02). Plain-text edits (no markers on
5039
+ * either side) keep inheriting the context style so partial replacements
5040
+ * inside a styled span never lose formatting.
5041
+ */
5042
+ _edit_declares_emphasis(edit) {
5043
+ for (const text of [edit?.target_text, edit?.new_text]) {
5044
+ if (!text || !text.includes("**") && !text.includes("_")) continue;
5045
+ const segments = this._parse_inline_markdown(text);
5046
+ if (segments.some(([, props]) => props && Object.keys(props).length > 0)) {
5047
+ return true;
5048
+ }
5049
+ }
5050
+ return false;
5051
+ }
4884
5052
  _parse_inline_markdown(text, baseStyle = {}) {
4885
5053
  if (!text) return [];
4886
5054
  const tokenPattern = /(\*\*.*?\*\*)|(_.*?_)/;
@@ -5147,6 +5315,16 @@ var RedlineEngine = class _RedlineEngine {
5147
5315
  continue;
5148
5316
  const is_regex = edit.regex || false;
5149
5317
  const match_mode = edit.match_mode || "strict";
5318
+ if (is_regex) {
5319
+ try {
5320
+ new RegExp(edit.target_text);
5321
+ } catch (regex_err) {
5322
+ errors.push(
5323
+ `- Edit ${i + 1 + index_offset} Failed: target_text is not a valid regular expression (${regex_err?.message ?? regex_err}). Fix the pattern, or set "regex": false to match the text literally.`
5324
+ );
5325
+ continue;
5326
+ }
5327
+ }
5150
5328
  let matches = this.mapper.find_all_match_indices(
5151
5329
  edit.target_text,
5152
5330
  is_regex
@@ -5422,6 +5600,45 @@ var RedlineEngine = class _RedlineEngine {
5422
5600
  }
5423
5601
  validate_review_actions(actions) {
5424
5602
  const errors = [];
5603
+ const seen_resolutions = /* @__PURE__ */ new Map();
5604
+ const seen_replies = /* @__PURE__ */ new Set();
5605
+ for (let i = 0; i < actions.length; i++) {
5606
+ const action = actions[i];
5607
+ const type = action.type;
5608
+ const target_id = action.target_id ?? "";
5609
+ if (type === "reply") {
5610
+ if (!String(action.text ?? "").trim()) {
5611
+ errors.push(
5612
+ `- Action ${i + 1} Failed: reply text for ${target_id} is empty or whitespace-only. Word would show a blank comment bubble \u2014 provide the reply content in 'text'.`
5613
+ );
5614
+ continue;
5615
+ }
5616
+ const reply_key = `${target_id}\0${String(action.text).trim()}`;
5617
+ if (seen_replies.has(reply_key)) {
5618
+ errors.push(
5619
+ `- Action ${i + 1} Failed: duplicate reply \u2014 this batch already replies to ${target_id} with the same text. Remove the duplicate action.`
5620
+ );
5621
+ }
5622
+ seen_replies.add(reply_key);
5623
+ } else if (type === "accept" || type === "reject") {
5624
+ const prior = seen_resolutions.get(target_id);
5625
+ if (prior !== void 0) {
5626
+ const [first_idx, first_type] = prior;
5627
+ if (first_type === type) {
5628
+ errors.push(
5629
+ `- Action ${i + 1} Failed: duplicate action \u2014 Action ${first_idx + 1} in this batch already applies '${type}' to ${target_id}. A change can only be resolved once; remove the duplicate action.`
5630
+ );
5631
+ } else {
5632
+ errors.push(
5633
+ `- Action ${i + 1} Failed: conflicting actions \u2014 Action ${first_idx + 1} in this batch applies '${first_type}' to ${target_id}, but this action applies '${type}'. Decide the outcome and keep exactly one of them.`
5634
+ );
5635
+ }
5636
+ } else {
5637
+ seen_resolutions.set(target_id, [i, type]);
5638
+ }
5639
+ }
5640
+ }
5641
+ if (errors.length > 0) return errors;
5425
5642
  for (let i = 0; i < actions.length; i++) {
5426
5643
  const action = actions[i];
5427
5644
  const type = action.type;
@@ -5726,6 +5943,14 @@ var RedlineEngine = class _RedlineEngine {
5726
5943
  actions_skipped: skipped_actions,
5727
5944
  edits_applied: applied_edits,
5728
5945
  edits_skipped: skipped_edits,
5946
+ // edits_applied counts change OBJECTS; this is the total number of
5947
+ // document occurrences they modified (match_mode="all" fan-out), so
5948
+ // automation never has to guess which of the two a count means
5949
+ // (QA 2026-07-19 F-21).
5950
+ occurrences_modified: edits_reports.reduce(
5951
+ (sum, r) => sum + (r.occurrences_modified || 0),
5952
+ 0
5953
+ ),
5729
5954
  skipped_details: this.skipped_details,
5730
5955
  edits: edits_reports,
5731
5956
  engine: "node",
@@ -5747,6 +5972,7 @@ var RedlineEngine = class _RedlineEngine {
5747
5972
  }
5748
5973
  edit._applied_status = false;
5749
5974
  edit._error_msg = null;
5975
+ edit._any_sub_failure = false;
5750
5976
  }
5751
5977
  for (const edit of edits) {
5752
5978
  if (typeof edit !== "object" || edit === null) continue;
@@ -5846,17 +6072,18 @@ var RedlineEngine = class _RedlineEngine {
5846
6072
  this.skipped_details.push(msg);
5847
6073
  edit._applied_status = false;
5848
6074
  edit._error_msg = msg;
6075
+ edit._any_sub_failure = true;
5849
6076
  const parent = edit._parent_edit_ref;
5850
6077
  if (parent) {
5851
6078
  parent._applied_status = false;
5852
6079
  parent._error_msg = msg;
6080
+ parent._any_sub_failure = true;
5853
6081
  }
5854
6082
  continue;
5855
6083
  }
5856
6084
  let success = false;
5857
6085
  if (edit.type === "modify") {
5858
- const rebuild = edit._split_group_id !== void 0 && edit._split_group_id !== null;
5859
- success = this._apply_single_edit_indexed(edit, orig_new, rebuild);
6086
+ success = this._apply_single_edit_indexed(edit, orig_new, false);
5860
6087
  } else if (edit.type === "insert_row" || edit.type === "delete_row") {
5861
6088
  success = this._apply_table_edit(edit, false);
5862
6089
  }
@@ -5906,8 +6133,10 @@ var RedlineEngine = class _RedlineEngine {
5906
6133
  this.skipped_details.push(msg);
5907
6134
  edit._applied_status = false;
5908
6135
  edit._error_msg = msg;
6136
+ edit._any_sub_failure = true;
5909
6137
  const parent = edit._parent_edit_ref;
5910
6138
  if (parent) {
6139
+ parent._any_sub_failure = true;
5911
6140
  if (!parent._applied_status) {
5912
6141
  parent._applied_status = false;
5913
6142
  parent._error_msg = msg;
@@ -5915,7 +6144,20 @@ var RedlineEngine = class _RedlineEngine {
5915
6144
  }
5916
6145
  }
5917
6146
  }
5918
- return [applied, skipped];
6147
+ let applied_logical = 0;
6148
+ let skipped_logical = 0;
6149
+ for (const input_edit of edits) {
6150
+ if (typeof input_edit !== "object" || input_edit === null) {
6151
+ skipped_logical++;
6152
+ continue;
6153
+ }
6154
+ if (input_edit._applied_status && !input_edit._any_sub_failure) {
6155
+ applied_logical++;
6156
+ } else {
6157
+ skipped_logical++;
6158
+ }
6159
+ }
6160
+ return [applied_logical, skipped_logical];
5919
6161
  }
5920
6162
  apply_review_actions(actions) {
5921
6163
  let applied = 0;
@@ -6408,6 +6650,7 @@ var RedlineEngine = class _RedlineEngine {
6408
6650
  op = "MODIFICATION";
6409
6651
  }
6410
6652
  }
6653
+ const suppress_emphasis = this._edit_declares_emphasis(edit);
6411
6654
  if (op === "STYLE_ONLY" || op === "STYLE_AND_TEXT") {
6412
6655
  const [anchor_run, anchor_para] = active_mapper.get_insertion_anchor(
6413
6656
  start_idx,
@@ -6608,7 +6851,8 @@ var RedlineEngine = class _RedlineEngine {
6608
6851
  clean_text,
6609
6852
  anchor_run,
6610
6853
  ins_id,
6611
- xmlDoc
6854
+ xmlDoc,
6855
+ suppress_emphasis
6612
6856
  );
6613
6857
  if (content_ins) new_p.appendChild(content_ins);
6614
6858
  body.insertBefore(new_p, _bug233_target_para);
@@ -6640,7 +6884,12 @@ var RedlineEngine = class _RedlineEngine {
6640
6884
  final_new_text,
6641
6885
  anchor_run,
6642
6886
  anchor_para,
6643
- ins_id
6887
+ ins_id,
6888
+ null,
6889
+ suppress_emphasis,
6890
+ // Paragraph-start insertions attach BEFORE the anchor (see
6891
+ // before_anchor below): the suffix relocation must know.
6892
+ start_idx === 0
6644
6893
  );
6645
6894
  if (!result.first_node) return false;
6646
6895
  const is_inline_first = result.first_node.tagName === "w:ins";
@@ -6791,7 +7040,8 @@ var RedlineEngine = class _RedlineEngine {
6791
7040
  ins_id,
6792
7041
  // The insertion physically follows the deletion block; the style
6793
7042
  // run was detached when the deletion cloned it into <w:del>.
6794
- last_del
7043
+ last_del,
7044
+ suppress_emphasis
6795
7045
  );
6796
7046
  if (result.first_node) {
6797
7047
  const is_inline_first = result.first_node.tagName === "w:ins";
@@ -6825,7 +7075,9 @@ var RedlineEngine = class _RedlineEngine {
6825
7075
  edit.new_text,
6826
7076
  anchor,
6827
7077
  first_span.paragraph,
6828
- ins_id
7078
+ ins_id,
7079
+ null,
7080
+ suppress_emphasis
6829
7081
  );
6830
7082
  if (result.first_node) {
6831
7083
  p1_el.appendChild(result.first_node);
@@ -7309,6 +7561,26 @@ function normalize_change_dates(doc) {
7309
7561
  }
7310
7562
  return count ? [`Track change timestamps: ${count} normalized`] : [];
7311
7563
  }
7564
+ function normalize_comment_dates(doc) {
7565
+ let count = 0;
7566
+ const fixed = "2025-01-01T00:00:00Z";
7567
+ for (const part of doc.pkg.parts) {
7568
+ if (!part.contentType.includes("comments")) continue;
7569
+ for (const el of findAllDescendants(part._element, "w:comment")) {
7570
+ if (el.hasAttribute("w:date")) {
7571
+ el.setAttribute("w:date", fixed);
7572
+ count++;
7573
+ }
7574
+ }
7575
+ for (const el of findAllDescendants(part._element, "w16cex:commentExtensible")) {
7576
+ if (el.hasAttribute("w16cex:dateUtc")) {
7577
+ el.setAttribute("w16cex:dateUtc", fixed);
7578
+ count++;
7579
+ }
7580
+ }
7581
+ }
7582
+ return count ? [`Comment timestamps: ${count} normalized`] : [];
7583
+ }
7312
7584
  function scrub_doc_properties(doc) {
7313
7585
  const lines = [];
7314
7586
  const corePart = doc.pkg.getPartByPath("docProps/core.xml");
@@ -8052,8 +8324,10 @@ function build_paragraph_text(paragraph, comments_map, cleanView, style_cache, d
8052
8324
  const new_wrappers = cleanView ? ["", ""] : _get_wrappers(active_ins, active_del, active_comments, active_fmt);
8053
8325
  const new_style = [prefix, suffix];
8054
8326
  if (pending_text && new_wrappers[0] === current_wrappers[0] && new_wrappers[1] === current_wrappers[1]) {
8055
- if (new_style[0] === current_style[0] && new_style[1] === current_style[1] && current_style[0] !== "" && pending_text.endsWith(current_style[1]) && seg.startsWith(new_style[0])) {
8056
- pending_text = pending_text.slice(0, -current_style[1].length) + seg.slice(new_style[0].length);
8327
+ const escaped_prefix = new_style[0].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8328
+ const lead_match = new_style[0] !== "" || new_style[1] !== "" ? seg.match(new RegExp("^(\\s*)" + escaped_prefix)) : null;
8329
+ if (new_style[0] === current_style[0] && new_style[1] === current_style[1] && current_style[0] !== "" && pending_text.endsWith(current_style[1]) && lead_match !== null) {
8330
+ pending_text = pending_text.slice(0, -current_style[1].length) + lead_match[1] + seg.slice(lead_match[0].length);
8057
8331
  } else {
8058
8332
  pending_text += seg;
8059
8333
  }
@@ -8820,7 +9094,7 @@ function _collect_footnote_ids_fast(owned_items) {
8820
9094
  }
8821
9095
 
8822
9096
  // src/sanitize/core.ts
8823
- import { strFromU8 as strFromU82, unzipSync as unzipSync2 } from "fflate";
9097
+ import { strFromU8 as strFromU82, unzipSync as unzipSync3 } from "fflate";
8824
9098
 
8825
9099
  // src/sanitize/report.ts
8826
9100
  var SanitizeReport = class {
@@ -8975,6 +9249,7 @@ async function finalize_document(doc, options) {
8975
9249
  for (const w of warnings) report.warnings.push(w);
8976
9250
  for (const w of detect_watermarks(doc)) report.warnings.push(w);
8977
9251
  report.add_transform_lines(normalize_change_dates(doc));
9252
+ report.add_transform_lines(normalize_comment_dates(doc));
8978
9253
  if (options.protection_mode === "read_only" || options.protection_mode === "encrypt") {
8979
9254
  if (options.protection_mode === "encrypt") {
8980
9255
  report.warnings.push("Encryption mode (AES compound wrappers) is strictly unsupported in the zero-dependency Node engine. Falling back to native Word Read-Only lock.");
@@ -9033,7 +9308,7 @@ var VERIFIED_CORE_FIELDS = [
9033
9308
  ["version", "version (cp:version)"]
9034
9309
  ];
9035
9310
  function verifySanitizedPackage(outBuffer) {
9036
- const unzipped = unzipSync2(new Uint8Array(outBuffer));
9311
+ const unzipped = unzipSync3(new Uint8Array(outBuffer));
9037
9312
  const names = Object.keys(unzipped);
9038
9313
  const problems = [];
9039
9314
  if (names.includes("docProps/custom.xml")) {
@@ -9072,6 +9347,7 @@ export {
9072
9347
  USER_PATTERN_TIMEOUT_MS,
9073
9348
  _extractTextFromDoc,
9074
9349
  apply_edits_to_markdown,
9350
+ collect_media_difference_warnings,
9075
9351
  create_unified_diff,
9076
9352
  create_word_patch_diff,
9077
9353
  describe_illegal_control_chars,