@adeu/core 1.23.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 +478 -122
- 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 +477 -122
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/diff.ts +152 -12
- package/src/engine.ts +267 -57
- package/src/index.ts +1 -1
- package/src/ingest.ts +11 -2
- package/src/mapper.ts +99 -47
- package/src/repro_qa_report_v6.test.ts +486 -0
- package/src/repro_qa_report_v7.test.ts +380 -0
- package/src/sanitize/core.ts +60 -1
- package/src/sanitize/report.ts +5 -3
- package/src/sanitize/sanitize.test.ts +5 -4
- package/src/sanitize/transforms.ts +123 -16
- 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(" | ")) {
|
|
@@ -2787,7 +2863,8 @@ function _row_ops_for_table(table_o, table_m, opcodes, warnings) {
|
|
|
2787
2863
|
type: "modify",
|
|
2788
2864
|
target_text: o_txt,
|
|
2789
2865
|
new_text: m_txt,
|
|
2790
|
-
comment: "Diff: Table row modified"
|
|
2866
|
+
comment: "Diff: Table row modified",
|
|
2867
|
+
_is_table_edit: true
|
|
2791
2868
|
});
|
|
2792
2869
|
}
|
|
2793
2870
|
}
|
|
@@ -2825,8 +2902,12 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2825
2902
|
warnings.push(
|
|
2826
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.`
|
|
2827
2904
|
);
|
|
2828
|
-
const flat =
|
|
2829
|
-
|
|
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,
|
|
2830
2911
|
warnings
|
|
2831
2912
|
);
|
|
2832
2913
|
return { edits: [...flat], warnings };
|
|
@@ -2889,14 +2970,18 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2889
2970
|
if (row_opcodes !== null) {
|
|
2890
2971
|
edits.push(..._row_ops_for_table(t_o, t_m, row_opcodes, warnings));
|
|
2891
2972
|
} else {
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2973
|
+
for (let k = 0; k < t_o.rows.length; k++) {
|
|
2974
|
+
const o_txt = t_o.rows[k].cells.join(" | ");
|
|
2975
|
+
const m_txt = t_m.rows[k].cells.join(" | ");
|
|
2976
|
+
if (o_txt === m_txt) continue;
|
|
2977
|
+
edits.push({
|
|
2978
|
+
type: "modify",
|
|
2979
|
+
target_text: o_txt,
|
|
2980
|
+
new_text: m_txt,
|
|
2981
|
+
comment: "Diff: Table row modified",
|
|
2982
|
+
_is_table_edit: true
|
|
2983
|
+
});
|
|
2898
2984
|
}
|
|
2899
|
-
edits.push(...tbl_edits);
|
|
2900
2985
|
}
|
|
2901
2986
|
}
|
|
2902
2987
|
}
|
|
@@ -2914,7 +2999,7 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2914
2999
|
};
|
|
2915
3000
|
let ambiguous_anchor_warned = false;
|
|
2916
3001
|
for (const e of edits) {
|
|
2917
|
-
if ((e.type === "insert_row" || e.type === "delete_row") && !ambiguous_anchor_warned) {
|
|
3002
|
+
if ((e.type === "insert_row" || e.type === "delete_row" || e._is_table_edit) && !ambiguous_anchor_warned) {
|
|
2918
3003
|
if (e.target_text && countOccurrences(text_orig, e.target_text) > 1) {
|
|
2919
3004
|
warnings.push(
|
|
2920
3005
|
`The row anchor "${e.target_text.substring(0, 60)}" appears more than once in the document. Applying this diff from its JSON output may be rejected as ambiguous \u2014 make the anchor rows unique, or apply the row changes with explicit insert_row/delete_row edits.`
|
|
@@ -2926,12 +3011,63 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2926
3011
|
const modify_edits = edits.filter(
|
|
2927
3012
|
(e) => e.type === "modify"
|
|
2928
3013
|
);
|
|
2929
|
-
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
|
+
);
|
|
2930
3021
|
const final_edits = edits.filter(
|
|
2931
3022
|
(e) => e.type !== "modify" || kept_modifies.has(e)
|
|
2932
3023
|
);
|
|
2933
3024
|
return { edits: final_edits, warnings };
|
|
2934
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
|
+
}
|
|
2935
3071
|
function create_unified_diff(original_text, modified_text, context_lines = 3) {
|
|
2936
3072
|
const dmp = new diff_match_patch.diff_match_patch();
|
|
2937
3073
|
dmp.Diff_Timeout = 2;
|
|
@@ -3886,6 +4022,13 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
3886
4022
|
console.error("generate_edits_from_text failed, falling back to wholesale edit", e);
|
|
3887
4023
|
raw_sub_edits = [];
|
|
3888
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
|
+
);
|
|
3889
4032
|
if (!raw_sub_edits || raw_sub_edits.length === 0) {
|
|
3890
4033
|
const fallback_edit = {
|
|
3891
4034
|
type: "modify",
|
|
@@ -4666,7 +4809,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4666
4809
|
*
|
|
4667
4810
|
* Does NOT attach comments; callers handle that.
|
|
4668
4811
|
*/
|
|
4669
|
-
_track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id) {
|
|
4812
|
+
_track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id, positional_anchor_el = null, suppress_emphasis = false) {
|
|
4670
4813
|
if (!text) {
|
|
4671
4814
|
return {
|
|
4672
4815
|
first_node: null,
|
|
@@ -4687,7 +4830,29 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4687
4830
|
}
|
|
4688
4831
|
current_p = walker;
|
|
4689
4832
|
}
|
|
4690
|
-
|
|
4833
|
+
const suffix_nodes = [];
|
|
4834
|
+
const pos_source = positional_anchor_el && positional_anchor_el.parentNode ? positional_anchor_el : anchor_run !== null && anchor_run._element.parentNode ? anchor_run._element : null;
|
|
4835
|
+
if (current_p !== null && pos_source !== null) {
|
|
4836
|
+
let pos_anchor = pos_source;
|
|
4837
|
+
while (pos_anchor && pos_anchor.parentNode !== current_p) {
|
|
4838
|
+
pos_anchor = pos_anchor.parentNode;
|
|
4839
|
+
if (pos_anchor === current_p) {
|
|
4840
|
+
pos_anchor = null;
|
|
4841
|
+
break;
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
if (pos_anchor) {
|
|
4845
|
+
const relocatable = /* @__PURE__ */ new Set(["w:r", "w:ins", "w:del"]);
|
|
4846
|
+
let nxt = pos_anchor.nextSibling;
|
|
4847
|
+
while (nxt) {
|
|
4848
|
+
if (nxt.nodeType === 1 && relocatable.has(nxt.tagName)) {
|
|
4849
|
+
suffix_nodes.push(nxt);
|
|
4850
|
+
}
|
|
4851
|
+
nxt = nxt.nextSibling;
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
while (lines.length > 1 && lines[lines.length - 1] === "" && suffix_nodes.length === 0) {
|
|
4691
4856
|
lines.pop();
|
|
4692
4857
|
}
|
|
4693
4858
|
if (lines.length === 0) {
|
|
@@ -4708,7 +4873,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4708
4873
|
first_clean === lines[0] ? lines[0] : lines[0],
|
|
4709
4874
|
anchor_run,
|
|
4710
4875
|
reuse_id,
|
|
4711
|
-
xmlDoc
|
|
4876
|
+
xmlDoc,
|
|
4877
|
+
suppress_emphasis
|
|
4712
4878
|
);
|
|
4713
4879
|
first_node = inline_ins;
|
|
4714
4880
|
}
|
|
@@ -4772,7 +4938,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4772
4938
|
clean_text,
|
|
4773
4939
|
anchor_run,
|
|
4774
4940
|
reuse_id,
|
|
4775
|
-
xmlDoc
|
|
4941
|
+
xmlDoc,
|
|
4942
|
+
suppress_emphasis
|
|
4776
4943
|
);
|
|
4777
4944
|
if (content_ins) {
|
|
4778
4945
|
new_p.appendChild(content_ins);
|
|
@@ -4785,6 +4952,12 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4785
4952
|
first_node = new_p;
|
|
4786
4953
|
}
|
|
4787
4954
|
}
|
|
4955
|
+
if (!block_mode && last_p && suffix_nodes.length > 0) {
|
|
4956
|
+
for (const node of suffix_nodes) {
|
|
4957
|
+
node.parentNode?.removeChild(node);
|
|
4958
|
+
last_p.appendChild(node);
|
|
4959
|
+
}
|
|
4960
|
+
}
|
|
4788
4961
|
return { first_node, last_p, last_ins, used_block_mode: block_mode };
|
|
4789
4962
|
}
|
|
4790
4963
|
/**
|
|
@@ -4792,7 +4965,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4792
4965
|
* <w:r> elements representing the inline markdown segments of `line_text`.
|
|
4793
4966
|
* Returns null if line_text is empty.
|
|
4794
4967
|
*/
|
|
4795
|
-
_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) {
|
|
4796
4969
|
if (!line_text && line_text !== "") return null;
|
|
4797
4970
|
const ins = this._create_track_change_tag("w:ins", "", reuse_id);
|
|
4798
4971
|
const segments = this._parse_inline_markdown(line_text);
|
|
@@ -4805,15 +4978,11 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4805
4978
|
const anchor_rPr = findChild(anchor_run._element, "w:rPr");
|
|
4806
4979
|
if (anchor_rPr) {
|
|
4807
4980
|
const clone = anchor_rPr.cloneNode(true);
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
"w:
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
"w:iCs",
|
|
4814
|
-
"w:b",
|
|
4815
|
-
"w:bCs"
|
|
4816
|
-
]) {
|
|
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) {
|
|
4817
4986
|
const found = findChild(clone, tag);
|
|
4818
4987
|
if (found) clone.removeChild(found);
|
|
4819
4988
|
}
|
|
@@ -4842,12 +5011,33 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4842
5011
|
if (stripped_text.startsWith("* ") || stripped_text.startsWith("- ")) {
|
|
4843
5012
|
return [stripped_text.substring(2).trim(), "List Paragraph"];
|
|
4844
5013
|
}
|
|
4845
|
-
const match = stripped_text.match(
|
|
5014
|
+
const match = stripped_text.match(/^1\.\s+/);
|
|
4846
5015
|
if (match) {
|
|
4847
5016
|
return [stripped_text.substring(match[0].length).trim(), "List Number"];
|
|
4848
5017
|
}
|
|
4849
5018
|
return [text, null];
|
|
4850
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
|
+
}
|
|
4851
5041
|
_parse_inline_markdown(text, baseStyle = {}) {
|
|
4852
5042
|
if (!text) return [];
|
|
4853
5043
|
const tokenPattern = /(\*\*.*?\*\*)|(_.*?_)/;
|
|
@@ -5114,6 +5304,16 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5114
5304
|
continue;
|
|
5115
5305
|
const is_regex = edit.regex || false;
|
|
5116
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
|
+
}
|
|
5117
5317
|
let matches = this.mapper.find_all_match_indices(
|
|
5118
5318
|
edit.target_text,
|
|
5119
5319
|
is_regex
|
|
@@ -5693,6 +5893,14 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5693
5893
|
actions_skipped: skipped_actions,
|
|
5694
5894
|
edits_applied: applied_edits,
|
|
5695
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
|
+
),
|
|
5696
5904
|
skipped_details: this.skipped_details,
|
|
5697
5905
|
edits: edits_reports,
|
|
5698
5906
|
engine: "node",
|
|
@@ -5714,6 +5922,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5714
5922
|
}
|
|
5715
5923
|
edit._applied_status = false;
|
|
5716
5924
|
edit._error_msg = null;
|
|
5925
|
+
edit._any_sub_failure = false;
|
|
5717
5926
|
}
|
|
5718
5927
|
for (const edit of edits) {
|
|
5719
5928
|
if (typeof edit !== "object" || edit === null) continue;
|
|
@@ -5724,14 +5933,17 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5724
5933
|
resolved_edits.push([edit, edit.new_text || null]);
|
|
5725
5934
|
} else if (edit.type === "insert_row" || edit.type === "delete_row") {
|
|
5726
5935
|
let matches = this.mapper.find_all_match_indices(edit.target_text);
|
|
5936
|
+
let resolved_mapper = this.mapper;
|
|
5727
5937
|
if (matches.length === 0) {
|
|
5728
5938
|
if (!this.clean_mapper) {
|
|
5729
5939
|
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
5730
5940
|
}
|
|
5731
5941
|
matches = this.clean_mapper.find_all_match_indices(edit.target_text);
|
|
5942
|
+
resolved_mapper = this.clean_mapper;
|
|
5732
5943
|
}
|
|
5733
5944
|
if (matches.length > 0) {
|
|
5734
5945
|
edit._resolved_start_idx = matches[0][0];
|
|
5946
|
+
edit._active_mapper_ref = resolved_mapper;
|
|
5735
5947
|
resolved_edits.push([edit, null]);
|
|
5736
5948
|
} else {
|
|
5737
5949
|
skipped++;
|
|
@@ -5810,17 +6022,18 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5810
6022
|
this.skipped_details.push(msg);
|
|
5811
6023
|
edit._applied_status = false;
|
|
5812
6024
|
edit._error_msg = msg;
|
|
6025
|
+
edit._any_sub_failure = true;
|
|
5813
6026
|
const parent = edit._parent_edit_ref;
|
|
5814
6027
|
if (parent) {
|
|
5815
6028
|
parent._applied_status = false;
|
|
5816
6029
|
parent._error_msg = msg;
|
|
6030
|
+
parent._any_sub_failure = true;
|
|
5817
6031
|
}
|
|
5818
6032
|
continue;
|
|
5819
6033
|
}
|
|
5820
6034
|
let success = false;
|
|
5821
6035
|
if (edit.type === "modify") {
|
|
5822
|
-
|
|
5823
|
-
success = this._apply_single_edit_indexed(edit, orig_new, rebuild);
|
|
6036
|
+
success = this._apply_single_edit_indexed(edit, orig_new, false);
|
|
5824
6037
|
} else if (edit.type === "insert_row" || edit.type === "delete_row") {
|
|
5825
6038
|
success = this._apply_table_edit(edit, false);
|
|
5826
6039
|
}
|
|
@@ -5870,8 +6083,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5870
6083
|
this.skipped_details.push(msg);
|
|
5871
6084
|
edit._applied_status = false;
|
|
5872
6085
|
edit._error_msg = msg;
|
|
6086
|
+
edit._any_sub_failure = true;
|
|
5873
6087
|
const parent = edit._parent_edit_ref;
|
|
5874
6088
|
if (parent) {
|
|
6089
|
+
parent._any_sub_failure = true;
|
|
5875
6090
|
if (!parent._applied_status) {
|
|
5876
6091
|
parent._applied_status = false;
|
|
5877
6092
|
parent._error_msg = msg;
|
|
@@ -5879,7 +6094,20 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5879
6094
|
}
|
|
5880
6095
|
}
|
|
5881
6096
|
}
|
|
5882
|
-
|
|
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];
|
|
5883
6111
|
}
|
|
5884
6112
|
apply_review_actions(actions) {
|
|
5885
6113
|
let applied = 0;
|
|
@@ -5980,7 +6208,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5980
6208
|
}
|
|
5981
6209
|
_apply_table_edit(edit, rebuild_map) {
|
|
5982
6210
|
const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
|
|
5983
|
-
const
|
|
6211
|
+
const active_mapper = edit._active_mapper_ref || this.mapper;
|
|
6212
|
+
const [anchor_run, anchor_para] = active_mapper.get_insertion_anchor(
|
|
5984
6213
|
start_idx,
|
|
5985
6214
|
rebuild_map
|
|
5986
6215
|
);
|
|
@@ -6161,9 +6390,14 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6161
6390
|
if (overlaps_virtual_pipe) {
|
|
6162
6391
|
const actual_cells = actual_doc_text.split("|");
|
|
6163
6392
|
const new_cells = current_effective_new_text.split("|");
|
|
6164
|
-
if (actual_cells.length
|
|
6393
|
+
if (actual_cells.length !== new_cells.length) {
|
|
6394
|
+
throw new BatchValidationError([
|
|
6395
|
+
`Target text spans ${actual_cells.length} table cells, but replacement provides ${new_cells.length}. To modify text without altering table structure (rows or columns), ensure the replacement contains the exact same number of '|' separators (e.g., replace with 'CellC | ' to empty the second cell).`
|
|
6396
|
+
]);
|
|
6397
|
+
}
|
|
6398
|
+
if (actual_cells.length > 1) {
|
|
6165
6399
|
const sub_edits2 = [];
|
|
6166
|
-
let
|
|
6400
|
+
let cell_start_in_target = 0;
|
|
6167
6401
|
let target_comment_idx = 0;
|
|
6168
6402
|
for (let idx = 0; idx < actual_cells.length; idx++) {
|
|
6169
6403
|
if (actual_cells[idx].trim() !== new_cells[idx].trim()) {
|
|
@@ -6176,13 +6410,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6176
6410
|
const n_cell = new_cells[cell_idx];
|
|
6177
6411
|
const a_clean = a_cell.trim();
|
|
6178
6412
|
const n_clean = n_cell.trim();
|
|
6179
|
-
|
|
6180
|
-
if (a_clean) {
|
|
6181
|
-
actual_start = this.mapper.full_text.indexOf(a_clean, search_offset);
|
|
6182
|
-
if (actual_start === -1 || actual_start > search_offset + 10) {
|
|
6183
|
-
actual_start = search_offset;
|
|
6184
|
-
}
|
|
6185
|
-
}
|
|
6413
|
+
const actual_start = start_idx + cell_start_in_target + (a_clean ? a_cell.indexOf(a_clean) : 0);
|
|
6186
6414
|
const should_attach_comment = edit.comment !== null && edit.comment !== void 0 && cell_idx === target_comment_idx;
|
|
6187
6415
|
if (a_clean !== n_clean || should_attach_comment) {
|
|
6188
6416
|
const cell_sub_edits = this._word_diff_sub_edits(
|
|
@@ -6199,24 +6427,12 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6199
6427
|
sub_edits2.push(se);
|
|
6200
6428
|
}
|
|
6201
6429
|
}
|
|
6202
|
-
|
|
6203
|
-
search_offset = actual_start + a_clean.length;
|
|
6204
|
-
}
|
|
6205
|
-
const next_pipe = this.mapper.full_text.indexOf(" | ", search_offset);
|
|
6206
|
-
if (next_pipe !== -1 && next_pipe <= search_offset + 10) {
|
|
6207
|
-
search_offset = next_pipe + 3;
|
|
6208
|
-
} else {
|
|
6209
|
-
search_offset += a_cell.length + 1;
|
|
6210
|
-
}
|
|
6430
|
+
cell_start_in_target += a_cell.length + 1;
|
|
6211
6431
|
}
|
|
6212
6432
|
for (const sub of sub_edits2) {
|
|
6213
6433
|
all_sub_edits.push(sub);
|
|
6214
6434
|
}
|
|
6215
6435
|
continue;
|
|
6216
|
-
} else {
|
|
6217
|
-
throw new BatchValidationError([
|
|
6218
|
-
`Target text spans ${actual_cells.length} table cells, but replacement provides ${new_cells.length}. To modify text without altering table structure (rows or columns), ensure the replacement contains the exact same number of '|' separators (e.g., replace with 'CellC | ' to empty the second cell).`
|
|
6219
|
-
]);
|
|
6220
6436
|
}
|
|
6221
6437
|
}
|
|
6222
6438
|
let has_markdown = false;
|
|
@@ -6310,6 +6526,19 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6310
6526
|
continue;
|
|
6311
6527
|
}
|
|
6312
6528
|
}
|
|
6529
|
+
if (!final_target && final_new) {
|
|
6530
|
+
all_sub_edits.push({
|
|
6531
|
+
type: "modify",
|
|
6532
|
+
target_text: "",
|
|
6533
|
+
new_text: final_new,
|
|
6534
|
+
comment: edit.comment,
|
|
6535
|
+
_resolved_start_idx: effective_start_idx,
|
|
6536
|
+
_match_start_index: effective_start_idx,
|
|
6537
|
+
_internal_op: "INSERTION",
|
|
6538
|
+
_active_mapper_ref: active_mapper
|
|
6539
|
+
});
|
|
6540
|
+
continue;
|
|
6541
|
+
}
|
|
6313
6542
|
const sub_edits = this._word_diff_sub_edits(
|
|
6314
6543
|
actual_doc_text,
|
|
6315
6544
|
current_effective_new_text,
|
|
@@ -6371,6 +6600,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6371
6600
|
op = "MODIFICATION";
|
|
6372
6601
|
}
|
|
6373
6602
|
}
|
|
6603
|
+
const suppress_emphasis = this._edit_declares_emphasis(edit);
|
|
6374
6604
|
if (op === "STYLE_ONLY" || op === "STYLE_AND_TEXT") {
|
|
6375
6605
|
const [anchor_run, anchor_para] = active_mapper.get_insertion_anchor(
|
|
6376
6606
|
start_idx,
|
|
@@ -6571,7 +6801,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6571
6801
|
clean_text,
|
|
6572
6802
|
anchor_run,
|
|
6573
6803
|
ins_id,
|
|
6574
|
-
xmlDoc
|
|
6804
|
+
xmlDoc,
|
|
6805
|
+
suppress_emphasis
|
|
6575
6806
|
);
|
|
6576
6807
|
if (content_ins) new_p.appendChild(content_ins);
|
|
6577
6808
|
body.insertBefore(new_p, _bug233_target_para);
|
|
@@ -6603,7 +6834,9 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6603
6834
|
final_new_text,
|
|
6604
6835
|
anchor_run,
|
|
6605
6836
|
anchor_para,
|
|
6606
|
-
ins_id
|
|
6837
|
+
ins_id,
|
|
6838
|
+
null,
|
|
6839
|
+
suppress_emphasis
|
|
6607
6840
|
);
|
|
6608
6841
|
if (!result.first_node) return false;
|
|
6609
6842
|
const is_inline_first = result.first_node.tagName === "w:ins";
|
|
@@ -6751,7 +6984,11 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6751
6984
|
edit.new_text,
|
|
6752
6985
|
style_source_run,
|
|
6753
6986
|
mod_anchor_para,
|
|
6754
|
-
ins_id
|
|
6987
|
+
ins_id,
|
|
6988
|
+
// The insertion physically follows the deletion block; the style
|
|
6989
|
+
// run was detached when the deletion cloned it into <w:del>.
|
|
6990
|
+
last_del,
|
|
6991
|
+
suppress_emphasis
|
|
6755
6992
|
);
|
|
6756
6993
|
if (result.first_node) {
|
|
6757
6994
|
const is_inline_first = result.first_node.tagName === "w:ins";
|
|
@@ -6785,7 +7022,9 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6785
7022
|
edit.new_text,
|
|
6786
7023
|
anchor,
|
|
6787
7024
|
first_span.paragraph,
|
|
6788
|
-
ins_id
|
|
7025
|
+
ins_id,
|
|
7026
|
+
null,
|
|
7027
|
+
suppress_emphasis
|
|
6789
7028
|
);
|
|
6790
7029
|
if (result.first_node) {
|
|
6791
7030
|
p1_el.appendChild(result.first_node);
|
|
@@ -7269,6 +7508,26 @@ function normalize_change_dates(doc) {
|
|
|
7269
7508
|
}
|
|
7270
7509
|
return count ? [`Track change timestamps: ${count} normalized`] : [];
|
|
7271
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
|
+
}
|
|
7272
7531
|
function scrub_doc_properties(doc) {
|
|
7273
7532
|
const lines = [];
|
|
7274
7533
|
const corePart = doc.pkg.getPartByPath("docProps/core.xml");
|
|
@@ -7298,7 +7557,10 @@ function scrub_doc_properties(doc) {
|
|
|
7298
7557
|
["keywords", "Keywords"],
|
|
7299
7558
|
["subject", "Subject"],
|
|
7300
7559
|
["contentStatus", "Content status"],
|
|
7301
|
-
["description", "Description/comments"]
|
|
7560
|
+
["description", "Description/comments"],
|
|
7561
|
+
["identifier", "Identifier"],
|
|
7562
|
+
["language", "Language"],
|
|
7563
|
+
["version", "Version"]
|
|
7302
7564
|
];
|
|
7303
7565
|
for (const [local, label] of leakFields) {
|
|
7304
7566
|
findDescendantsByLocalName(corePart._element, local).forEach((c) => {
|
|
@@ -7356,27 +7618,73 @@ function scrub_timestamps(doc) {
|
|
|
7356
7618
|
}
|
|
7357
7619
|
return modified ? ["Timestamps normalized to epoch"] : [];
|
|
7358
7620
|
}
|
|
7359
|
-
function
|
|
7360
|
-
const
|
|
7361
|
-
|
|
7362
|
-
const
|
|
7363
|
-
|
|
7364
|
-
const removeRelationsTo = (relsPart) => {
|
|
7621
|
+
function ejectPackageMembers(doc, matcher, relTargetMatcher) {
|
|
7622
|
+
const pkg = doc.pkg;
|
|
7623
|
+
const normalized = (p) => p.startsWith("/") ? p.substring(1) : p;
|
|
7624
|
+
for (const part of pkg.parts) {
|
|
7625
|
+
if (!part.partname.endsWith(".rels")) continue;
|
|
7365
7626
|
const toRemove = [];
|
|
7366
|
-
for (const rel of findAllDescendants(
|
|
7367
|
-
const target = rel.getAttribute("Target");
|
|
7368
|
-
if (target
|
|
7627
|
+
for (const rel of findAllDescendants(part._element, "Relationship")) {
|
|
7628
|
+
const target = rel.getAttribute("Target") || "";
|
|
7629
|
+
if (relTargetMatcher(target)) {
|
|
7630
|
+
toRemove.push(rel);
|
|
7631
|
+
const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
|
|
7632
|
+
const sourcePart = pkg.getPartByPath(sourcePath);
|
|
7633
|
+
if (sourcePart) {
|
|
7634
|
+
const relId = rel.getAttribute("Id");
|
|
7635
|
+
if (relId) sourcePart.rels.delete(relId);
|
|
7636
|
+
}
|
|
7637
|
+
}
|
|
7369
7638
|
}
|
|
7370
7639
|
toRemove.forEach((r) => r.parentNode?.removeChild(r));
|
|
7371
|
-
}
|
|
7372
|
-
const
|
|
7373
|
-
if (
|
|
7374
|
-
|
|
7375
|
-
|
|
7640
|
+
}
|
|
7641
|
+
const ctPart = pkg.getPartByPath("[Content_Types].xml");
|
|
7642
|
+
if (ctPart) {
|
|
7643
|
+
const toRemove = [];
|
|
7644
|
+
for (const override of findAllDescendants(ctPart._element, "Override")) {
|
|
7645
|
+
const partName = override.getAttribute("PartName") || "";
|
|
7646
|
+
if (matcher(normalized(partName))) toRemove.push(override);
|
|
7647
|
+
}
|
|
7648
|
+
toRemove.forEach((o) => o.parentNode?.removeChild(o));
|
|
7649
|
+
}
|
|
7650
|
+
pkg.parts = pkg.parts.filter((p) => !matcher(normalized(p.partname)));
|
|
7651
|
+
for (const key of Object.keys(pkg.unzipped)) {
|
|
7652
|
+
if (matcher(normalized(key))) delete pkg.unzipped[key];
|
|
7653
|
+
}
|
|
7654
|
+
}
|
|
7655
|
+
function strip_custom_xml(doc) {
|
|
7656
|
+
const isCustomXml = (path) => path.startsWith("customXml/");
|
|
7657
|
+
const customParts = doc.pkg.parts.filter(
|
|
7658
|
+
(p) => isCustomXml(p.partname.startsWith("/") ? p.partname.substring(1) : p.partname)
|
|
7659
|
+
);
|
|
7660
|
+
const leakedMembers = Object.keys(doc.pkg.unzipped).filter(isCustomXml);
|
|
7661
|
+
if (customParts.length === 0 && leakedMembers.length === 0) return [];
|
|
7662
|
+
ejectPackageMembers(doc, isCustomXml, (target) => target.includes("customXml"));
|
|
7376
7663
|
for (const sdtPr of findAllDescendants(doc.element, "w:sdtPr")) {
|
|
7377
7664
|
findChildren(sdtPr, "w:dataBinding").forEach((b) => sdtPr.removeChild(b));
|
|
7378
7665
|
}
|
|
7379
|
-
return [`Custom XML parts: ${customParts.length} removed`];
|
|
7666
|
+
return [`Custom XML parts: ${Math.max(customParts.length, leakedMembers.length)} removed`];
|
|
7667
|
+
}
|
|
7668
|
+
var CUSTOM_PROPS_PATH = "docProps/custom.xml";
|
|
7669
|
+
function strip_custom_properties(doc) {
|
|
7670
|
+
const part = doc.pkg.getPartByPath(CUSTOM_PROPS_PATH);
|
|
7671
|
+
const leaked = Object.keys(doc.pkg.unzipped).includes(CUSTOM_PROPS_PATH);
|
|
7672
|
+
if (!part && !leaked) return [];
|
|
7673
|
+
const lines = [];
|
|
7674
|
+
if (part) {
|
|
7675
|
+
for (const prop of findDescendantsByLocalName(part._element, "property")) {
|
|
7676
|
+
const name = prop.getAttribute("name") || "(unnamed)";
|
|
7677
|
+
const value = (prop.textContent || "").trim();
|
|
7678
|
+
lines.push(` Custom property removed: ${name} = "${_truncate(value, 60)}"`);
|
|
7679
|
+
}
|
|
7680
|
+
}
|
|
7681
|
+
ejectPackageMembers(
|
|
7682
|
+
doc,
|
|
7683
|
+
(path) => path === CUSTOM_PROPS_PATH,
|
|
7684
|
+
(target) => target.includes("docProps/custom.xml")
|
|
7685
|
+
);
|
|
7686
|
+
const count = Math.max(lines.length, 1);
|
|
7687
|
+
return [`Custom document properties: ${count} removed (docProps/custom.xml)`].concat(lines);
|
|
7380
7688
|
}
|
|
7381
7689
|
function strip_image_alt_text(doc) {
|
|
7382
7690
|
let count = 0;
|
|
@@ -7963,8 +8271,10 @@ function build_paragraph_text(paragraph, comments_map, cleanView, style_cache, d
|
|
|
7963
8271
|
const new_wrappers = cleanView ? ["", ""] : _get_wrappers(active_ins, active_del, active_comments, active_fmt);
|
|
7964
8272
|
const new_style = [prefix, suffix];
|
|
7965
8273
|
if (pending_text && new_wrappers[0] === current_wrappers[0] && new_wrappers[1] === current_wrappers[1]) {
|
|
7966
|
-
|
|
7967
|
-
|
|
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);
|
|
7968
8278
|
} else {
|
|
7969
8279
|
pending_text += seg;
|
|
7970
8280
|
}
|
|
@@ -8730,6 +9040,9 @@ function _collect_footnote_ids_fast(owned_items) {
|
|
|
8730
9040
|
return ordered;
|
|
8731
9041
|
}
|
|
8732
9042
|
|
|
9043
|
+
// src/sanitize/core.ts
|
|
9044
|
+
import { strFromU8 as strFromU82, unzipSync as unzipSync3 } from "fflate";
|
|
9045
|
+
|
|
8733
9046
|
// src/sanitize/report.ts
|
|
8734
9047
|
var SanitizeReport = class {
|
|
8735
9048
|
filename;
|
|
@@ -8764,7 +9077,7 @@ var SanitizeReport = class {
|
|
|
8764
9077
|
} else {
|
|
8765
9078
|
this.removed_comment_lines.push(line);
|
|
8766
9079
|
}
|
|
8767
|
-
} else if (lower.includes("author") || lower.includes("template") || lower.includes("company") || lower.includes("manager") || lower.includes("metadata") || lower.includes("timestamp") || lower.includes("custom xml") || lower.includes("last modified by") || lower.includes("revision count") || lower.includes("last printed")) {
|
|
9080
|
+
} else if (lower.includes("author") || lower.includes("template") || lower.includes("company") || lower.includes("manager") || lower.includes("metadata") || lower.includes("timestamp") || lower.includes("custom xml") || lower.includes("custom propert") || lower.includes("identifier") || lower.includes("language") || lower.includes("version") || lower.includes("last modified by") || lower.includes("revision count") || lower.includes("last printed")) {
|
|
8768
9081
|
this.metadata_lines.push(line);
|
|
8769
9082
|
} else if (lower.includes("hyperlink") || lower.includes("warning")) {
|
|
8770
9083
|
this.warnings.push(line);
|
|
@@ -8877,11 +9190,13 @@ async function finalize_document(doc, options) {
|
|
|
8877
9190
|
report.add_transform_lines(scrub_doc_properties(doc));
|
|
8878
9191
|
report.add_transform_lines(scrub_timestamps(doc));
|
|
8879
9192
|
report.add_transform_lines(strip_custom_xml(doc));
|
|
9193
|
+
report.add_transform_lines(strip_custom_properties(doc));
|
|
8880
9194
|
report.add_transform_lines(strip_image_alt_text(doc));
|
|
8881
9195
|
const warnings = audit_hyperlinks(doc);
|
|
8882
9196
|
for (const w of warnings) report.warnings.push(w);
|
|
8883
9197
|
for (const w of detect_watermarks(doc)) report.warnings.push(w);
|
|
8884
9198
|
report.add_transform_lines(normalize_change_dates(doc));
|
|
9199
|
+
report.add_transform_lines(normalize_comment_dates(doc));
|
|
8885
9200
|
if (options.protection_mode === "read_only" || options.protection_mode === "encrypt") {
|
|
8886
9201
|
if (options.protection_mode === "encrypt") {
|
|
8887
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.");
|
|
@@ -8924,8 +9239,47 @@ async function finalize_document(doc, options) {
|
|
|
8924
9239
|
}
|
|
8925
9240
|
if (report.warnings.length > 0) report.status = "clean_with_warnings";
|
|
8926
9241
|
const outBuffer = await doc.save();
|
|
9242
|
+
verifySanitizedPackage(outBuffer);
|
|
8927
9243
|
return { reportText: report.render(), outBuffer };
|
|
8928
9244
|
}
|
|
9245
|
+
var VERIFIED_CORE_FIELDS = [
|
|
9246
|
+
["creator", "author (dc:creator)"],
|
|
9247
|
+
["lastModifiedBy", "last modified by (cp:lastModifiedBy)"],
|
|
9248
|
+
["identifier", "identifier (dc:identifier)"],
|
|
9249
|
+
["description", "description (dc:description)"],
|
|
9250
|
+
["keywords", "keywords (cp:keywords)"],
|
|
9251
|
+
["category", "category (cp:category)"],
|
|
9252
|
+
["subject", "subject (dc:subject)"],
|
|
9253
|
+
["contentStatus", "content status (cp:contentStatus)"],
|
|
9254
|
+
["language", "language (dc:language)"],
|
|
9255
|
+
["version", "version (cp:version)"]
|
|
9256
|
+
];
|
|
9257
|
+
function verifySanitizedPackage(outBuffer) {
|
|
9258
|
+
const unzipped = unzipSync3(new Uint8Array(outBuffer));
|
|
9259
|
+
const names = Object.keys(unzipped);
|
|
9260
|
+
const problems = [];
|
|
9261
|
+
if (names.includes("docProps/custom.xml")) {
|
|
9262
|
+
problems.push("docProps/custom.xml (custom document properties) is still in the package");
|
|
9263
|
+
}
|
|
9264
|
+
if (names.some((n) => n.startsWith("customXml/"))) {
|
|
9265
|
+
problems.push("customXml/* parts are still in the package");
|
|
9266
|
+
}
|
|
9267
|
+
if (names.includes("docProps/core.xml")) {
|
|
9268
|
+
const core = parseXml(strFromU82(unzipped["docProps/core.xml"]));
|
|
9269
|
+
for (const [local, label] of VERIFIED_CORE_FIELDS) {
|
|
9270
|
+
for (const el of findDescendantsByLocalName(core.documentElement, local)) {
|
|
9271
|
+
if ((el.textContent || "").trim()) {
|
|
9272
|
+
problems.push(`core property ${label} still contains a value`);
|
|
9273
|
+
}
|
|
9274
|
+
}
|
|
9275
|
+
}
|
|
9276
|
+
}
|
|
9277
|
+
if (problems.length > 0) {
|
|
9278
|
+
throw new Error(
|
|
9279
|
+
"Sanitize integrity check failed \u2014 the saved package still contains metadata this run claims to remove:\n - " + problems.join("\n - ") + "\nRefusing to report a clean document."
|
|
9280
|
+
);
|
|
9281
|
+
}
|
|
9282
|
+
}
|
|
8929
9283
|
|
|
8930
9284
|
// src/index.ts
|
|
8931
9285
|
function identifyEngine() {
|
|
@@ -8940,6 +9294,7 @@ export {
|
|
|
8940
9294
|
USER_PATTERN_TIMEOUT_MS,
|
|
8941
9295
|
_extractTextFromDoc,
|
|
8942
9296
|
apply_edits_to_markdown,
|
|
9297
|
+
collect_media_difference_warnings,
|
|
8943
9298
|
create_unified_diff,
|
|
8944
9299
|
create_word_patch_diff,
|
|
8945
9300
|
describe_illegal_control_chars,
|