@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.cjs +354 -77
- 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 +352 -76
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/diff.ts +129 -4
- package/src/engine.ts +228 -24
- package/src/index.ts +1 -1
- package/src/ingest.ts +11 -2
- package/src/mapper.ts +107 -47
- package/src/repro_qa_report_v7.test.ts +380 -0
- package/src/repro_qa_report_v8.test.ts +265 -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 = 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 () 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, 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
|
|
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
|
-
|
|
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
|
-
|
|
4842
|
-
|
|
4843
|
-
"w:
|
|
4844
|
-
|
|
4845
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8056
|
-
|
|
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
|
|
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 =
|
|
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,
|