@adeu/core 1.25.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +354 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +352 -76
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/diff.ts +129 -4
- package/src/engine.ts +228 -24
- package/src/index.ts +1 -1
- package/src/ingest.ts +11 -2
- package/src/mapper.ts +107 -47
- package/src/repro_qa_report_v7.test.ts +380 -0
- package/src/repro_qa_report_v8.test.ts +265 -0
- package/src/sanitize/core.ts +3 -0
- package/src/sanitize/transforms.ts +29 -0
- package/src/utils/docx.ts +30 -2
package/dist/index.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 = Math.max(start_idx, first_real_span.start) - 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, insert_before = false) {
|
|
4733
4872
|
if (!text) {
|
|
4734
4873
|
return {
|
|
4735
4874
|
first_node: null,
|
|
@@ -4751,7 +4890,11 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4751
4890
|
current_p = walker;
|
|
4752
4891
|
}
|
|
4753
4892
|
const suffix_nodes = [];
|
|
4754
|
-
const
|
|
4893
|
+
const relocatable = /* @__PURE__ */ new Set(["w:r", "w:ins", "w:del"]);
|
|
4894
|
+
const pos_from_positional = positional_anchor_el && positional_anchor_el.parentNode ? positional_anchor_el : null;
|
|
4895
|
+
const pos_from_anchor_run = anchor_run !== null && anchor_run._element.parentNode ? anchor_run._element : null;
|
|
4896
|
+
const pos_source = pos_from_positional ?? pos_from_anchor_run;
|
|
4897
|
+
const suffix_includes_anchor = insert_before && pos_from_positional === null && pos_from_anchor_run !== null;
|
|
4755
4898
|
if (current_p !== null && pos_source !== null) {
|
|
4756
4899
|
let pos_anchor = pos_source;
|
|
4757
4900
|
while (pos_anchor && pos_anchor.parentNode !== current_p) {
|
|
@@ -4762,8 +4905,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4762
4905
|
}
|
|
4763
4906
|
}
|
|
4764
4907
|
if (pos_anchor) {
|
|
4765
|
-
|
|
4766
|
-
let nxt = pos_anchor.nextSibling;
|
|
4908
|
+
let nxt = suffix_includes_anchor ? pos_anchor : pos_anchor.nextSibling;
|
|
4767
4909
|
while (nxt) {
|
|
4768
4910
|
if (nxt.nodeType === 1 && relocatable.has(nxt.tagName)) {
|
|
4769
4911
|
suffix_nodes.push(nxt);
|
|
@@ -4771,6 +4913,14 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4771
4913
|
nxt = nxt.nextSibling;
|
|
4772
4914
|
}
|
|
4773
4915
|
}
|
|
4916
|
+
} else if (current_p !== null && insert_before) {
|
|
4917
|
+
let child = current_p.firstChild;
|
|
4918
|
+
while (child) {
|
|
4919
|
+
if (child.nodeType === 1 && relocatable.has(child.tagName)) {
|
|
4920
|
+
suffix_nodes.push(child);
|
|
4921
|
+
}
|
|
4922
|
+
child = child.nextSibling;
|
|
4923
|
+
}
|
|
4774
4924
|
}
|
|
4775
4925
|
while (lines.length > 1 && lines[lines.length - 1] === "" && suffix_nodes.length === 0) {
|
|
4776
4926
|
lines.pop();
|
|
@@ -4793,7 +4943,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4793
4943
|
first_clean === lines[0] ? lines[0] : lines[0],
|
|
4794
4944
|
anchor_run,
|
|
4795
4945
|
reuse_id,
|
|
4796
|
-
xmlDoc
|
|
4946
|
+
xmlDoc,
|
|
4947
|
+
suppress_emphasis
|
|
4797
4948
|
);
|
|
4798
4949
|
first_node = inline_ins;
|
|
4799
4950
|
}
|
|
@@ -4857,7 +5008,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4857
5008
|
clean_text,
|
|
4858
5009
|
anchor_run,
|
|
4859
5010
|
reuse_id,
|
|
4860
|
-
xmlDoc
|
|
5011
|
+
xmlDoc,
|
|
5012
|
+
suppress_emphasis
|
|
4861
5013
|
);
|
|
4862
5014
|
if (content_ins) {
|
|
4863
5015
|
new_p.appendChild(content_ins);
|
|
@@ -4883,7 +5035,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4883
5035
|
* <w:r> elements representing the inline markdown segments of `line_text`.
|
|
4884
5036
|
* Returns null if line_text is empty.
|
|
4885
5037
|
*/
|
|
4886
|
-
_build_tracked_ins_for_line(line_text, anchor_run, reuse_id, xmlDoc) {
|
|
5038
|
+
_build_tracked_ins_for_line(line_text, anchor_run, reuse_id, xmlDoc, suppress_emphasis = false) {
|
|
4887
5039
|
if (!line_text && line_text !== "") return null;
|
|
4888
5040
|
const ins = this._create_track_change_tag("w:ins", "", reuse_id);
|
|
4889
5041
|
const segments = this._parse_inline_markdown(line_text);
|
|
@@ -4896,15 +5048,11 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4896
5048
|
const anchor_rPr = findChild(anchor_run._element, "w:rPr");
|
|
4897
5049
|
if (anchor_rPr) {
|
|
4898
5050
|
const clone = anchor_rPr.cloneNode(true);
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
"w:
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
"w:iCs",
|
|
4905
|
-
"w:b",
|
|
4906
|
-
"w:bCs"
|
|
4907
|
-
]) {
|
|
5051
|
+
const strip_tags = ["w:vanish", "w:strike", "w:dstrike", "w:i", "w:iCs"];
|
|
5052
|
+
if (suppress_emphasis) {
|
|
5053
|
+
strip_tags.push("w:b", "w:bCs");
|
|
5054
|
+
}
|
|
5055
|
+
for (const tag of strip_tags) {
|
|
4908
5056
|
const found = findChild(clone, tag);
|
|
4909
5057
|
if (found) clone.removeChild(found);
|
|
4910
5058
|
}
|
|
@@ -4939,6 +5087,27 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4939
5087
|
}
|
|
4940
5088
|
return [text, null];
|
|
4941
5089
|
}
|
|
5090
|
+
/**
|
|
5091
|
+
* True when this edit's target or replacement text carries explicit
|
|
5092
|
+
* bold/italic markers, making the markers AUTHORITATIVE for the inserted
|
|
5093
|
+
* runs' formatting. Replacing `**X**` with `_X_` must yield italic-only
|
|
5094
|
+
* text, and replacing `**X**` with `X` must yield plain text — inheriting
|
|
5095
|
+
* the replaced span's run properties on top of (or instead of) the
|
|
5096
|
+
* requested markers silently produces the wrong document while the report
|
|
5097
|
+
* claims success (QA 2026-07-19 F-02). Plain-text edits (no markers on
|
|
5098
|
+
* either side) keep inheriting the context style so partial replacements
|
|
5099
|
+
* inside a styled span never lose formatting.
|
|
5100
|
+
*/
|
|
5101
|
+
_edit_declares_emphasis(edit) {
|
|
5102
|
+
for (const text of [edit?.target_text, edit?.new_text]) {
|
|
5103
|
+
if (!text || !text.includes("**") && !text.includes("_")) continue;
|
|
5104
|
+
const segments = this._parse_inline_markdown(text);
|
|
5105
|
+
if (segments.some(([, props]) => props && Object.keys(props).length > 0)) {
|
|
5106
|
+
return true;
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
return false;
|
|
5110
|
+
}
|
|
4942
5111
|
_parse_inline_markdown(text, baseStyle = {}) {
|
|
4943
5112
|
if (!text) return [];
|
|
4944
5113
|
const tokenPattern = /(\*\*.*?\*\*)|(_.*?_)/;
|
|
@@ -5205,6 +5374,16 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5205
5374
|
continue;
|
|
5206
5375
|
const is_regex = edit.regex || false;
|
|
5207
5376
|
const match_mode = edit.match_mode || "strict";
|
|
5377
|
+
if (is_regex) {
|
|
5378
|
+
try {
|
|
5379
|
+
new RegExp(edit.target_text);
|
|
5380
|
+
} catch (regex_err) {
|
|
5381
|
+
errors.push(
|
|
5382
|
+
`- 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.`
|
|
5383
|
+
);
|
|
5384
|
+
continue;
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5208
5387
|
let matches = this.mapper.find_all_match_indices(
|
|
5209
5388
|
edit.target_text,
|
|
5210
5389
|
is_regex
|
|
@@ -5480,6 +5659,45 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5480
5659
|
}
|
|
5481
5660
|
validate_review_actions(actions) {
|
|
5482
5661
|
const errors = [];
|
|
5662
|
+
const seen_resolutions = /* @__PURE__ */ new Map();
|
|
5663
|
+
const seen_replies = /* @__PURE__ */ new Set();
|
|
5664
|
+
for (let i = 0; i < actions.length; i++) {
|
|
5665
|
+
const action = actions[i];
|
|
5666
|
+
const type = action.type;
|
|
5667
|
+
const target_id = action.target_id ?? "";
|
|
5668
|
+
if (type === "reply") {
|
|
5669
|
+
if (!String(action.text ?? "").trim()) {
|
|
5670
|
+
errors.push(
|
|
5671
|
+
`- Action ${i + 1} Failed: reply text for ${target_id} is empty or whitespace-only. Word would show a blank comment bubble \u2014 provide the reply content in 'text'.`
|
|
5672
|
+
);
|
|
5673
|
+
continue;
|
|
5674
|
+
}
|
|
5675
|
+
const reply_key = `${target_id}\0${String(action.text).trim()}`;
|
|
5676
|
+
if (seen_replies.has(reply_key)) {
|
|
5677
|
+
errors.push(
|
|
5678
|
+
`- Action ${i + 1} Failed: duplicate reply \u2014 this batch already replies to ${target_id} with the same text. Remove the duplicate action.`
|
|
5679
|
+
);
|
|
5680
|
+
}
|
|
5681
|
+
seen_replies.add(reply_key);
|
|
5682
|
+
} else if (type === "accept" || type === "reject") {
|
|
5683
|
+
const prior = seen_resolutions.get(target_id);
|
|
5684
|
+
if (prior !== void 0) {
|
|
5685
|
+
const [first_idx, first_type] = prior;
|
|
5686
|
+
if (first_type === type) {
|
|
5687
|
+
errors.push(
|
|
5688
|
+
`- Action ${i + 1} Failed: duplicate action \u2014 Action ${first_idx + 1} in this batch already applies '${type}' to ${target_id}. A change can only be resolved once; remove the duplicate action.`
|
|
5689
|
+
);
|
|
5690
|
+
} else {
|
|
5691
|
+
errors.push(
|
|
5692
|
+
`- Action ${i + 1} Failed: conflicting actions \u2014 Action ${first_idx + 1} in this batch applies '${first_type}' to ${target_id}, but this action applies '${type}'. Decide the outcome and keep exactly one of them.`
|
|
5693
|
+
);
|
|
5694
|
+
}
|
|
5695
|
+
} else {
|
|
5696
|
+
seen_resolutions.set(target_id, [i, type]);
|
|
5697
|
+
}
|
|
5698
|
+
}
|
|
5699
|
+
}
|
|
5700
|
+
if (errors.length > 0) return errors;
|
|
5483
5701
|
for (let i = 0; i < actions.length; i++) {
|
|
5484
5702
|
const action = actions[i];
|
|
5485
5703
|
const type = action.type;
|
|
@@ -5784,6 +6002,14 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5784
6002
|
actions_skipped: skipped_actions,
|
|
5785
6003
|
edits_applied: applied_edits,
|
|
5786
6004
|
edits_skipped: skipped_edits,
|
|
6005
|
+
// edits_applied counts change OBJECTS; this is the total number of
|
|
6006
|
+
// document occurrences they modified (match_mode="all" fan-out), so
|
|
6007
|
+
// automation never has to guess which of the two a count means
|
|
6008
|
+
// (QA 2026-07-19 F-21).
|
|
6009
|
+
occurrences_modified: edits_reports.reduce(
|
|
6010
|
+
(sum, r) => sum + (r.occurrences_modified || 0),
|
|
6011
|
+
0
|
|
6012
|
+
),
|
|
5787
6013
|
skipped_details: this.skipped_details,
|
|
5788
6014
|
edits: edits_reports,
|
|
5789
6015
|
engine: "node",
|
|
@@ -5805,6 +6031,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5805
6031
|
}
|
|
5806
6032
|
edit._applied_status = false;
|
|
5807
6033
|
edit._error_msg = null;
|
|
6034
|
+
edit._any_sub_failure = false;
|
|
5808
6035
|
}
|
|
5809
6036
|
for (const edit of edits) {
|
|
5810
6037
|
if (typeof edit !== "object" || edit === null) continue;
|
|
@@ -5904,17 +6131,18 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5904
6131
|
this.skipped_details.push(msg);
|
|
5905
6132
|
edit._applied_status = false;
|
|
5906
6133
|
edit._error_msg = msg;
|
|
6134
|
+
edit._any_sub_failure = true;
|
|
5907
6135
|
const parent = edit._parent_edit_ref;
|
|
5908
6136
|
if (parent) {
|
|
5909
6137
|
parent._applied_status = false;
|
|
5910
6138
|
parent._error_msg = msg;
|
|
6139
|
+
parent._any_sub_failure = true;
|
|
5911
6140
|
}
|
|
5912
6141
|
continue;
|
|
5913
6142
|
}
|
|
5914
6143
|
let success = false;
|
|
5915
6144
|
if (edit.type === "modify") {
|
|
5916
|
-
|
|
5917
|
-
success = this._apply_single_edit_indexed(edit, orig_new, rebuild);
|
|
6145
|
+
success = this._apply_single_edit_indexed(edit, orig_new, false);
|
|
5918
6146
|
} else if (edit.type === "insert_row" || edit.type === "delete_row") {
|
|
5919
6147
|
success = this._apply_table_edit(edit, false);
|
|
5920
6148
|
}
|
|
@@ -5964,8 +6192,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5964
6192
|
this.skipped_details.push(msg);
|
|
5965
6193
|
edit._applied_status = false;
|
|
5966
6194
|
edit._error_msg = msg;
|
|
6195
|
+
edit._any_sub_failure = true;
|
|
5967
6196
|
const parent = edit._parent_edit_ref;
|
|
5968
6197
|
if (parent) {
|
|
6198
|
+
parent._any_sub_failure = true;
|
|
5969
6199
|
if (!parent._applied_status) {
|
|
5970
6200
|
parent._applied_status = false;
|
|
5971
6201
|
parent._error_msg = msg;
|
|
@@ -5973,7 +6203,20 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5973
6203
|
}
|
|
5974
6204
|
}
|
|
5975
6205
|
}
|
|
5976
|
-
|
|
6206
|
+
let applied_logical = 0;
|
|
6207
|
+
let skipped_logical = 0;
|
|
6208
|
+
for (const input_edit of edits) {
|
|
6209
|
+
if (typeof input_edit !== "object" || input_edit === null) {
|
|
6210
|
+
skipped_logical++;
|
|
6211
|
+
continue;
|
|
6212
|
+
}
|
|
6213
|
+
if (input_edit._applied_status && !input_edit._any_sub_failure) {
|
|
6214
|
+
applied_logical++;
|
|
6215
|
+
} else {
|
|
6216
|
+
skipped_logical++;
|
|
6217
|
+
}
|
|
6218
|
+
}
|
|
6219
|
+
return [applied_logical, skipped_logical];
|
|
5977
6220
|
}
|
|
5978
6221
|
apply_review_actions(actions) {
|
|
5979
6222
|
let applied = 0;
|
|
@@ -6466,6 +6709,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6466
6709
|
op = "MODIFICATION";
|
|
6467
6710
|
}
|
|
6468
6711
|
}
|
|
6712
|
+
const suppress_emphasis = this._edit_declares_emphasis(edit);
|
|
6469
6713
|
if (op === "STYLE_ONLY" || op === "STYLE_AND_TEXT") {
|
|
6470
6714
|
const [anchor_run, anchor_para] = active_mapper.get_insertion_anchor(
|
|
6471
6715
|
start_idx,
|
|
@@ -6666,7 +6910,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6666
6910
|
clean_text,
|
|
6667
6911
|
anchor_run,
|
|
6668
6912
|
ins_id,
|
|
6669
|
-
xmlDoc
|
|
6913
|
+
xmlDoc,
|
|
6914
|
+
suppress_emphasis
|
|
6670
6915
|
);
|
|
6671
6916
|
if (content_ins) new_p.appendChild(content_ins);
|
|
6672
6917
|
body.insertBefore(new_p, _bug233_target_para);
|
|
@@ -6698,7 +6943,12 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6698
6943
|
final_new_text,
|
|
6699
6944
|
anchor_run,
|
|
6700
6945
|
anchor_para,
|
|
6701
|
-
ins_id
|
|
6946
|
+
ins_id,
|
|
6947
|
+
null,
|
|
6948
|
+
suppress_emphasis,
|
|
6949
|
+
// Paragraph-start insertions attach BEFORE the anchor (see
|
|
6950
|
+
// before_anchor below): the suffix relocation must know.
|
|
6951
|
+
start_idx === 0
|
|
6702
6952
|
);
|
|
6703
6953
|
if (!result.first_node) return false;
|
|
6704
6954
|
const is_inline_first = result.first_node.tagName === "w:ins";
|
|
@@ -6849,7 +7099,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6849
7099
|
ins_id,
|
|
6850
7100
|
// The insertion physically follows the deletion block; the style
|
|
6851
7101
|
// run was detached when the deletion cloned it into <w:del>.
|
|
6852
|
-
last_del
|
|
7102
|
+
last_del,
|
|
7103
|
+
suppress_emphasis
|
|
6853
7104
|
);
|
|
6854
7105
|
if (result.first_node) {
|
|
6855
7106
|
const is_inline_first = result.first_node.tagName === "w:ins";
|
|
@@ -6883,7 +7134,9 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6883
7134
|
edit.new_text,
|
|
6884
7135
|
anchor,
|
|
6885
7136
|
first_span.paragraph,
|
|
6886
|
-
ins_id
|
|
7137
|
+
ins_id,
|
|
7138
|
+
null,
|
|
7139
|
+
suppress_emphasis
|
|
6887
7140
|
);
|
|
6888
7141
|
if (result.first_node) {
|
|
6889
7142
|
p1_el.appendChild(result.first_node);
|
|
@@ -7367,6 +7620,26 @@ function normalize_change_dates(doc) {
|
|
|
7367
7620
|
}
|
|
7368
7621
|
return count ? [`Track change timestamps: ${count} normalized`] : [];
|
|
7369
7622
|
}
|
|
7623
|
+
function normalize_comment_dates(doc) {
|
|
7624
|
+
let count = 0;
|
|
7625
|
+
const fixed = "2025-01-01T00:00:00Z";
|
|
7626
|
+
for (const part of doc.pkg.parts) {
|
|
7627
|
+
if (!part.contentType.includes("comments")) continue;
|
|
7628
|
+
for (const el of findAllDescendants(part._element, "w:comment")) {
|
|
7629
|
+
if (el.hasAttribute("w:date")) {
|
|
7630
|
+
el.setAttribute("w:date", fixed);
|
|
7631
|
+
count++;
|
|
7632
|
+
}
|
|
7633
|
+
}
|
|
7634
|
+
for (const el of findAllDescendants(part._element, "w16cex:commentExtensible")) {
|
|
7635
|
+
if (el.hasAttribute("w16cex:dateUtc")) {
|
|
7636
|
+
el.setAttribute("w16cex:dateUtc", fixed);
|
|
7637
|
+
count++;
|
|
7638
|
+
}
|
|
7639
|
+
}
|
|
7640
|
+
}
|
|
7641
|
+
return count ? [`Comment timestamps: ${count} normalized`] : [];
|
|
7642
|
+
}
|
|
7370
7643
|
function scrub_doc_properties(doc) {
|
|
7371
7644
|
const lines = [];
|
|
7372
7645
|
const corePart = doc.pkg.getPartByPath("docProps/core.xml");
|
|
@@ -8110,8 +8383,10 @@ function build_paragraph_text(paragraph, comments_map, cleanView, style_cache, d
|
|
|
8110
8383
|
const new_wrappers = cleanView ? ["", ""] : _get_wrappers(active_ins, active_del, active_comments, active_fmt);
|
|
8111
8384
|
const new_style = [prefix, suffix];
|
|
8112
8385
|
if (pending_text && new_wrappers[0] === current_wrappers[0] && new_wrappers[1] === current_wrappers[1]) {
|
|
8113
|
-
|
|
8114
|
-
|
|
8386
|
+
const escaped_prefix = new_style[0].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8387
|
+
const lead_match = new_style[0] !== "" || new_style[1] !== "" ? seg.match(new RegExp("^(\\s*)" + escaped_prefix)) : null;
|
|
8388
|
+
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) {
|
|
8389
|
+
pending_text = pending_text.slice(0, -current_style[1].length) + lead_match[1] + seg.slice(lead_match[0].length);
|
|
8115
8390
|
} else {
|
|
8116
8391
|
pending_text += seg;
|
|
8117
8392
|
}
|
|
@@ -8878,7 +9153,7 @@ function _collect_footnote_ids_fast(owned_items) {
|
|
|
8878
9153
|
}
|
|
8879
9154
|
|
|
8880
9155
|
// src/sanitize/core.ts
|
|
8881
|
-
var
|
|
9156
|
+
var import_fflate3 = require("fflate");
|
|
8882
9157
|
|
|
8883
9158
|
// src/sanitize/report.ts
|
|
8884
9159
|
var SanitizeReport = class {
|
|
@@ -9033,6 +9308,7 @@ async function finalize_document(doc, options) {
|
|
|
9033
9308
|
for (const w of warnings) report.warnings.push(w);
|
|
9034
9309
|
for (const w of detect_watermarks(doc)) report.warnings.push(w);
|
|
9035
9310
|
report.add_transform_lines(normalize_change_dates(doc));
|
|
9311
|
+
report.add_transform_lines(normalize_comment_dates(doc));
|
|
9036
9312
|
if (options.protection_mode === "read_only" || options.protection_mode === "encrypt") {
|
|
9037
9313
|
if (options.protection_mode === "encrypt") {
|
|
9038
9314
|
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 +9367,7 @@ var VERIFIED_CORE_FIELDS = [
|
|
|
9091
9367
|
["version", "version (cp:version)"]
|
|
9092
9368
|
];
|
|
9093
9369
|
function verifySanitizedPackage(outBuffer) {
|
|
9094
|
-
const unzipped = (0,
|
|
9370
|
+
const unzipped = (0, import_fflate3.unzipSync)(new Uint8Array(outBuffer));
|
|
9095
9371
|
const names = Object.keys(unzipped);
|
|
9096
9372
|
const problems = [];
|
|
9097
9373
|
if (names.includes("docProps/custom.xml")) {
|
|
@@ -9101,7 +9377,7 @@ function verifySanitizedPackage(outBuffer) {
|
|
|
9101
9377
|
problems.push("customXml/* parts are still in the package");
|
|
9102
9378
|
}
|
|
9103
9379
|
if (names.includes("docProps/core.xml")) {
|
|
9104
|
-
const core = parseXml((0,
|
|
9380
|
+
const core = parseXml((0, import_fflate3.strFromU8)(unzipped["docProps/core.xml"]));
|
|
9105
9381
|
for (const [local, label] of VERIFIED_CORE_FIELDS) {
|
|
9106
9382
|
for (const el of findDescendantsByLocalName(core.documentElement, local)) {
|
|
9107
9383
|
if ((el.textContent || "").trim()) {
|
|
@@ -9131,6 +9407,7 @@ function identifyEngine() {
|
|
|
9131
9407
|
USER_PATTERN_TIMEOUT_MS,
|
|
9132
9408
|
_extractTextFromDoc,
|
|
9133
9409
|
apply_edits_to_markdown,
|
|
9410
|
+
collect_media_difference_warnings,
|
|
9134
9411
|
create_unified_diff,
|
|
9135
9412
|
create_word_patch_diff,
|
|
9136
9413
|
describe_illegal_control_chars,
|