@adeu/core 1.25.0 → 1.26.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.cjs CHANGED
@@ -38,6 +38,7 @@ __export(index_exports, {
38
38
  USER_PATTERN_TIMEOUT_MS: () => USER_PATTERN_TIMEOUT_MS,
39
39
  _extractTextFromDoc: () => _extractTextFromDoc,
40
40
  apply_edits_to_markdown: () => apply_edits_to_markdown,
41
+ collect_media_difference_warnings: () => collect_media_difference_warnings,
41
42
  create_unified_diff: () => create_unified_diff,
42
43
  create_word_patch_diff: () => create_word_patch_diff,
43
44
  describe_illegal_control_chars: () => describe_illegal_control_chars,
@@ -1286,12 +1287,27 @@ function get_run_style_markers(run, is_heading = null) {
1286
1287
  }
1287
1288
  return [prefix, suffix];
1288
1289
  }
1290
+ function split_boundary_whitespace(text) {
1291
+ const core = text.trim();
1292
+ if (!core) return ["", "", text];
1293
+ const lead_len = text.length - text.trimStart().length;
1294
+ return [
1295
+ text.substring(0, lead_len),
1296
+ text.substring(lead_len, lead_len + core.length),
1297
+ text.substring(lead_len + core.length)
1298
+ ];
1299
+ }
1289
1300
  function apply_formatting_to_segments(text, prefix, suffix) {
1290
1301
  if (!prefix && !suffix) return text;
1291
1302
  if (!text) return "";
1292
- if (!text.includes("\n")) return `${prefix}${text}${suffix}`;
1303
+ const wrap = (segment) => {
1304
+ const [lead, core, trail] = split_boundary_whitespace(segment);
1305
+ if (!core) return segment;
1306
+ return `${lead}${prefix}${core}${suffix}${trail}`;
1307
+ };
1308
+ if (!text.includes("\n")) return wrap(text);
1293
1309
  const parts = text.split("\n");
1294
- return parts.map((p) => p ? `${prefix}${p}${suffix}` : "").join("\n");
1310
+ return parts.map((p) => p ? wrap(p) : "").join("\n");
1295
1311
  }
1296
1312
  function get_run_text(run) {
1297
1313
  let text = "";
@@ -1744,7 +1760,7 @@ ${header}`;
1744
1760
  this._add_virtual_text(s_tok, current, paragraph);
1745
1761
  current += s_tok.length;
1746
1762
  }
1747
- for (const [kind, txt, r_obj, i_id, d_id, c_ids] of pending_runs) {
1763
+ for (const [kind, txt, r_obj, r_off, i_id, d_id, c_ids] of pending_runs) {
1748
1764
  if (kind === "virtual") {
1749
1765
  this._add_virtual_text(txt, current, paragraph, active_hyperlink_id);
1750
1766
  } else {
@@ -1758,7 +1774,8 @@ ${header}`;
1758
1774
  del_id: d_id || void 0,
1759
1775
  hyperlink_id: active_hyperlink_id || void 0,
1760
1776
  comment_ids: c_ids.length > 0 ? c_ids : void 0,
1761
- part_index: this._current_part_index
1777
+ part_index: this._current_part_index,
1778
+ run_offset: r_off
1762
1779
  };
1763
1780
  this.spans.push(s);
1764
1781
  this._text_chunks.push(txt);
@@ -1793,20 +1810,42 @@ ${header}`;
1793
1810
  if (text === "" || /^\s*$/.test(text)) continue;
1794
1811
  leading_strip_active = false;
1795
1812
  }
1813
+ let run_local = 0;
1814
+ const append_wrapped = (segment) => {
1815
+ const [lead, core, trail] = split_boundary_whitespace(segment);
1816
+ if (!core) {
1817
+ run_parts.push(["real", segment, item, run_local]);
1818
+ run_local += segment.length;
1819
+ return;
1820
+ }
1821
+ if (lead) {
1822
+ run_parts.push(["real", lead, item, run_local]);
1823
+ run_local += lead.length;
1824
+ }
1825
+ if (prefix) run_parts.push(["virtual", prefix, null, 0]);
1826
+ run_parts.push(["real", core, item, run_local]);
1827
+ run_local += core.length;
1828
+ if (suffix) run_parts.push(["virtual", suffix, null, 0]);
1829
+ if (trail) {
1830
+ run_parts.push(["real", trail, item, run_local]);
1831
+ run_local += trail.length;
1832
+ }
1833
+ };
1796
1834
  if (text.includes("\n") && (prefix || suffix)) {
1797
1835
  const parts = text.split("\n");
1798
1836
  for (let idx = 0; idx < parts.length; idx++) {
1799
- if (idx > 0) run_parts.push(["real", "\n", item]);
1800
- if (parts[idx]) {
1801
- if (prefix) run_parts.push(["virtual", prefix, null]);
1802
- run_parts.push(["real", parts[idx], item]);
1803
- if (suffix) run_parts.push(["virtual", suffix, null]);
1837
+ if (idx > 0) {
1838
+ run_parts.push(["real", "\n", item, run_local]);
1839
+ run_local += 1;
1804
1840
  }
1841
+ if (parts[idx]) append_wrapped(parts[idx]);
1805
1842
  }
1843
+ } else if ((prefix || suffix) && text) {
1844
+ append_wrapped(text);
1806
1845
  } else {
1807
- if (prefix) run_parts.push(["virtual", prefix, null]);
1808
- if (text) run_parts.push(["real", text, item]);
1809
- if (suffix) run_parts.push(["virtual", suffix, null]);
1846
+ if (prefix) run_parts.push(["virtual", prefix, null, 0]);
1847
+ if (text) run_parts.push(["real", text, item, 0]);
1848
+ if (suffix) run_parts.push(["virtual", suffix, null, 0]);
1810
1849
  }
1811
1850
  if (this.clean_view && Object.keys(active_del).length > 0) {
1812
1851
  }
@@ -1830,7 +1869,7 @@ ${header}`;
1830
1869
  skip_leading_prefix = true;
1831
1870
  }
1832
1871
  const curr_comment_ids = Array.from(active_ids);
1833
- for (const [kind, txt, r_obj] of run_parts) {
1872
+ for (const [kind, txt, r_obj, r_off] of run_parts) {
1834
1873
  if (skip_leading_prefix && kind === "virtual" && txt === new_style[0]) {
1835
1874
  skip_leading_prefix = false;
1836
1875
  continue;
@@ -1839,6 +1878,7 @@ ${header}`;
1839
1878
  kind,
1840
1879
  txt,
1841
1880
  r_obj,
1881
+ r_off,
1842
1882
  curr_ins_id,
1843
1883
  curr_del_id,
1844
1884
  curr_comment_ids
@@ -1850,11 +1890,12 @@ ${header}`;
1850
1890
  current_wrappers = new_wrappers;
1851
1891
  current_style = new_style;
1852
1892
  const curr_comment_ids = Array.from(active_ids);
1853
- for (const [kind, txt, r_obj] of run_parts) {
1893
+ for (const [kind, txt, r_obj, r_off] of run_parts) {
1854
1894
  pending_runs.push([
1855
1895
  kind,
1856
1896
  txt,
1857
1897
  r_obj,
1898
+ r_off,
1858
1899
  curr_ins_id,
1859
1900
  curr_del_id,
1860
1901
  curr_comment_ids
@@ -2266,39 +2307,43 @@ ${header}`;
2266
2307
  (s) => s.end > start_idx && s.start < end_idx
2267
2308
  );
2268
2309
  if (affected_spans.length === 0) return [];
2269
- const working_runs = affected_spans.filter((s) => s.run !== null).map((s) => s.run);
2270
- if (working_runs.length === 0) return [];
2271
- let dom_modified = false;
2272
- const first_real_span = affected_spans.find((s) => s.run !== null);
2273
- let start_split_adjustment = 0;
2274
- if (first_real_span) {
2275
- const local_start = start_idx - first_real_span.start;
2276
- if (local_start > 0) {
2277
- const idx_in_working = 0;
2278
- const [, right_run] = this._split_run_at_index(
2279
- working_runs[idx_in_working],
2280
- local_start
2281
- );
2282
- working_runs[idx_in_working] = right_run;
2283
- dom_modified = true;
2284
- start_split_adjustment = local_start;
2310
+ const real_spans = affected_spans.filter((s) => s.run !== null);
2311
+ if (real_spans.length === 0) return [];
2312
+ const working_runs = [];
2313
+ for (const s of real_spans) {
2314
+ if (!working_runs.some((r) => r === s.run)) {
2315
+ working_runs.push(s.run);
2285
2316
  }
2286
2317
  }
2287
- const last_real_span = [...affected_spans].reverse().find((s) => s.run !== null);
2288
- if (last_real_span) {
2289
- const is_same_run = first_real_span === last_real_span;
2290
- const run_to_split = working_runs[working_runs.length - 1];
2291
- let overlap_end = Math.min(last_real_span.end, end_idx);
2292
- let local_end = overlap_end - last_real_span.start;
2293
- if (is_same_run && start_split_adjustment > 0) {
2294
- local_end -= start_split_adjustment;
2295
- }
2296
- const run_text = get_run_text(run_to_split);
2297
- if (local_end > 0 && local_end < run_text.length) {
2298
- const [left_run] = this._split_run_at_index(run_to_split, local_end);
2299
- working_runs[working_runs.length - 1] = left_run;
2300
- dom_modified = true;
2301
- }
2318
+ let dom_modified = false;
2319
+ const first_real_span = real_spans[0];
2320
+ let start_split_adjustment = 0;
2321
+ const local_start = start_idx - first_real_span.start + (first_real_span.run_offset || 0);
2322
+ if (local_start > 0) {
2323
+ const split_source = working_runs[0];
2324
+ const [, right_run] = this._split_run_at_index(
2325
+ split_source,
2326
+ local_start
2327
+ );
2328
+ for (let w = 0; w < working_runs.length; w++) {
2329
+ if (working_runs[w] === split_source) working_runs[w] = right_run;
2330
+ }
2331
+ dom_modified = true;
2332
+ start_split_adjustment = local_start;
2333
+ }
2334
+ const last_real_span = real_spans[real_spans.length - 1];
2335
+ const is_same_run = first_real_span.run === last_real_span.run;
2336
+ const run_to_split = working_runs[working_runs.length - 1];
2337
+ const overlap_end = Math.min(last_real_span.end, end_idx);
2338
+ let local_end = overlap_end - last_real_span.start + (last_real_span.run_offset || 0);
2339
+ if (is_same_run && start_split_adjustment > 0) {
2340
+ local_end -= start_split_adjustment;
2341
+ }
2342
+ const run_text = get_run_text(run_to_split);
2343
+ if (local_end > 0 && local_end < run_text.length) {
2344
+ const [left_run] = this._split_run_at_index(run_to_split, local_end);
2345
+ working_runs[working_runs.length - 1] = left_run;
2346
+ dom_modified = true;
2302
2347
  }
2303
2348
  if (dom_modified && rebuild_map) {
2304
2349
  this._build_map();
@@ -2335,7 +2380,7 @@ ${header}`;
2335
2380
  }
2336
2381
  return [null, span.paragraph];
2337
2382
  } else {
2338
- const offset = index - span.start;
2383
+ const offset = index - span.start + (span.run_offset || 0);
2339
2384
  const [left] = this._split_run_at_index(span.run, offset);
2340
2385
  if (rebuild_map) this._build_map();
2341
2386
  return [left, span.paragraph];
@@ -2406,6 +2451,7 @@ ${header}`;
2406
2451
  };
2407
2452
 
2408
2453
  // src/diff.ts
2454
+ var import_fflate2 = require("fflate");
2409
2455
  var import_diff_match_patch = __toESM(require("diff-match-patch"), 1);
2410
2456
  function _count_standalone_underscores(s) {
2411
2457
  let count = 0;
@@ -2743,6 +2789,37 @@ function _drop_image_marker_hunks(edits, warnings) {
2743
2789
  }
2744
2790
  return kept;
2745
2791
  }
2792
+ function _drop_marker_interior_hunks(edits, text_orig, warnings) {
2793
+ const marker_spans = [];
2794
+ for (const m of text_orig.matchAll(_IMAGE_MARKER_RE)) {
2795
+ marker_spans.push([m.index, m.index + m[0].length]);
2796
+ }
2797
+ if (marker_spans.length === 0) return edits;
2798
+ const kept = [];
2799
+ let warned = false;
2800
+ for (const e of edits) {
2801
+ const idx = e._match_start_index;
2802
+ if (idx === void 0 || idx === null) {
2803
+ kept.push(e);
2804
+ continue;
2805
+ }
2806
+ const end = idx + (e.target_text || "").length;
2807
+ const cuts_into_marker = marker_spans.some(
2808
+ ([m_start, m_end]) => m_start < end && idx < m_end && !(idx <= m_start && m_end <= end)
2809
+ );
2810
+ if (!cuts_into_marker) {
2811
+ kept.push(e);
2812
+ continue;
2813
+ }
2814
+ if (!warned) {
2815
+ warnings.push(
2816
+ "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."
2817
+ );
2818
+ warned = true;
2819
+ }
2820
+ }
2821
+ return kept;
2822
+ }
2746
2823
  function _rows_are_plain(text, table) {
2747
2824
  for (const row of table.rows) {
2748
2825
  if (text.substring(row.start, row.end) !== row.cells.join(" | ")) {
@@ -2884,8 +2961,12 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
2884
2961
  warnings.push(
2885
2962
  `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.`
2886
2963
  );
2887
- const flat = _drop_image_marker_hunks(
2888
- generate_edits_from_text(text_orig, text_mod),
2964
+ const flat = _drop_marker_interior_hunks(
2965
+ _drop_image_marker_hunks(
2966
+ generate_edits_from_text(text_orig, text_mod),
2967
+ warnings
2968
+ ),
2969
+ text_orig,
2889
2970
  warnings
2890
2971
  );
2891
2972
  return { edits: [...flat], warnings };
@@ -2989,12 +3070,63 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
2989
3070
  const modify_edits = edits.filter(
2990
3071
  (e) => e.type === "modify"
2991
3072
  );
2992
- const kept_modifies = new Set(_drop_image_marker_hunks(modify_edits, warnings));
3073
+ const kept_modifies = new Set(
3074
+ _drop_marker_interior_hunks(
3075
+ _drop_image_marker_hunks(modify_edits, warnings),
3076
+ text_orig,
3077
+ warnings
3078
+ )
3079
+ );
2993
3080
  const final_edits = edits.filter(
2994
3081
  (e) => e.type !== "modify" || kept_modifies.has(e)
2995
3082
  );
2996
3083
  return { edits: final_edits, warnings };
2997
3084
  }
3085
+ function collect_media_difference_warnings(original_docx, modified_docx) {
3086
+ const media_hashes = (data) => {
3087
+ const hashes = /* @__PURE__ */ new Map();
3088
+ try {
3089
+ const unzipped = (0, import_fflate2.unzipSync)(data);
3090
+ for (const [name, bytes] of Object.entries(unzipped)) {
3091
+ if (!name.startsWith("word/media/")) continue;
3092
+ let h1 = 2166136261;
3093
+ let h2 = 3421674724;
3094
+ for (let i = 0; i < bytes.length; i++) {
3095
+ h1 = Math.imul(h1 ^ bytes[i], 16777619) >>> 0;
3096
+ h2 = Math.imul(h2 ^ bytes[bytes.length - 1 - i], 16777619) >>> 0;
3097
+ }
3098
+ hashes.set(name, `${bytes.length}:${h1.toString(16)}:${h2.toString(16)}`);
3099
+ }
3100
+ } catch {
3101
+ return /* @__PURE__ */ new Map();
3102
+ }
3103
+ return hashes;
3104
+ };
3105
+ const hashes_orig = media_hashes(original_docx);
3106
+ const hashes_mod = media_hashes(modified_docx);
3107
+ const changed = [];
3108
+ const added = [];
3109
+ const removed = [];
3110
+ for (const [name, hash] of hashes_orig) {
3111
+ if (!hashes_mod.has(name)) removed.push(name);
3112
+ else if (hashes_mod.get(name) !== hash) changed.push(name);
3113
+ }
3114
+ for (const name of hashes_mod.keys()) {
3115
+ if (!hashes_orig.has(name)) added.push(name);
3116
+ }
3117
+ changed.sort();
3118
+ added.sort();
3119
+ removed.sort();
3120
+ if (changed.length + added.length + removed.length === 0) return [];
3121
+ const parts = [];
3122
+ if (changed.length) parts.push(`${changed.length} changed`);
3123
+ if (added.length) parts.push(`${added.length} added`);
3124
+ if (removed.length) parts.push(`${removed.length} removed`);
3125
+ const names = [...changed, ...added, ...removed].slice(0, 5).join(", ");
3126
+ return [
3127
+ `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.`
3128
+ ];
3129
+ }
2998
3130
  function create_unified_diff(original_text, modified_text, context_lines = 3) {
2999
3131
  const dmp = new import_diff_match_patch.default.diff_match_patch();
3000
3132
  dmp.Diff_Timeout = 2;
@@ -3949,6 +4081,13 @@ var RedlineEngine = class _RedlineEngine {
3949
4081
  console.error("generate_edits_from_text failed, falling back to wholesale edit", e);
3950
4082
  raw_sub_edits = [];
3951
4083
  }
4084
+ const _marker_only = (text) => {
4085
+ const stripped = text.trim();
4086
+ return stripped.length > 0 && /^[*_]+$/.test(stripped);
4087
+ };
4088
+ raw_sub_edits = raw_sub_edits.filter(
4089
+ (e) => !((!e.target_text || _marker_only(e.target_text)) && (!e.new_text || _marker_only(e.new_text)) && (e.target_text || e.new_text))
4090
+ );
3952
4091
  if (!raw_sub_edits || raw_sub_edits.length === 0) {
3953
4092
  const fallback_edit = {
3954
4093
  type: "modify",
@@ -4729,7 +4868,7 @@ var RedlineEngine = class _RedlineEngine {
4729
4868
  *
4730
4869
  * Does NOT attach comments; callers handle that.
4731
4870
  */
4732
- _track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id, positional_anchor_el = null) {
4871
+ _track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id, positional_anchor_el = null, suppress_emphasis = false) {
4733
4872
  if (!text) {
4734
4873
  return {
4735
4874
  first_node: null,
@@ -4793,7 +4932,8 @@ var RedlineEngine = class _RedlineEngine {
4793
4932
  first_clean === lines[0] ? lines[0] : lines[0],
4794
4933
  anchor_run,
4795
4934
  reuse_id,
4796
- xmlDoc
4935
+ xmlDoc,
4936
+ suppress_emphasis
4797
4937
  );
4798
4938
  first_node = inline_ins;
4799
4939
  }
@@ -4857,7 +4997,8 @@ var RedlineEngine = class _RedlineEngine {
4857
4997
  clean_text,
4858
4998
  anchor_run,
4859
4999
  reuse_id,
4860
- xmlDoc
5000
+ xmlDoc,
5001
+ suppress_emphasis
4861
5002
  );
4862
5003
  if (content_ins) {
4863
5004
  new_p.appendChild(content_ins);
@@ -4883,7 +5024,7 @@ var RedlineEngine = class _RedlineEngine {
4883
5024
  * <w:r> elements representing the inline markdown segments of `line_text`.
4884
5025
  * Returns null if line_text is empty.
4885
5026
  */
4886
- _build_tracked_ins_for_line(line_text, anchor_run, reuse_id, xmlDoc) {
5027
+ _build_tracked_ins_for_line(line_text, anchor_run, reuse_id, xmlDoc, suppress_emphasis = false) {
4887
5028
  if (!line_text && line_text !== "") return null;
4888
5029
  const ins = this._create_track_change_tag("w:ins", "", reuse_id);
4889
5030
  const segments = this._parse_inline_markdown(line_text);
@@ -4896,15 +5037,11 @@ var RedlineEngine = class _RedlineEngine {
4896
5037
  const anchor_rPr = findChild(anchor_run._element, "w:rPr");
4897
5038
  if (anchor_rPr) {
4898
5039
  const clone = anchor_rPr.cloneNode(true);
4899
- for (const tag of [
4900
- "w:vanish",
4901
- "w:strike",
4902
- "w:dstrike",
4903
- "w:i",
4904
- "w:iCs",
4905
- "w:b",
4906
- "w:bCs"
4907
- ]) {
5040
+ const strip_tags = ["w:vanish", "w:strike", "w:dstrike", "w:i", "w:iCs"];
5041
+ if (suppress_emphasis) {
5042
+ strip_tags.push("w:b", "w:bCs");
5043
+ }
5044
+ for (const tag of strip_tags) {
4908
5045
  const found = findChild(clone, tag);
4909
5046
  if (found) clone.removeChild(found);
4910
5047
  }
@@ -4939,6 +5076,27 @@ var RedlineEngine = class _RedlineEngine {
4939
5076
  }
4940
5077
  return [text, null];
4941
5078
  }
5079
+ /**
5080
+ * True when this edit's target or replacement text carries explicit
5081
+ * bold/italic markers, making the markers AUTHORITATIVE for the inserted
5082
+ * runs' formatting. Replacing `**X**` with `_X_` must yield italic-only
5083
+ * text, and replacing `**X**` with `X` must yield plain text — inheriting
5084
+ * the replaced span's run properties on top of (or instead of) the
5085
+ * requested markers silently produces the wrong document while the report
5086
+ * claims success (QA 2026-07-19 F-02). Plain-text edits (no markers on
5087
+ * either side) keep inheriting the context style so partial replacements
5088
+ * inside a styled span never lose formatting.
5089
+ */
5090
+ _edit_declares_emphasis(edit) {
5091
+ for (const text of [edit?.target_text, edit?.new_text]) {
5092
+ if (!text || !text.includes("**") && !text.includes("_")) continue;
5093
+ const segments = this._parse_inline_markdown(text);
5094
+ if (segments.some(([, props]) => props && Object.keys(props).length > 0)) {
5095
+ return true;
5096
+ }
5097
+ }
5098
+ return false;
5099
+ }
4942
5100
  _parse_inline_markdown(text, baseStyle = {}) {
4943
5101
  if (!text) return [];
4944
5102
  const tokenPattern = /(\*\*.*?\*\*)|(_.*?_)/;
@@ -5205,6 +5363,16 @@ var RedlineEngine = class _RedlineEngine {
5205
5363
  continue;
5206
5364
  const is_regex = edit.regex || false;
5207
5365
  const match_mode = edit.match_mode || "strict";
5366
+ if (is_regex) {
5367
+ try {
5368
+ new RegExp(edit.target_text);
5369
+ } catch (regex_err) {
5370
+ errors.push(
5371
+ `- 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.`
5372
+ );
5373
+ continue;
5374
+ }
5375
+ }
5208
5376
  let matches = this.mapper.find_all_match_indices(
5209
5377
  edit.target_text,
5210
5378
  is_regex
@@ -5784,6 +5952,14 @@ var RedlineEngine = class _RedlineEngine {
5784
5952
  actions_skipped: skipped_actions,
5785
5953
  edits_applied: applied_edits,
5786
5954
  edits_skipped: skipped_edits,
5955
+ // edits_applied counts change OBJECTS; this is the total number of
5956
+ // document occurrences they modified (match_mode="all" fan-out), so
5957
+ // automation never has to guess which of the two a count means
5958
+ // (QA 2026-07-19 F-21).
5959
+ occurrences_modified: edits_reports.reduce(
5960
+ (sum, r) => sum + (r.occurrences_modified || 0),
5961
+ 0
5962
+ ),
5787
5963
  skipped_details: this.skipped_details,
5788
5964
  edits: edits_reports,
5789
5965
  engine: "node",
@@ -5805,6 +5981,7 @@ var RedlineEngine = class _RedlineEngine {
5805
5981
  }
5806
5982
  edit._applied_status = false;
5807
5983
  edit._error_msg = null;
5984
+ edit._any_sub_failure = false;
5808
5985
  }
5809
5986
  for (const edit of edits) {
5810
5987
  if (typeof edit !== "object" || edit === null) continue;
@@ -5904,17 +6081,18 @@ var RedlineEngine = class _RedlineEngine {
5904
6081
  this.skipped_details.push(msg);
5905
6082
  edit._applied_status = false;
5906
6083
  edit._error_msg = msg;
6084
+ edit._any_sub_failure = true;
5907
6085
  const parent = edit._parent_edit_ref;
5908
6086
  if (parent) {
5909
6087
  parent._applied_status = false;
5910
6088
  parent._error_msg = msg;
6089
+ parent._any_sub_failure = true;
5911
6090
  }
5912
6091
  continue;
5913
6092
  }
5914
6093
  let success = false;
5915
6094
  if (edit.type === "modify") {
5916
- const rebuild = edit._split_group_id !== void 0 && edit._split_group_id !== null;
5917
- success = this._apply_single_edit_indexed(edit, orig_new, rebuild);
6095
+ success = this._apply_single_edit_indexed(edit, orig_new, false);
5918
6096
  } else if (edit.type === "insert_row" || edit.type === "delete_row") {
5919
6097
  success = this._apply_table_edit(edit, false);
5920
6098
  }
@@ -5964,8 +6142,10 @@ var RedlineEngine = class _RedlineEngine {
5964
6142
  this.skipped_details.push(msg);
5965
6143
  edit._applied_status = false;
5966
6144
  edit._error_msg = msg;
6145
+ edit._any_sub_failure = true;
5967
6146
  const parent = edit._parent_edit_ref;
5968
6147
  if (parent) {
6148
+ parent._any_sub_failure = true;
5969
6149
  if (!parent._applied_status) {
5970
6150
  parent._applied_status = false;
5971
6151
  parent._error_msg = msg;
@@ -5973,7 +6153,20 @@ var RedlineEngine = class _RedlineEngine {
5973
6153
  }
5974
6154
  }
5975
6155
  }
5976
- return [applied, skipped];
6156
+ let applied_logical = 0;
6157
+ let skipped_logical = 0;
6158
+ for (const input_edit of edits) {
6159
+ if (typeof input_edit !== "object" || input_edit === null) {
6160
+ skipped_logical++;
6161
+ continue;
6162
+ }
6163
+ if (input_edit._applied_status && !input_edit._any_sub_failure) {
6164
+ applied_logical++;
6165
+ } else {
6166
+ skipped_logical++;
6167
+ }
6168
+ }
6169
+ return [applied_logical, skipped_logical];
5977
6170
  }
5978
6171
  apply_review_actions(actions) {
5979
6172
  let applied = 0;
@@ -6466,6 +6659,7 @@ var RedlineEngine = class _RedlineEngine {
6466
6659
  op = "MODIFICATION";
6467
6660
  }
6468
6661
  }
6662
+ const suppress_emphasis = this._edit_declares_emphasis(edit);
6469
6663
  if (op === "STYLE_ONLY" || op === "STYLE_AND_TEXT") {
6470
6664
  const [anchor_run, anchor_para] = active_mapper.get_insertion_anchor(
6471
6665
  start_idx,
@@ -6666,7 +6860,8 @@ var RedlineEngine = class _RedlineEngine {
6666
6860
  clean_text,
6667
6861
  anchor_run,
6668
6862
  ins_id,
6669
- xmlDoc
6863
+ xmlDoc,
6864
+ suppress_emphasis
6670
6865
  );
6671
6866
  if (content_ins) new_p.appendChild(content_ins);
6672
6867
  body.insertBefore(new_p, _bug233_target_para);
@@ -6698,7 +6893,9 @@ var RedlineEngine = class _RedlineEngine {
6698
6893
  final_new_text,
6699
6894
  anchor_run,
6700
6895
  anchor_para,
6701
- ins_id
6896
+ ins_id,
6897
+ null,
6898
+ suppress_emphasis
6702
6899
  );
6703
6900
  if (!result.first_node) return false;
6704
6901
  const is_inline_first = result.first_node.tagName === "w:ins";
@@ -6849,7 +7046,8 @@ var RedlineEngine = class _RedlineEngine {
6849
7046
  ins_id,
6850
7047
  // The insertion physically follows the deletion block; the style
6851
7048
  // run was detached when the deletion cloned it into <w:del>.
6852
- last_del
7049
+ last_del,
7050
+ suppress_emphasis
6853
7051
  );
6854
7052
  if (result.first_node) {
6855
7053
  const is_inline_first = result.first_node.tagName === "w:ins";
@@ -6883,7 +7081,9 @@ var RedlineEngine = class _RedlineEngine {
6883
7081
  edit.new_text,
6884
7082
  anchor,
6885
7083
  first_span.paragraph,
6886
- ins_id
7084
+ ins_id,
7085
+ null,
7086
+ suppress_emphasis
6887
7087
  );
6888
7088
  if (result.first_node) {
6889
7089
  p1_el.appendChild(result.first_node);
@@ -7367,6 +7567,26 @@ function normalize_change_dates(doc) {
7367
7567
  }
7368
7568
  return count ? [`Track change timestamps: ${count} normalized`] : [];
7369
7569
  }
7570
+ function normalize_comment_dates(doc) {
7571
+ let count = 0;
7572
+ const fixed = "2025-01-01T00:00:00Z";
7573
+ for (const part of doc.pkg.parts) {
7574
+ if (!part.contentType.includes("comments")) continue;
7575
+ for (const el of findAllDescendants(part._element, "w:comment")) {
7576
+ if (el.hasAttribute("w:date")) {
7577
+ el.setAttribute("w:date", fixed);
7578
+ count++;
7579
+ }
7580
+ }
7581
+ for (const el of findAllDescendants(part._element, "w16cex:commentExtensible")) {
7582
+ if (el.hasAttribute("w16cex:dateUtc")) {
7583
+ el.setAttribute("w16cex:dateUtc", fixed);
7584
+ count++;
7585
+ }
7586
+ }
7587
+ }
7588
+ return count ? [`Comment timestamps: ${count} normalized`] : [];
7589
+ }
7370
7590
  function scrub_doc_properties(doc) {
7371
7591
  const lines = [];
7372
7592
  const corePart = doc.pkg.getPartByPath("docProps/core.xml");
@@ -8110,8 +8330,10 @@ function build_paragraph_text(paragraph, comments_map, cleanView, style_cache, d
8110
8330
  const new_wrappers = cleanView ? ["", ""] : _get_wrappers(active_ins, active_del, active_comments, active_fmt);
8111
8331
  const new_style = [prefix, suffix];
8112
8332
  if (pending_text && new_wrappers[0] === current_wrappers[0] && new_wrappers[1] === current_wrappers[1]) {
8113
- 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])) {
8114
- pending_text = pending_text.slice(0, -current_style[1].length) + seg.slice(new_style[0].length);
8333
+ const escaped_prefix = new_style[0].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8334
+ const lead_match = new_style[0] !== "" || new_style[1] !== "" ? seg.match(new RegExp("^(\\s*)" + escaped_prefix)) : null;
8335
+ 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) {
8336
+ pending_text = pending_text.slice(0, -current_style[1].length) + lead_match[1] + seg.slice(lead_match[0].length);
8115
8337
  } else {
8116
8338
  pending_text += seg;
8117
8339
  }
@@ -8878,7 +9100,7 @@ function _collect_footnote_ids_fast(owned_items) {
8878
9100
  }
8879
9101
 
8880
9102
  // src/sanitize/core.ts
8881
- var import_fflate2 = require("fflate");
9103
+ var import_fflate3 = require("fflate");
8882
9104
 
8883
9105
  // src/sanitize/report.ts
8884
9106
  var SanitizeReport = class {
@@ -9033,6 +9255,7 @@ async function finalize_document(doc, options) {
9033
9255
  for (const w of warnings) report.warnings.push(w);
9034
9256
  for (const w of detect_watermarks(doc)) report.warnings.push(w);
9035
9257
  report.add_transform_lines(normalize_change_dates(doc));
9258
+ report.add_transform_lines(normalize_comment_dates(doc));
9036
9259
  if (options.protection_mode === "read_only" || options.protection_mode === "encrypt") {
9037
9260
  if (options.protection_mode === "encrypt") {
9038
9261
  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.");
@@ -9091,7 +9314,7 @@ var VERIFIED_CORE_FIELDS = [
9091
9314
  ["version", "version (cp:version)"]
9092
9315
  ];
9093
9316
  function verifySanitizedPackage(outBuffer) {
9094
- const unzipped = (0, import_fflate2.unzipSync)(new Uint8Array(outBuffer));
9317
+ const unzipped = (0, import_fflate3.unzipSync)(new Uint8Array(outBuffer));
9095
9318
  const names = Object.keys(unzipped);
9096
9319
  const problems = [];
9097
9320
  if (names.includes("docProps/custom.xml")) {
@@ -9101,7 +9324,7 @@ function verifySanitizedPackage(outBuffer) {
9101
9324
  problems.push("customXml/* parts are still in the package");
9102
9325
  }
9103
9326
  if (names.includes("docProps/core.xml")) {
9104
- const core = parseXml((0, import_fflate2.strFromU8)(unzipped["docProps/core.xml"]));
9327
+ const core = parseXml((0, import_fflate3.strFromU8)(unzipped["docProps/core.xml"]));
9105
9328
  for (const [local, label] of VERIFIED_CORE_FIELDS) {
9106
9329
  for (const el of findDescendantsByLocalName(core.documentElement, local)) {
9107
9330
  if ((el.textContent || "").trim()) {
@@ -9131,6 +9354,7 @@ function identifyEngine() {
9131
9354
  USER_PATTERN_TIMEOUT_MS,
9132
9355
  _extractTextFromDoc,
9133
9356
  apply_edits_to_markdown,
9357
+ collect_media_difference_warnings,
9134
9358
  create_unified_diff,
9135
9359
  create_word_patch_diff,
9136
9360
  describe_illegal_control_chars,