@adeu/core 1.25.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +298 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +296 -73
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/diff.ts +129 -4
- package/src/engine.ts +135 -18
- package/src/index.ts +1 -1
- package/src/ingest.ts +11 -2
- package/src/mapper.ts +99 -47
- package/src/repro_qa_report_v7.test.ts +380 -0
- package/src/sanitize/core.ts +3 -0
- package/src/sanitize/transforms.ts +29 -0
- package/src/utils/docx.ts +30 -2
package/dist/index.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(" | ")) {
|
|
@@ -2884,8 +2961,12 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2884
2961
|
warnings.push(
|
|
2885
2962
|
`The documents have different part layouts (${kinds_o.join(" + ") || "none"} vs ${kinds_m.join(" + ") || "none"}); comparing flattened text instead. Header/footer additions or removals cannot be expressed as text edits.`
|
|
2886
2963
|
);
|
|
2887
|
-
const flat =
|
|
2888
|
-
|
|
2964
|
+
const flat = _drop_marker_interior_hunks(
|
|
2965
|
+
_drop_image_marker_hunks(
|
|
2966
|
+
generate_edits_from_text(text_orig, text_mod),
|
|
2967
|
+
warnings
|
|
2968
|
+
),
|
|
2969
|
+
text_orig,
|
|
2889
2970
|
warnings
|
|
2890
2971
|
);
|
|
2891
2972
|
return { edits: [...flat], warnings };
|
|
@@ -2989,12 +3070,63 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2989
3070
|
const modify_edits = edits.filter(
|
|
2990
3071
|
(e) => e.type === "modify"
|
|
2991
3072
|
);
|
|
2992
|
-
const kept_modifies = new Set(
|
|
3073
|
+
const kept_modifies = new Set(
|
|
3074
|
+
_drop_marker_interior_hunks(
|
|
3075
|
+
_drop_image_marker_hunks(modify_edits, warnings),
|
|
3076
|
+
text_orig,
|
|
3077
|
+
warnings
|
|
3078
|
+
)
|
|
3079
|
+
);
|
|
2993
3080
|
const final_edits = edits.filter(
|
|
2994
3081
|
(e) => e.type !== "modify" || kept_modifies.has(e)
|
|
2995
3082
|
);
|
|
2996
3083
|
return { edits: final_edits, warnings };
|
|
2997
3084
|
}
|
|
3085
|
+
function collect_media_difference_warnings(original_docx, modified_docx) {
|
|
3086
|
+
const media_hashes = (data) => {
|
|
3087
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
3088
|
+
try {
|
|
3089
|
+
const unzipped = (0, import_fflate2.unzipSync)(data);
|
|
3090
|
+
for (const [name, bytes] of Object.entries(unzipped)) {
|
|
3091
|
+
if (!name.startsWith("word/media/")) continue;
|
|
3092
|
+
let h1 = 2166136261;
|
|
3093
|
+
let h2 = 3421674724;
|
|
3094
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
3095
|
+
h1 = Math.imul(h1 ^ bytes[i], 16777619) >>> 0;
|
|
3096
|
+
h2 = Math.imul(h2 ^ bytes[bytes.length - 1 - i], 16777619) >>> 0;
|
|
3097
|
+
}
|
|
3098
|
+
hashes.set(name, `${bytes.length}:${h1.toString(16)}:${h2.toString(16)}`);
|
|
3099
|
+
}
|
|
3100
|
+
} catch {
|
|
3101
|
+
return /* @__PURE__ */ new Map();
|
|
3102
|
+
}
|
|
3103
|
+
return hashes;
|
|
3104
|
+
};
|
|
3105
|
+
const hashes_orig = media_hashes(original_docx);
|
|
3106
|
+
const hashes_mod = media_hashes(modified_docx);
|
|
3107
|
+
const changed = [];
|
|
3108
|
+
const added = [];
|
|
3109
|
+
const removed = [];
|
|
3110
|
+
for (const [name, hash] of hashes_orig) {
|
|
3111
|
+
if (!hashes_mod.has(name)) removed.push(name);
|
|
3112
|
+
else if (hashes_mod.get(name) !== hash) changed.push(name);
|
|
3113
|
+
}
|
|
3114
|
+
for (const name of hashes_mod.keys()) {
|
|
3115
|
+
if (!hashes_orig.has(name)) added.push(name);
|
|
3116
|
+
}
|
|
3117
|
+
changed.sort();
|
|
3118
|
+
added.sort();
|
|
3119
|
+
removed.sort();
|
|
3120
|
+
if (changed.length + added.length + removed.length === 0) return [];
|
|
3121
|
+
const parts = [];
|
|
3122
|
+
if (changed.length) parts.push(`${changed.length} changed`);
|
|
3123
|
+
if (added.length) parts.push(`${added.length} added`);
|
|
3124
|
+
if (removed.length) parts.push(`${removed.length} removed`);
|
|
3125
|
+
const names = [...changed, ...added, ...removed].slice(0, 5).join(", ");
|
|
3126
|
+
return [
|
|
3127
|
+
`The documents' embedded media differ (${parts.join(", ")}: ${names}). This diff compares TEXT only \u2014 an empty edit list does not mean the documents are visually identical. Image changes must be applied manually in Word.`
|
|
3128
|
+
];
|
|
3129
|
+
}
|
|
2998
3130
|
function create_unified_diff(original_text, modified_text, context_lines = 3) {
|
|
2999
3131
|
const dmp = new import_diff_match_patch.default.diff_match_patch();
|
|
3000
3132
|
dmp.Diff_Timeout = 2;
|
|
@@ -3949,6 +4081,13 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
3949
4081
|
console.error("generate_edits_from_text failed, falling back to wholesale edit", e);
|
|
3950
4082
|
raw_sub_edits = [];
|
|
3951
4083
|
}
|
|
4084
|
+
const _marker_only = (text) => {
|
|
4085
|
+
const stripped = text.trim();
|
|
4086
|
+
return stripped.length > 0 && /^[*_]+$/.test(stripped);
|
|
4087
|
+
};
|
|
4088
|
+
raw_sub_edits = raw_sub_edits.filter(
|
|
4089
|
+
(e) => !((!e.target_text || _marker_only(e.target_text)) && (!e.new_text || _marker_only(e.new_text)) && (e.target_text || e.new_text))
|
|
4090
|
+
);
|
|
3952
4091
|
if (!raw_sub_edits || raw_sub_edits.length === 0) {
|
|
3953
4092
|
const fallback_edit = {
|
|
3954
4093
|
type: "modify",
|
|
@@ -4729,7 +4868,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4729
4868
|
*
|
|
4730
4869
|
* Does NOT attach comments; callers handle that.
|
|
4731
4870
|
*/
|
|
4732
|
-
_track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id, positional_anchor_el = null) {
|
|
4871
|
+
_track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id, positional_anchor_el = null, suppress_emphasis = false) {
|
|
4733
4872
|
if (!text) {
|
|
4734
4873
|
return {
|
|
4735
4874
|
first_node: null,
|
|
@@ -4793,7 +4932,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4793
4932
|
first_clean === lines[0] ? lines[0] : lines[0],
|
|
4794
4933
|
anchor_run,
|
|
4795
4934
|
reuse_id,
|
|
4796
|
-
xmlDoc
|
|
4935
|
+
xmlDoc,
|
|
4936
|
+
suppress_emphasis
|
|
4797
4937
|
);
|
|
4798
4938
|
first_node = inline_ins;
|
|
4799
4939
|
}
|
|
@@ -4857,7 +4997,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4857
4997
|
clean_text,
|
|
4858
4998
|
anchor_run,
|
|
4859
4999
|
reuse_id,
|
|
4860
|
-
xmlDoc
|
|
5000
|
+
xmlDoc,
|
|
5001
|
+
suppress_emphasis
|
|
4861
5002
|
);
|
|
4862
5003
|
if (content_ins) {
|
|
4863
5004
|
new_p.appendChild(content_ins);
|
|
@@ -4883,7 +5024,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4883
5024
|
* <w:r> elements representing the inline markdown segments of `line_text`.
|
|
4884
5025
|
* Returns null if line_text is empty.
|
|
4885
5026
|
*/
|
|
4886
|
-
_build_tracked_ins_for_line(line_text, anchor_run, reuse_id, xmlDoc) {
|
|
5027
|
+
_build_tracked_ins_for_line(line_text, anchor_run, reuse_id, xmlDoc, suppress_emphasis = false) {
|
|
4887
5028
|
if (!line_text && line_text !== "") return null;
|
|
4888
5029
|
const ins = this._create_track_change_tag("w:ins", "", reuse_id);
|
|
4889
5030
|
const segments = this._parse_inline_markdown(line_text);
|
|
@@ -4896,15 +5037,11 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4896
5037
|
const anchor_rPr = findChild(anchor_run._element, "w:rPr");
|
|
4897
5038
|
if (anchor_rPr) {
|
|
4898
5039
|
const clone = anchor_rPr.cloneNode(true);
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
"w:
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
"w:iCs",
|
|
4905
|
-
"w:b",
|
|
4906
|
-
"w:bCs"
|
|
4907
|
-
]) {
|
|
5040
|
+
const strip_tags = ["w:vanish", "w:strike", "w:dstrike", "w:i", "w:iCs"];
|
|
5041
|
+
if (suppress_emphasis) {
|
|
5042
|
+
strip_tags.push("w:b", "w:bCs");
|
|
5043
|
+
}
|
|
5044
|
+
for (const tag of strip_tags) {
|
|
4908
5045
|
const found = findChild(clone, tag);
|
|
4909
5046
|
if (found) clone.removeChild(found);
|
|
4910
5047
|
}
|
|
@@ -4939,6 +5076,27 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4939
5076
|
}
|
|
4940
5077
|
return [text, null];
|
|
4941
5078
|
}
|
|
5079
|
+
/**
|
|
5080
|
+
* True when this edit's target or replacement text carries explicit
|
|
5081
|
+
* bold/italic markers, making the markers AUTHORITATIVE for the inserted
|
|
5082
|
+
* runs' formatting. Replacing `**X**` with `_X_` must yield italic-only
|
|
5083
|
+
* text, and replacing `**X**` with `X` must yield plain text — inheriting
|
|
5084
|
+
* the replaced span's run properties on top of (or instead of) the
|
|
5085
|
+
* requested markers silently produces the wrong document while the report
|
|
5086
|
+
* claims success (QA 2026-07-19 F-02). Plain-text edits (no markers on
|
|
5087
|
+
* either side) keep inheriting the context style so partial replacements
|
|
5088
|
+
* inside a styled span never lose formatting.
|
|
5089
|
+
*/
|
|
5090
|
+
_edit_declares_emphasis(edit) {
|
|
5091
|
+
for (const text of [edit?.target_text, edit?.new_text]) {
|
|
5092
|
+
if (!text || !text.includes("**") && !text.includes("_")) continue;
|
|
5093
|
+
const segments = this._parse_inline_markdown(text);
|
|
5094
|
+
if (segments.some(([, props]) => props && Object.keys(props).length > 0)) {
|
|
5095
|
+
return true;
|
|
5096
|
+
}
|
|
5097
|
+
}
|
|
5098
|
+
return false;
|
|
5099
|
+
}
|
|
4942
5100
|
_parse_inline_markdown(text, baseStyle = {}) {
|
|
4943
5101
|
if (!text) return [];
|
|
4944
5102
|
const tokenPattern = /(\*\*.*?\*\*)|(_.*?_)/;
|
|
@@ -5205,6 +5363,16 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5205
5363
|
continue;
|
|
5206
5364
|
const is_regex = edit.regex || false;
|
|
5207
5365
|
const match_mode = edit.match_mode || "strict";
|
|
5366
|
+
if (is_regex) {
|
|
5367
|
+
try {
|
|
5368
|
+
new RegExp(edit.target_text);
|
|
5369
|
+
} catch (regex_err) {
|
|
5370
|
+
errors.push(
|
|
5371
|
+
`- Edit ${i + 1 + index_offset} Failed: target_text is not a valid regular expression (${regex_err?.message ?? regex_err}). Fix the pattern, or set "regex": false to match the text literally.`
|
|
5372
|
+
);
|
|
5373
|
+
continue;
|
|
5374
|
+
}
|
|
5375
|
+
}
|
|
5208
5376
|
let matches = this.mapper.find_all_match_indices(
|
|
5209
5377
|
edit.target_text,
|
|
5210
5378
|
is_regex
|
|
@@ -5784,6 +5952,14 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5784
5952
|
actions_skipped: skipped_actions,
|
|
5785
5953
|
edits_applied: applied_edits,
|
|
5786
5954
|
edits_skipped: skipped_edits,
|
|
5955
|
+
// edits_applied counts change OBJECTS; this is the total number of
|
|
5956
|
+
// document occurrences they modified (match_mode="all" fan-out), so
|
|
5957
|
+
// automation never has to guess which of the two a count means
|
|
5958
|
+
// (QA 2026-07-19 F-21).
|
|
5959
|
+
occurrences_modified: edits_reports.reduce(
|
|
5960
|
+
(sum, r) => sum + (r.occurrences_modified || 0),
|
|
5961
|
+
0
|
|
5962
|
+
),
|
|
5787
5963
|
skipped_details: this.skipped_details,
|
|
5788
5964
|
edits: edits_reports,
|
|
5789
5965
|
engine: "node",
|
|
@@ -5805,6 +5981,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5805
5981
|
}
|
|
5806
5982
|
edit._applied_status = false;
|
|
5807
5983
|
edit._error_msg = null;
|
|
5984
|
+
edit._any_sub_failure = false;
|
|
5808
5985
|
}
|
|
5809
5986
|
for (const edit of edits) {
|
|
5810
5987
|
if (typeof edit !== "object" || edit === null) continue;
|
|
@@ -5904,17 +6081,18 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5904
6081
|
this.skipped_details.push(msg);
|
|
5905
6082
|
edit._applied_status = false;
|
|
5906
6083
|
edit._error_msg = msg;
|
|
6084
|
+
edit._any_sub_failure = true;
|
|
5907
6085
|
const parent = edit._parent_edit_ref;
|
|
5908
6086
|
if (parent) {
|
|
5909
6087
|
parent._applied_status = false;
|
|
5910
6088
|
parent._error_msg = msg;
|
|
6089
|
+
parent._any_sub_failure = true;
|
|
5911
6090
|
}
|
|
5912
6091
|
continue;
|
|
5913
6092
|
}
|
|
5914
6093
|
let success = false;
|
|
5915
6094
|
if (edit.type === "modify") {
|
|
5916
|
-
|
|
5917
|
-
success = this._apply_single_edit_indexed(edit, orig_new, rebuild);
|
|
6095
|
+
success = this._apply_single_edit_indexed(edit, orig_new, false);
|
|
5918
6096
|
} else if (edit.type === "insert_row" || edit.type === "delete_row") {
|
|
5919
6097
|
success = this._apply_table_edit(edit, false);
|
|
5920
6098
|
}
|
|
@@ -5964,8 +6142,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5964
6142
|
this.skipped_details.push(msg);
|
|
5965
6143
|
edit._applied_status = false;
|
|
5966
6144
|
edit._error_msg = msg;
|
|
6145
|
+
edit._any_sub_failure = true;
|
|
5967
6146
|
const parent = edit._parent_edit_ref;
|
|
5968
6147
|
if (parent) {
|
|
6148
|
+
parent._any_sub_failure = true;
|
|
5969
6149
|
if (!parent._applied_status) {
|
|
5970
6150
|
parent._applied_status = false;
|
|
5971
6151
|
parent._error_msg = msg;
|
|
@@ -5973,7 +6153,20 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5973
6153
|
}
|
|
5974
6154
|
}
|
|
5975
6155
|
}
|
|
5976
|
-
|
|
6156
|
+
let applied_logical = 0;
|
|
6157
|
+
let skipped_logical = 0;
|
|
6158
|
+
for (const input_edit of edits) {
|
|
6159
|
+
if (typeof input_edit !== "object" || input_edit === null) {
|
|
6160
|
+
skipped_logical++;
|
|
6161
|
+
continue;
|
|
6162
|
+
}
|
|
6163
|
+
if (input_edit._applied_status && !input_edit._any_sub_failure) {
|
|
6164
|
+
applied_logical++;
|
|
6165
|
+
} else {
|
|
6166
|
+
skipped_logical++;
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
6169
|
+
return [applied_logical, skipped_logical];
|
|
5977
6170
|
}
|
|
5978
6171
|
apply_review_actions(actions) {
|
|
5979
6172
|
let applied = 0;
|
|
@@ -6466,6 +6659,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6466
6659
|
op = "MODIFICATION";
|
|
6467
6660
|
}
|
|
6468
6661
|
}
|
|
6662
|
+
const suppress_emphasis = this._edit_declares_emphasis(edit);
|
|
6469
6663
|
if (op === "STYLE_ONLY" || op === "STYLE_AND_TEXT") {
|
|
6470
6664
|
const [anchor_run, anchor_para] = active_mapper.get_insertion_anchor(
|
|
6471
6665
|
start_idx,
|
|
@@ -6666,7 +6860,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6666
6860
|
clean_text,
|
|
6667
6861
|
anchor_run,
|
|
6668
6862
|
ins_id,
|
|
6669
|
-
xmlDoc
|
|
6863
|
+
xmlDoc,
|
|
6864
|
+
suppress_emphasis
|
|
6670
6865
|
);
|
|
6671
6866
|
if (content_ins) new_p.appendChild(content_ins);
|
|
6672
6867
|
body.insertBefore(new_p, _bug233_target_para);
|
|
@@ -6698,7 +6893,9 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6698
6893
|
final_new_text,
|
|
6699
6894
|
anchor_run,
|
|
6700
6895
|
anchor_para,
|
|
6701
|
-
ins_id
|
|
6896
|
+
ins_id,
|
|
6897
|
+
null,
|
|
6898
|
+
suppress_emphasis
|
|
6702
6899
|
);
|
|
6703
6900
|
if (!result.first_node) return false;
|
|
6704
6901
|
const is_inline_first = result.first_node.tagName === "w:ins";
|
|
@@ -6849,7 +7046,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6849
7046
|
ins_id,
|
|
6850
7047
|
// The insertion physically follows the deletion block; the style
|
|
6851
7048
|
// run was detached when the deletion cloned it into <w:del>.
|
|
6852
|
-
last_del
|
|
7049
|
+
last_del,
|
|
7050
|
+
suppress_emphasis
|
|
6853
7051
|
);
|
|
6854
7052
|
if (result.first_node) {
|
|
6855
7053
|
const is_inline_first = result.first_node.tagName === "w:ins";
|
|
@@ -6883,7 +7081,9 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6883
7081
|
edit.new_text,
|
|
6884
7082
|
anchor,
|
|
6885
7083
|
first_span.paragraph,
|
|
6886
|
-
ins_id
|
|
7084
|
+
ins_id,
|
|
7085
|
+
null,
|
|
7086
|
+
suppress_emphasis
|
|
6887
7087
|
);
|
|
6888
7088
|
if (result.first_node) {
|
|
6889
7089
|
p1_el.appendChild(result.first_node);
|
|
@@ -7367,6 +7567,26 @@ function normalize_change_dates(doc) {
|
|
|
7367
7567
|
}
|
|
7368
7568
|
return count ? [`Track change timestamps: ${count} normalized`] : [];
|
|
7369
7569
|
}
|
|
7570
|
+
function normalize_comment_dates(doc) {
|
|
7571
|
+
let count = 0;
|
|
7572
|
+
const fixed = "2025-01-01T00:00:00Z";
|
|
7573
|
+
for (const part of doc.pkg.parts) {
|
|
7574
|
+
if (!part.contentType.includes("comments")) continue;
|
|
7575
|
+
for (const el of findAllDescendants(part._element, "w:comment")) {
|
|
7576
|
+
if (el.hasAttribute("w:date")) {
|
|
7577
|
+
el.setAttribute("w:date", fixed);
|
|
7578
|
+
count++;
|
|
7579
|
+
}
|
|
7580
|
+
}
|
|
7581
|
+
for (const el of findAllDescendants(part._element, "w16cex:commentExtensible")) {
|
|
7582
|
+
if (el.hasAttribute("w16cex:dateUtc")) {
|
|
7583
|
+
el.setAttribute("w16cex:dateUtc", fixed);
|
|
7584
|
+
count++;
|
|
7585
|
+
}
|
|
7586
|
+
}
|
|
7587
|
+
}
|
|
7588
|
+
return count ? [`Comment timestamps: ${count} normalized`] : [];
|
|
7589
|
+
}
|
|
7370
7590
|
function scrub_doc_properties(doc) {
|
|
7371
7591
|
const lines = [];
|
|
7372
7592
|
const corePart = doc.pkg.getPartByPath("docProps/core.xml");
|
|
@@ -8110,8 +8330,10 @@ function build_paragraph_text(paragraph, comments_map, cleanView, style_cache, d
|
|
|
8110
8330
|
const new_wrappers = cleanView ? ["", ""] : _get_wrappers(active_ins, active_del, active_comments, active_fmt);
|
|
8111
8331
|
const new_style = [prefix, suffix];
|
|
8112
8332
|
if (pending_text && new_wrappers[0] === current_wrappers[0] && new_wrappers[1] === current_wrappers[1]) {
|
|
8113
|
-
|
|
8114
|
-
|
|
8333
|
+
const escaped_prefix = new_style[0].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8334
|
+
const lead_match = new_style[0] !== "" || new_style[1] !== "" ? seg.match(new RegExp("^(\\s*)" + escaped_prefix)) : null;
|
|
8335
|
+
if (new_style[0] === current_style[0] && new_style[1] === current_style[1] && current_style[0] !== "" && pending_text.endsWith(current_style[1]) && lead_match !== null) {
|
|
8336
|
+
pending_text = pending_text.slice(0, -current_style[1].length) + lead_match[1] + seg.slice(lead_match[0].length);
|
|
8115
8337
|
} else {
|
|
8116
8338
|
pending_text += seg;
|
|
8117
8339
|
}
|
|
@@ -8878,7 +9100,7 @@ function _collect_footnote_ids_fast(owned_items) {
|
|
|
8878
9100
|
}
|
|
8879
9101
|
|
|
8880
9102
|
// src/sanitize/core.ts
|
|
8881
|
-
var
|
|
9103
|
+
var import_fflate3 = require("fflate");
|
|
8882
9104
|
|
|
8883
9105
|
// src/sanitize/report.ts
|
|
8884
9106
|
var SanitizeReport = class {
|
|
@@ -9033,6 +9255,7 @@ async function finalize_document(doc, options) {
|
|
|
9033
9255
|
for (const w of warnings) report.warnings.push(w);
|
|
9034
9256
|
for (const w of detect_watermarks(doc)) report.warnings.push(w);
|
|
9035
9257
|
report.add_transform_lines(normalize_change_dates(doc));
|
|
9258
|
+
report.add_transform_lines(normalize_comment_dates(doc));
|
|
9036
9259
|
if (options.protection_mode === "read_only" || options.protection_mode === "encrypt") {
|
|
9037
9260
|
if (options.protection_mode === "encrypt") {
|
|
9038
9261
|
report.warnings.push("Encryption mode (AES compound wrappers) is strictly unsupported in the zero-dependency Node engine. Falling back to native Word Read-Only lock.");
|
|
@@ -9091,7 +9314,7 @@ var VERIFIED_CORE_FIELDS = [
|
|
|
9091
9314
|
["version", "version (cp:version)"]
|
|
9092
9315
|
];
|
|
9093
9316
|
function verifySanitizedPackage(outBuffer) {
|
|
9094
|
-
const unzipped = (0,
|
|
9317
|
+
const unzipped = (0, import_fflate3.unzipSync)(new Uint8Array(outBuffer));
|
|
9095
9318
|
const names = Object.keys(unzipped);
|
|
9096
9319
|
const problems = [];
|
|
9097
9320
|
if (names.includes("docProps/custom.xml")) {
|
|
@@ -9101,7 +9324,7 @@ function verifySanitizedPackage(outBuffer) {
|
|
|
9101
9324
|
problems.push("customXml/* parts are still in the package");
|
|
9102
9325
|
}
|
|
9103
9326
|
if (names.includes("docProps/core.xml")) {
|
|
9104
|
-
const core = parseXml((0,
|
|
9327
|
+
const core = parseXml((0, import_fflate3.strFromU8)(unzipped["docProps/core.xml"]));
|
|
9105
9328
|
for (const [local, label] of VERIFIED_CORE_FIELDS) {
|
|
9106
9329
|
for (const el of findDescendantsByLocalName(core.documentElement, local)) {
|
|
9107
9330
|
if ((el.textContent || "").trim()) {
|
|
@@ -9131,6 +9354,7 @@ function identifyEngine() {
|
|
|
9131
9354
|
USER_PATTERN_TIMEOUT_MS,
|
|
9132
9355
|
_extractTextFromDoc,
|
|
9133
9356
|
apply_edits_to_markdown,
|
|
9357
|
+
collect_media_difference_warnings,
|
|
9134
9358
|
create_unified_diff,
|
|
9135
9359
|
create_word_patch_diff,
|
|
9136
9360
|
describe_illegal_control_chars,
|