@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.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
USER_PATTERN_TIMEOUT_MS: () => USER_PATTERN_TIMEOUT_MS,
|
|
39
39
|
_extractTextFromDoc: () => _extractTextFromDoc,
|
|
40
40
|
apply_edits_to_markdown: () => apply_edits_to_markdown,
|
|
41
|
+
collect_media_difference_warnings: () => collect_media_difference_warnings,
|
|
41
42
|
create_unified_diff: () => create_unified_diff,
|
|
42
43
|
create_word_patch_diff: () => create_word_patch_diff,
|
|
43
44
|
describe_illegal_control_chars: () => describe_illegal_control_chars,
|
|
@@ -1286,12 +1287,27 @@ function get_run_style_markers(run, is_heading = null) {
|
|
|
1286
1287
|
}
|
|
1287
1288
|
return [prefix, suffix];
|
|
1288
1289
|
}
|
|
1290
|
+
function split_boundary_whitespace(text) {
|
|
1291
|
+
const core = text.trim();
|
|
1292
|
+
if (!core) return ["", "", text];
|
|
1293
|
+
const lead_len = text.length - text.trimStart().length;
|
|
1294
|
+
return [
|
|
1295
|
+
text.substring(0, lead_len),
|
|
1296
|
+
text.substring(lead_len, lead_len + core.length),
|
|
1297
|
+
text.substring(lead_len + core.length)
|
|
1298
|
+
];
|
|
1299
|
+
}
|
|
1289
1300
|
function apply_formatting_to_segments(text, prefix, suffix) {
|
|
1290
1301
|
if (!prefix && !suffix) return text;
|
|
1291
1302
|
if (!text) return "";
|
|
1292
|
-
|
|
1303
|
+
const wrap = (segment) => {
|
|
1304
|
+
const [lead, core, trail] = split_boundary_whitespace(segment);
|
|
1305
|
+
if (!core) return segment;
|
|
1306
|
+
return `${lead}${prefix}${core}${suffix}${trail}`;
|
|
1307
|
+
};
|
|
1308
|
+
if (!text.includes("\n")) return wrap(text);
|
|
1293
1309
|
const parts = text.split("\n");
|
|
1294
|
-
return parts.map((p) => p ?
|
|
1310
|
+
return parts.map((p) => p ? wrap(p) : "").join("\n");
|
|
1295
1311
|
}
|
|
1296
1312
|
function get_run_text(run) {
|
|
1297
1313
|
let text = "";
|
|
@@ -1744,7 +1760,7 @@ ${header}`;
|
|
|
1744
1760
|
this._add_virtual_text(s_tok, current, paragraph);
|
|
1745
1761
|
current += s_tok.length;
|
|
1746
1762
|
}
|
|
1747
|
-
for (const [kind, txt, r_obj, i_id, d_id, c_ids] of pending_runs) {
|
|
1763
|
+
for (const [kind, txt, r_obj, r_off, i_id, d_id, c_ids] of pending_runs) {
|
|
1748
1764
|
if (kind === "virtual") {
|
|
1749
1765
|
this._add_virtual_text(txt, current, paragraph, active_hyperlink_id);
|
|
1750
1766
|
} else {
|
|
@@ -1758,7 +1774,8 @@ ${header}`;
|
|
|
1758
1774
|
del_id: d_id || void 0,
|
|
1759
1775
|
hyperlink_id: active_hyperlink_id || void 0,
|
|
1760
1776
|
comment_ids: c_ids.length > 0 ? c_ids : void 0,
|
|
1761
|
-
part_index: this._current_part_index
|
|
1777
|
+
part_index: this._current_part_index,
|
|
1778
|
+
run_offset: r_off
|
|
1762
1779
|
};
|
|
1763
1780
|
this.spans.push(s);
|
|
1764
1781
|
this._text_chunks.push(txt);
|
|
@@ -1793,20 +1810,42 @@ ${header}`;
|
|
|
1793
1810
|
if (text === "" || /^\s*$/.test(text)) continue;
|
|
1794
1811
|
leading_strip_active = false;
|
|
1795
1812
|
}
|
|
1813
|
+
let run_local = 0;
|
|
1814
|
+
const append_wrapped = (segment) => {
|
|
1815
|
+
const [lead, core, trail] = split_boundary_whitespace(segment);
|
|
1816
|
+
if (!core) {
|
|
1817
|
+
run_parts.push(["real", segment, item, run_local]);
|
|
1818
|
+
run_local += segment.length;
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
if (lead) {
|
|
1822
|
+
run_parts.push(["real", lead, item, run_local]);
|
|
1823
|
+
run_local += lead.length;
|
|
1824
|
+
}
|
|
1825
|
+
if (prefix) run_parts.push(["virtual", prefix, null, 0]);
|
|
1826
|
+
run_parts.push(["real", core, item, run_local]);
|
|
1827
|
+
run_local += core.length;
|
|
1828
|
+
if (suffix) run_parts.push(["virtual", suffix, null, 0]);
|
|
1829
|
+
if (trail) {
|
|
1830
|
+
run_parts.push(["real", trail, item, run_local]);
|
|
1831
|
+
run_local += trail.length;
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1796
1834
|
if (text.includes("\n") && (prefix || suffix)) {
|
|
1797
1835
|
const parts = text.split("\n");
|
|
1798
1836
|
for (let idx = 0; idx < parts.length; idx++) {
|
|
1799
|
-
if (idx > 0)
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
run_parts.push(["real", parts[idx], item]);
|
|
1803
|
-
if (suffix) run_parts.push(["virtual", suffix, null]);
|
|
1837
|
+
if (idx > 0) {
|
|
1838
|
+
run_parts.push(["real", "\n", item, run_local]);
|
|
1839
|
+
run_local += 1;
|
|
1804
1840
|
}
|
|
1841
|
+
if (parts[idx]) append_wrapped(parts[idx]);
|
|
1805
1842
|
}
|
|
1843
|
+
} else if ((prefix || suffix) && text) {
|
|
1844
|
+
append_wrapped(text);
|
|
1806
1845
|
} else {
|
|
1807
|
-
if (prefix) run_parts.push(["virtual", prefix, null]);
|
|
1808
|
-
if (text) run_parts.push(["real", text, item]);
|
|
1809
|
-
if (suffix) run_parts.push(["virtual", suffix, null]);
|
|
1846
|
+
if (prefix) run_parts.push(["virtual", prefix, null, 0]);
|
|
1847
|
+
if (text) run_parts.push(["real", text, item, 0]);
|
|
1848
|
+
if (suffix) run_parts.push(["virtual", suffix, null, 0]);
|
|
1810
1849
|
}
|
|
1811
1850
|
if (this.clean_view && Object.keys(active_del).length > 0) {
|
|
1812
1851
|
}
|
|
@@ -1830,7 +1869,7 @@ ${header}`;
|
|
|
1830
1869
|
skip_leading_prefix = true;
|
|
1831
1870
|
}
|
|
1832
1871
|
const curr_comment_ids = Array.from(active_ids);
|
|
1833
|
-
for (const [kind, txt, r_obj] of run_parts) {
|
|
1872
|
+
for (const [kind, txt, r_obj, r_off] of run_parts) {
|
|
1834
1873
|
if (skip_leading_prefix && kind === "virtual" && txt === new_style[0]) {
|
|
1835
1874
|
skip_leading_prefix = false;
|
|
1836
1875
|
continue;
|
|
@@ -1839,6 +1878,7 @@ ${header}`;
|
|
|
1839
1878
|
kind,
|
|
1840
1879
|
txt,
|
|
1841
1880
|
r_obj,
|
|
1881
|
+
r_off,
|
|
1842
1882
|
curr_ins_id,
|
|
1843
1883
|
curr_del_id,
|
|
1844
1884
|
curr_comment_ids
|
|
@@ -1850,11 +1890,12 @@ ${header}`;
|
|
|
1850
1890
|
current_wrappers = new_wrappers;
|
|
1851
1891
|
current_style = new_style;
|
|
1852
1892
|
const curr_comment_ids = Array.from(active_ids);
|
|
1853
|
-
for (const [kind, txt, r_obj] of run_parts) {
|
|
1893
|
+
for (const [kind, txt, r_obj, r_off] of run_parts) {
|
|
1854
1894
|
pending_runs.push([
|
|
1855
1895
|
kind,
|
|
1856
1896
|
txt,
|
|
1857
1897
|
r_obj,
|
|
1898
|
+
r_off,
|
|
1858
1899
|
curr_ins_id,
|
|
1859
1900
|
curr_del_id,
|
|
1860
1901
|
curr_comment_ids
|
|
@@ -2266,39 +2307,43 @@ ${header}`;
|
|
|
2266
2307
|
(s) => s.end > start_idx && s.start < end_idx
|
|
2267
2308
|
);
|
|
2268
2309
|
if (affected_spans.length === 0) return [];
|
|
2269
|
-
const
|
|
2270
|
-
if (
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
const local_start = start_idx - first_real_span.start;
|
|
2276
|
-
if (local_start > 0) {
|
|
2277
|
-
const idx_in_working = 0;
|
|
2278
|
-
const [, right_run] = this._split_run_at_index(
|
|
2279
|
-
working_runs[idx_in_working],
|
|
2280
|
-
local_start
|
|
2281
|
-
);
|
|
2282
|
-
working_runs[idx_in_working] = right_run;
|
|
2283
|
-
dom_modified = true;
|
|
2284
|
-
start_split_adjustment = local_start;
|
|
2310
|
+
const real_spans = affected_spans.filter((s) => s.run !== null);
|
|
2311
|
+
if (real_spans.length === 0) return [];
|
|
2312
|
+
const working_runs = [];
|
|
2313
|
+
for (const s of real_spans) {
|
|
2314
|
+
if (!working_runs.some((r) => r === s.run)) {
|
|
2315
|
+
working_runs.push(s.run);
|
|
2285
2316
|
}
|
|
2286
2317
|
}
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2318
|
+
let dom_modified = false;
|
|
2319
|
+
const first_real_span = real_spans[0];
|
|
2320
|
+
let start_split_adjustment = 0;
|
|
2321
|
+
const local_start = start_idx - first_real_span.start + (first_real_span.run_offset || 0);
|
|
2322
|
+
if (local_start > 0) {
|
|
2323
|
+
const split_source = working_runs[0];
|
|
2324
|
+
const [, right_run] = this._split_run_at_index(
|
|
2325
|
+
split_source,
|
|
2326
|
+
local_start
|
|
2327
|
+
);
|
|
2328
|
+
for (let w = 0; w < working_runs.length; w++) {
|
|
2329
|
+
if (working_runs[w] === split_source) working_runs[w] = right_run;
|
|
2330
|
+
}
|
|
2331
|
+
dom_modified = true;
|
|
2332
|
+
start_split_adjustment = local_start;
|
|
2333
|
+
}
|
|
2334
|
+
const last_real_span = real_spans[real_spans.length - 1];
|
|
2335
|
+
const is_same_run = first_real_span.run === last_real_span.run;
|
|
2336
|
+
const run_to_split = working_runs[working_runs.length - 1];
|
|
2337
|
+
const overlap_end = Math.min(last_real_span.end, end_idx);
|
|
2338
|
+
let local_end = overlap_end - last_real_span.start + (last_real_span.run_offset || 0);
|
|
2339
|
+
if (is_same_run && start_split_adjustment > 0) {
|
|
2340
|
+
local_end -= start_split_adjustment;
|
|
2341
|
+
}
|
|
2342
|
+
const run_text = get_run_text(run_to_split);
|
|
2343
|
+
if (local_end > 0 && local_end < run_text.length) {
|
|
2344
|
+
const [left_run] = this._split_run_at_index(run_to_split, local_end);
|
|
2345
|
+
working_runs[working_runs.length - 1] = left_run;
|
|
2346
|
+
dom_modified = true;
|
|
2302
2347
|
}
|
|
2303
2348
|
if (dom_modified && rebuild_map) {
|
|
2304
2349
|
this._build_map();
|
|
@@ -2335,7 +2380,7 @@ ${header}`;
|
|
|
2335
2380
|
}
|
|
2336
2381
|
return [null, span.paragraph];
|
|
2337
2382
|
} else {
|
|
2338
|
-
const offset = index - span.start;
|
|
2383
|
+
const offset = index - span.start + (span.run_offset || 0);
|
|
2339
2384
|
const [left] = this._split_run_at_index(span.run, offset);
|
|
2340
2385
|
if (rebuild_map) this._build_map();
|
|
2341
2386
|
return [left, span.paragraph];
|
|
@@ -2406,6 +2451,7 @@ ${header}`;
|
|
|
2406
2451
|
};
|
|
2407
2452
|
|
|
2408
2453
|
// src/diff.ts
|
|
2454
|
+
var import_fflate2 = require("fflate");
|
|
2409
2455
|
var import_diff_match_patch = __toESM(require("diff-match-patch"), 1);
|
|
2410
2456
|
function _count_standalone_underscores(s) {
|
|
2411
2457
|
let count = 0;
|
|
@@ -2743,6 +2789,37 @@ function _drop_image_marker_hunks(edits, warnings) {
|
|
|
2743
2789
|
}
|
|
2744
2790
|
return kept;
|
|
2745
2791
|
}
|
|
2792
|
+
function _drop_marker_interior_hunks(edits, text_orig, warnings) {
|
|
2793
|
+
const marker_spans = [];
|
|
2794
|
+
for (const m of text_orig.matchAll(_IMAGE_MARKER_RE)) {
|
|
2795
|
+
marker_spans.push([m.index, m.index + m[0].length]);
|
|
2796
|
+
}
|
|
2797
|
+
if (marker_spans.length === 0) return edits;
|
|
2798
|
+
const kept = [];
|
|
2799
|
+
let warned = false;
|
|
2800
|
+
for (const e of edits) {
|
|
2801
|
+
const idx = e._match_start_index;
|
|
2802
|
+
if (idx === void 0 || idx === null) {
|
|
2803
|
+
kept.push(e);
|
|
2804
|
+
continue;
|
|
2805
|
+
}
|
|
2806
|
+
const end = idx + (e.target_text || "").length;
|
|
2807
|
+
const cuts_into_marker = marker_spans.some(
|
|
2808
|
+
([m_start, m_end]) => m_start < end && idx < m_end && !(idx <= m_start && m_end <= end)
|
|
2809
|
+
);
|
|
2810
|
+
if (!cuts_into_marker) {
|
|
2811
|
+
kept.push(e);
|
|
2812
|
+
continue;
|
|
2813
|
+
}
|
|
2814
|
+
if (!warned) {
|
|
2815
|
+
warnings.push(
|
|
2816
|
+
"An image's alternative text differs between the documents. Image markers () are read-only projections, so this difference cannot be expressed as a text edit \u2014 it was skipped; update the image or its alt text manually in Word."
|
|
2817
|
+
);
|
|
2818
|
+
warned = true;
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
return kept;
|
|
2822
|
+
}
|
|
2746
2823
|
function _rows_are_plain(text, table) {
|
|
2747
2824
|
for (const row of table.rows) {
|
|
2748
2825
|
if (text.substring(row.start, row.end) !== row.cells.join(" | ")) {
|
|
@@ -2845,7 +2922,8 @@ function _row_ops_for_table(table_o, table_m, opcodes, warnings) {
|
|
|
2845
2922
|
type: "modify",
|
|
2846
2923
|
target_text: o_txt,
|
|
2847
2924
|
new_text: m_txt,
|
|
2848
|
-
comment: "Diff: Table row modified"
|
|
2925
|
+
comment: "Diff: Table row modified",
|
|
2926
|
+
_is_table_edit: true
|
|
2849
2927
|
});
|
|
2850
2928
|
}
|
|
2851
2929
|
}
|
|
@@ -2883,8 +2961,12 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2883
2961
|
warnings.push(
|
|
2884
2962
|
`The documents have different part layouts (${kinds_o.join(" + ") || "none"} vs ${kinds_m.join(" + ") || "none"}); comparing flattened text instead. Header/footer additions or removals cannot be expressed as text edits.`
|
|
2885
2963
|
);
|
|
2886
|
-
const flat =
|
|
2887
|
-
|
|
2964
|
+
const flat = _drop_marker_interior_hunks(
|
|
2965
|
+
_drop_image_marker_hunks(
|
|
2966
|
+
generate_edits_from_text(text_orig, text_mod),
|
|
2967
|
+
warnings
|
|
2968
|
+
),
|
|
2969
|
+
text_orig,
|
|
2888
2970
|
warnings
|
|
2889
2971
|
);
|
|
2890
2972
|
return { edits: [...flat], warnings };
|
|
@@ -2947,14 +3029,18 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2947
3029
|
if (row_opcodes !== null) {
|
|
2948
3030
|
edits.push(..._row_ops_for_table(t_o, t_m, row_opcodes, warnings));
|
|
2949
3031
|
} else {
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
3032
|
+
for (let k = 0; k < t_o.rows.length; k++) {
|
|
3033
|
+
const o_txt = t_o.rows[k].cells.join(" | ");
|
|
3034
|
+
const m_txt = t_m.rows[k].cells.join(" | ");
|
|
3035
|
+
if (o_txt === m_txt) continue;
|
|
3036
|
+
edits.push({
|
|
3037
|
+
type: "modify",
|
|
3038
|
+
target_text: o_txt,
|
|
3039
|
+
new_text: m_txt,
|
|
3040
|
+
comment: "Diff: Table row modified",
|
|
3041
|
+
_is_table_edit: true
|
|
3042
|
+
});
|
|
2956
3043
|
}
|
|
2957
|
-
edits.push(...tbl_edits);
|
|
2958
3044
|
}
|
|
2959
3045
|
}
|
|
2960
3046
|
}
|
|
@@ -2972,7 +3058,7 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2972
3058
|
};
|
|
2973
3059
|
let ambiguous_anchor_warned = false;
|
|
2974
3060
|
for (const e of edits) {
|
|
2975
|
-
if ((e.type === "insert_row" || e.type === "delete_row") && !ambiguous_anchor_warned) {
|
|
3061
|
+
if ((e.type === "insert_row" || e.type === "delete_row" || e._is_table_edit) && !ambiguous_anchor_warned) {
|
|
2976
3062
|
if (e.target_text && countOccurrences(text_orig, e.target_text) > 1) {
|
|
2977
3063
|
warnings.push(
|
|
2978
3064
|
`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.`
|
|
@@ -2984,12 +3070,63 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2984
3070
|
const modify_edits = edits.filter(
|
|
2985
3071
|
(e) => e.type === "modify"
|
|
2986
3072
|
);
|
|
2987
|
-
const kept_modifies = new Set(
|
|
3073
|
+
const kept_modifies = new Set(
|
|
3074
|
+
_drop_marker_interior_hunks(
|
|
3075
|
+
_drop_image_marker_hunks(modify_edits, warnings),
|
|
3076
|
+
text_orig,
|
|
3077
|
+
warnings
|
|
3078
|
+
)
|
|
3079
|
+
);
|
|
2988
3080
|
const final_edits = edits.filter(
|
|
2989
3081
|
(e) => e.type !== "modify" || kept_modifies.has(e)
|
|
2990
3082
|
);
|
|
2991
3083
|
return { edits: final_edits, warnings };
|
|
2992
3084
|
}
|
|
3085
|
+
function collect_media_difference_warnings(original_docx, modified_docx) {
|
|
3086
|
+
const media_hashes = (data) => {
|
|
3087
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
3088
|
+
try {
|
|
3089
|
+
const unzipped = (0, import_fflate2.unzipSync)(data);
|
|
3090
|
+
for (const [name, bytes] of Object.entries(unzipped)) {
|
|
3091
|
+
if (!name.startsWith("word/media/")) continue;
|
|
3092
|
+
let h1 = 2166136261;
|
|
3093
|
+
let h2 = 3421674724;
|
|
3094
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
3095
|
+
h1 = Math.imul(h1 ^ bytes[i], 16777619) >>> 0;
|
|
3096
|
+
h2 = Math.imul(h2 ^ bytes[bytes.length - 1 - i], 16777619) >>> 0;
|
|
3097
|
+
}
|
|
3098
|
+
hashes.set(name, `${bytes.length}:${h1.toString(16)}:${h2.toString(16)}`);
|
|
3099
|
+
}
|
|
3100
|
+
} catch {
|
|
3101
|
+
return /* @__PURE__ */ new Map();
|
|
3102
|
+
}
|
|
3103
|
+
return hashes;
|
|
3104
|
+
};
|
|
3105
|
+
const hashes_orig = media_hashes(original_docx);
|
|
3106
|
+
const hashes_mod = media_hashes(modified_docx);
|
|
3107
|
+
const changed = [];
|
|
3108
|
+
const added = [];
|
|
3109
|
+
const removed = [];
|
|
3110
|
+
for (const [name, hash] of hashes_orig) {
|
|
3111
|
+
if (!hashes_mod.has(name)) removed.push(name);
|
|
3112
|
+
else if (hashes_mod.get(name) !== hash) changed.push(name);
|
|
3113
|
+
}
|
|
3114
|
+
for (const name of hashes_mod.keys()) {
|
|
3115
|
+
if (!hashes_orig.has(name)) added.push(name);
|
|
3116
|
+
}
|
|
3117
|
+
changed.sort();
|
|
3118
|
+
added.sort();
|
|
3119
|
+
removed.sort();
|
|
3120
|
+
if (changed.length + added.length + removed.length === 0) return [];
|
|
3121
|
+
const parts = [];
|
|
3122
|
+
if (changed.length) parts.push(`${changed.length} changed`);
|
|
3123
|
+
if (added.length) parts.push(`${added.length} added`);
|
|
3124
|
+
if (removed.length) parts.push(`${removed.length} removed`);
|
|
3125
|
+
const names = [...changed, ...added, ...removed].slice(0, 5).join(", ");
|
|
3126
|
+
return [
|
|
3127
|
+
`The documents' embedded media differ (${parts.join(", ")}: ${names}). This diff compares TEXT only \u2014 an empty edit list does not mean the documents are visually identical. Image changes must be applied manually in Word.`
|
|
3128
|
+
];
|
|
3129
|
+
}
|
|
2993
3130
|
function create_unified_diff(original_text, modified_text, context_lines = 3) {
|
|
2994
3131
|
const dmp = new import_diff_match_patch.default.diff_match_patch();
|
|
2995
3132
|
dmp.Diff_Timeout = 2;
|
|
@@ -3944,6 +4081,13 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
3944
4081
|
console.error("generate_edits_from_text failed, falling back to wholesale edit", e);
|
|
3945
4082
|
raw_sub_edits = [];
|
|
3946
4083
|
}
|
|
4084
|
+
const _marker_only = (text) => {
|
|
4085
|
+
const stripped = text.trim();
|
|
4086
|
+
return stripped.length > 0 && /^[*_]+$/.test(stripped);
|
|
4087
|
+
};
|
|
4088
|
+
raw_sub_edits = raw_sub_edits.filter(
|
|
4089
|
+
(e) => !((!e.target_text || _marker_only(e.target_text)) && (!e.new_text || _marker_only(e.new_text)) && (e.target_text || e.new_text))
|
|
4090
|
+
);
|
|
3947
4091
|
if (!raw_sub_edits || raw_sub_edits.length === 0) {
|
|
3948
4092
|
const fallback_edit = {
|
|
3949
4093
|
type: "modify",
|
|
@@ -4724,7 +4868,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4724
4868
|
*
|
|
4725
4869
|
* Does NOT attach comments; callers handle that.
|
|
4726
4870
|
*/
|
|
4727
|
-
_track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id) {
|
|
4871
|
+
_track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id, positional_anchor_el = null, suppress_emphasis = false) {
|
|
4728
4872
|
if (!text) {
|
|
4729
4873
|
return {
|
|
4730
4874
|
first_node: null,
|
|
@@ -4745,7 +4889,29 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4745
4889
|
}
|
|
4746
4890
|
current_p = walker;
|
|
4747
4891
|
}
|
|
4748
|
-
|
|
4892
|
+
const suffix_nodes = [];
|
|
4893
|
+
const pos_source = positional_anchor_el && positional_anchor_el.parentNode ? positional_anchor_el : anchor_run !== null && anchor_run._element.parentNode ? anchor_run._element : null;
|
|
4894
|
+
if (current_p !== null && pos_source !== null) {
|
|
4895
|
+
let pos_anchor = pos_source;
|
|
4896
|
+
while (pos_anchor && pos_anchor.parentNode !== current_p) {
|
|
4897
|
+
pos_anchor = pos_anchor.parentNode;
|
|
4898
|
+
if (pos_anchor === current_p) {
|
|
4899
|
+
pos_anchor = null;
|
|
4900
|
+
break;
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4903
|
+
if (pos_anchor) {
|
|
4904
|
+
const relocatable = /* @__PURE__ */ new Set(["w:r", "w:ins", "w:del"]);
|
|
4905
|
+
let nxt = pos_anchor.nextSibling;
|
|
4906
|
+
while (nxt) {
|
|
4907
|
+
if (nxt.nodeType === 1 && relocatable.has(nxt.tagName)) {
|
|
4908
|
+
suffix_nodes.push(nxt);
|
|
4909
|
+
}
|
|
4910
|
+
nxt = nxt.nextSibling;
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4913
|
+
}
|
|
4914
|
+
while (lines.length > 1 && lines[lines.length - 1] === "" && suffix_nodes.length === 0) {
|
|
4749
4915
|
lines.pop();
|
|
4750
4916
|
}
|
|
4751
4917
|
if (lines.length === 0) {
|
|
@@ -4766,7 +4932,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4766
4932
|
first_clean === lines[0] ? lines[0] : lines[0],
|
|
4767
4933
|
anchor_run,
|
|
4768
4934
|
reuse_id,
|
|
4769
|
-
xmlDoc
|
|
4935
|
+
xmlDoc,
|
|
4936
|
+
suppress_emphasis
|
|
4770
4937
|
);
|
|
4771
4938
|
first_node = inline_ins;
|
|
4772
4939
|
}
|
|
@@ -4830,7 +4997,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4830
4997
|
clean_text,
|
|
4831
4998
|
anchor_run,
|
|
4832
4999
|
reuse_id,
|
|
4833
|
-
xmlDoc
|
|
5000
|
+
xmlDoc,
|
|
5001
|
+
suppress_emphasis
|
|
4834
5002
|
);
|
|
4835
5003
|
if (content_ins) {
|
|
4836
5004
|
new_p.appendChild(content_ins);
|
|
@@ -4843,6 +5011,12 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4843
5011
|
first_node = new_p;
|
|
4844
5012
|
}
|
|
4845
5013
|
}
|
|
5014
|
+
if (!block_mode && last_p && suffix_nodes.length > 0) {
|
|
5015
|
+
for (const node of suffix_nodes) {
|
|
5016
|
+
node.parentNode?.removeChild(node);
|
|
5017
|
+
last_p.appendChild(node);
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
4846
5020
|
return { first_node, last_p, last_ins, used_block_mode: block_mode };
|
|
4847
5021
|
}
|
|
4848
5022
|
/**
|
|
@@ -4850,7 +5024,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4850
5024
|
* <w:r> elements representing the inline markdown segments of `line_text`.
|
|
4851
5025
|
* Returns null if line_text is empty.
|
|
4852
5026
|
*/
|
|
4853
|
-
_build_tracked_ins_for_line(line_text, anchor_run, reuse_id, xmlDoc) {
|
|
5027
|
+
_build_tracked_ins_for_line(line_text, anchor_run, reuse_id, xmlDoc, suppress_emphasis = false) {
|
|
4854
5028
|
if (!line_text && line_text !== "") return null;
|
|
4855
5029
|
const ins = this._create_track_change_tag("w:ins", "", reuse_id);
|
|
4856
5030
|
const segments = this._parse_inline_markdown(line_text);
|
|
@@ -4863,15 +5037,11 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4863
5037
|
const anchor_rPr = findChild(anchor_run._element, "w:rPr");
|
|
4864
5038
|
if (anchor_rPr) {
|
|
4865
5039
|
const clone = anchor_rPr.cloneNode(true);
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
"w:
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
"w:iCs",
|
|
4872
|
-
"w:b",
|
|
4873
|
-
"w:bCs"
|
|
4874
|
-
]) {
|
|
5040
|
+
const strip_tags = ["w:vanish", "w:strike", "w:dstrike", "w:i", "w:iCs"];
|
|
5041
|
+
if (suppress_emphasis) {
|
|
5042
|
+
strip_tags.push("w:b", "w:bCs");
|
|
5043
|
+
}
|
|
5044
|
+
for (const tag of strip_tags) {
|
|
4875
5045
|
const found = findChild(clone, tag);
|
|
4876
5046
|
if (found) clone.removeChild(found);
|
|
4877
5047
|
}
|
|
@@ -4900,12 +5070,33 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4900
5070
|
if (stripped_text.startsWith("* ") || stripped_text.startsWith("- ")) {
|
|
4901
5071
|
return [stripped_text.substring(2).trim(), "List Paragraph"];
|
|
4902
5072
|
}
|
|
4903
|
-
const match = stripped_text.match(
|
|
5073
|
+
const match = stripped_text.match(/^1\.\s+/);
|
|
4904
5074
|
if (match) {
|
|
4905
5075
|
return [stripped_text.substring(match[0].length).trim(), "List Number"];
|
|
4906
5076
|
}
|
|
4907
5077
|
return [text, null];
|
|
4908
5078
|
}
|
|
5079
|
+
/**
|
|
5080
|
+
* True when this edit's target or replacement text carries explicit
|
|
5081
|
+
* bold/italic markers, making the markers AUTHORITATIVE for the inserted
|
|
5082
|
+
* runs' formatting. Replacing `**X**` with `_X_` must yield italic-only
|
|
5083
|
+
* text, and replacing `**X**` with `X` must yield plain text — inheriting
|
|
5084
|
+
* the replaced span's run properties on top of (or instead of) the
|
|
5085
|
+
* requested markers silently produces the wrong document while the report
|
|
5086
|
+
* claims success (QA 2026-07-19 F-02). Plain-text edits (no markers on
|
|
5087
|
+
* either side) keep inheriting the context style so partial replacements
|
|
5088
|
+
* inside a styled span never lose formatting.
|
|
5089
|
+
*/
|
|
5090
|
+
_edit_declares_emphasis(edit) {
|
|
5091
|
+
for (const text of [edit?.target_text, edit?.new_text]) {
|
|
5092
|
+
if (!text || !text.includes("**") && !text.includes("_")) continue;
|
|
5093
|
+
const segments = this._parse_inline_markdown(text);
|
|
5094
|
+
if (segments.some(([, props]) => props && Object.keys(props).length > 0)) {
|
|
5095
|
+
return true;
|
|
5096
|
+
}
|
|
5097
|
+
}
|
|
5098
|
+
return false;
|
|
5099
|
+
}
|
|
4909
5100
|
_parse_inline_markdown(text, baseStyle = {}) {
|
|
4910
5101
|
if (!text) return [];
|
|
4911
5102
|
const tokenPattern = /(\*\*.*?\*\*)|(_.*?_)/;
|
|
@@ -5172,6 +5363,16 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5172
5363
|
continue;
|
|
5173
5364
|
const is_regex = edit.regex || false;
|
|
5174
5365
|
const match_mode = edit.match_mode || "strict";
|
|
5366
|
+
if (is_regex) {
|
|
5367
|
+
try {
|
|
5368
|
+
new RegExp(edit.target_text);
|
|
5369
|
+
} catch (regex_err) {
|
|
5370
|
+
errors.push(
|
|
5371
|
+
`- Edit ${i + 1 + index_offset} Failed: target_text is not a valid regular expression (${regex_err?.message ?? regex_err}). Fix the pattern, or set "regex": false to match the text literally.`
|
|
5372
|
+
);
|
|
5373
|
+
continue;
|
|
5374
|
+
}
|
|
5375
|
+
}
|
|
5175
5376
|
let matches = this.mapper.find_all_match_indices(
|
|
5176
5377
|
edit.target_text,
|
|
5177
5378
|
is_regex
|
|
@@ -5751,6 +5952,14 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5751
5952
|
actions_skipped: skipped_actions,
|
|
5752
5953
|
edits_applied: applied_edits,
|
|
5753
5954
|
edits_skipped: skipped_edits,
|
|
5955
|
+
// edits_applied counts change OBJECTS; this is the total number of
|
|
5956
|
+
// document occurrences they modified (match_mode="all" fan-out), so
|
|
5957
|
+
// automation never has to guess which of the two a count means
|
|
5958
|
+
// (QA 2026-07-19 F-21).
|
|
5959
|
+
occurrences_modified: edits_reports.reduce(
|
|
5960
|
+
(sum, r) => sum + (r.occurrences_modified || 0),
|
|
5961
|
+
0
|
|
5962
|
+
),
|
|
5754
5963
|
skipped_details: this.skipped_details,
|
|
5755
5964
|
edits: edits_reports,
|
|
5756
5965
|
engine: "node",
|
|
@@ -5772,6 +5981,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5772
5981
|
}
|
|
5773
5982
|
edit._applied_status = false;
|
|
5774
5983
|
edit._error_msg = null;
|
|
5984
|
+
edit._any_sub_failure = false;
|
|
5775
5985
|
}
|
|
5776
5986
|
for (const edit of edits) {
|
|
5777
5987
|
if (typeof edit !== "object" || edit === null) continue;
|
|
@@ -5782,14 +5992,17 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5782
5992
|
resolved_edits.push([edit, edit.new_text || null]);
|
|
5783
5993
|
} else if (edit.type === "insert_row" || edit.type === "delete_row") {
|
|
5784
5994
|
let matches = this.mapper.find_all_match_indices(edit.target_text);
|
|
5995
|
+
let resolved_mapper = this.mapper;
|
|
5785
5996
|
if (matches.length === 0) {
|
|
5786
5997
|
if (!this.clean_mapper) {
|
|
5787
5998
|
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
5788
5999
|
}
|
|
5789
6000
|
matches = this.clean_mapper.find_all_match_indices(edit.target_text);
|
|
6001
|
+
resolved_mapper = this.clean_mapper;
|
|
5790
6002
|
}
|
|
5791
6003
|
if (matches.length > 0) {
|
|
5792
6004
|
edit._resolved_start_idx = matches[0][0];
|
|
6005
|
+
edit._active_mapper_ref = resolved_mapper;
|
|
5793
6006
|
resolved_edits.push([edit, null]);
|
|
5794
6007
|
} else {
|
|
5795
6008
|
skipped++;
|
|
@@ -5868,17 +6081,18 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5868
6081
|
this.skipped_details.push(msg);
|
|
5869
6082
|
edit._applied_status = false;
|
|
5870
6083
|
edit._error_msg = msg;
|
|
6084
|
+
edit._any_sub_failure = true;
|
|
5871
6085
|
const parent = edit._parent_edit_ref;
|
|
5872
6086
|
if (parent) {
|
|
5873
6087
|
parent._applied_status = false;
|
|
5874
6088
|
parent._error_msg = msg;
|
|
6089
|
+
parent._any_sub_failure = true;
|
|
5875
6090
|
}
|
|
5876
6091
|
continue;
|
|
5877
6092
|
}
|
|
5878
6093
|
let success = false;
|
|
5879
6094
|
if (edit.type === "modify") {
|
|
5880
|
-
|
|
5881
|
-
success = this._apply_single_edit_indexed(edit, orig_new, rebuild);
|
|
6095
|
+
success = this._apply_single_edit_indexed(edit, orig_new, false);
|
|
5882
6096
|
} else if (edit.type === "insert_row" || edit.type === "delete_row") {
|
|
5883
6097
|
success = this._apply_table_edit(edit, false);
|
|
5884
6098
|
}
|
|
@@ -5928,8 +6142,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5928
6142
|
this.skipped_details.push(msg);
|
|
5929
6143
|
edit._applied_status = false;
|
|
5930
6144
|
edit._error_msg = msg;
|
|
6145
|
+
edit._any_sub_failure = true;
|
|
5931
6146
|
const parent = edit._parent_edit_ref;
|
|
5932
6147
|
if (parent) {
|
|
6148
|
+
parent._any_sub_failure = true;
|
|
5933
6149
|
if (!parent._applied_status) {
|
|
5934
6150
|
parent._applied_status = false;
|
|
5935
6151
|
parent._error_msg = msg;
|
|
@@ -5937,7 +6153,20 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5937
6153
|
}
|
|
5938
6154
|
}
|
|
5939
6155
|
}
|
|
5940
|
-
|
|
6156
|
+
let applied_logical = 0;
|
|
6157
|
+
let skipped_logical = 0;
|
|
6158
|
+
for (const input_edit of edits) {
|
|
6159
|
+
if (typeof input_edit !== "object" || input_edit === null) {
|
|
6160
|
+
skipped_logical++;
|
|
6161
|
+
continue;
|
|
6162
|
+
}
|
|
6163
|
+
if (input_edit._applied_status && !input_edit._any_sub_failure) {
|
|
6164
|
+
applied_logical++;
|
|
6165
|
+
} else {
|
|
6166
|
+
skipped_logical++;
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
6169
|
+
return [applied_logical, skipped_logical];
|
|
5941
6170
|
}
|
|
5942
6171
|
apply_review_actions(actions) {
|
|
5943
6172
|
let applied = 0;
|
|
@@ -6038,7 +6267,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6038
6267
|
}
|
|
6039
6268
|
_apply_table_edit(edit, rebuild_map) {
|
|
6040
6269
|
const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
|
|
6041
|
-
const
|
|
6270
|
+
const active_mapper = edit._active_mapper_ref || this.mapper;
|
|
6271
|
+
const [anchor_run, anchor_para] = active_mapper.get_insertion_anchor(
|
|
6042
6272
|
start_idx,
|
|
6043
6273
|
rebuild_map
|
|
6044
6274
|
);
|
|
@@ -6219,9 +6449,14 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6219
6449
|
if (overlaps_virtual_pipe) {
|
|
6220
6450
|
const actual_cells = actual_doc_text.split("|");
|
|
6221
6451
|
const new_cells = current_effective_new_text.split("|");
|
|
6222
|
-
if (actual_cells.length
|
|
6452
|
+
if (actual_cells.length !== new_cells.length) {
|
|
6453
|
+
throw new BatchValidationError([
|
|
6454
|
+
`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).`
|
|
6455
|
+
]);
|
|
6456
|
+
}
|
|
6457
|
+
if (actual_cells.length > 1) {
|
|
6223
6458
|
const sub_edits2 = [];
|
|
6224
|
-
let
|
|
6459
|
+
let cell_start_in_target = 0;
|
|
6225
6460
|
let target_comment_idx = 0;
|
|
6226
6461
|
for (let idx = 0; idx < actual_cells.length; idx++) {
|
|
6227
6462
|
if (actual_cells[idx].trim() !== new_cells[idx].trim()) {
|
|
@@ -6234,13 +6469,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6234
6469
|
const n_cell = new_cells[cell_idx];
|
|
6235
6470
|
const a_clean = a_cell.trim();
|
|
6236
6471
|
const n_clean = n_cell.trim();
|
|
6237
|
-
|
|
6238
|
-
if (a_clean) {
|
|
6239
|
-
actual_start = this.mapper.full_text.indexOf(a_clean, search_offset);
|
|
6240
|
-
if (actual_start === -1 || actual_start > search_offset + 10) {
|
|
6241
|
-
actual_start = search_offset;
|
|
6242
|
-
}
|
|
6243
|
-
}
|
|
6472
|
+
const actual_start = start_idx + cell_start_in_target + (a_clean ? a_cell.indexOf(a_clean) : 0);
|
|
6244
6473
|
const should_attach_comment = edit.comment !== null && edit.comment !== void 0 && cell_idx === target_comment_idx;
|
|
6245
6474
|
if (a_clean !== n_clean || should_attach_comment) {
|
|
6246
6475
|
const cell_sub_edits = this._word_diff_sub_edits(
|
|
@@ -6257,24 +6486,12 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6257
6486
|
sub_edits2.push(se);
|
|
6258
6487
|
}
|
|
6259
6488
|
}
|
|
6260
|
-
|
|
6261
|
-
search_offset = actual_start + a_clean.length;
|
|
6262
|
-
}
|
|
6263
|
-
const next_pipe = this.mapper.full_text.indexOf(" | ", search_offset);
|
|
6264
|
-
if (next_pipe !== -1 && next_pipe <= search_offset + 10) {
|
|
6265
|
-
search_offset = next_pipe + 3;
|
|
6266
|
-
} else {
|
|
6267
|
-
search_offset += a_cell.length + 1;
|
|
6268
|
-
}
|
|
6489
|
+
cell_start_in_target += a_cell.length + 1;
|
|
6269
6490
|
}
|
|
6270
6491
|
for (const sub of sub_edits2) {
|
|
6271
6492
|
all_sub_edits.push(sub);
|
|
6272
6493
|
}
|
|
6273
6494
|
continue;
|
|
6274
|
-
} else {
|
|
6275
|
-
throw new BatchValidationError([
|
|
6276
|
-
`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).`
|
|
6277
|
-
]);
|
|
6278
6495
|
}
|
|
6279
6496
|
}
|
|
6280
6497
|
let has_markdown = false;
|
|
@@ -6368,6 +6585,19 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6368
6585
|
continue;
|
|
6369
6586
|
}
|
|
6370
6587
|
}
|
|
6588
|
+
if (!final_target && final_new) {
|
|
6589
|
+
all_sub_edits.push({
|
|
6590
|
+
type: "modify",
|
|
6591
|
+
target_text: "",
|
|
6592
|
+
new_text: final_new,
|
|
6593
|
+
comment: edit.comment,
|
|
6594
|
+
_resolved_start_idx: effective_start_idx,
|
|
6595
|
+
_match_start_index: effective_start_idx,
|
|
6596
|
+
_internal_op: "INSERTION",
|
|
6597
|
+
_active_mapper_ref: active_mapper
|
|
6598
|
+
});
|
|
6599
|
+
continue;
|
|
6600
|
+
}
|
|
6371
6601
|
const sub_edits = this._word_diff_sub_edits(
|
|
6372
6602
|
actual_doc_text,
|
|
6373
6603
|
current_effective_new_text,
|
|
@@ -6429,6 +6659,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6429
6659
|
op = "MODIFICATION";
|
|
6430
6660
|
}
|
|
6431
6661
|
}
|
|
6662
|
+
const suppress_emphasis = this._edit_declares_emphasis(edit);
|
|
6432
6663
|
if (op === "STYLE_ONLY" || op === "STYLE_AND_TEXT") {
|
|
6433
6664
|
const [anchor_run, anchor_para] = active_mapper.get_insertion_anchor(
|
|
6434
6665
|
start_idx,
|
|
@@ -6629,7 +6860,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6629
6860
|
clean_text,
|
|
6630
6861
|
anchor_run,
|
|
6631
6862
|
ins_id,
|
|
6632
|
-
xmlDoc
|
|
6863
|
+
xmlDoc,
|
|
6864
|
+
suppress_emphasis
|
|
6633
6865
|
);
|
|
6634
6866
|
if (content_ins) new_p.appendChild(content_ins);
|
|
6635
6867
|
body.insertBefore(new_p, _bug233_target_para);
|
|
@@ -6661,7 +6893,9 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6661
6893
|
final_new_text,
|
|
6662
6894
|
anchor_run,
|
|
6663
6895
|
anchor_para,
|
|
6664
|
-
ins_id
|
|
6896
|
+
ins_id,
|
|
6897
|
+
null,
|
|
6898
|
+
suppress_emphasis
|
|
6665
6899
|
);
|
|
6666
6900
|
if (!result.first_node) return false;
|
|
6667
6901
|
const is_inline_first = result.first_node.tagName === "w:ins";
|
|
@@ -6809,7 +7043,11 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6809
7043
|
edit.new_text,
|
|
6810
7044
|
style_source_run,
|
|
6811
7045
|
mod_anchor_para,
|
|
6812
|
-
ins_id
|
|
7046
|
+
ins_id,
|
|
7047
|
+
// The insertion physically follows the deletion block; the style
|
|
7048
|
+
// run was detached when the deletion cloned it into <w:del>.
|
|
7049
|
+
last_del,
|
|
7050
|
+
suppress_emphasis
|
|
6813
7051
|
);
|
|
6814
7052
|
if (result.first_node) {
|
|
6815
7053
|
const is_inline_first = result.first_node.tagName === "w:ins";
|
|
@@ -6843,7 +7081,9 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6843
7081
|
edit.new_text,
|
|
6844
7082
|
anchor,
|
|
6845
7083
|
first_span.paragraph,
|
|
6846
|
-
ins_id
|
|
7084
|
+
ins_id,
|
|
7085
|
+
null,
|
|
7086
|
+
suppress_emphasis
|
|
6847
7087
|
);
|
|
6848
7088
|
if (result.first_node) {
|
|
6849
7089
|
p1_el.appendChild(result.first_node);
|
|
@@ -7327,6 +7567,26 @@ function normalize_change_dates(doc) {
|
|
|
7327
7567
|
}
|
|
7328
7568
|
return count ? [`Track change timestamps: ${count} normalized`] : [];
|
|
7329
7569
|
}
|
|
7570
|
+
function normalize_comment_dates(doc) {
|
|
7571
|
+
let count = 0;
|
|
7572
|
+
const fixed = "2025-01-01T00:00:00Z";
|
|
7573
|
+
for (const part of doc.pkg.parts) {
|
|
7574
|
+
if (!part.contentType.includes("comments")) continue;
|
|
7575
|
+
for (const el of findAllDescendants(part._element, "w:comment")) {
|
|
7576
|
+
if (el.hasAttribute("w:date")) {
|
|
7577
|
+
el.setAttribute("w:date", fixed);
|
|
7578
|
+
count++;
|
|
7579
|
+
}
|
|
7580
|
+
}
|
|
7581
|
+
for (const el of findAllDescendants(part._element, "w16cex:commentExtensible")) {
|
|
7582
|
+
if (el.hasAttribute("w16cex:dateUtc")) {
|
|
7583
|
+
el.setAttribute("w16cex:dateUtc", fixed);
|
|
7584
|
+
count++;
|
|
7585
|
+
}
|
|
7586
|
+
}
|
|
7587
|
+
}
|
|
7588
|
+
return count ? [`Comment timestamps: ${count} normalized`] : [];
|
|
7589
|
+
}
|
|
7330
7590
|
function scrub_doc_properties(doc) {
|
|
7331
7591
|
const lines = [];
|
|
7332
7592
|
const corePart = doc.pkg.getPartByPath("docProps/core.xml");
|
|
@@ -7356,7 +7616,10 @@ function scrub_doc_properties(doc) {
|
|
|
7356
7616
|
["keywords", "Keywords"],
|
|
7357
7617
|
["subject", "Subject"],
|
|
7358
7618
|
["contentStatus", "Content status"],
|
|
7359
|
-
["description", "Description/comments"]
|
|
7619
|
+
["description", "Description/comments"],
|
|
7620
|
+
["identifier", "Identifier"],
|
|
7621
|
+
["language", "Language"],
|
|
7622
|
+
["version", "Version"]
|
|
7360
7623
|
];
|
|
7361
7624
|
for (const [local, label] of leakFields) {
|
|
7362
7625
|
findDescendantsByLocalName(corePart._element, local).forEach((c) => {
|
|
@@ -7414,27 +7677,73 @@ function scrub_timestamps(doc) {
|
|
|
7414
7677
|
}
|
|
7415
7678
|
return modified ? ["Timestamps normalized to epoch"] : [];
|
|
7416
7679
|
}
|
|
7417
|
-
function
|
|
7418
|
-
const
|
|
7419
|
-
|
|
7420
|
-
const
|
|
7421
|
-
|
|
7422
|
-
const removeRelationsTo = (relsPart) => {
|
|
7680
|
+
function ejectPackageMembers(doc, matcher, relTargetMatcher) {
|
|
7681
|
+
const pkg = doc.pkg;
|
|
7682
|
+
const normalized = (p) => p.startsWith("/") ? p.substring(1) : p;
|
|
7683
|
+
for (const part of pkg.parts) {
|
|
7684
|
+
if (!part.partname.endsWith(".rels")) continue;
|
|
7423
7685
|
const toRemove = [];
|
|
7424
|
-
for (const rel of findAllDescendants(
|
|
7425
|
-
const target = rel.getAttribute("Target");
|
|
7426
|
-
if (target
|
|
7686
|
+
for (const rel of findAllDescendants(part._element, "Relationship")) {
|
|
7687
|
+
const target = rel.getAttribute("Target") || "";
|
|
7688
|
+
if (relTargetMatcher(target)) {
|
|
7689
|
+
toRemove.push(rel);
|
|
7690
|
+
const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
|
|
7691
|
+
const sourcePart = pkg.getPartByPath(sourcePath);
|
|
7692
|
+
if (sourcePart) {
|
|
7693
|
+
const relId = rel.getAttribute("Id");
|
|
7694
|
+
if (relId) sourcePart.rels.delete(relId);
|
|
7695
|
+
}
|
|
7696
|
+
}
|
|
7427
7697
|
}
|
|
7428
7698
|
toRemove.forEach((r) => r.parentNode?.removeChild(r));
|
|
7429
|
-
}
|
|
7430
|
-
const
|
|
7431
|
-
if (
|
|
7432
|
-
|
|
7433
|
-
|
|
7699
|
+
}
|
|
7700
|
+
const ctPart = pkg.getPartByPath("[Content_Types].xml");
|
|
7701
|
+
if (ctPart) {
|
|
7702
|
+
const toRemove = [];
|
|
7703
|
+
for (const override of findAllDescendants(ctPart._element, "Override")) {
|
|
7704
|
+
const partName = override.getAttribute("PartName") || "";
|
|
7705
|
+
if (matcher(normalized(partName))) toRemove.push(override);
|
|
7706
|
+
}
|
|
7707
|
+
toRemove.forEach((o) => o.parentNode?.removeChild(o));
|
|
7708
|
+
}
|
|
7709
|
+
pkg.parts = pkg.parts.filter((p) => !matcher(normalized(p.partname)));
|
|
7710
|
+
for (const key of Object.keys(pkg.unzipped)) {
|
|
7711
|
+
if (matcher(normalized(key))) delete pkg.unzipped[key];
|
|
7712
|
+
}
|
|
7713
|
+
}
|
|
7714
|
+
function strip_custom_xml(doc) {
|
|
7715
|
+
const isCustomXml = (path) => path.startsWith("customXml/");
|
|
7716
|
+
const customParts = doc.pkg.parts.filter(
|
|
7717
|
+
(p) => isCustomXml(p.partname.startsWith("/") ? p.partname.substring(1) : p.partname)
|
|
7718
|
+
);
|
|
7719
|
+
const leakedMembers = Object.keys(doc.pkg.unzipped).filter(isCustomXml);
|
|
7720
|
+
if (customParts.length === 0 && leakedMembers.length === 0) return [];
|
|
7721
|
+
ejectPackageMembers(doc, isCustomXml, (target) => target.includes("customXml"));
|
|
7434
7722
|
for (const sdtPr of findAllDescendants(doc.element, "w:sdtPr")) {
|
|
7435
7723
|
findChildren(sdtPr, "w:dataBinding").forEach((b) => sdtPr.removeChild(b));
|
|
7436
7724
|
}
|
|
7437
|
-
return [`Custom XML parts: ${customParts.length} removed`];
|
|
7725
|
+
return [`Custom XML parts: ${Math.max(customParts.length, leakedMembers.length)} removed`];
|
|
7726
|
+
}
|
|
7727
|
+
var CUSTOM_PROPS_PATH = "docProps/custom.xml";
|
|
7728
|
+
function strip_custom_properties(doc) {
|
|
7729
|
+
const part = doc.pkg.getPartByPath(CUSTOM_PROPS_PATH);
|
|
7730
|
+
const leaked = Object.keys(doc.pkg.unzipped).includes(CUSTOM_PROPS_PATH);
|
|
7731
|
+
if (!part && !leaked) return [];
|
|
7732
|
+
const lines = [];
|
|
7733
|
+
if (part) {
|
|
7734
|
+
for (const prop of findDescendantsByLocalName(part._element, "property")) {
|
|
7735
|
+
const name = prop.getAttribute("name") || "(unnamed)";
|
|
7736
|
+
const value = (prop.textContent || "").trim();
|
|
7737
|
+
lines.push(` Custom property removed: ${name} = "${_truncate(value, 60)}"`);
|
|
7738
|
+
}
|
|
7739
|
+
}
|
|
7740
|
+
ejectPackageMembers(
|
|
7741
|
+
doc,
|
|
7742
|
+
(path) => path === CUSTOM_PROPS_PATH,
|
|
7743
|
+
(target) => target.includes("docProps/custom.xml")
|
|
7744
|
+
);
|
|
7745
|
+
const count = Math.max(lines.length, 1);
|
|
7746
|
+
return [`Custom document properties: ${count} removed (docProps/custom.xml)`].concat(lines);
|
|
7438
7747
|
}
|
|
7439
7748
|
function strip_image_alt_text(doc) {
|
|
7440
7749
|
let count = 0;
|
|
@@ -8021,8 +8330,10 @@ function build_paragraph_text(paragraph, comments_map, cleanView, style_cache, d
|
|
|
8021
8330
|
const new_wrappers = cleanView ? ["", ""] : _get_wrappers(active_ins, active_del, active_comments, active_fmt);
|
|
8022
8331
|
const new_style = [prefix, suffix];
|
|
8023
8332
|
if (pending_text && new_wrappers[0] === current_wrappers[0] && new_wrappers[1] === current_wrappers[1]) {
|
|
8024
|
-
|
|
8025
|
-
|
|
8333
|
+
const escaped_prefix = new_style[0].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8334
|
+
const lead_match = new_style[0] !== "" || new_style[1] !== "" ? seg.match(new RegExp("^(\\s*)" + escaped_prefix)) : null;
|
|
8335
|
+
if (new_style[0] === current_style[0] && new_style[1] === current_style[1] && current_style[0] !== "" && pending_text.endsWith(current_style[1]) && lead_match !== null) {
|
|
8336
|
+
pending_text = pending_text.slice(0, -current_style[1].length) + lead_match[1] + seg.slice(lead_match[0].length);
|
|
8026
8337
|
} else {
|
|
8027
8338
|
pending_text += seg;
|
|
8028
8339
|
}
|
|
@@ -8788,6 +9099,9 @@ function _collect_footnote_ids_fast(owned_items) {
|
|
|
8788
9099
|
return ordered;
|
|
8789
9100
|
}
|
|
8790
9101
|
|
|
9102
|
+
// src/sanitize/core.ts
|
|
9103
|
+
var import_fflate3 = require("fflate");
|
|
9104
|
+
|
|
8791
9105
|
// src/sanitize/report.ts
|
|
8792
9106
|
var SanitizeReport = class {
|
|
8793
9107
|
filename;
|
|
@@ -8822,7 +9136,7 @@ var SanitizeReport = class {
|
|
|
8822
9136
|
} else {
|
|
8823
9137
|
this.removed_comment_lines.push(line);
|
|
8824
9138
|
}
|
|
8825
|
-
} 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")) {
|
|
9139
|
+
} 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")) {
|
|
8826
9140
|
this.metadata_lines.push(line);
|
|
8827
9141
|
} else if (lower.includes("hyperlink") || lower.includes("warning")) {
|
|
8828
9142
|
this.warnings.push(line);
|
|
@@ -8935,11 +9249,13 @@ async function finalize_document(doc, options) {
|
|
|
8935
9249
|
report.add_transform_lines(scrub_doc_properties(doc));
|
|
8936
9250
|
report.add_transform_lines(scrub_timestamps(doc));
|
|
8937
9251
|
report.add_transform_lines(strip_custom_xml(doc));
|
|
9252
|
+
report.add_transform_lines(strip_custom_properties(doc));
|
|
8938
9253
|
report.add_transform_lines(strip_image_alt_text(doc));
|
|
8939
9254
|
const warnings = audit_hyperlinks(doc);
|
|
8940
9255
|
for (const w of warnings) report.warnings.push(w);
|
|
8941
9256
|
for (const w of detect_watermarks(doc)) report.warnings.push(w);
|
|
8942
9257
|
report.add_transform_lines(normalize_change_dates(doc));
|
|
9258
|
+
report.add_transform_lines(normalize_comment_dates(doc));
|
|
8943
9259
|
if (options.protection_mode === "read_only" || options.protection_mode === "encrypt") {
|
|
8944
9260
|
if (options.protection_mode === "encrypt") {
|
|
8945
9261
|
report.warnings.push("Encryption mode (AES compound wrappers) is strictly unsupported in the zero-dependency Node engine. Falling back to native Word Read-Only lock.");
|
|
@@ -8982,8 +9298,47 @@ async function finalize_document(doc, options) {
|
|
|
8982
9298
|
}
|
|
8983
9299
|
if (report.warnings.length > 0) report.status = "clean_with_warnings";
|
|
8984
9300
|
const outBuffer = await doc.save();
|
|
9301
|
+
verifySanitizedPackage(outBuffer);
|
|
8985
9302
|
return { reportText: report.render(), outBuffer };
|
|
8986
9303
|
}
|
|
9304
|
+
var VERIFIED_CORE_FIELDS = [
|
|
9305
|
+
["creator", "author (dc:creator)"],
|
|
9306
|
+
["lastModifiedBy", "last modified by (cp:lastModifiedBy)"],
|
|
9307
|
+
["identifier", "identifier (dc:identifier)"],
|
|
9308
|
+
["description", "description (dc:description)"],
|
|
9309
|
+
["keywords", "keywords (cp:keywords)"],
|
|
9310
|
+
["category", "category (cp:category)"],
|
|
9311
|
+
["subject", "subject (dc:subject)"],
|
|
9312
|
+
["contentStatus", "content status (cp:contentStatus)"],
|
|
9313
|
+
["language", "language (dc:language)"],
|
|
9314
|
+
["version", "version (cp:version)"]
|
|
9315
|
+
];
|
|
9316
|
+
function verifySanitizedPackage(outBuffer) {
|
|
9317
|
+
const unzipped = (0, import_fflate3.unzipSync)(new Uint8Array(outBuffer));
|
|
9318
|
+
const names = Object.keys(unzipped);
|
|
9319
|
+
const problems = [];
|
|
9320
|
+
if (names.includes("docProps/custom.xml")) {
|
|
9321
|
+
problems.push("docProps/custom.xml (custom document properties) is still in the package");
|
|
9322
|
+
}
|
|
9323
|
+
if (names.some((n) => n.startsWith("customXml/"))) {
|
|
9324
|
+
problems.push("customXml/* parts are still in the package");
|
|
9325
|
+
}
|
|
9326
|
+
if (names.includes("docProps/core.xml")) {
|
|
9327
|
+
const core = parseXml((0, import_fflate3.strFromU8)(unzipped["docProps/core.xml"]));
|
|
9328
|
+
for (const [local, label] of VERIFIED_CORE_FIELDS) {
|
|
9329
|
+
for (const el of findDescendantsByLocalName(core.documentElement, local)) {
|
|
9330
|
+
if ((el.textContent || "").trim()) {
|
|
9331
|
+
problems.push(`core property ${label} still contains a value`);
|
|
9332
|
+
}
|
|
9333
|
+
}
|
|
9334
|
+
}
|
|
9335
|
+
}
|
|
9336
|
+
if (problems.length > 0) {
|
|
9337
|
+
throw new Error(
|
|
9338
|
+
"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."
|
|
9339
|
+
);
|
|
9340
|
+
}
|
|
9341
|
+
}
|
|
8987
9342
|
|
|
8988
9343
|
// src/index.ts
|
|
8989
9344
|
function identifyEngine() {
|
|
@@ -8999,6 +9354,7 @@ function identifyEngine() {
|
|
|
8999
9354
|
USER_PATTERN_TIMEOUT_MS,
|
|
9000
9355
|
_extractTextFromDoc,
|
|
9001
9356
|
apply_edits_to_markdown,
|
|
9357
|
+
collect_media_difference_warnings,
|
|
9002
9358
|
create_unified_diff,
|
|
9003
9359
|
create_word_patch_diff,
|
|
9004
9360
|
describe_illegal_control_chars,
|