@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 +298 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +296 -73
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/diff.ts +129 -4
- package/src/engine.ts +135 -18
- package/src/index.ts +1 -1
- package/src/ingest.ts +11 -2
- package/src/mapper.ts +99 -47
- package/src/repro_qa_report_v7.test.ts +380 -0
- package/src/sanitize/core.ts +3 -0
- package/src/sanitize/transforms.ts +29 -0
- package/src/utils/docx.ts +30 -2
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
|
-
|
|
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 ?
|
|
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)
|
|
1742
|
-
|
|
1743
|
-
|
|
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
|
|
2212
|
-
if (
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
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
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2259
|
+
let dom_modified = false;
|
|
2260
|
+
const first_real_span = real_spans[0];
|
|
2261
|
+
let start_split_adjustment = 0;
|
|
2262
|
+
const local_start = start_idx - 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 () 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 =
|
|
2830
|
-
|
|
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(
|
|
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) {
|
|
4675
4813
|
if (!text) {
|
|
4676
4814
|
return {
|
|
4677
4815
|
first_node: null,
|
|
@@ -4735,7 +4873,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4735
4873
|
first_clean === lines[0] ? lines[0] : lines[0],
|
|
4736
4874
|
anchor_run,
|
|
4737
4875
|
reuse_id,
|
|
4738
|
-
xmlDoc
|
|
4876
|
+
xmlDoc,
|
|
4877
|
+
suppress_emphasis
|
|
4739
4878
|
);
|
|
4740
4879
|
first_node = inline_ins;
|
|
4741
4880
|
}
|
|
@@ -4799,7 +4938,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4799
4938
|
clean_text,
|
|
4800
4939
|
anchor_run,
|
|
4801
4940
|
reuse_id,
|
|
4802
|
-
xmlDoc
|
|
4941
|
+
xmlDoc,
|
|
4942
|
+
suppress_emphasis
|
|
4803
4943
|
);
|
|
4804
4944
|
if (content_ins) {
|
|
4805
4945
|
new_p.appendChild(content_ins);
|
|
@@ -4825,7 +4965,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4825
4965
|
* <w:r> elements representing the inline markdown segments of `line_text`.
|
|
4826
4966
|
* Returns null if line_text is empty.
|
|
4827
4967
|
*/
|
|
4828
|
-
_build_tracked_ins_for_line(line_text, anchor_run, reuse_id, xmlDoc) {
|
|
4968
|
+
_build_tracked_ins_for_line(line_text, anchor_run, reuse_id, xmlDoc, suppress_emphasis = false) {
|
|
4829
4969
|
if (!line_text && line_text !== "") return null;
|
|
4830
4970
|
const ins = this._create_track_change_tag("w:ins", "", reuse_id);
|
|
4831
4971
|
const segments = this._parse_inline_markdown(line_text);
|
|
@@ -4838,15 +4978,11 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4838
4978
|
const anchor_rPr = findChild(anchor_run._element, "w:rPr");
|
|
4839
4979
|
if (anchor_rPr) {
|
|
4840
4980
|
const clone = anchor_rPr.cloneNode(true);
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
"w:
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
"w:iCs",
|
|
4847
|
-
"w:b",
|
|
4848
|
-
"w:bCs"
|
|
4849
|
-
]) {
|
|
4981
|
+
const strip_tags = ["w:vanish", "w:strike", "w:dstrike", "w:i", "w:iCs"];
|
|
4982
|
+
if (suppress_emphasis) {
|
|
4983
|
+
strip_tags.push("w:b", "w:bCs");
|
|
4984
|
+
}
|
|
4985
|
+
for (const tag of strip_tags) {
|
|
4850
4986
|
const found = findChild(clone, tag);
|
|
4851
4987
|
if (found) clone.removeChild(found);
|
|
4852
4988
|
}
|
|
@@ -4881,6 +5017,27 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4881
5017
|
}
|
|
4882
5018
|
return [text, null];
|
|
4883
5019
|
}
|
|
5020
|
+
/**
|
|
5021
|
+
* True when this edit's target or replacement text carries explicit
|
|
5022
|
+
* bold/italic markers, making the markers AUTHORITATIVE for the inserted
|
|
5023
|
+
* runs' formatting. Replacing `**X**` with `_X_` must yield italic-only
|
|
5024
|
+
* text, and replacing `**X**` with `X` must yield plain text — inheriting
|
|
5025
|
+
* the replaced span's run properties on top of (or instead of) the
|
|
5026
|
+
* requested markers silently produces the wrong document while the report
|
|
5027
|
+
* claims success (QA 2026-07-19 F-02). Plain-text edits (no markers on
|
|
5028
|
+
* either side) keep inheriting the context style so partial replacements
|
|
5029
|
+
* inside a styled span never lose formatting.
|
|
5030
|
+
*/
|
|
5031
|
+
_edit_declares_emphasis(edit) {
|
|
5032
|
+
for (const text of [edit?.target_text, edit?.new_text]) {
|
|
5033
|
+
if (!text || !text.includes("**") && !text.includes("_")) continue;
|
|
5034
|
+
const segments = this._parse_inline_markdown(text);
|
|
5035
|
+
if (segments.some(([, props]) => props && Object.keys(props).length > 0)) {
|
|
5036
|
+
return true;
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
return false;
|
|
5040
|
+
}
|
|
4884
5041
|
_parse_inline_markdown(text, baseStyle = {}) {
|
|
4885
5042
|
if (!text) return [];
|
|
4886
5043
|
const tokenPattern = /(\*\*.*?\*\*)|(_.*?_)/;
|
|
@@ -5147,6 +5304,16 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5147
5304
|
continue;
|
|
5148
5305
|
const is_regex = edit.regex || false;
|
|
5149
5306
|
const match_mode = edit.match_mode || "strict";
|
|
5307
|
+
if (is_regex) {
|
|
5308
|
+
try {
|
|
5309
|
+
new RegExp(edit.target_text);
|
|
5310
|
+
} catch (regex_err) {
|
|
5311
|
+
errors.push(
|
|
5312
|
+
`- 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.`
|
|
5313
|
+
);
|
|
5314
|
+
continue;
|
|
5315
|
+
}
|
|
5316
|
+
}
|
|
5150
5317
|
let matches = this.mapper.find_all_match_indices(
|
|
5151
5318
|
edit.target_text,
|
|
5152
5319
|
is_regex
|
|
@@ -5726,6 +5893,14 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5726
5893
|
actions_skipped: skipped_actions,
|
|
5727
5894
|
edits_applied: applied_edits,
|
|
5728
5895
|
edits_skipped: skipped_edits,
|
|
5896
|
+
// edits_applied counts change OBJECTS; this is the total number of
|
|
5897
|
+
// document occurrences they modified (match_mode="all" fan-out), so
|
|
5898
|
+
// automation never has to guess which of the two a count means
|
|
5899
|
+
// (QA 2026-07-19 F-21).
|
|
5900
|
+
occurrences_modified: edits_reports.reduce(
|
|
5901
|
+
(sum, r) => sum + (r.occurrences_modified || 0),
|
|
5902
|
+
0
|
|
5903
|
+
),
|
|
5729
5904
|
skipped_details: this.skipped_details,
|
|
5730
5905
|
edits: edits_reports,
|
|
5731
5906
|
engine: "node",
|
|
@@ -5747,6 +5922,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5747
5922
|
}
|
|
5748
5923
|
edit._applied_status = false;
|
|
5749
5924
|
edit._error_msg = null;
|
|
5925
|
+
edit._any_sub_failure = false;
|
|
5750
5926
|
}
|
|
5751
5927
|
for (const edit of edits) {
|
|
5752
5928
|
if (typeof edit !== "object" || edit === null) continue;
|
|
@@ -5846,17 +6022,18 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5846
6022
|
this.skipped_details.push(msg);
|
|
5847
6023
|
edit._applied_status = false;
|
|
5848
6024
|
edit._error_msg = msg;
|
|
6025
|
+
edit._any_sub_failure = true;
|
|
5849
6026
|
const parent = edit._parent_edit_ref;
|
|
5850
6027
|
if (parent) {
|
|
5851
6028
|
parent._applied_status = false;
|
|
5852
6029
|
parent._error_msg = msg;
|
|
6030
|
+
parent._any_sub_failure = true;
|
|
5853
6031
|
}
|
|
5854
6032
|
continue;
|
|
5855
6033
|
}
|
|
5856
6034
|
let success = false;
|
|
5857
6035
|
if (edit.type === "modify") {
|
|
5858
|
-
|
|
5859
|
-
success = this._apply_single_edit_indexed(edit, orig_new, rebuild);
|
|
6036
|
+
success = this._apply_single_edit_indexed(edit, orig_new, false);
|
|
5860
6037
|
} else if (edit.type === "insert_row" || edit.type === "delete_row") {
|
|
5861
6038
|
success = this._apply_table_edit(edit, false);
|
|
5862
6039
|
}
|
|
@@ -5906,8 +6083,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5906
6083
|
this.skipped_details.push(msg);
|
|
5907
6084
|
edit._applied_status = false;
|
|
5908
6085
|
edit._error_msg = msg;
|
|
6086
|
+
edit._any_sub_failure = true;
|
|
5909
6087
|
const parent = edit._parent_edit_ref;
|
|
5910
6088
|
if (parent) {
|
|
6089
|
+
parent._any_sub_failure = true;
|
|
5911
6090
|
if (!parent._applied_status) {
|
|
5912
6091
|
parent._applied_status = false;
|
|
5913
6092
|
parent._error_msg = msg;
|
|
@@ -5915,7 +6094,20 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5915
6094
|
}
|
|
5916
6095
|
}
|
|
5917
6096
|
}
|
|
5918
|
-
|
|
6097
|
+
let applied_logical = 0;
|
|
6098
|
+
let skipped_logical = 0;
|
|
6099
|
+
for (const input_edit of edits) {
|
|
6100
|
+
if (typeof input_edit !== "object" || input_edit === null) {
|
|
6101
|
+
skipped_logical++;
|
|
6102
|
+
continue;
|
|
6103
|
+
}
|
|
6104
|
+
if (input_edit._applied_status && !input_edit._any_sub_failure) {
|
|
6105
|
+
applied_logical++;
|
|
6106
|
+
} else {
|
|
6107
|
+
skipped_logical++;
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
return [applied_logical, skipped_logical];
|
|
5919
6111
|
}
|
|
5920
6112
|
apply_review_actions(actions) {
|
|
5921
6113
|
let applied = 0;
|
|
@@ -6408,6 +6600,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6408
6600
|
op = "MODIFICATION";
|
|
6409
6601
|
}
|
|
6410
6602
|
}
|
|
6603
|
+
const suppress_emphasis = this._edit_declares_emphasis(edit);
|
|
6411
6604
|
if (op === "STYLE_ONLY" || op === "STYLE_AND_TEXT") {
|
|
6412
6605
|
const [anchor_run, anchor_para] = active_mapper.get_insertion_anchor(
|
|
6413
6606
|
start_idx,
|
|
@@ -6608,7 +6801,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6608
6801
|
clean_text,
|
|
6609
6802
|
anchor_run,
|
|
6610
6803
|
ins_id,
|
|
6611
|
-
xmlDoc
|
|
6804
|
+
xmlDoc,
|
|
6805
|
+
suppress_emphasis
|
|
6612
6806
|
);
|
|
6613
6807
|
if (content_ins) new_p.appendChild(content_ins);
|
|
6614
6808
|
body.insertBefore(new_p, _bug233_target_para);
|
|
@@ -6640,7 +6834,9 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6640
6834
|
final_new_text,
|
|
6641
6835
|
anchor_run,
|
|
6642
6836
|
anchor_para,
|
|
6643
|
-
ins_id
|
|
6837
|
+
ins_id,
|
|
6838
|
+
null,
|
|
6839
|
+
suppress_emphasis
|
|
6644
6840
|
);
|
|
6645
6841
|
if (!result.first_node) return false;
|
|
6646
6842
|
const is_inline_first = result.first_node.tagName === "w:ins";
|
|
@@ -6791,7 +6987,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6791
6987
|
ins_id,
|
|
6792
6988
|
// The insertion physically follows the deletion block; the style
|
|
6793
6989
|
// run was detached when the deletion cloned it into <w:del>.
|
|
6794
|
-
last_del
|
|
6990
|
+
last_del,
|
|
6991
|
+
suppress_emphasis
|
|
6795
6992
|
);
|
|
6796
6993
|
if (result.first_node) {
|
|
6797
6994
|
const is_inline_first = result.first_node.tagName === "w:ins";
|
|
@@ -6825,7 +7022,9 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6825
7022
|
edit.new_text,
|
|
6826
7023
|
anchor,
|
|
6827
7024
|
first_span.paragraph,
|
|
6828
|
-
ins_id
|
|
7025
|
+
ins_id,
|
|
7026
|
+
null,
|
|
7027
|
+
suppress_emphasis
|
|
6829
7028
|
);
|
|
6830
7029
|
if (result.first_node) {
|
|
6831
7030
|
p1_el.appendChild(result.first_node);
|
|
@@ -7309,6 +7508,26 @@ function normalize_change_dates(doc) {
|
|
|
7309
7508
|
}
|
|
7310
7509
|
return count ? [`Track change timestamps: ${count} normalized`] : [];
|
|
7311
7510
|
}
|
|
7511
|
+
function normalize_comment_dates(doc) {
|
|
7512
|
+
let count = 0;
|
|
7513
|
+
const fixed = "2025-01-01T00:00:00Z";
|
|
7514
|
+
for (const part of doc.pkg.parts) {
|
|
7515
|
+
if (!part.contentType.includes("comments")) continue;
|
|
7516
|
+
for (const el of findAllDescendants(part._element, "w:comment")) {
|
|
7517
|
+
if (el.hasAttribute("w:date")) {
|
|
7518
|
+
el.setAttribute("w:date", fixed);
|
|
7519
|
+
count++;
|
|
7520
|
+
}
|
|
7521
|
+
}
|
|
7522
|
+
for (const el of findAllDescendants(part._element, "w16cex:commentExtensible")) {
|
|
7523
|
+
if (el.hasAttribute("w16cex:dateUtc")) {
|
|
7524
|
+
el.setAttribute("w16cex:dateUtc", fixed);
|
|
7525
|
+
count++;
|
|
7526
|
+
}
|
|
7527
|
+
}
|
|
7528
|
+
}
|
|
7529
|
+
return count ? [`Comment timestamps: ${count} normalized`] : [];
|
|
7530
|
+
}
|
|
7312
7531
|
function scrub_doc_properties(doc) {
|
|
7313
7532
|
const lines = [];
|
|
7314
7533
|
const corePart = doc.pkg.getPartByPath("docProps/core.xml");
|
|
@@ -8052,8 +8271,10 @@ function build_paragraph_text(paragraph, comments_map, cleanView, style_cache, d
|
|
|
8052
8271
|
const new_wrappers = cleanView ? ["", ""] : _get_wrappers(active_ins, active_del, active_comments, active_fmt);
|
|
8053
8272
|
const new_style = [prefix, suffix];
|
|
8054
8273
|
if (pending_text && new_wrappers[0] === current_wrappers[0] && new_wrappers[1] === current_wrappers[1]) {
|
|
8055
|
-
|
|
8056
|
-
|
|
8274
|
+
const escaped_prefix = new_style[0].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8275
|
+
const lead_match = new_style[0] !== "" || new_style[1] !== "" ? seg.match(new RegExp("^(\\s*)" + escaped_prefix)) : null;
|
|
8276
|
+
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) {
|
|
8277
|
+
pending_text = pending_text.slice(0, -current_style[1].length) + lead_match[1] + seg.slice(lead_match[0].length);
|
|
8057
8278
|
} else {
|
|
8058
8279
|
pending_text += seg;
|
|
8059
8280
|
}
|
|
@@ -8820,7 +9041,7 @@ function _collect_footnote_ids_fast(owned_items) {
|
|
|
8820
9041
|
}
|
|
8821
9042
|
|
|
8822
9043
|
// src/sanitize/core.ts
|
|
8823
|
-
import { strFromU8 as strFromU82, unzipSync as
|
|
9044
|
+
import { strFromU8 as strFromU82, unzipSync as unzipSync3 } from "fflate";
|
|
8824
9045
|
|
|
8825
9046
|
// src/sanitize/report.ts
|
|
8826
9047
|
var SanitizeReport = class {
|
|
@@ -8975,6 +9196,7 @@ async function finalize_document(doc, options) {
|
|
|
8975
9196
|
for (const w of warnings) report.warnings.push(w);
|
|
8976
9197
|
for (const w of detect_watermarks(doc)) report.warnings.push(w);
|
|
8977
9198
|
report.add_transform_lines(normalize_change_dates(doc));
|
|
9199
|
+
report.add_transform_lines(normalize_comment_dates(doc));
|
|
8978
9200
|
if (options.protection_mode === "read_only" || options.protection_mode === "encrypt") {
|
|
8979
9201
|
if (options.protection_mode === "encrypt") {
|
|
8980
9202
|
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 +9255,7 @@ var VERIFIED_CORE_FIELDS = [
|
|
|
9033
9255
|
["version", "version (cp:version)"]
|
|
9034
9256
|
];
|
|
9035
9257
|
function verifySanitizedPackage(outBuffer) {
|
|
9036
|
-
const unzipped =
|
|
9258
|
+
const unzipped = unzipSync3(new Uint8Array(outBuffer));
|
|
9037
9259
|
const names = Object.keys(unzipped);
|
|
9038
9260
|
const problems = [];
|
|
9039
9261
|
if (names.includes("docProps/custom.xml")) {
|
|
@@ -9072,6 +9294,7 @@ export {
|
|
|
9072
9294
|
USER_PATTERN_TIMEOUT_MS,
|
|
9073
9295
|
_extractTextFromDoc,
|
|
9074
9296
|
apply_edits_to_markdown,
|
|
9297
|
+
collect_media_difference_warnings,
|
|
9075
9298
|
create_unified_diff,
|
|
9076
9299
|
create_word_patch_diff,
|
|
9077
9300
|
describe_illegal_control_chars,
|