@adeu/core 1.26.0 → 1.28.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 +622 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -1
- package/dist/index.d.ts +54 -1
- package/dist/index.js +622 -71
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/diff.ts +236 -3
- package/src/engine.atomic.test.ts +5 -1
- package/src/engine.batch.test.ts +16 -6
- package/src/engine.ts +416 -58
- package/src/ingest.test.ts +3 -1
- package/src/ingest.ts +16 -3
- package/src/mapper.ts +83 -10
- package/src/repro_qa_mcp_issues_2026_07_20.test.ts +224 -0
- package/src/repro_qa_report_v8.test.ts +265 -0
- package/src/repro_qa_report_v9.test.ts +392 -0
- package/src/sanitize/core.ts +11 -0
- package/src/sanitize/report.ts +9 -7
- package/src/sanitize/transforms.ts +39 -0
- package/src/utils/docx.ts +87 -0
package/dist/index.cjs
CHANGED
|
@@ -1226,6 +1226,18 @@ function get_paragraph_prefix(paragraph, style_cache, default_pstyle) {
|
|
|
1226
1226
|
if (custom_level !== null) return "#".repeat(custom_level) + " ";
|
|
1227
1227
|
}
|
|
1228
1228
|
if (!style_name || style_name === "Normal") {
|
|
1229
|
+
let is_inside_tc = false;
|
|
1230
|
+
let curr = paragraph._element;
|
|
1231
|
+
while (curr) {
|
|
1232
|
+
if (curr.tagName === "w:tc") {
|
|
1233
|
+
is_inside_tc = true;
|
|
1234
|
+
break;
|
|
1235
|
+
}
|
|
1236
|
+
curr = curr.parentNode;
|
|
1237
|
+
}
|
|
1238
|
+
if (is_inside_tc) {
|
|
1239
|
+
return "";
|
|
1240
|
+
}
|
|
1229
1241
|
const text = paragraph.text.trim();
|
|
1230
1242
|
if (text && text.length < 100 && text === text.toUpperCase()) {
|
|
1231
1243
|
let is_bold = false;
|
|
@@ -1297,6 +1309,50 @@ function split_boundary_whitespace(text) {
|
|
|
1297
1309
|
text.substring(lead_len + core.length)
|
|
1298
1310
|
];
|
|
1299
1311
|
}
|
|
1312
|
+
function compute_change_pair_map(states_list) {
|
|
1313
|
+
const groups = [];
|
|
1314
|
+
let current = [];
|
|
1315
|
+
const seen_ids = /* @__PURE__ */ new Set();
|
|
1316
|
+
for (const [ins_map, del_map] of states_list) {
|
|
1317
|
+
const ins_entries = Object.entries(ins_map);
|
|
1318
|
+
const del_entries = Object.entries(del_map);
|
|
1319
|
+
if (ins_entries.length === 0 && del_entries.length === 0) {
|
|
1320
|
+
if (current.length > 0) {
|
|
1321
|
+
groups.push(current);
|
|
1322
|
+
current = [];
|
|
1323
|
+
}
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
const state_new = [];
|
|
1327
|
+
for (const [uid, meta] of ins_entries) {
|
|
1328
|
+
if (!seen_ids.has(uid)) {
|
|
1329
|
+
state_new.push([uid, meta && meta.author || "Unknown"]);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
for (const [uid, meta] of del_entries) {
|
|
1333
|
+
if (!seen_ids.has(uid)) {
|
|
1334
|
+
state_new.push([uid, meta && meta.author || "Unknown"]);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
for (const [uid, author] of state_new) {
|
|
1338
|
+
seen_ids.add(uid);
|
|
1339
|
+
if (current.length > 0 && current[current.length - 1][1] !== author) {
|
|
1340
|
+
groups.push(current);
|
|
1341
|
+
current = [];
|
|
1342
|
+
}
|
|
1343
|
+
current.push([uid, author]);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
if (current.length > 0) groups.push(current);
|
|
1347
|
+
const pair_map = {};
|
|
1348
|
+
for (const group of groups) {
|
|
1349
|
+
if (group.length < 2) continue;
|
|
1350
|
+
for (const [uid] of group) {
|
|
1351
|
+
pair_map[uid] = group.filter(([u]) => u !== uid).map(([u]) => `Chg:${u}`).join(", ");
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return pair_map;
|
|
1355
|
+
}
|
|
1300
1356
|
function apply_formatting_to_segments(text, prefix, suffix) {
|
|
1301
1357
|
if (!prefix && !suffix) return text;
|
|
1302
1358
|
if (!text) return "";
|
|
@@ -1704,6 +1760,11 @@ ${header}`;
|
|
|
1704
1760
|
if (paraId && firstP) {
|
|
1705
1761
|
const cellPara = new Paragraph(firstP, cell);
|
|
1706
1762
|
this._add_virtual_text("", current, cellPara);
|
|
1763
|
+
const has_content = current > cell_start;
|
|
1764
|
+
if (has_content) {
|
|
1765
|
+
this._add_virtual_text(" ", current, cellPara);
|
|
1766
|
+
current += 1;
|
|
1767
|
+
}
|
|
1707
1768
|
const anchor = `{#cell:${paraId}}`;
|
|
1708
1769
|
this._add_virtual_text(anchor, current, cellPara);
|
|
1709
1770
|
current += anchor.length;
|
|
@@ -2054,6 +2115,8 @@ ${header}`;
|
|
|
2054
2115
|
const change_lines = [];
|
|
2055
2116
|
const comment_lines = [];
|
|
2056
2117
|
const seen_sigs = /* @__PURE__ */ new Set();
|
|
2118
|
+
const pair_map = compute_change_pair_map(states_list);
|
|
2119
|
+
const pairSuffix = (uid) => pair_map[uid] ? ` (pairs with ${pair_map[uid]})` : "";
|
|
2057
2120
|
for (const [ins_map, del_map, comments_set, fmt_map] of states_list) {
|
|
2058
2121
|
for (const [uid, meta] of Object.entries(
|
|
2059
2122
|
ins_map
|
|
@@ -2061,7 +2124,7 @@ ${header}`;
|
|
|
2061
2124
|
const sig = `Chg:${uid}`;
|
|
2062
2125
|
if (!seen_sigs.has(sig)) {
|
|
2063
2126
|
const auth = meta.author || "Unknown";
|
|
2064
|
-
change_lines.push(`[${sig} insert] ${auth}`);
|
|
2127
|
+
change_lines.push(`[${sig} insert] ${auth}${pairSuffix(uid)}`);
|
|
2065
2128
|
seen_sigs.add(sig);
|
|
2066
2129
|
}
|
|
2067
2130
|
}
|
|
@@ -2071,7 +2134,7 @@ ${header}`;
|
|
|
2071
2134
|
const sig = `Chg:${uid}`;
|
|
2072
2135
|
if (!seen_sigs.has(sig)) {
|
|
2073
2136
|
const auth = meta.author || "Unknown";
|
|
2074
|
-
change_lines.push(`[${sig} delete] ${auth}`);
|
|
2137
|
+
change_lines.push(`[${sig} delete] ${auth}${pairSuffix(uid)}`);
|
|
2075
2138
|
seen_sigs.add(sig);
|
|
2076
2139
|
}
|
|
2077
2140
|
}
|
|
@@ -2211,21 +2274,60 @@ ${header}`;
|
|
|
2211
2274
|
}
|
|
2212
2275
|
return results;
|
|
2213
2276
|
}
|
|
2277
|
+
/**
|
|
2278
|
+
* True when no run-backed span overlaps [start, start+length): the range
|
|
2279
|
+
* covers only virtual projection text — meta bubbles (change/comment
|
|
2280
|
+
* headers, timestamps), style markers, list prefixes. Such text does not
|
|
2281
|
+
* exist in the document, so it can neither satisfy a match nor count
|
|
2282
|
+
* toward ambiguity (QA 2026-07-19 ADEU-QA-002 C): an edit targeting "4"
|
|
2283
|
+
* used to be rejected as "appears 8 times" because a comment bubble's
|
|
2284
|
+
* timestamp matched.
|
|
2285
|
+
*
|
|
2286
|
+
* Anchor tokens ({#Bookmark}, {#cell:paraId}) are the exception: they are
|
|
2287
|
+
* deliberate virtual TARGETING surfaces (empty-cell writes, bookmark
|
|
2288
|
+
* anchors) and must stay matchable.
|
|
2289
|
+
*/
|
|
2290
|
+
range_is_virtual_only(start, length) {
|
|
2291
|
+
const end = start + length;
|
|
2292
|
+
const overlapping = this.spans.filter(
|
|
2293
|
+
(s) => s.end > start && s.start < end
|
|
2294
|
+
);
|
|
2295
|
+
if (overlapping.some((s) => s.run !== null)) return false;
|
|
2296
|
+
return !overlapping.some(
|
|
2297
|
+
(s) => s.run === null && s.text.startsWith("{#")
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
/** Filters find_all_match_indices output down to matches that touch at
|
|
2301
|
+
* least one run-backed span. See range_is_virtual_only. */
|
|
2302
|
+
drop_virtual_only_matches(matches) {
|
|
2303
|
+
return matches.filter(
|
|
2304
|
+
([start, length]) => !this.range_is_virtual_only(start, length)
|
|
2305
|
+
);
|
|
2306
|
+
}
|
|
2214
2307
|
find_match_index(target_text, is_regex = false) {
|
|
2215
2308
|
if (is_regex) {
|
|
2216
2309
|
try {
|
|
2217
2310
|
const match = userSearch(target_text, this.full_text);
|
|
2218
|
-
if (match
|
|
2311
|
+
if (match && !this.range_is_virtual_only(match.start, match.end - match.start)) {
|
|
2312
|
+
return [match.start, match.end - match.start];
|
|
2313
|
+
}
|
|
2219
2314
|
} catch (e) {
|
|
2220
2315
|
if (e instanceof RegexTimeoutError) throw e;
|
|
2221
2316
|
}
|
|
2222
2317
|
return [-1, 0];
|
|
2223
2318
|
}
|
|
2224
|
-
let
|
|
2225
|
-
|
|
2319
|
+
let from = 0;
|
|
2320
|
+
while (true) {
|
|
2321
|
+
const idx = this.full_text.indexOf(target_text, from);
|
|
2322
|
+
if (idx === -1) break;
|
|
2323
|
+
if (!this.range_is_virtual_only(idx, target_text.length)) {
|
|
2324
|
+
return [idx, target_text.length];
|
|
2325
|
+
}
|
|
2326
|
+
from = idx + 1;
|
|
2327
|
+
}
|
|
2226
2328
|
const norm_full = this._replace_smart_quotes(this.full_text);
|
|
2227
2329
|
const norm_target = this._replace_smart_quotes(target_text);
|
|
2228
|
-
start_idx = norm_full.indexOf(norm_target);
|
|
2330
|
+
let start_idx = norm_full.indexOf(norm_target);
|
|
2229
2331
|
if (start_idx !== -1) return [start_idx, target_text.length];
|
|
2230
2332
|
const stripped_target = this._strip_markdown_formatting(target_text);
|
|
2231
2333
|
if (this.full_text.includes(stripped_target)) {
|
|
@@ -2235,9 +2337,12 @@ ${header}`;
|
|
|
2235
2337
|
const plain_first = this._find_plain_projection_matches(target_text);
|
|
2236
2338
|
if (plain_first.length > 0) return plain_first[0];
|
|
2237
2339
|
try {
|
|
2238
|
-
const pattern = new RegExp(this._make_fuzzy_regex(target_text));
|
|
2239
|
-
const match
|
|
2240
|
-
|
|
2340
|
+
const pattern = new RegExp(this._make_fuzzy_regex(target_text), "g");
|
|
2341
|
+
for (const match of this.full_text.matchAll(pattern)) {
|
|
2342
|
+
if (!this.range_is_virtual_only(match.index, match[0].length)) {
|
|
2343
|
+
return [match.index, match[0].length];
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2241
2346
|
} catch (e) {
|
|
2242
2347
|
}
|
|
2243
2348
|
return [-1, 0];
|
|
@@ -2318,7 +2423,7 @@ ${header}`;
|
|
|
2318
2423
|
let dom_modified = false;
|
|
2319
2424
|
const first_real_span = real_spans[0];
|
|
2320
2425
|
let start_split_adjustment = 0;
|
|
2321
|
-
const local_start = start_idx - first_real_span.start + (first_real_span.run_offset || 0);
|
|
2426
|
+
const local_start = Math.max(start_idx, first_real_span.start) - first_real_span.start + (first_real_span.run_offset || 0);
|
|
2322
2427
|
if (local_start > 0) {
|
|
2323
2428
|
const split_source = working_runs[0];
|
|
2324
2429
|
const [, right_run] = this._split_run_at_index(
|
|
@@ -2766,6 +2871,153 @@ function _sequence_opcodes(a, b) {
|
|
|
2766
2871
|
flushPending();
|
|
2767
2872
|
return ops;
|
|
2768
2873
|
}
|
|
2874
|
+
function _is_table_blob(block) {
|
|
2875
|
+
const lines = block.split("\n");
|
|
2876
|
+
return lines.length > 0 && lines.every((line) => line.includes(" | "));
|
|
2877
|
+
}
|
|
2878
|
+
function _table_blob_row_edits(orig_blob, mod_blob) {
|
|
2879
|
+
const rows_o = orig_blob.split("\n");
|
|
2880
|
+
const rows_m = mod_blob.split("\n");
|
|
2881
|
+
if (rows_o.length !== rows_m.length) return null;
|
|
2882
|
+
const row_edits = [];
|
|
2883
|
+
for (let k = 0; k < rows_o.length; k++) {
|
|
2884
|
+
if (rows_o[k] === rows_m[k]) continue;
|
|
2885
|
+
row_edits.push({
|
|
2886
|
+
type: "modify",
|
|
2887
|
+
target_text: rows_o[k],
|
|
2888
|
+
new_text: rows_m[k],
|
|
2889
|
+
comment: "Diff: Table row modified",
|
|
2890
|
+
_is_table_edit: true
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
return row_edits;
|
|
2894
|
+
}
|
|
2895
|
+
function _split_cross_paragraph_hunks(edits) {
|
|
2896
|
+
const out = [];
|
|
2897
|
+
for (const e of edits) {
|
|
2898
|
+
const target = e.target_text || "";
|
|
2899
|
+
const newText = e.new_text || "";
|
|
2900
|
+
const idx = e._match_start_index;
|
|
2901
|
+
if (idx === void 0 || idx === null || !target.includes("\n\n") || target.split("\n\n").length - 1 === newText.split("\n\n").length - 1) {
|
|
2902
|
+
out.push(e);
|
|
2903
|
+
continue;
|
|
2904
|
+
}
|
|
2905
|
+
const parts = target.split("\n\n");
|
|
2906
|
+
if (parts.length < 2 || !parts[0].trim() || !parts[parts.length - 1].trim()) {
|
|
2907
|
+
out.push(e);
|
|
2908
|
+
continue;
|
|
2909
|
+
}
|
|
2910
|
+
let offset = idx;
|
|
2911
|
+
for (const piece of parts.slice(0, -1)) {
|
|
2912
|
+
out.push({
|
|
2913
|
+
type: "modify",
|
|
2914
|
+
target_text: piece + "\n\n",
|
|
2915
|
+
new_text: "",
|
|
2916
|
+
comment: e.comment || "Diff: Text deleted",
|
|
2917
|
+
_match_start_index: offset
|
|
2918
|
+
});
|
|
2919
|
+
offset += piece.length + 2;
|
|
2920
|
+
}
|
|
2921
|
+
out.push({
|
|
2922
|
+
type: "modify",
|
|
2923
|
+
target_text: parts[parts.length - 1],
|
|
2924
|
+
new_text: newText,
|
|
2925
|
+
comment: e.comment,
|
|
2926
|
+
_match_start_index: offset
|
|
2927
|
+
});
|
|
2928
|
+
}
|
|
2929
|
+
return out;
|
|
2930
|
+
}
|
|
2931
|
+
function generate_edits_via_paragraph_alignment(original_text, modified_text) {
|
|
2932
|
+
const orig_paragraphs = original_text.split("\n\n");
|
|
2933
|
+
const mod_paragraphs = modified_text.split("\n\n");
|
|
2934
|
+
const orig_offsets = [];
|
|
2935
|
+
let current_offset = 0;
|
|
2936
|
+
for (const p of orig_paragraphs) {
|
|
2937
|
+
orig_offsets.push(current_offset);
|
|
2938
|
+
current_offset += p.length + 2;
|
|
2939
|
+
}
|
|
2940
|
+
const opcodes = _sequence_opcodes(orig_paragraphs, mod_paragraphs);
|
|
2941
|
+
const edits = [];
|
|
2942
|
+
for (const [tag, i1, i2, j1, j2] of opcodes) {
|
|
2943
|
+
if (tag === "equal") continue;
|
|
2944
|
+
const offset = i1 < orig_offsets.length ? orig_offsets[i1] : original_text.length;
|
|
2945
|
+
if (tag === "delete") {
|
|
2946
|
+
if (i2 < orig_paragraphs.length) {
|
|
2947
|
+
let piece_offset = offset;
|
|
2948
|
+
for (let k = i1; k < i2; k++) {
|
|
2949
|
+
const piece = orig_paragraphs[k] + "\n\n";
|
|
2950
|
+
edits.push({
|
|
2951
|
+
type: "modify",
|
|
2952
|
+
target_text: piece,
|
|
2953
|
+
new_text: "",
|
|
2954
|
+
comment: "Diff: Text deleted",
|
|
2955
|
+
_match_start_index: piece_offset
|
|
2956
|
+
});
|
|
2957
|
+
piece_offset += piece.length;
|
|
2958
|
+
}
|
|
2959
|
+
} else {
|
|
2960
|
+
let deleted_text = orig_paragraphs.slice(i1, i2).join("\n\n");
|
|
2961
|
+
let del_offset = offset;
|
|
2962
|
+
if (i1 > 0) {
|
|
2963
|
+
deleted_text = "\n\n" + deleted_text;
|
|
2964
|
+
del_offset -= 2;
|
|
2965
|
+
}
|
|
2966
|
+
edits.push({
|
|
2967
|
+
type: "modify",
|
|
2968
|
+
target_text: deleted_text,
|
|
2969
|
+
new_text: "",
|
|
2970
|
+
comment: "Diff: Text deleted",
|
|
2971
|
+
_match_start_index: del_offset
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
} else if (tag === "insert") {
|
|
2975
|
+
let inserted_text = mod_paragraphs.slice(j1, j2).join("\n\n");
|
|
2976
|
+
if (i1 < orig_paragraphs.length) {
|
|
2977
|
+
inserted_text = inserted_text + "\n\n";
|
|
2978
|
+
} else {
|
|
2979
|
+
inserted_text = "\n\n" + inserted_text;
|
|
2980
|
+
}
|
|
2981
|
+
edits.push({
|
|
2982
|
+
type: "modify",
|
|
2983
|
+
target_text: "",
|
|
2984
|
+
new_text: inserted_text,
|
|
2985
|
+
comment: "Diff: Text inserted",
|
|
2986
|
+
_match_start_index: offset
|
|
2987
|
+
});
|
|
2988
|
+
} else if (tag === "replace") {
|
|
2989
|
+
if (i2 - i1 === j2 - j1 && Array.from({ length: i2 - i1 }).some(
|
|
2990
|
+
(_, k) => _is_table_blob(orig_paragraphs[i1 + k]) || _is_table_blob(mod_paragraphs[j1 + k])
|
|
2991
|
+
)) {
|
|
2992
|
+
for (let k = 0; k < i2 - i1; k++) {
|
|
2993
|
+
const orig_p = orig_paragraphs[i1 + k];
|
|
2994
|
+
const mod_p = mod_paragraphs[j1 + k];
|
|
2995
|
+
if (orig_p === mod_p) continue;
|
|
2996
|
+
const pair_offset = orig_offsets[i1 + k];
|
|
2997
|
+
const row_edits = _is_table_blob(orig_p) && _is_table_blob(mod_p) ? _table_blob_row_edits(orig_p, mod_p) : null;
|
|
2998
|
+
if (row_edits !== null) {
|
|
2999
|
+
edits.push(...row_edits);
|
|
3000
|
+
continue;
|
|
3001
|
+
}
|
|
3002
|
+
const pair_edits = generate_edits_from_text(orig_p, mod_p);
|
|
3003
|
+
for (const ce of pair_edits) {
|
|
3004
|
+
ce._match_start_index = (ce._match_start_index || 0) + pair_offset;
|
|
3005
|
+
edits.push(ce);
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
continue;
|
|
3009
|
+
}
|
|
3010
|
+
const orig_chunk = orig_paragraphs.slice(i1, i2).join("\n\n");
|
|
3011
|
+
const mod_chunk = mod_paragraphs.slice(j1, j2).join("\n\n");
|
|
3012
|
+
const chunk_edits = generate_edits_from_text(orig_chunk, mod_chunk);
|
|
3013
|
+
for (const ce of chunk_edits) {
|
|
3014
|
+
ce._match_start_index = (ce._match_start_index || 0) + offset;
|
|
3015
|
+
edits.push(ce);
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
return _split_cross_paragraph_hunks(edits);
|
|
3020
|
+
}
|
|
2769
3021
|
var _IMAGE_MARKER_RE = /!\[[^\]]*\]\(docx-image:[^)]*\)/g;
|
|
2770
3022
|
function _drop_image_marker_hunks(edits, warnings) {
|
|
2771
3023
|
const _normalized = (s) => s.replace(_IMAGE_MARKER_RE, "").replace(/\s+/g, " ").trim();
|
|
@@ -2963,7 +3215,7 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2963
3215
|
);
|
|
2964
3216
|
const flat = _drop_marker_interior_hunks(
|
|
2965
3217
|
_drop_image_marker_hunks(
|
|
2966
|
-
|
|
3218
|
+
generate_edits_via_paragraph_alignment(text_orig, text_mod),
|
|
2967
3219
|
warnings
|
|
2968
3220
|
),
|
|
2969
3221
|
text_orig,
|
|
@@ -2989,7 +3241,7 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2989
3241
|
);
|
|
2990
3242
|
}
|
|
2991
3243
|
if (!tables_alignable) {
|
|
2992
|
-
const part_edits =
|
|
3244
|
+
const part_edits = generate_edits_via_paragraph_alignment(
|
|
2993
3245
|
text_orig.substring(po_start, po_end),
|
|
2994
3246
|
text_mod.substring(pm_start, pm_end)
|
|
2995
3247
|
);
|
|
@@ -3014,7 +3266,7 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
3014
3266
|
const seg_o_end = boundaries_o[seg_idx + 1][0];
|
|
3015
3267
|
const seg_m_start = boundaries_m[seg_idx][1];
|
|
3016
3268
|
const seg_m_end = boundaries_m[seg_idx + 1][0];
|
|
3017
|
-
const seg_edits =
|
|
3269
|
+
const seg_edits = generate_edits_via_paragraph_alignment(
|
|
3018
3270
|
text_orig.substring(seg_o_start, seg_o_end),
|
|
3019
3271
|
text_mod.substring(seg_m_start, seg_m_end)
|
|
3020
3272
|
);
|
|
@@ -4868,7 +5120,25 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4868
5120
|
*
|
|
4869
5121
|
* Does NOT attach comments; callers handle that.
|
|
4870
5122
|
*/
|
|
4871
|
-
|
|
5123
|
+
_clone_pPr_scrubbing_headings(existing_pPr) {
|
|
5124
|
+
const pPr_clone = existing_pPr.cloneNode(true);
|
|
5125
|
+
const pStyle_el = findChild(pPr_clone, "w:pStyle");
|
|
5126
|
+
if (pStyle_el) {
|
|
5127
|
+
const style_val = pStyle_el.getAttribute("w:val");
|
|
5128
|
+
if (style_val) {
|
|
5129
|
+
const is_heading = style_val.startsWith("Heading") || style_val === "Title" || style_val.replace(/\s+/g, "").startsWith("Heading");
|
|
5130
|
+
if (is_heading) {
|
|
5131
|
+
pPr_clone.removeChild(pStyle_el);
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
const outlineLvl_el = findChild(pPr_clone, "w:outlineLvl");
|
|
5136
|
+
if (outlineLvl_el) {
|
|
5137
|
+
pPr_clone.removeChild(outlineLvl_el);
|
|
5138
|
+
}
|
|
5139
|
+
return pPr_clone;
|
|
5140
|
+
}
|
|
5141
|
+
_track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id, positional_anchor_el = null, suppress_emphasis = false, insert_before = false) {
|
|
4872
5142
|
if (!text) {
|
|
4873
5143
|
return {
|
|
4874
5144
|
first_node: null,
|
|
@@ -4890,7 +5160,11 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4890
5160
|
current_p = walker;
|
|
4891
5161
|
}
|
|
4892
5162
|
const suffix_nodes = [];
|
|
4893
|
-
const
|
|
5163
|
+
const relocatable = /* @__PURE__ */ new Set(["w:r", "w:ins", "w:del"]);
|
|
5164
|
+
const pos_from_positional = positional_anchor_el && positional_anchor_el.parentNode ? positional_anchor_el : null;
|
|
5165
|
+
const pos_from_anchor_run = anchor_run !== null && anchor_run._element.parentNode ? anchor_run._element : null;
|
|
5166
|
+
const pos_source = pos_from_positional ?? pos_from_anchor_run;
|
|
5167
|
+
const suffix_includes_anchor = insert_before && pos_from_positional === null && pos_from_anchor_run !== null;
|
|
4894
5168
|
if (current_p !== null && pos_source !== null) {
|
|
4895
5169
|
let pos_anchor = pos_source;
|
|
4896
5170
|
while (pos_anchor && pos_anchor.parentNode !== current_p) {
|
|
@@ -4901,8 +5175,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4901
5175
|
}
|
|
4902
5176
|
}
|
|
4903
5177
|
if (pos_anchor) {
|
|
4904
|
-
|
|
4905
|
-
let nxt = pos_anchor.nextSibling;
|
|
5178
|
+
let nxt = suffix_includes_anchor ? pos_anchor : pos_anchor.nextSibling;
|
|
4906
5179
|
while (nxt) {
|
|
4907
5180
|
if (nxt.nodeType === 1 && relocatable.has(nxt.tagName)) {
|
|
4908
5181
|
suffix_nodes.push(nxt);
|
|
@@ -4910,6 +5183,14 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4910
5183
|
nxt = nxt.nextSibling;
|
|
4911
5184
|
}
|
|
4912
5185
|
}
|
|
5186
|
+
} else if (current_p !== null && insert_before) {
|
|
5187
|
+
let child = current_p.firstChild;
|
|
5188
|
+
while (child) {
|
|
5189
|
+
if (child.nodeType === 1 && relocatable.has(child.tagName)) {
|
|
5190
|
+
suffix_nodes.push(child);
|
|
5191
|
+
}
|
|
5192
|
+
child = child.nextSibling;
|
|
5193
|
+
}
|
|
4913
5194
|
}
|
|
4914
5195
|
while (lines.length > 1 && lines[lines.length - 1] === "" && suffix_nodes.length === 0) {
|
|
4915
5196
|
lines.pop();
|
|
@@ -4978,7 +5259,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4978
5259
|
} else {
|
|
4979
5260
|
const existing_pPr = findChild(current_p, "w:pPr");
|
|
4980
5261
|
if (existing_pPr) {
|
|
4981
|
-
new_p.appendChild(
|
|
5262
|
+
new_p.appendChild(this._clone_pPr_scrubbing_headings(existing_pPr));
|
|
4982
5263
|
}
|
|
4983
5264
|
}
|
|
4984
5265
|
let pPr = findChild(new_p, "w:pPr");
|
|
@@ -5373,18 +5654,16 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5373
5654
|
continue;
|
|
5374
5655
|
}
|
|
5375
5656
|
}
|
|
5376
|
-
let matches = this.mapper.
|
|
5377
|
-
edit.target_text,
|
|
5378
|
-
is_regex
|
|
5657
|
+
let matches = this.mapper.drop_virtual_only_matches(
|
|
5658
|
+
this.mapper.find_all_match_indices(edit.target_text, is_regex)
|
|
5379
5659
|
);
|
|
5380
5660
|
let activeText = this.mapper.full_text;
|
|
5381
5661
|
let target_mapper = this.mapper;
|
|
5382
5662
|
if (matches.length === 0) {
|
|
5383
5663
|
if (!this.clean_mapper)
|
|
5384
5664
|
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
5385
|
-
matches = this.clean_mapper.
|
|
5386
|
-
edit.target_text,
|
|
5387
|
-
is_regex
|
|
5665
|
+
matches = this.clean_mapper.drop_virtual_only_matches(
|
|
5666
|
+
this.clean_mapper.find_all_match_indices(edit.target_text, is_regex)
|
|
5388
5667
|
);
|
|
5389
5668
|
if (matches.length > 0) {
|
|
5390
5669
|
activeText = this.clean_mapper.full_text;
|
|
@@ -5407,9 +5686,11 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5407
5686
|
if (!this.original_mapper) {
|
|
5408
5687
|
this.original_mapper = new DocumentMapper(this.doc, false, true);
|
|
5409
5688
|
}
|
|
5410
|
-
const orig_matches = this.original_mapper.
|
|
5411
|
-
|
|
5412
|
-
|
|
5689
|
+
const orig_matches = this.original_mapper.drop_virtual_only_matches(
|
|
5690
|
+
this.original_mapper.find_all_match_indices(
|
|
5691
|
+
edit.target_text,
|
|
5692
|
+
is_regex
|
|
5693
|
+
)
|
|
5413
5694
|
);
|
|
5414
5695
|
if (orig_matches.length > 0) {
|
|
5415
5696
|
is_deleted_text = true;
|
|
@@ -5631,10 +5912,15 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5631
5912
|
}
|
|
5632
5913
|
static _column_count_at(mapper, start, length) {
|
|
5633
5914
|
for (const s of mapper.spans) {
|
|
5634
|
-
if (s.
|
|
5915
|
+
if (s.end <= start || s.start >= start + length) {
|
|
5635
5916
|
continue;
|
|
5636
5917
|
}
|
|
5637
|
-
let curr =
|
|
5918
|
+
let curr = null;
|
|
5919
|
+
if (s.run !== null) {
|
|
5920
|
+
curr = s.run._element;
|
|
5921
|
+
} else if (s.paragraph !== null) {
|
|
5922
|
+
curr = s.paragraph._element;
|
|
5923
|
+
}
|
|
5638
5924
|
while (curr) {
|
|
5639
5925
|
if (curr.nodeType === 1 && curr.tagName === "w:tr") {
|
|
5640
5926
|
return findAllDescendants(curr, "w:tc").filter(
|
|
@@ -5648,6 +5934,45 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5648
5934
|
}
|
|
5649
5935
|
validate_review_actions(actions) {
|
|
5650
5936
|
const errors = [];
|
|
5937
|
+
const seen_resolutions = /* @__PURE__ */ new Map();
|
|
5938
|
+
const seen_replies = /* @__PURE__ */ new Set();
|
|
5939
|
+
for (let i = 0; i < actions.length; i++) {
|
|
5940
|
+
const action = actions[i];
|
|
5941
|
+
const type = action.type;
|
|
5942
|
+
const target_id = action.target_id ?? "";
|
|
5943
|
+
if (type === "reply") {
|
|
5944
|
+
if (!String(action.text ?? "").trim()) {
|
|
5945
|
+
errors.push(
|
|
5946
|
+
`- 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'.`
|
|
5947
|
+
);
|
|
5948
|
+
continue;
|
|
5949
|
+
}
|
|
5950
|
+
const reply_key = `${target_id}\0${String(action.text).trim()}`;
|
|
5951
|
+
if (seen_replies.has(reply_key)) {
|
|
5952
|
+
errors.push(
|
|
5953
|
+
`- Action ${i + 1} Failed: duplicate reply \u2014 this batch already replies to ${target_id} with the same text. Remove the duplicate action.`
|
|
5954
|
+
);
|
|
5955
|
+
}
|
|
5956
|
+
seen_replies.add(reply_key);
|
|
5957
|
+
} else if (type === "accept" || type === "reject") {
|
|
5958
|
+
const prior = seen_resolutions.get(target_id);
|
|
5959
|
+
if (prior !== void 0) {
|
|
5960
|
+
const [first_idx, first_type] = prior;
|
|
5961
|
+
if (first_type === type) {
|
|
5962
|
+
errors.push(
|
|
5963
|
+
`- 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.`
|
|
5964
|
+
);
|
|
5965
|
+
} else {
|
|
5966
|
+
errors.push(
|
|
5967
|
+
`- 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.`
|
|
5968
|
+
);
|
|
5969
|
+
}
|
|
5970
|
+
} else {
|
|
5971
|
+
seen_resolutions.set(target_id, [i, type]);
|
|
5972
|
+
}
|
|
5973
|
+
}
|
|
5974
|
+
}
|
|
5975
|
+
if (errors.length > 0) return errors;
|
|
5651
5976
|
for (let i = 0; i < actions.length; i++) {
|
|
5652
5977
|
const action = actions[i];
|
|
5653
5978
|
const type = action.type;
|
|
@@ -5735,7 +6060,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5735
6060
|
(c) => c === null || typeof c !== "object" || !["accept", "reject", "reply"].includes(c.type)
|
|
5736
6061
|
);
|
|
5737
6062
|
if (!dry_run_mode) {
|
|
5738
|
-
|
|
6063
|
+
let action_errors = actions.length > 0 ? this.validate_review_actions(actions) : [];
|
|
6064
|
+
if (actions.length > 0 && action_errors.length === 0) {
|
|
6065
|
+
action_errors = this.validate_action_pairing(actions);
|
|
6066
|
+
}
|
|
5739
6067
|
const validate_edits_now = edits.length > 0 && action_errors.length > 0;
|
|
5740
6068
|
const edit_errors = validate_edits_now ? this.validate_edits(edits) : [];
|
|
5741
6069
|
const all_errors = [...action_errors, ...edit_errors];
|
|
@@ -5744,7 +6072,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5744
6072
|
}
|
|
5745
6073
|
} else {
|
|
5746
6074
|
if (actions.length > 0) {
|
|
5747
|
-
|
|
6075
|
+
let action_errors = this.validate_review_actions(actions);
|
|
6076
|
+
if (action_errors.length === 0) {
|
|
6077
|
+
action_errors = this.validate_action_pairing(actions);
|
|
6078
|
+
}
|
|
5748
6079
|
if (action_errors.length > 0) {
|
|
5749
6080
|
throw new BatchValidationError(action_errors);
|
|
5750
6081
|
}
|
|
@@ -5752,10 +6083,12 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5752
6083
|
}
|
|
5753
6084
|
let applied_actions = 0;
|
|
5754
6085
|
let skipped_actions = 0;
|
|
6086
|
+
let already_resolved_actions = 0;
|
|
5755
6087
|
if (actions.length > 0) {
|
|
5756
6088
|
const res = this.apply_review_actions(actions);
|
|
5757
6089
|
applied_actions = res[0];
|
|
5758
6090
|
skipped_actions = res[1];
|
|
6091
|
+
already_resolved_actions = res[2];
|
|
5759
6092
|
if (skipped_actions > 0) {
|
|
5760
6093
|
throw new BatchValidationError(this.skipped_details);
|
|
5761
6094
|
}
|
|
@@ -5950,6 +6283,11 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5950
6283
|
return {
|
|
5951
6284
|
actions_applied: applied_actions,
|
|
5952
6285
|
actions_skipped: skipped_actions,
|
|
6286
|
+
// Actions whose target was already resolved by an earlier action of
|
|
6287
|
+
// this batch (via its replacement pair): consistent no-ops, never
|
|
6288
|
+
// counted as applied — every reported "applied" action causes an
|
|
6289
|
+
// observable state transition (ADEU-QA-004).
|
|
6290
|
+
actions_already_resolved: already_resolved_actions,
|
|
5953
6291
|
edits_applied: applied_edits,
|
|
5954
6292
|
edits_skipped: skipped_edits,
|
|
5955
6293
|
// edits_applied counts change OBJECTS; this is the total number of
|
|
@@ -5991,13 +6329,17 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5991
6329
|
edit._resolved_start_idx = edit._match_start_index;
|
|
5992
6330
|
resolved_edits.push([edit, edit.new_text || null]);
|
|
5993
6331
|
} else if (edit.type === "insert_row" || edit.type === "delete_row") {
|
|
5994
|
-
let matches = this.mapper.
|
|
6332
|
+
let matches = this.mapper.drop_virtual_only_matches(
|
|
6333
|
+
this.mapper.find_all_match_indices(edit.target_text)
|
|
6334
|
+
);
|
|
5995
6335
|
let resolved_mapper = this.mapper;
|
|
5996
6336
|
if (matches.length === 0) {
|
|
5997
6337
|
if (!this.clean_mapper) {
|
|
5998
6338
|
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
5999
6339
|
}
|
|
6000
|
-
matches = this.clean_mapper.
|
|
6340
|
+
matches = this.clean_mapper.drop_virtual_only_matches(
|
|
6341
|
+
this.clean_mapper.find_all_match_indices(edit.target_text)
|
|
6342
|
+
);
|
|
6001
6343
|
resolved_mapper = this.clean_mapper;
|
|
6002
6344
|
}
|
|
6003
6345
|
if (matches.length > 0) {
|
|
@@ -6168,10 +6510,149 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6168
6510
|
}
|
|
6169
6511
|
return [applied_logical, skipped_logical];
|
|
6170
6512
|
}
|
|
6513
|
+
/**
|
|
6514
|
+
* True when the paragraph still carries visible content (w:t text, w:tab,
|
|
6515
|
+
* w:br) that is NOT wrapped in a tracked deletion — i.e. the paragraph
|
|
6516
|
+
* would render non-empty in the accepted document.
|
|
6517
|
+
*/
|
|
6518
|
+
_paragraph_has_visible_content(p_elem) {
|
|
6519
|
+
for (const tag of ["w:t", "w:tab", "w:br"]) {
|
|
6520
|
+
const nodes = findAllDescendants(p_elem, tag);
|
|
6521
|
+
for (const node of nodes) {
|
|
6522
|
+
let is_deleted = false;
|
|
6523
|
+
let curr = node.parentNode;
|
|
6524
|
+
while (curr && curr !== p_elem.parentNode) {
|
|
6525
|
+
if (curr.tagName === "w:del") {
|
|
6526
|
+
is_deleted = true;
|
|
6527
|
+
break;
|
|
6528
|
+
}
|
|
6529
|
+
curr = curr.parentNode;
|
|
6530
|
+
}
|
|
6531
|
+
if (!is_deleted) {
|
|
6532
|
+
if (tag === "w:t" && !node.textContent) continue;
|
|
6533
|
+
return true;
|
|
6534
|
+
}
|
|
6535
|
+
}
|
|
6536
|
+
}
|
|
6537
|
+
return false;
|
|
6538
|
+
}
|
|
6539
|
+
/**
|
|
6540
|
+
* All contiguous same-author w:ins/w:del siblings that form one logical
|
|
6541
|
+
* modification block with `node` (a replacement's del+ins pair). Mirrors
|
|
6542
|
+
* the Python engine's _get_paired_nodes: comment range markers and
|
|
6543
|
+
* rPr/pPr are transparent; a different author or any other element breaks
|
|
6544
|
+
* the group.
|
|
6545
|
+
*/
|
|
6546
|
+
_get_paired_nodes(node) {
|
|
6547
|
+
const pairs = [];
|
|
6548
|
+
const author = node.getAttribute("w:author");
|
|
6549
|
+
const transparent = /* @__PURE__ */ new Set([
|
|
6550
|
+
"w:commentRangeStart",
|
|
6551
|
+
"w:commentRangeEnd",
|
|
6552
|
+
"w:commentReference",
|
|
6553
|
+
"w:rPr",
|
|
6554
|
+
"w:pPr"
|
|
6555
|
+
]);
|
|
6556
|
+
const walk = (start, dir) => {
|
|
6557
|
+
let cur = dir === "next" ? start.nextSibling : start.previousSibling;
|
|
6558
|
+
while (cur) {
|
|
6559
|
+
if (cur.nodeType !== 1) {
|
|
6560
|
+
cur = dir === "next" ? cur.nextSibling : cur.previousSibling;
|
|
6561
|
+
continue;
|
|
6562
|
+
}
|
|
6563
|
+
const el = cur;
|
|
6564
|
+
if (transparent.has(el.tagName)) {
|
|
6565
|
+
cur = dir === "next" ? cur.nextSibling : cur.previousSibling;
|
|
6566
|
+
continue;
|
|
6567
|
+
}
|
|
6568
|
+
if ((el.tagName === "w:ins" || el.tagName === "w:del") && el.getAttribute("w:author") === author) {
|
|
6569
|
+
pairs.push(el);
|
|
6570
|
+
cur = dir === "next" ? cur.nextSibling : cur.previousSibling;
|
|
6571
|
+
continue;
|
|
6572
|
+
}
|
|
6573
|
+
break;
|
|
6574
|
+
}
|
|
6575
|
+
};
|
|
6576
|
+
walk(node, "next");
|
|
6577
|
+
walk(node, "prev");
|
|
6578
|
+
return pairs;
|
|
6579
|
+
}
|
|
6580
|
+
/**
|
|
6581
|
+
* All revision ids that resolve as ONE unit with `target_id`: the ids of
|
|
6582
|
+
* every contiguous same-author w:ins/w:del sibling of its elements (a
|
|
6583
|
+
* replacement's del+ins pair), plus the id itself.
|
|
6584
|
+
*/
|
|
6585
|
+
_resolution_group_ids(target_id) {
|
|
6586
|
+
const nodes = [
|
|
6587
|
+
...findAllDescendants(this.doc.element, "w:ins"),
|
|
6588
|
+
...findAllDescendants(this.doc.element, "w:del")
|
|
6589
|
+
].filter((n) => n.getAttribute("w:id") === target_id);
|
|
6590
|
+
const group = /* @__PURE__ */ new Set();
|
|
6591
|
+
if (nodes.length === 0) return group;
|
|
6592
|
+
group.add(target_id);
|
|
6593
|
+
for (const node of nodes) {
|
|
6594
|
+
for (const paired of this._get_paired_nodes(node)) {
|
|
6595
|
+
const pid = paired.getAttribute("w:id");
|
|
6596
|
+
if (pid) group.add(pid);
|
|
6597
|
+
}
|
|
6598
|
+
}
|
|
6599
|
+
return group;
|
|
6600
|
+
}
|
|
6601
|
+
/**
|
|
6602
|
+
* Document-aware validation (QA 2026-07-19 ADEU-QA-004): a replacement's
|
|
6603
|
+
* del+ins pair carries two distinct ids but resolves as one unit, so a
|
|
6604
|
+
* batch that accepts one side and rejects the other is contradictory.
|
|
6605
|
+
* Rejecting it up front — before any action mutates the document — keeps
|
|
6606
|
+
* the batch transactional.
|
|
6607
|
+
*/
|
|
6608
|
+
validate_action_pairing(actions) {
|
|
6609
|
+
const errors = [];
|
|
6610
|
+
const group_first = /* @__PURE__ */ new Map();
|
|
6611
|
+
for (let pos = 0; pos < actions.length; pos++) {
|
|
6612
|
+
const act = actions[pos];
|
|
6613
|
+
if (act.type !== "accept" && act.type !== "reject") continue;
|
|
6614
|
+
const raw_id = String(act.target_id ?? "");
|
|
6615
|
+
if (raw_id.startsWith("Com:")) continue;
|
|
6616
|
+
const target_id = raw_id.startsWith("Chg:") ? raw_id.slice(4) : raw_id;
|
|
6617
|
+
const group = this._resolution_group_ids(target_id);
|
|
6618
|
+
if (group.size === 0) continue;
|
|
6619
|
+
let conflict = null;
|
|
6620
|
+
for (const gid of group) {
|
|
6621
|
+
const prior = group_first.get(gid);
|
|
6622
|
+
if (prior !== void 0 && prior[1] !== act.type) {
|
|
6623
|
+
conflict = prior;
|
|
6624
|
+
break;
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6627
|
+
if (conflict !== null) {
|
|
6628
|
+
const [first_pos, first_type, first_id] = conflict;
|
|
6629
|
+
errors.push(
|
|
6630
|
+
`- Action ${pos + 1} Failed: conflicting actions on one replacement \u2014 Action ${first_pos + 1} applies '${first_type}' to Chg:${first_id}, and Chg:${target_id} is part of the same change (a replacement's contiguous del+ins pair resolves as one unit, so '${first_type}' already decides both sides). Accepting one side and rejecting the other is contradictory \u2014 decide the outcome and submit exactly one action for the pair.`
|
|
6631
|
+
);
|
|
6632
|
+
continue;
|
|
6633
|
+
}
|
|
6634
|
+
for (const gid of group) {
|
|
6635
|
+
if (!group_first.has(gid)) {
|
|
6636
|
+
group_first.set(gid, [pos, act.type, target_id]);
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
6639
|
+
}
|
|
6640
|
+
return errors;
|
|
6641
|
+
}
|
|
6642
|
+
/**
|
|
6643
|
+
* Returns [applied, skipped, already_resolved]. `applied` counts actions
|
|
6644
|
+
* that caused an observable state transition; an action naming an id an
|
|
6645
|
+
* earlier action of this batch already resolved (via its replacement pair)
|
|
6646
|
+
* is counted in `already_resolved` instead — never as applied
|
|
6647
|
+
* (QA 2026-07-19 ADEU-QA-004).
|
|
6648
|
+
*/
|
|
6171
6649
|
apply_review_actions(actions) {
|
|
6172
6650
|
let applied = 0;
|
|
6173
6651
|
let skipped = 0;
|
|
6174
|
-
|
|
6652
|
+
let already_resolved = 0;
|
|
6653
|
+
const resolved_history = /* @__PURE__ */ new Map();
|
|
6654
|
+
for (let pos = 0; pos < actions.length; pos++) {
|
|
6655
|
+
const action = actions[pos];
|
|
6175
6656
|
const type = action.type;
|
|
6176
6657
|
if (type === "reply") {
|
|
6177
6658
|
const cid = action.target_id.replace("Com:", "");
|
|
@@ -6185,6 +6666,21 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6185
6666
|
continue;
|
|
6186
6667
|
}
|
|
6187
6668
|
const target_id = action.target_id.replace("Chg:", "");
|
|
6669
|
+
const prior_type = resolved_history.get(target_id);
|
|
6670
|
+
if (prior_type !== void 0) {
|
|
6671
|
+
if (prior_type === type) {
|
|
6672
|
+
already_resolved++;
|
|
6673
|
+
this.skipped_details.push(
|
|
6674
|
+
`- Note: Action ${pos + 1} ('${type}' on ${action.target_id}) had no additional effect \u2014 the change was already resolved together with its replacement pair by an earlier action in this batch. Counted as already_resolved, not applied.`
|
|
6675
|
+
);
|
|
6676
|
+
continue;
|
|
6677
|
+
}
|
|
6678
|
+
this.skipped_details.push(
|
|
6679
|
+
`- Action ${pos + 1} Failed: contradictory action \u2014 '${type}' on ${action.target_id}, but the change was already resolved as '${prior_type}' together with its replacement pair by an earlier action in this batch.`
|
|
6680
|
+
);
|
|
6681
|
+
skipped++;
|
|
6682
|
+
continue;
|
|
6683
|
+
}
|
|
6188
6684
|
const all_ins = findAllDescendants(this.doc.element, "w:ins").filter(
|
|
6189
6685
|
(n) => n.getAttribute("w:id") === target_id
|
|
6190
6686
|
);
|
|
@@ -6209,7 +6705,18 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6209
6705
|
);
|
|
6210
6706
|
continue;
|
|
6211
6707
|
}
|
|
6708
|
+
const group_nodes = new Set(all_nodes);
|
|
6212
6709
|
for (const node of all_nodes) {
|
|
6710
|
+
for (const paired of this._get_paired_nodes(node)) {
|
|
6711
|
+
group_nodes.add(paired);
|
|
6712
|
+
}
|
|
6713
|
+
}
|
|
6714
|
+
const resolved_now = /* @__PURE__ */ new Set();
|
|
6715
|
+
for (const node of group_nodes) {
|
|
6716
|
+
const rid = node.getAttribute("w:id");
|
|
6717
|
+
if (rid) resolved_now.add(rid);
|
|
6718
|
+
}
|
|
6719
|
+
for (const node of group_nodes) {
|
|
6213
6720
|
const is_ins = node.tagName === "w:ins";
|
|
6214
6721
|
const parent_tag = node.parentNode ? node.parentNode.tagName : "";
|
|
6215
6722
|
const is_trPr = parent_tag === "w:trPr";
|
|
@@ -6261,9 +6768,12 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6261
6768
|
}
|
|
6262
6769
|
}
|
|
6263
6770
|
}
|
|
6771
|
+
for (const rid of resolved_now) {
|
|
6772
|
+
resolved_history.set(rid, type);
|
|
6773
|
+
}
|
|
6264
6774
|
applied++;
|
|
6265
6775
|
}
|
|
6266
|
-
return [applied, skipped];
|
|
6776
|
+
return [applied, skipped, already_resolved];
|
|
6267
6777
|
}
|
|
6268
6778
|
_apply_table_edit(edit, rebuild_map) {
|
|
6269
6779
|
const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
|
|
@@ -6323,17 +6833,15 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6323
6833
|
if (!edit.target_text) return null;
|
|
6324
6834
|
const is_regex = edit.regex || false;
|
|
6325
6835
|
const match_mode = edit.match_mode || "strict";
|
|
6326
|
-
let matches = this.mapper.
|
|
6327
|
-
edit.target_text,
|
|
6328
|
-
is_regex
|
|
6836
|
+
let matches = this.mapper.drop_virtual_only_matches(
|
|
6837
|
+
this.mapper.find_all_match_indices(edit.target_text, is_regex)
|
|
6329
6838
|
);
|
|
6330
6839
|
let use_clean_map = false;
|
|
6331
6840
|
if (matches.length === 0) {
|
|
6332
6841
|
if (!this.clean_mapper)
|
|
6333
6842
|
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
6334
|
-
matches = this.clean_mapper.
|
|
6335
|
-
edit.target_text,
|
|
6336
|
-
is_regex
|
|
6843
|
+
matches = this.clean_mapper.drop_virtual_only_matches(
|
|
6844
|
+
this.clean_mapper.find_all_match_indices(edit.target_text, is_regex)
|
|
6337
6845
|
);
|
|
6338
6846
|
if (matches.length > 0) use_clean_map = true;
|
|
6339
6847
|
else return null;
|
|
@@ -6843,7 +7351,9 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6843
7351
|
this._set_paragraph_style(new_p, style_name);
|
|
6844
7352
|
} else {
|
|
6845
7353
|
const existing_pPr = findChild(_bug233_target_para, "w:pPr");
|
|
6846
|
-
if (existing_pPr)
|
|
7354
|
+
if (existing_pPr) {
|
|
7355
|
+
new_p.appendChild(this._clone_pPr_scrubbing_headings(existing_pPr));
|
|
7356
|
+
}
|
|
6847
7357
|
}
|
|
6848
7358
|
let pPr = findChild(new_p, "w:pPr");
|
|
6849
7359
|
if (!pPr) {
|
|
@@ -6895,7 +7405,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6895
7405
|
anchor_para,
|
|
6896
7406
|
ins_id,
|
|
6897
7407
|
null,
|
|
6898
|
-
suppress_emphasis
|
|
7408
|
+
suppress_emphasis,
|
|
7409
|
+
// Paragraph-start insertions attach BEFORE the anchor (see
|
|
7410
|
+
// before_anchor below): the suffix relocation must know.
|
|
7411
|
+
start_idx === 0
|
|
6899
7412
|
);
|
|
6900
7413
|
if (!result.first_node) return false;
|
|
6901
7414
|
const is_inline_first = result.first_node.tagName === "w:ins";
|
|
@@ -7098,7 +7611,24 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
7098
7611
|
p2_element = getNextElement(p2_element);
|
|
7099
7612
|
}
|
|
7100
7613
|
if (p2_element && p2_element.tagName === "w:p") {
|
|
7614
|
+
const p1_fully_deleted = !this._paragraph_has_visible_content(p1_element);
|
|
7101
7615
|
let pPr = findChild(p1_element, "w:pPr");
|
|
7616
|
+
if (p1_fully_deleted) {
|
|
7617
|
+
const p2_pPr = findChild(p2_element, "w:pPr");
|
|
7618
|
+
const adopted = p2_pPr ? p2_pPr.cloneNode(true) : p1_element.ownerDocument.createElement("w:pPr");
|
|
7619
|
+
if (pPr) {
|
|
7620
|
+
const sect = findChild(pPr, "w:sectPr");
|
|
7621
|
+
if (sect && !findChild(adopted, "w:sectPr")) {
|
|
7622
|
+
adopted.appendChild(sect.cloneNode(true));
|
|
7623
|
+
}
|
|
7624
|
+
p1_element.removeChild(pPr);
|
|
7625
|
+
}
|
|
7626
|
+
p1_element.insertBefore(
|
|
7627
|
+
adopted,
|
|
7628
|
+
p1_element.firstChild
|
|
7629
|
+
);
|
|
7630
|
+
pPr = adopted;
|
|
7631
|
+
}
|
|
7102
7632
|
if (!pPr) {
|
|
7103
7633
|
pPr = p1_element.ownerDocument.createElement("w:pPr");
|
|
7104
7634
|
p1_element.insertBefore(
|
|
@@ -7111,8 +7641,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
7111
7641
|
rPr = p1_element.ownerDocument.createElement("w:rPr");
|
|
7112
7642
|
pPr.appendChild(rPr);
|
|
7113
7643
|
}
|
|
7114
|
-
|
|
7115
|
-
|
|
7644
|
+
if (!findChild(rPr, "w:del")) {
|
|
7645
|
+
const del_mark = this._create_track_change_tag("w:del");
|
|
7646
|
+
rPr.appendChild(del_mark);
|
|
7647
|
+
}
|
|
7116
7648
|
const children = Array.from(p2_element.childNodes);
|
|
7117
7649
|
for (const child of children) {
|
|
7118
7650
|
if (child.nodeType === 1 && child.tagName === "w:pPr") {
|
|
@@ -7166,27 +7698,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
7166
7698
|
}
|
|
7167
7699
|
}
|
|
7168
7700
|
for (const p_elem of affected_ps) {
|
|
7169
|
-
|
|
7170
|
-
for (const tag of ["w:t", "w:tab", "w:br"]) {
|
|
7171
|
-
const nodes = findAllDescendants(p_elem, tag);
|
|
7172
|
-
for (const node of nodes) {
|
|
7173
|
-
let is_deleted = false;
|
|
7174
|
-
let curr = node.parentNode;
|
|
7175
|
-
while (curr && curr !== p_elem.parentNode) {
|
|
7176
|
-
if (curr.tagName === "w:del") {
|
|
7177
|
-
is_deleted = true;
|
|
7178
|
-
break;
|
|
7179
|
-
}
|
|
7180
|
-
curr = curr.parentNode;
|
|
7181
|
-
}
|
|
7182
|
-
if (!is_deleted) {
|
|
7183
|
-
if (tag === "w:t" && !node.textContent) continue;
|
|
7184
|
-
has_visible = true;
|
|
7185
|
-
break;
|
|
7186
|
-
}
|
|
7187
|
-
}
|
|
7188
|
-
if (has_visible) break;
|
|
7189
|
-
}
|
|
7701
|
+
const has_visible = this._paragraph_has_visible_content(p_elem);
|
|
7190
7702
|
if (!has_visible) {
|
|
7191
7703
|
let pPr = findChild(p_elem, "w:pPr");
|
|
7192
7704
|
if (!pPr) {
|
|
@@ -7745,6 +8257,31 @@ function strip_custom_properties(doc) {
|
|
|
7745
8257
|
const count = Math.max(lines.length, 1);
|
|
7746
8258
|
return [`Custom document properties: ${count} removed (docProps/custom.xml)`].concat(lines);
|
|
7747
8259
|
}
|
|
8260
|
+
function strip_document_variables(doc) {
|
|
8261
|
+
const settingsPart = doc.pkg.getPartByPath("word/settings.xml");
|
|
8262
|
+
if (!settingsPart) return [];
|
|
8263
|
+
const names = [];
|
|
8264
|
+
let removedAny = false;
|
|
8265
|
+
for (const container of findDescendantsByLocalName(settingsPart._element, "docVars")) {
|
|
8266
|
+
for (const child of Array.from(container.childNodes)) {
|
|
8267
|
+
const el = child;
|
|
8268
|
+
const tag = el.tagName || "";
|
|
8269
|
+
if (tag === "docVar" || tag.endsWith(":docVar")) {
|
|
8270
|
+
names.push(el.getAttribute("w:name") || el.getAttribute("name") || "(unnamed)");
|
|
8271
|
+
}
|
|
8272
|
+
}
|
|
8273
|
+
container.parentNode?.removeChild(container);
|
|
8274
|
+
removedAny = true;
|
|
8275
|
+
}
|
|
8276
|
+
for (const stray of findDescendantsByLocalName(settingsPart._element, "docVar")) {
|
|
8277
|
+
names.push(stray.getAttribute("w:name") || stray.getAttribute("name") || "(unnamed)");
|
|
8278
|
+
stray.parentNode?.removeChild(stray);
|
|
8279
|
+
removedAny = true;
|
|
8280
|
+
}
|
|
8281
|
+
if (!removedAny) return [];
|
|
8282
|
+
const shown = Array.from(new Set(names)).sort().join(", ") || "(unnamed)";
|
|
8283
|
+
return [`Document variables: ${names.length} removed (${shown})`];
|
|
8284
|
+
}
|
|
7748
8285
|
function strip_image_alt_text(doc) {
|
|
7749
8286
|
let count = 0;
|
|
7750
8287
|
for (const docPr of findDescendantsByLocalName(doc.element, "docPr")) {
|
|
@@ -8266,7 +8803,8 @@ function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets
|
|
|
8266
8803
|
const firstP = cell._element.getElementsByTagName("w:p")[0];
|
|
8267
8804
|
const paraId = firstP ? firstP.getAttribute("w14:paraId") : null;
|
|
8268
8805
|
if (paraId) {
|
|
8269
|
-
const
|
|
8806
|
+
const space_pad = cell_content ? " " : "";
|
|
8807
|
+
const anchor = `${space_pad}{#cell:${paraId}}`;
|
|
8270
8808
|
cell_content = cell_content + anchor;
|
|
8271
8809
|
}
|
|
8272
8810
|
}
|
|
@@ -8531,6 +9069,8 @@ function _build_merged_meta_block(states_list, comments_map) {
|
|
|
8531
9069
|
const change_lines = [];
|
|
8532
9070
|
const comment_lines = [];
|
|
8533
9071
|
const seen_sigs = /* @__PURE__ */ new Set();
|
|
9072
|
+
const pair_map = compute_change_pair_map(states_list);
|
|
9073
|
+
const pairSuffix = (uid) => pair_map[uid] ? ` (pairs with ${pair_map[uid]})` : "";
|
|
8534
9074
|
for (const [ins_map, del_map, comments_set, fmt_map] of states_list) {
|
|
8535
9075
|
let render_comment2 = function(cid) {
|
|
8536
9076
|
if (!comments_map[cid]) return;
|
|
@@ -8559,7 +9099,9 @@ function _build_merged_meta_block(states_list, comments_map) {
|
|
|
8559
9099
|
)) {
|
|
8560
9100
|
const sig = `Chg:${uid}`;
|
|
8561
9101
|
if (!seen_sigs.has(sig)) {
|
|
8562
|
-
change_lines.push(
|
|
9102
|
+
change_lines.push(
|
|
9103
|
+
`[${sig} insert] ${meta.author || "Unknown"}${pairSuffix(uid)}`
|
|
9104
|
+
);
|
|
8563
9105
|
seen_sigs.add(sig);
|
|
8564
9106
|
}
|
|
8565
9107
|
}
|
|
@@ -8568,7 +9110,9 @@ function _build_merged_meta_block(states_list, comments_map) {
|
|
|
8568
9110
|
)) {
|
|
8569
9111
|
const sig = `Chg:${uid}`;
|
|
8570
9112
|
if (!seen_sigs.has(sig)) {
|
|
8571
|
-
change_lines.push(
|
|
9113
|
+
change_lines.push(
|
|
9114
|
+
`[${sig} delete] ${meta.author || "Unknown"}${pairSuffix(uid)}`
|
|
9115
|
+
);
|
|
8572
9116
|
seen_sigs.add(sig);
|
|
8573
9117
|
}
|
|
8574
9118
|
}
|
|
@@ -9130,14 +9674,14 @@ var SanitizeReport = class {
|
|
|
9130
9674
|
const lower = line.toLowerCase();
|
|
9131
9675
|
if (lower.includes("tracked change") || lower.includes("insertion") || lower.includes("deletion") || lower.includes("accepted")) {
|
|
9132
9676
|
this.change_lines.push(line);
|
|
9677
|
+
} 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("document variable") || lower.includes("language") || lower.includes("version") || lower.includes("last modified by") || lower.includes("revision count") || lower.includes("last printed") || lower.includes("description/comments")) {
|
|
9678
|
+
this.metadata_lines.push(line);
|
|
9133
9679
|
} else if (lower.includes("comment") || lower.includes("[open]") || lower.includes("[resolved]")) {
|
|
9134
9680
|
if (lower.includes("kept") || lower.includes("visible")) {
|
|
9135
9681
|
this.kept_comment_lines.push(line);
|
|
9136
9682
|
} else {
|
|
9137
9683
|
this.removed_comment_lines.push(line);
|
|
9138
9684
|
}
|
|
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")) {
|
|
9140
|
-
this.metadata_lines.push(line);
|
|
9141
9685
|
} else if (lower.includes("hyperlink") || lower.includes("warning")) {
|
|
9142
9686
|
this.warnings.push(line);
|
|
9143
9687
|
} else {
|
|
@@ -9250,6 +9794,7 @@ async function finalize_document(doc, options) {
|
|
|
9250
9794
|
report.add_transform_lines(scrub_timestamps(doc));
|
|
9251
9795
|
report.add_transform_lines(strip_custom_xml(doc));
|
|
9252
9796
|
report.add_transform_lines(strip_custom_properties(doc));
|
|
9797
|
+
report.add_transform_lines(strip_document_variables(doc));
|
|
9253
9798
|
report.add_transform_lines(strip_image_alt_text(doc));
|
|
9254
9799
|
const warnings = audit_hyperlinks(doc);
|
|
9255
9800
|
for (const w of warnings) report.warnings.push(w);
|
|
@@ -9333,6 +9878,12 @@ function verifySanitizedPackage(outBuffer) {
|
|
|
9333
9878
|
}
|
|
9334
9879
|
}
|
|
9335
9880
|
}
|
|
9881
|
+
if (names.includes("word/settings.xml")) {
|
|
9882
|
+
const settings = parseXml((0, import_fflate3.strFromU8)(unzipped["word/settings.xml"]));
|
|
9883
|
+
if (findDescendantsByLocalName(settings.documentElement, "docVar").length > 0) {
|
|
9884
|
+
problems.push("word/settings.xml still contains document variables (w:docVar)");
|
|
9885
|
+
}
|
|
9886
|
+
}
|
|
9336
9887
|
if (problems.length > 0) {
|
|
9337
9888
|
throw new Error(
|
|
9338
9889
|
"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."
|