@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.js
CHANGED
|
@@ -1167,6 +1167,18 @@ function get_paragraph_prefix(paragraph, style_cache, default_pstyle) {
|
|
|
1167
1167
|
if (custom_level !== null) return "#".repeat(custom_level) + " ";
|
|
1168
1168
|
}
|
|
1169
1169
|
if (!style_name || style_name === "Normal") {
|
|
1170
|
+
let is_inside_tc = false;
|
|
1171
|
+
let curr = paragraph._element;
|
|
1172
|
+
while (curr) {
|
|
1173
|
+
if (curr.tagName === "w:tc") {
|
|
1174
|
+
is_inside_tc = true;
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
curr = curr.parentNode;
|
|
1178
|
+
}
|
|
1179
|
+
if (is_inside_tc) {
|
|
1180
|
+
return "";
|
|
1181
|
+
}
|
|
1170
1182
|
const text = paragraph.text.trim();
|
|
1171
1183
|
if (text && text.length < 100 && text === text.toUpperCase()) {
|
|
1172
1184
|
let is_bold = false;
|
|
@@ -1238,6 +1250,50 @@ function split_boundary_whitespace(text) {
|
|
|
1238
1250
|
text.substring(lead_len + core.length)
|
|
1239
1251
|
];
|
|
1240
1252
|
}
|
|
1253
|
+
function compute_change_pair_map(states_list) {
|
|
1254
|
+
const groups = [];
|
|
1255
|
+
let current = [];
|
|
1256
|
+
const seen_ids = /* @__PURE__ */ new Set();
|
|
1257
|
+
for (const [ins_map, del_map] of states_list) {
|
|
1258
|
+
const ins_entries = Object.entries(ins_map);
|
|
1259
|
+
const del_entries = Object.entries(del_map);
|
|
1260
|
+
if (ins_entries.length === 0 && del_entries.length === 0) {
|
|
1261
|
+
if (current.length > 0) {
|
|
1262
|
+
groups.push(current);
|
|
1263
|
+
current = [];
|
|
1264
|
+
}
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
const state_new = [];
|
|
1268
|
+
for (const [uid, meta] of ins_entries) {
|
|
1269
|
+
if (!seen_ids.has(uid)) {
|
|
1270
|
+
state_new.push([uid, meta && meta.author || "Unknown"]);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
for (const [uid, meta] of del_entries) {
|
|
1274
|
+
if (!seen_ids.has(uid)) {
|
|
1275
|
+
state_new.push([uid, meta && meta.author || "Unknown"]);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
for (const [uid, author] of state_new) {
|
|
1279
|
+
seen_ids.add(uid);
|
|
1280
|
+
if (current.length > 0 && current[current.length - 1][1] !== author) {
|
|
1281
|
+
groups.push(current);
|
|
1282
|
+
current = [];
|
|
1283
|
+
}
|
|
1284
|
+
current.push([uid, author]);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
if (current.length > 0) groups.push(current);
|
|
1288
|
+
const pair_map = {};
|
|
1289
|
+
for (const group of groups) {
|
|
1290
|
+
if (group.length < 2) continue;
|
|
1291
|
+
for (const [uid] of group) {
|
|
1292
|
+
pair_map[uid] = group.filter(([u]) => u !== uid).map(([u]) => `Chg:${u}`).join(", ");
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
return pair_map;
|
|
1296
|
+
}
|
|
1241
1297
|
function apply_formatting_to_segments(text, prefix, suffix) {
|
|
1242
1298
|
if (!prefix && !suffix) return text;
|
|
1243
1299
|
if (!text) return "";
|
|
@@ -1645,6 +1701,11 @@ ${header}`;
|
|
|
1645
1701
|
if (paraId && firstP) {
|
|
1646
1702
|
const cellPara = new Paragraph(firstP, cell);
|
|
1647
1703
|
this._add_virtual_text("", current, cellPara);
|
|
1704
|
+
const has_content = current > cell_start;
|
|
1705
|
+
if (has_content) {
|
|
1706
|
+
this._add_virtual_text(" ", current, cellPara);
|
|
1707
|
+
current += 1;
|
|
1708
|
+
}
|
|
1648
1709
|
const anchor = `{#cell:${paraId}}`;
|
|
1649
1710
|
this._add_virtual_text(anchor, current, cellPara);
|
|
1650
1711
|
current += anchor.length;
|
|
@@ -1995,6 +2056,8 @@ ${header}`;
|
|
|
1995
2056
|
const change_lines = [];
|
|
1996
2057
|
const comment_lines = [];
|
|
1997
2058
|
const seen_sigs = /* @__PURE__ */ new Set();
|
|
2059
|
+
const pair_map = compute_change_pair_map(states_list);
|
|
2060
|
+
const pairSuffix = (uid) => pair_map[uid] ? ` (pairs with ${pair_map[uid]})` : "";
|
|
1998
2061
|
for (const [ins_map, del_map, comments_set, fmt_map] of states_list) {
|
|
1999
2062
|
for (const [uid, meta] of Object.entries(
|
|
2000
2063
|
ins_map
|
|
@@ -2002,7 +2065,7 @@ ${header}`;
|
|
|
2002
2065
|
const sig = `Chg:${uid}`;
|
|
2003
2066
|
if (!seen_sigs.has(sig)) {
|
|
2004
2067
|
const auth = meta.author || "Unknown";
|
|
2005
|
-
change_lines.push(`[${sig} insert] ${auth}`);
|
|
2068
|
+
change_lines.push(`[${sig} insert] ${auth}${pairSuffix(uid)}`);
|
|
2006
2069
|
seen_sigs.add(sig);
|
|
2007
2070
|
}
|
|
2008
2071
|
}
|
|
@@ -2012,7 +2075,7 @@ ${header}`;
|
|
|
2012
2075
|
const sig = `Chg:${uid}`;
|
|
2013
2076
|
if (!seen_sigs.has(sig)) {
|
|
2014
2077
|
const auth = meta.author || "Unknown";
|
|
2015
|
-
change_lines.push(`[${sig} delete] ${auth}`);
|
|
2078
|
+
change_lines.push(`[${sig} delete] ${auth}${pairSuffix(uid)}`);
|
|
2016
2079
|
seen_sigs.add(sig);
|
|
2017
2080
|
}
|
|
2018
2081
|
}
|
|
@@ -2152,21 +2215,60 @@ ${header}`;
|
|
|
2152
2215
|
}
|
|
2153
2216
|
return results;
|
|
2154
2217
|
}
|
|
2218
|
+
/**
|
|
2219
|
+
* True when no run-backed span overlaps [start, start+length): the range
|
|
2220
|
+
* covers only virtual projection text — meta bubbles (change/comment
|
|
2221
|
+
* headers, timestamps), style markers, list prefixes. Such text does not
|
|
2222
|
+
* exist in the document, so it can neither satisfy a match nor count
|
|
2223
|
+
* toward ambiguity (QA 2026-07-19 ADEU-QA-002 C): an edit targeting "4"
|
|
2224
|
+
* used to be rejected as "appears 8 times" because a comment bubble's
|
|
2225
|
+
* timestamp matched.
|
|
2226
|
+
*
|
|
2227
|
+
* Anchor tokens ({#Bookmark}, {#cell:paraId}) are the exception: they are
|
|
2228
|
+
* deliberate virtual TARGETING surfaces (empty-cell writes, bookmark
|
|
2229
|
+
* anchors) and must stay matchable.
|
|
2230
|
+
*/
|
|
2231
|
+
range_is_virtual_only(start, length) {
|
|
2232
|
+
const end = start + length;
|
|
2233
|
+
const overlapping = this.spans.filter(
|
|
2234
|
+
(s) => s.end > start && s.start < end
|
|
2235
|
+
);
|
|
2236
|
+
if (overlapping.some((s) => s.run !== null)) return false;
|
|
2237
|
+
return !overlapping.some(
|
|
2238
|
+
(s) => s.run === null && s.text.startsWith("{#")
|
|
2239
|
+
);
|
|
2240
|
+
}
|
|
2241
|
+
/** Filters find_all_match_indices output down to matches that touch at
|
|
2242
|
+
* least one run-backed span. See range_is_virtual_only. */
|
|
2243
|
+
drop_virtual_only_matches(matches) {
|
|
2244
|
+
return matches.filter(
|
|
2245
|
+
([start, length]) => !this.range_is_virtual_only(start, length)
|
|
2246
|
+
);
|
|
2247
|
+
}
|
|
2155
2248
|
find_match_index(target_text, is_regex = false) {
|
|
2156
2249
|
if (is_regex) {
|
|
2157
2250
|
try {
|
|
2158
2251
|
const match = userSearch(target_text, this.full_text);
|
|
2159
|
-
if (match
|
|
2252
|
+
if (match && !this.range_is_virtual_only(match.start, match.end - match.start)) {
|
|
2253
|
+
return [match.start, match.end - match.start];
|
|
2254
|
+
}
|
|
2160
2255
|
} catch (e) {
|
|
2161
2256
|
if (e instanceof RegexTimeoutError) throw e;
|
|
2162
2257
|
}
|
|
2163
2258
|
return [-1, 0];
|
|
2164
2259
|
}
|
|
2165
|
-
let
|
|
2166
|
-
|
|
2260
|
+
let from = 0;
|
|
2261
|
+
while (true) {
|
|
2262
|
+
const idx = this.full_text.indexOf(target_text, from);
|
|
2263
|
+
if (idx === -1) break;
|
|
2264
|
+
if (!this.range_is_virtual_only(idx, target_text.length)) {
|
|
2265
|
+
return [idx, target_text.length];
|
|
2266
|
+
}
|
|
2267
|
+
from = idx + 1;
|
|
2268
|
+
}
|
|
2167
2269
|
const norm_full = this._replace_smart_quotes(this.full_text);
|
|
2168
2270
|
const norm_target = this._replace_smart_quotes(target_text);
|
|
2169
|
-
start_idx = norm_full.indexOf(norm_target);
|
|
2271
|
+
let start_idx = norm_full.indexOf(norm_target);
|
|
2170
2272
|
if (start_idx !== -1) return [start_idx, target_text.length];
|
|
2171
2273
|
const stripped_target = this._strip_markdown_formatting(target_text);
|
|
2172
2274
|
if (this.full_text.includes(stripped_target)) {
|
|
@@ -2176,9 +2278,12 @@ ${header}`;
|
|
|
2176
2278
|
const plain_first = this._find_plain_projection_matches(target_text);
|
|
2177
2279
|
if (plain_first.length > 0) return plain_first[0];
|
|
2178
2280
|
try {
|
|
2179
|
-
const pattern = new RegExp(this._make_fuzzy_regex(target_text));
|
|
2180
|
-
const match
|
|
2181
|
-
|
|
2281
|
+
const pattern = new RegExp(this._make_fuzzy_regex(target_text), "g");
|
|
2282
|
+
for (const match of this.full_text.matchAll(pattern)) {
|
|
2283
|
+
if (!this.range_is_virtual_only(match.index, match[0].length)) {
|
|
2284
|
+
return [match.index, match[0].length];
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2182
2287
|
} catch (e) {
|
|
2183
2288
|
}
|
|
2184
2289
|
return [-1, 0];
|
|
@@ -2259,7 +2364,7 @@ ${header}`;
|
|
|
2259
2364
|
let dom_modified = false;
|
|
2260
2365
|
const first_real_span = real_spans[0];
|
|
2261
2366
|
let start_split_adjustment = 0;
|
|
2262
|
-
const local_start = start_idx - first_real_span.start + (first_real_span.run_offset || 0);
|
|
2367
|
+
const local_start = Math.max(start_idx, first_real_span.start) - first_real_span.start + (first_real_span.run_offset || 0);
|
|
2263
2368
|
if (local_start > 0) {
|
|
2264
2369
|
const split_source = working_runs[0];
|
|
2265
2370
|
const [, right_run] = this._split_run_at_index(
|
|
@@ -2707,6 +2812,153 @@ function _sequence_opcodes(a, b) {
|
|
|
2707
2812
|
flushPending();
|
|
2708
2813
|
return ops;
|
|
2709
2814
|
}
|
|
2815
|
+
function _is_table_blob(block) {
|
|
2816
|
+
const lines = block.split("\n");
|
|
2817
|
+
return lines.length > 0 && lines.every((line) => line.includes(" | "));
|
|
2818
|
+
}
|
|
2819
|
+
function _table_blob_row_edits(orig_blob, mod_blob) {
|
|
2820
|
+
const rows_o = orig_blob.split("\n");
|
|
2821
|
+
const rows_m = mod_blob.split("\n");
|
|
2822
|
+
if (rows_o.length !== rows_m.length) return null;
|
|
2823
|
+
const row_edits = [];
|
|
2824
|
+
for (let k = 0; k < rows_o.length; k++) {
|
|
2825
|
+
if (rows_o[k] === rows_m[k]) continue;
|
|
2826
|
+
row_edits.push({
|
|
2827
|
+
type: "modify",
|
|
2828
|
+
target_text: rows_o[k],
|
|
2829
|
+
new_text: rows_m[k],
|
|
2830
|
+
comment: "Diff: Table row modified",
|
|
2831
|
+
_is_table_edit: true
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
return row_edits;
|
|
2835
|
+
}
|
|
2836
|
+
function _split_cross_paragraph_hunks(edits) {
|
|
2837
|
+
const out = [];
|
|
2838
|
+
for (const e of edits) {
|
|
2839
|
+
const target = e.target_text || "";
|
|
2840
|
+
const newText = e.new_text || "";
|
|
2841
|
+
const idx = e._match_start_index;
|
|
2842
|
+
if (idx === void 0 || idx === null || !target.includes("\n\n") || target.split("\n\n").length - 1 === newText.split("\n\n").length - 1) {
|
|
2843
|
+
out.push(e);
|
|
2844
|
+
continue;
|
|
2845
|
+
}
|
|
2846
|
+
const parts = target.split("\n\n");
|
|
2847
|
+
if (parts.length < 2 || !parts[0].trim() || !parts[parts.length - 1].trim()) {
|
|
2848
|
+
out.push(e);
|
|
2849
|
+
continue;
|
|
2850
|
+
}
|
|
2851
|
+
let offset = idx;
|
|
2852
|
+
for (const piece of parts.slice(0, -1)) {
|
|
2853
|
+
out.push({
|
|
2854
|
+
type: "modify",
|
|
2855
|
+
target_text: piece + "\n\n",
|
|
2856
|
+
new_text: "",
|
|
2857
|
+
comment: e.comment || "Diff: Text deleted",
|
|
2858
|
+
_match_start_index: offset
|
|
2859
|
+
});
|
|
2860
|
+
offset += piece.length + 2;
|
|
2861
|
+
}
|
|
2862
|
+
out.push({
|
|
2863
|
+
type: "modify",
|
|
2864
|
+
target_text: parts[parts.length - 1],
|
|
2865
|
+
new_text: newText,
|
|
2866
|
+
comment: e.comment,
|
|
2867
|
+
_match_start_index: offset
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
return out;
|
|
2871
|
+
}
|
|
2872
|
+
function generate_edits_via_paragraph_alignment(original_text, modified_text) {
|
|
2873
|
+
const orig_paragraphs = original_text.split("\n\n");
|
|
2874
|
+
const mod_paragraphs = modified_text.split("\n\n");
|
|
2875
|
+
const orig_offsets = [];
|
|
2876
|
+
let current_offset = 0;
|
|
2877
|
+
for (const p of orig_paragraphs) {
|
|
2878
|
+
orig_offsets.push(current_offset);
|
|
2879
|
+
current_offset += p.length + 2;
|
|
2880
|
+
}
|
|
2881
|
+
const opcodes = _sequence_opcodes(orig_paragraphs, mod_paragraphs);
|
|
2882
|
+
const edits = [];
|
|
2883
|
+
for (const [tag, i1, i2, j1, j2] of opcodes) {
|
|
2884
|
+
if (tag === "equal") continue;
|
|
2885
|
+
const offset = i1 < orig_offsets.length ? orig_offsets[i1] : original_text.length;
|
|
2886
|
+
if (tag === "delete") {
|
|
2887
|
+
if (i2 < orig_paragraphs.length) {
|
|
2888
|
+
let piece_offset = offset;
|
|
2889
|
+
for (let k = i1; k < i2; k++) {
|
|
2890
|
+
const piece = orig_paragraphs[k] + "\n\n";
|
|
2891
|
+
edits.push({
|
|
2892
|
+
type: "modify",
|
|
2893
|
+
target_text: piece,
|
|
2894
|
+
new_text: "",
|
|
2895
|
+
comment: "Diff: Text deleted",
|
|
2896
|
+
_match_start_index: piece_offset
|
|
2897
|
+
});
|
|
2898
|
+
piece_offset += piece.length;
|
|
2899
|
+
}
|
|
2900
|
+
} else {
|
|
2901
|
+
let deleted_text = orig_paragraphs.slice(i1, i2).join("\n\n");
|
|
2902
|
+
let del_offset = offset;
|
|
2903
|
+
if (i1 > 0) {
|
|
2904
|
+
deleted_text = "\n\n" + deleted_text;
|
|
2905
|
+
del_offset -= 2;
|
|
2906
|
+
}
|
|
2907
|
+
edits.push({
|
|
2908
|
+
type: "modify",
|
|
2909
|
+
target_text: deleted_text,
|
|
2910
|
+
new_text: "",
|
|
2911
|
+
comment: "Diff: Text deleted",
|
|
2912
|
+
_match_start_index: del_offset
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
} else if (tag === "insert") {
|
|
2916
|
+
let inserted_text = mod_paragraphs.slice(j1, j2).join("\n\n");
|
|
2917
|
+
if (i1 < orig_paragraphs.length) {
|
|
2918
|
+
inserted_text = inserted_text + "\n\n";
|
|
2919
|
+
} else {
|
|
2920
|
+
inserted_text = "\n\n" + inserted_text;
|
|
2921
|
+
}
|
|
2922
|
+
edits.push({
|
|
2923
|
+
type: "modify",
|
|
2924
|
+
target_text: "",
|
|
2925
|
+
new_text: inserted_text,
|
|
2926
|
+
comment: "Diff: Text inserted",
|
|
2927
|
+
_match_start_index: offset
|
|
2928
|
+
});
|
|
2929
|
+
} else if (tag === "replace") {
|
|
2930
|
+
if (i2 - i1 === j2 - j1 && Array.from({ length: i2 - i1 }).some(
|
|
2931
|
+
(_, k) => _is_table_blob(orig_paragraphs[i1 + k]) || _is_table_blob(mod_paragraphs[j1 + k])
|
|
2932
|
+
)) {
|
|
2933
|
+
for (let k = 0; k < i2 - i1; k++) {
|
|
2934
|
+
const orig_p = orig_paragraphs[i1 + k];
|
|
2935
|
+
const mod_p = mod_paragraphs[j1 + k];
|
|
2936
|
+
if (orig_p === mod_p) continue;
|
|
2937
|
+
const pair_offset = orig_offsets[i1 + k];
|
|
2938
|
+
const row_edits = _is_table_blob(orig_p) && _is_table_blob(mod_p) ? _table_blob_row_edits(orig_p, mod_p) : null;
|
|
2939
|
+
if (row_edits !== null) {
|
|
2940
|
+
edits.push(...row_edits);
|
|
2941
|
+
continue;
|
|
2942
|
+
}
|
|
2943
|
+
const pair_edits = generate_edits_from_text(orig_p, mod_p);
|
|
2944
|
+
for (const ce of pair_edits) {
|
|
2945
|
+
ce._match_start_index = (ce._match_start_index || 0) + pair_offset;
|
|
2946
|
+
edits.push(ce);
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
continue;
|
|
2950
|
+
}
|
|
2951
|
+
const orig_chunk = orig_paragraphs.slice(i1, i2).join("\n\n");
|
|
2952
|
+
const mod_chunk = mod_paragraphs.slice(j1, j2).join("\n\n");
|
|
2953
|
+
const chunk_edits = generate_edits_from_text(orig_chunk, mod_chunk);
|
|
2954
|
+
for (const ce of chunk_edits) {
|
|
2955
|
+
ce._match_start_index = (ce._match_start_index || 0) + offset;
|
|
2956
|
+
edits.push(ce);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
return _split_cross_paragraph_hunks(edits);
|
|
2961
|
+
}
|
|
2710
2962
|
var _IMAGE_MARKER_RE = /!\[[^\]]*\]\(docx-image:[^)]*\)/g;
|
|
2711
2963
|
function _drop_image_marker_hunks(edits, warnings) {
|
|
2712
2964
|
const _normalized = (s) => s.replace(_IMAGE_MARKER_RE, "").replace(/\s+/g, " ").trim();
|
|
@@ -2904,7 +3156,7 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2904
3156
|
);
|
|
2905
3157
|
const flat = _drop_marker_interior_hunks(
|
|
2906
3158
|
_drop_image_marker_hunks(
|
|
2907
|
-
|
|
3159
|
+
generate_edits_via_paragraph_alignment(text_orig, text_mod),
|
|
2908
3160
|
warnings
|
|
2909
3161
|
),
|
|
2910
3162
|
text_orig,
|
|
@@ -2930,7 +3182,7 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2930
3182
|
);
|
|
2931
3183
|
}
|
|
2932
3184
|
if (!tables_alignable) {
|
|
2933
|
-
const part_edits =
|
|
3185
|
+
const part_edits = generate_edits_via_paragraph_alignment(
|
|
2934
3186
|
text_orig.substring(po_start, po_end),
|
|
2935
3187
|
text_mod.substring(pm_start, pm_end)
|
|
2936
3188
|
);
|
|
@@ -2955,7 +3207,7 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2955
3207
|
const seg_o_end = boundaries_o[seg_idx + 1][0];
|
|
2956
3208
|
const seg_m_start = boundaries_m[seg_idx][1];
|
|
2957
3209
|
const seg_m_end = boundaries_m[seg_idx + 1][0];
|
|
2958
|
-
const seg_edits =
|
|
3210
|
+
const seg_edits = generate_edits_via_paragraph_alignment(
|
|
2959
3211
|
text_orig.substring(seg_o_start, seg_o_end),
|
|
2960
3212
|
text_mod.substring(seg_m_start, seg_m_end)
|
|
2961
3213
|
);
|
|
@@ -4809,7 +5061,25 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4809
5061
|
*
|
|
4810
5062
|
* Does NOT attach comments; callers handle that.
|
|
4811
5063
|
*/
|
|
4812
|
-
|
|
5064
|
+
_clone_pPr_scrubbing_headings(existing_pPr) {
|
|
5065
|
+
const pPr_clone = existing_pPr.cloneNode(true);
|
|
5066
|
+
const pStyle_el = findChild(pPr_clone, "w:pStyle");
|
|
5067
|
+
if (pStyle_el) {
|
|
5068
|
+
const style_val = pStyle_el.getAttribute("w:val");
|
|
5069
|
+
if (style_val) {
|
|
5070
|
+
const is_heading = style_val.startsWith("Heading") || style_val === "Title" || style_val.replace(/\s+/g, "").startsWith("Heading");
|
|
5071
|
+
if (is_heading) {
|
|
5072
|
+
pPr_clone.removeChild(pStyle_el);
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
}
|
|
5076
|
+
const outlineLvl_el = findChild(pPr_clone, "w:outlineLvl");
|
|
5077
|
+
if (outlineLvl_el) {
|
|
5078
|
+
pPr_clone.removeChild(outlineLvl_el);
|
|
5079
|
+
}
|
|
5080
|
+
return pPr_clone;
|
|
5081
|
+
}
|
|
5082
|
+
_track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id, positional_anchor_el = null, suppress_emphasis = false, insert_before = false) {
|
|
4813
5083
|
if (!text) {
|
|
4814
5084
|
return {
|
|
4815
5085
|
first_node: null,
|
|
@@ -4831,7 +5101,11 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4831
5101
|
current_p = walker;
|
|
4832
5102
|
}
|
|
4833
5103
|
const suffix_nodes = [];
|
|
4834
|
-
const
|
|
5104
|
+
const relocatable = /* @__PURE__ */ new Set(["w:r", "w:ins", "w:del"]);
|
|
5105
|
+
const pos_from_positional = positional_anchor_el && positional_anchor_el.parentNode ? positional_anchor_el : null;
|
|
5106
|
+
const pos_from_anchor_run = anchor_run !== null && anchor_run._element.parentNode ? anchor_run._element : null;
|
|
5107
|
+
const pos_source = pos_from_positional ?? pos_from_anchor_run;
|
|
5108
|
+
const suffix_includes_anchor = insert_before && pos_from_positional === null && pos_from_anchor_run !== null;
|
|
4835
5109
|
if (current_p !== null && pos_source !== null) {
|
|
4836
5110
|
let pos_anchor = pos_source;
|
|
4837
5111
|
while (pos_anchor && pos_anchor.parentNode !== current_p) {
|
|
@@ -4842,8 +5116,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4842
5116
|
}
|
|
4843
5117
|
}
|
|
4844
5118
|
if (pos_anchor) {
|
|
4845
|
-
|
|
4846
|
-
let nxt = pos_anchor.nextSibling;
|
|
5119
|
+
let nxt = suffix_includes_anchor ? pos_anchor : pos_anchor.nextSibling;
|
|
4847
5120
|
while (nxt) {
|
|
4848
5121
|
if (nxt.nodeType === 1 && relocatable.has(nxt.tagName)) {
|
|
4849
5122
|
suffix_nodes.push(nxt);
|
|
@@ -4851,6 +5124,14 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4851
5124
|
nxt = nxt.nextSibling;
|
|
4852
5125
|
}
|
|
4853
5126
|
}
|
|
5127
|
+
} else if (current_p !== null && insert_before) {
|
|
5128
|
+
let child = current_p.firstChild;
|
|
5129
|
+
while (child) {
|
|
5130
|
+
if (child.nodeType === 1 && relocatable.has(child.tagName)) {
|
|
5131
|
+
suffix_nodes.push(child);
|
|
5132
|
+
}
|
|
5133
|
+
child = child.nextSibling;
|
|
5134
|
+
}
|
|
4854
5135
|
}
|
|
4855
5136
|
while (lines.length > 1 && lines[lines.length - 1] === "" && suffix_nodes.length === 0) {
|
|
4856
5137
|
lines.pop();
|
|
@@ -4919,7 +5200,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4919
5200
|
} else {
|
|
4920
5201
|
const existing_pPr = findChild(current_p, "w:pPr");
|
|
4921
5202
|
if (existing_pPr) {
|
|
4922
|
-
new_p.appendChild(
|
|
5203
|
+
new_p.appendChild(this._clone_pPr_scrubbing_headings(existing_pPr));
|
|
4923
5204
|
}
|
|
4924
5205
|
}
|
|
4925
5206
|
let pPr = findChild(new_p, "w:pPr");
|
|
@@ -5314,18 +5595,16 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5314
5595
|
continue;
|
|
5315
5596
|
}
|
|
5316
5597
|
}
|
|
5317
|
-
let matches = this.mapper.
|
|
5318
|
-
edit.target_text,
|
|
5319
|
-
is_regex
|
|
5598
|
+
let matches = this.mapper.drop_virtual_only_matches(
|
|
5599
|
+
this.mapper.find_all_match_indices(edit.target_text, is_regex)
|
|
5320
5600
|
);
|
|
5321
5601
|
let activeText = this.mapper.full_text;
|
|
5322
5602
|
let target_mapper = this.mapper;
|
|
5323
5603
|
if (matches.length === 0) {
|
|
5324
5604
|
if (!this.clean_mapper)
|
|
5325
5605
|
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
5326
|
-
matches = this.clean_mapper.
|
|
5327
|
-
edit.target_text,
|
|
5328
|
-
is_regex
|
|
5606
|
+
matches = this.clean_mapper.drop_virtual_only_matches(
|
|
5607
|
+
this.clean_mapper.find_all_match_indices(edit.target_text, is_regex)
|
|
5329
5608
|
);
|
|
5330
5609
|
if (matches.length > 0) {
|
|
5331
5610
|
activeText = this.clean_mapper.full_text;
|
|
@@ -5348,9 +5627,11 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5348
5627
|
if (!this.original_mapper) {
|
|
5349
5628
|
this.original_mapper = new DocumentMapper(this.doc, false, true);
|
|
5350
5629
|
}
|
|
5351
|
-
const orig_matches = this.original_mapper.
|
|
5352
|
-
|
|
5353
|
-
|
|
5630
|
+
const orig_matches = this.original_mapper.drop_virtual_only_matches(
|
|
5631
|
+
this.original_mapper.find_all_match_indices(
|
|
5632
|
+
edit.target_text,
|
|
5633
|
+
is_regex
|
|
5634
|
+
)
|
|
5354
5635
|
);
|
|
5355
5636
|
if (orig_matches.length > 0) {
|
|
5356
5637
|
is_deleted_text = true;
|
|
@@ -5572,10 +5853,15 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5572
5853
|
}
|
|
5573
5854
|
static _column_count_at(mapper, start, length) {
|
|
5574
5855
|
for (const s of mapper.spans) {
|
|
5575
|
-
if (s.
|
|
5856
|
+
if (s.end <= start || s.start >= start + length) {
|
|
5576
5857
|
continue;
|
|
5577
5858
|
}
|
|
5578
|
-
let curr =
|
|
5859
|
+
let curr = null;
|
|
5860
|
+
if (s.run !== null) {
|
|
5861
|
+
curr = s.run._element;
|
|
5862
|
+
} else if (s.paragraph !== null) {
|
|
5863
|
+
curr = s.paragraph._element;
|
|
5864
|
+
}
|
|
5579
5865
|
while (curr) {
|
|
5580
5866
|
if (curr.nodeType === 1 && curr.tagName === "w:tr") {
|
|
5581
5867
|
return findAllDescendants(curr, "w:tc").filter(
|
|
@@ -5589,6 +5875,45 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5589
5875
|
}
|
|
5590
5876
|
validate_review_actions(actions) {
|
|
5591
5877
|
const errors = [];
|
|
5878
|
+
const seen_resolutions = /* @__PURE__ */ new Map();
|
|
5879
|
+
const seen_replies = /* @__PURE__ */ new Set();
|
|
5880
|
+
for (let i = 0; i < actions.length; i++) {
|
|
5881
|
+
const action = actions[i];
|
|
5882
|
+
const type = action.type;
|
|
5883
|
+
const target_id = action.target_id ?? "";
|
|
5884
|
+
if (type === "reply") {
|
|
5885
|
+
if (!String(action.text ?? "").trim()) {
|
|
5886
|
+
errors.push(
|
|
5887
|
+
`- 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'.`
|
|
5888
|
+
);
|
|
5889
|
+
continue;
|
|
5890
|
+
}
|
|
5891
|
+
const reply_key = `${target_id}\0${String(action.text).trim()}`;
|
|
5892
|
+
if (seen_replies.has(reply_key)) {
|
|
5893
|
+
errors.push(
|
|
5894
|
+
`- Action ${i + 1} Failed: duplicate reply \u2014 this batch already replies to ${target_id} with the same text. Remove the duplicate action.`
|
|
5895
|
+
);
|
|
5896
|
+
}
|
|
5897
|
+
seen_replies.add(reply_key);
|
|
5898
|
+
} else if (type === "accept" || type === "reject") {
|
|
5899
|
+
const prior = seen_resolutions.get(target_id);
|
|
5900
|
+
if (prior !== void 0) {
|
|
5901
|
+
const [first_idx, first_type] = prior;
|
|
5902
|
+
if (first_type === type) {
|
|
5903
|
+
errors.push(
|
|
5904
|
+
`- 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.`
|
|
5905
|
+
);
|
|
5906
|
+
} else {
|
|
5907
|
+
errors.push(
|
|
5908
|
+
`- 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.`
|
|
5909
|
+
);
|
|
5910
|
+
}
|
|
5911
|
+
} else {
|
|
5912
|
+
seen_resolutions.set(target_id, [i, type]);
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
5916
|
+
if (errors.length > 0) return errors;
|
|
5592
5917
|
for (let i = 0; i < actions.length; i++) {
|
|
5593
5918
|
const action = actions[i];
|
|
5594
5919
|
const type = action.type;
|
|
@@ -5676,7 +6001,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5676
6001
|
(c) => c === null || typeof c !== "object" || !["accept", "reject", "reply"].includes(c.type)
|
|
5677
6002
|
);
|
|
5678
6003
|
if (!dry_run_mode) {
|
|
5679
|
-
|
|
6004
|
+
let action_errors = actions.length > 0 ? this.validate_review_actions(actions) : [];
|
|
6005
|
+
if (actions.length > 0 && action_errors.length === 0) {
|
|
6006
|
+
action_errors = this.validate_action_pairing(actions);
|
|
6007
|
+
}
|
|
5680
6008
|
const validate_edits_now = edits.length > 0 && action_errors.length > 0;
|
|
5681
6009
|
const edit_errors = validate_edits_now ? this.validate_edits(edits) : [];
|
|
5682
6010
|
const all_errors = [...action_errors, ...edit_errors];
|
|
@@ -5685,7 +6013,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5685
6013
|
}
|
|
5686
6014
|
} else {
|
|
5687
6015
|
if (actions.length > 0) {
|
|
5688
|
-
|
|
6016
|
+
let action_errors = this.validate_review_actions(actions);
|
|
6017
|
+
if (action_errors.length === 0) {
|
|
6018
|
+
action_errors = this.validate_action_pairing(actions);
|
|
6019
|
+
}
|
|
5689
6020
|
if (action_errors.length > 0) {
|
|
5690
6021
|
throw new BatchValidationError(action_errors);
|
|
5691
6022
|
}
|
|
@@ -5693,10 +6024,12 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5693
6024
|
}
|
|
5694
6025
|
let applied_actions = 0;
|
|
5695
6026
|
let skipped_actions = 0;
|
|
6027
|
+
let already_resolved_actions = 0;
|
|
5696
6028
|
if (actions.length > 0) {
|
|
5697
6029
|
const res = this.apply_review_actions(actions);
|
|
5698
6030
|
applied_actions = res[0];
|
|
5699
6031
|
skipped_actions = res[1];
|
|
6032
|
+
already_resolved_actions = res[2];
|
|
5700
6033
|
if (skipped_actions > 0) {
|
|
5701
6034
|
throw new BatchValidationError(this.skipped_details);
|
|
5702
6035
|
}
|
|
@@ -5891,6 +6224,11 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5891
6224
|
return {
|
|
5892
6225
|
actions_applied: applied_actions,
|
|
5893
6226
|
actions_skipped: skipped_actions,
|
|
6227
|
+
// Actions whose target was already resolved by an earlier action of
|
|
6228
|
+
// this batch (via its replacement pair): consistent no-ops, never
|
|
6229
|
+
// counted as applied — every reported "applied" action causes an
|
|
6230
|
+
// observable state transition (ADEU-QA-004).
|
|
6231
|
+
actions_already_resolved: already_resolved_actions,
|
|
5894
6232
|
edits_applied: applied_edits,
|
|
5895
6233
|
edits_skipped: skipped_edits,
|
|
5896
6234
|
// edits_applied counts change OBJECTS; this is the total number of
|
|
@@ -5932,13 +6270,17 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5932
6270
|
edit._resolved_start_idx = edit._match_start_index;
|
|
5933
6271
|
resolved_edits.push([edit, edit.new_text || null]);
|
|
5934
6272
|
} else if (edit.type === "insert_row" || edit.type === "delete_row") {
|
|
5935
|
-
let matches = this.mapper.
|
|
6273
|
+
let matches = this.mapper.drop_virtual_only_matches(
|
|
6274
|
+
this.mapper.find_all_match_indices(edit.target_text)
|
|
6275
|
+
);
|
|
5936
6276
|
let resolved_mapper = this.mapper;
|
|
5937
6277
|
if (matches.length === 0) {
|
|
5938
6278
|
if (!this.clean_mapper) {
|
|
5939
6279
|
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
5940
6280
|
}
|
|
5941
|
-
matches = this.clean_mapper.
|
|
6281
|
+
matches = this.clean_mapper.drop_virtual_only_matches(
|
|
6282
|
+
this.clean_mapper.find_all_match_indices(edit.target_text)
|
|
6283
|
+
);
|
|
5942
6284
|
resolved_mapper = this.clean_mapper;
|
|
5943
6285
|
}
|
|
5944
6286
|
if (matches.length > 0) {
|
|
@@ -6109,10 +6451,149 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6109
6451
|
}
|
|
6110
6452
|
return [applied_logical, skipped_logical];
|
|
6111
6453
|
}
|
|
6454
|
+
/**
|
|
6455
|
+
* True when the paragraph still carries visible content (w:t text, w:tab,
|
|
6456
|
+
* w:br) that is NOT wrapped in a tracked deletion — i.e. the paragraph
|
|
6457
|
+
* would render non-empty in the accepted document.
|
|
6458
|
+
*/
|
|
6459
|
+
_paragraph_has_visible_content(p_elem) {
|
|
6460
|
+
for (const tag of ["w:t", "w:tab", "w:br"]) {
|
|
6461
|
+
const nodes = findAllDescendants(p_elem, tag);
|
|
6462
|
+
for (const node of nodes) {
|
|
6463
|
+
let is_deleted = false;
|
|
6464
|
+
let curr = node.parentNode;
|
|
6465
|
+
while (curr && curr !== p_elem.parentNode) {
|
|
6466
|
+
if (curr.tagName === "w:del") {
|
|
6467
|
+
is_deleted = true;
|
|
6468
|
+
break;
|
|
6469
|
+
}
|
|
6470
|
+
curr = curr.parentNode;
|
|
6471
|
+
}
|
|
6472
|
+
if (!is_deleted) {
|
|
6473
|
+
if (tag === "w:t" && !node.textContent) continue;
|
|
6474
|
+
return true;
|
|
6475
|
+
}
|
|
6476
|
+
}
|
|
6477
|
+
}
|
|
6478
|
+
return false;
|
|
6479
|
+
}
|
|
6480
|
+
/**
|
|
6481
|
+
* All contiguous same-author w:ins/w:del siblings that form one logical
|
|
6482
|
+
* modification block with `node` (a replacement's del+ins pair). Mirrors
|
|
6483
|
+
* the Python engine's _get_paired_nodes: comment range markers and
|
|
6484
|
+
* rPr/pPr are transparent; a different author or any other element breaks
|
|
6485
|
+
* the group.
|
|
6486
|
+
*/
|
|
6487
|
+
_get_paired_nodes(node) {
|
|
6488
|
+
const pairs = [];
|
|
6489
|
+
const author = node.getAttribute("w:author");
|
|
6490
|
+
const transparent = /* @__PURE__ */ new Set([
|
|
6491
|
+
"w:commentRangeStart",
|
|
6492
|
+
"w:commentRangeEnd",
|
|
6493
|
+
"w:commentReference",
|
|
6494
|
+
"w:rPr",
|
|
6495
|
+
"w:pPr"
|
|
6496
|
+
]);
|
|
6497
|
+
const walk = (start, dir) => {
|
|
6498
|
+
let cur = dir === "next" ? start.nextSibling : start.previousSibling;
|
|
6499
|
+
while (cur) {
|
|
6500
|
+
if (cur.nodeType !== 1) {
|
|
6501
|
+
cur = dir === "next" ? cur.nextSibling : cur.previousSibling;
|
|
6502
|
+
continue;
|
|
6503
|
+
}
|
|
6504
|
+
const el = cur;
|
|
6505
|
+
if (transparent.has(el.tagName)) {
|
|
6506
|
+
cur = dir === "next" ? cur.nextSibling : cur.previousSibling;
|
|
6507
|
+
continue;
|
|
6508
|
+
}
|
|
6509
|
+
if ((el.tagName === "w:ins" || el.tagName === "w:del") && el.getAttribute("w:author") === author) {
|
|
6510
|
+
pairs.push(el);
|
|
6511
|
+
cur = dir === "next" ? cur.nextSibling : cur.previousSibling;
|
|
6512
|
+
continue;
|
|
6513
|
+
}
|
|
6514
|
+
break;
|
|
6515
|
+
}
|
|
6516
|
+
};
|
|
6517
|
+
walk(node, "next");
|
|
6518
|
+
walk(node, "prev");
|
|
6519
|
+
return pairs;
|
|
6520
|
+
}
|
|
6521
|
+
/**
|
|
6522
|
+
* All revision ids that resolve as ONE unit with `target_id`: the ids of
|
|
6523
|
+
* every contiguous same-author w:ins/w:del sibling of its elements (a
|
|
6524
|
+
* replacement's del+ins pair), plus the id itself.
|
|
6525
|
+
*/
|
|
6526
|
+
_resolution_group_ids(target_id) {
|
|
6527
|
+
const nodes = [
|
|
6528
|
+
...findAllDescendants(this.doc.element, "w:ins"),
|
|
6529
|
+
...findAllDescendants(this.doc.element, "w:del")
|
|
6530
|
+
].filter((n) => n.getAttribute("w:id") === target_id);
|
|
6531
|
+
const group = /* @__PURE__ */ new Set();
|
|
6532
|
+
if (nodes.length === 0) return group;
|
|
6533
|
+
group.add(target_id);
|
|
6534
|
+
for (const node of nodes) {
|
|
6535
|
+
for (const paired of this._get_paired_nodes(node)) {
|
|
6536
|
+
const pid = paired.getAttribute("w:id");
|
|
6537
|
+
if (pid) group.add(pid);
|
|
6538
|
+
}
|
|
6539
|
+
}
|
|
6540
|
+
return group;
|
|
6541
|
+
}
|
|
6542
|
+
/**
|
|
6543
|
+
* Document-aware validation (QA 2026-07-19 ADEU-QA-004): a replacement's
|
|
6544
|
+
* del+ins pair carries two distinct ids but resolves as one unit, so a
|
|
6545
|
+
* batch that accepts one side and rejects the other is contradictory.
|
|
6546
|
+
* Rejecting it up front — before any action mutates the document — keeps
|
|
6547
|
+
* the batch transactional.
|
|
6548
|
+
*/
|
|
6549
|
+
validate_action_pairing(actions) {
|
|
6550
|
+
const errors = [];
|
|
6551
|
+
const group_first = /* @__PURE__ */ new Map();
|
|
6552
|
+
for (let pos = 0; pos < actions.length; pos++) {
|
|
6553
|
+
const act = actions[pos];
|
|
6554
|
+
if (act.type !== "accept" && act.type !== "reject") continue;
|
|
6555
|
+
const raw_id = String(act.target_id ?? "");
|
|
6556
|
+
if (raw_id.startsWith("Com:")) continue;
|
|
6557
|
+
const target_id = raw_id.startsWith("Chg:") ? raw_id.slice(4) : raw_id;
|
|
6558
|
+
const group = this._resolution_group_ids(target_id);
|
|
6559
|
+
if (group.size === 0) continue;
|
|
6560
|
+
let conflict = null;
|
|
6561
|
+
for (const gid of group) {
|
|
6562
|
+
const prior = group_first.get(gid);
|
|
6563
|
+
if (prior !== void 0 && prior[1] !== act.type) {
|
|
6564
|
+
conflict = prior;
|
|
6565
|
+
break;
|
|
6566
|
+
}
|
|
6567
|
+
}
|
|
6568
|
+
if (conflict !== null) {
|
|
6569
|
+
const [first_pos, first_type, first_id] = conflict;
|
|
6570
|
+
errors.push(
|
|
6571
|
+
`- 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.`
|
|
6572
|
+
);
|
|
6573
|
+
continue;
|
|
6574
|
+
}
|
|
6575
|
+
for (const gid of group) {
|
|
6576
|
+
if (!group_first.has(gid)) {
|
|
6577
|
+
group_first.set(gid, [pos, act.type, target_id]);
|
|
6578
|
+
}
|
|
6579
|
+
}
|
|
6580
|
+
}
|
|
6581
|
+
return errors;
|
|
6582
|
+
}
|
|
6583
|
+
/**
|
|
6584
|
+
* Returns [applied, skipped, already_resolved]. `applied` counts actions
|
|
6585
|
+
* that caused an observable state transition; an action naming an id an
|
|
6586
|
+
* earlier action of this batch already resolved (via its replacement pair)
|
|
6587
|
+
* is counted in `already_resolved` instead — never as applied
|
|
6588
|
+
* (QA 2026-07-19 ADEU-QA-004).
|
|
6589
|
+
*/
|
|
6112
6590
|
apply_review_actions(actions) {
|
|
6113
6591
|
let applied = 0;
|
|
6114
6592
|
let skipped = 0;
|
|
6115
|
-
|
|
6593
|
+
let already_resolved = 0;
|
|
6594
|
+
const resolved_history = /* @__PURE__ */ new Map();
|
|
6595
|
+
for (let pos = 0; pos < actions.length; pos++) {
|
|
6596
|
+
const action = actions[pos];
|
|
6116
6597
|
const type = action.type;
|
|
6117
6598
|
if (type === "reply") {
|
|
6118
6599
|
const cid = action.target_id.replace("Com:", "");
|
|
@@ -6126,6 +6607,21 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6126
6607
|
continue;
|
|
6127
6608
|
}
|
|
6128
6609
|
const target_id = action.target_id.replace("Chg:", "");
|
|
6610
|
+
const prior_type = resolved_history.get(target_id);
|
|
6611
|
+
if (prior_type !== void 0) {
|
|
6612
|
+
if (prior_type === type) {
|
|
6613
|
+
already_resolved++;
|
|
6614
|
+
this.skipped_details.push(
|
|
6615
|
+
`- 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.`
|
|
6616
|
+
);
|
|
6617
|
+
continue;
|
|
6618
|
+
}
|
|
6619
|
+
this.skipped_details.push(
|
|
6620
|
+
`- 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.`
|
|
6621
|
+
);
|
|
6622
|
+
skipped++;
|
|
6623
|
+
continue;
|
|
6624
|
+
}
|
|
6129
6625
|
const all_ins = findAllDescendants(this.doc.element, "w:ins").filter(
|
|
6130
6626
|
(n) => n.getAttribute("w:id") === target_id
|
|
6131
6627
|
);
|
|
@@ -6150,7 +6646,18 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6150
6646
|
);
|
|
6151
6647
|
continue;
|
|
6152
6648
|
}
|
|
6649
|
+
const group_nodes = new Set(all_nodes);
|
|
6153
6650
|
for (const node of all_nodes) {
|
|
6651
|
+
for (const paired of this._get_paired_nodes(node)) {
|
|
6652
|
+
group_nodes.add(paired);
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
6655
|
+
const resolved_now = /* @__PURE__ */ new Set();
|
|
6656
|
+
for (const node of group_nodes) {
|
|
6657
|
+
const rid = node.getAttribute("w:id");
|
|
6658
|
+
if (rid) resolved_now.add(rid);
|
|
6659
|
+
}
|
|
6660
|
+
for (const node of group_nodes) {
|
|
6154
6661
|
const is_ins = node.tagName === "w:ins";
|
|
6155
6662
|
const parent_tag = node.parentNode ? node.parentNode.tagName : "";
|
|
6156
6663
|
const is_trPr = parent_tag === "w:trPr";
|
|
@@ -6202,9 +6709,12 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6202
6709
|
}
|
|
6203
6710
|
}
|
|
6204
6711
|
}
|
|
6712
|
+
for (const rid of resolved_now) {
|
|
6713
|
+
resolved_history.set(rid, type);
|
|
6714
|
+
}
|
|
6205
6715
|
applied++;
|
|
6206
6716
|
}
|
|
6207
|
-
return [applied, skipped];
|
|
6717
|
+
return [applied, skipped, already_resolved];
|
|
6208
6718
|
}
|
|
6209
6719
|
_apply_table_edit(edit, rebuild_map) {
|
|
6210
6720
|
const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
|
|
@@ -6264,17 +6774,15 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6264
6774
|
if (!edit.target_text) return null;
|
|
6265
6775
|
const is_regex = edit.regex || false;
|
|
6266
6776
|
const match_mode = edit.match_mode || "strict";
|
|
6267
|
-
let matches = this.mapper.
|
|
6268
|
-
edit.target_text,
|
|
6269
|
-
is_regex
|
|
6777
|
+
let matches = this.mapper.drop_virtual_only_matches(
|
|
6778
|
+
this.mapper.find_all_match_indices(edit.target_text, is_regex)
|
|
6270
6779
|
);
|
|
6271
6780
|
let use_clean_map = false;
|
|
6272
6781
|
if (matches.length === 0) {
|
|
6273
6782
|
if (!this.clean_mapper)
|
|
6274
6783
|
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
6275
|
-
matches = this.clean_mapper.
|
|
6276
|
-
edit.target_text,
|
|
6277
|
-
is_regex
|
|
6784
|
+
matches = this.clean_mapper.drop_virtual_only_matches(
|
|
6785
|
+
this.clean_mapper.find_all_match_indices(edit.target_text, is_regex)
|
|
6278
6786
|
);
|
|
6279
6787
|
if (matches.length > 0) use_clean_map = true;
|
|
6280
6788
|
else return null;
|
|
@@ -6784,7 +7292,9 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6784
7292
|
this._set_paragraph_style(new_p, style_name);
|
|
6785
7293
|
} else {
|
|
6786
7294
|
const existing_pPr = findChild(_bug233_target_para, "w:pPr");
|
|
6787
|
-
if (existing_pPr)
|
|
7295
|
+
if (existing_pPr) {
|
|
7296
|
+
new_p.appendChild(this._clone_pPr_scrubbing_headings(existing_pPr));
|
|
7297
|
+
}
|
|
6788
7298
|
}
|
|
6789
7299
|
let pPr = findChild(new_p, "w:pPr");
|
|
6790
7300
|
if (!pPr) {
|
|
@@ -6836,7 +7346,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6836
7346
|
anchor_para,
|
|
6837
7347
|
ins_id,
|
|
6838
7348
|
null,
|
|
6839
|
-
suppress_emphasis
|
|
7349
|
+
suppress_emphasis,
|
|
7350
|
+
// Paragraph-start insertions attach BEFORE the anchor (see
|
|
7351
|
+
// before_anchor below): the suffix relocation must know.
|
|
7352
|
+
start_idx === 0
|
|
6840
7353
|
);
|
|
6841
7354
|
if (!result.first_node) return false;
|
|
6842
7355
|
const is_inline_first = result.first_node.tagName === "w:ins";
|
|
@@ -7039,7 +7552,24 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
7039
7552
|
p2_element = getNextElement(p2_element);
|
|
7040
7553
|
}
|
|
7041
7554
|
if (p2_element && p2_element.tagName === "w:p") {
|
|
7555
|
+
const p1_fully_deleted = !this._paragraph_has_visible_content(p1_element);
|
|
7042
7556
|
let pPr = findChild(p1_element, "w:pPr");
|
|
7557
|
+
if (p1_fully_deleted) {
|
|
7558
|
+
const p2_pPr = findChild(p2_element, "w:pPr");
|
|
7559
|
+
const adopted = p2_pPr ? p2_pPr.cloneNode(true) : p1_element.ownerDocument.createElement("w:pPr");
|
|
7560
|
+
if (pPr) {
|
|
7561
|
+
const sect = findChild(pPr, "w:sectPr");
|
|
7562
|
+
if (sect && !findChild(adopted, "w:sectPr")) {
|
|
7563
|
+
adopted.appendChild(sect.cloneNode(true));
|
|
7564
|
+
}
|
|
7565
|
+
p1_element.removeChild(pPr);
|
|
7566
|
+
}
|
|
7567
|
+
p1_element.insertBefore(
|
|
7568
|
+
adopted,
|
|
7569
|
+
p1_element.firstChild
|
|
7570
|
+
);
|
|
7571
|
+
pPr = adopted;
|
|
7572
|
+
}
|
|
7043
7573
|
if (!pPr) {
|
|
7044
7574
|
pPr = p1_element.ownerDocument.createElement("w:pPr");
|
|
7045
7575
|
p1_element.insertBefore(
|
|
@@ -7052,8 +7582,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
7052
7582
|
rPr = p1_element.ownerDocument.createElement("w:rPr");
|
|
7053
7583
|
pPr.appendChild(rPr);
|
|
7054
7584
|
}
|
|
7055
|
-
|
|
7056
|
-
|
|
7585
|
+
if (!findChild(rPr, "w:del")) {
|
|
7586
|
+
const del_mark = this._create_track_change_tag("w:del");
|
|
7587
|
+
rPr.appendChild(del_mark);
|
|
7588
|
+
}
|
|
7057
7589
|
const children = Array.from(p2_element.childNodes);
|
|
7058
7590
|
for (const child of children) {
|
|
7059
7591
|
if (child.nodeType === 1 && child.tagName === "w:pPr") {
|
|
@@ -7107,27 +7639,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
7107
7639
|
}
|
|
7108
7640
|
}
|
|
7109
7641
|
for (const p_elem of affected_ps) {
|
|
7110
|
-
|
|
7111
|
-
for (const tag of ["w:t", "w:tab", "w:br"]) {
|
|
7112
|
-
const nodes = findAllDescendants(p_elem, tag);
|
|
7113
|
-
for (const node of nodes) {
|
|
7114
|
-
let is_deleted = false;
|
|
7115
|
-
let curr = node.parentNode;
|
|
7116
|
-
while (curr && curr !== p_elem.parentNode) {
|
|
7117
|
-
if (curr.tagName === "w:del") {
|
|
7118
|
-
is_deleted = true;
|
|
7119
|
-
break;
|
|
7120
|
-
}
|
|
7121
|
-
curr = curr.parentNode;
|
|
7122
|
-
}
|
|
7123
|
-
if (!is_deleted) {
|
|
7124
|
-
if (tag === "w:t" && !node.textContent) continue;
|
|
7125
|
-
has_visible = true;
|
|
7126
|
-
break;
|
|
7127
|
-
}
|
|
7128
|
-
}
|
|
7129
|
-
if (has_visible) break;
|
|
7130
|
-
}
|
|
7642
|
+
const has_visible = this._paragraph_has_visible_content(p_elem);
|
|
7131
7643
|
if (!has_visible) {
|
|
7132
7644
|
let pPr = findChild(p_elem, "w:pPr");
|
|
7133
7645
|
if (!pPr) {
|
|
@@ -7686,6 +8198,31 @@ function strip_custom_properties(doc) {
|
|
|
7686
8198
|
const count = Math.max(lines.length, 1);
|
|
7687
8199
|
return [`Custom document properties: ${count} removed (docProps/custom.xml)`].concat(lines);
|
|
7688
8200
|
}
|
|
8201
|
+
function strip_document_variables(doc) {
|
|
8202
|
+
const settingsPart = doc.pkg.getPartByPath("word/settings.xml");
|
|
8203
|
+
if (!settingsPart) return [];
|
|
8204
|
+
const names = [];
|
|
8205
|
+
let removedAny = false;
|
|
8206
|
+
for (const container of findDescendantsByLocalName(settingsPart._element, "docVars")) {
|
|
8207
|
+
for (const child of Array.from(container.childNodes)) {
|
|
8208
|
+
const el = child;
|
|
8209
|
+
const tag = el.tagName || "";
|
|
8210
|
+
if (tag === "docVar" || tag.endsWith(":docVar")) {
|
|
8211
|
+
names.push(el.getAttribute("w:name") || el.getAttribute("name") || "(unnamed)");
|
|
8212
|
+
}
|
|
8213
|
+
}
|
|
8214
|
+
container.parentNode?.removeChild(container);
|
|
8215
|
+
removedAny = true;
|
|
8216
|
+
}
|
|
8217
|
+
for (const stray of findDescendantsByLocalName(settingsPart._element, "docVar")) {
|
|
8218
|
+
names.push(stray.getAttribute("w:name") || stray.getAttribute("name") || "(unnamed)");
|
|
8219
|
+
stray.parentNode?.removeChild(stray);
|
|
8220
|
+
removedAny = true;
|
|
8221
|
+
}
|
|
8222
|
+
if (!removedAny) return [];
|
|
8223
|
+
const shown = Array.from(new Set(names)).sort().join(", ") || "(unnamed)";
|
|
8224
|
+
return [`Document variables: ${names.length} removed (${shown})`];
|
|
8225
|
+
}
|
|
7689
8226
|
function strip_image_alt_text(doc) {
|
|
7690
8227
|
let count = 0;
|
|
7691
8228
|
for (const docPr of findDescendantsByLocalName(doc.element, "docPr")) {
|
|
@@ -8207,7 +8744,8 @@ function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets
|
|
|
8207
8744
|
const firstP = cell._element.getElementsByTagName("w:p")[0];
|
|
8208
8745
|
const paraId = firstP ? firstP.getAttribute("w14:paraId") : null;
|
|
8209
8746
|
if (paraId) {
|
|
8210
|
-
const
|
|
8747
|
+
const space_pad = cell_content ? " " : "";
|
|
8748
|
+
const anchor = `${space_pad}{#cell:${paraId}}`;
|
|
8211
8749
|
cell_content = cell_content + anchor;
|
|
8212
8750
|
}
|
|
8213
8751
|
}
|
|
@@ -8472,6 +9010,8 @@ function _build_merged_meta_block(states_list, comments_map) {
|
|
|
8472
9010
|
const change_lines = [];
|
|
8473
9011
|
const comment_lines = [];
|
|
8474
9012
|
const seen_sigs = /* @__PURE__ */ new Set();
|
|
9013
|
+
const pair_map = compute_change_pair_map(states_list);
|
|
9014
|
+
const pairSuffix = (uid) => pair_map[uid] ? ` (pairs with ${pair_map[uid]})` : "";
|
|
8475
9015
|
for (const [ins_map, del_map, comments_set, fmt_map] of states_list) {
|
|
8476
9016
|
let render_comment2 = function(cid) {
|
|
8477
9017
|
if (!comments_map[cid]) return;
|
|
@@ -8500,7 +9040,9 @@ function _build_merged_meta_block(states_list, comments_map) {
|
|
|
8500
9040
|
)) {
|
|
8501
9041
|
const sig = `Chg:${uid}`;
|
|
8502
9042
|
if (!seen_sigs.has(sig)) {
|
|
8503
|
-
change_lines.push(
|
|
9043
|
+
change_lines.push(
|
|
9044
|
+
`[${sig} insert] ${meta.author || "Unknown"}${pairSuffix(uid)}`
|
|
9045
|
+
);
|
|
8504
9046
|
seen_sigs.add(sig);
|
|
8505
9047
|
}
|
|
8506
9048
|
}
|
|
@@ -8509,7 +9051,9 @@ function _build_merged_meta_block(states_list, comments_map) {
|
|
|
8509
9051
|
)) {
|
|
8510
9052
|
const sig = `Chg:${uid}`;
|
|
8511
9053
|
if (!seen_sigs.has(sig)) {
|
|
8512
|
-
change_lines.push(
|
|
9054
|
+
change_lines.push(
|
|
9055
|
+
`[${sig} delete] ${meta.author || "Unknown"}${pairSuffix(uid)}`
|
|
9056
|
+
);
|
|
8513
9057
|
seen_sigs.add(sig);
|
|
8514
9058
|
}
|
|
8515
9059
|
}
|
|
@@ -9071,14 +9615,14 @@ var SanitizeReport = class {
|
|
|
9071
9615
|
const lower = line.toLowerCase();
|
|
9072
9616
|
if (lower.includes("tracked change") || lower.includes("insertion") || lower.includes("deletion") || lower.includes("accepted")) {
|
|
9073
9617
|
this.change_lines.push(line);
|
|
9618
|
+
} 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")) {
|
|
9619
|
+
this.metadata_lines.push(line);
|
|
9074
9620
|
} else if (lower.includes("comment") || lower.includes("[open]") || lower.includes("[resolved]")) {
|
|
9075
9621
|
if (lower.includes("kept") || lower.includes("visible")) {
|
|
9076
9622
|
this.kept_comment_lines.push(line);
|
|
9077
9623
|
} else {
|
|
9078
9624
|
this.removed_comment_lines.push(line);
|
|
9079
9625
|
}
|
|
9080
|
-
} else if (lower.includes("author") || lower.includes("template") || lower.includes("company") || lower.includes("manager") || lower.includes("metadata") || lower.includes("timestamp") || lower.includes("custom xml") || lower.includes("custom propert") || lower.includes("identifier") || lower.includes("language") || lower.includes("version") || lower.includes("last modified by") || lower.includes("revision count") || lower.includes("last printed")) {
|
|
9081
|
-
this.metadata_lines.push(line);
|
|
9082
9626
|
} else if (lower.includes("hyperlink") || lower.includes("warning")) {
|
|
9083
9627
|
this.warnings.push(line);
|
|
9084
9628
|
} else {
|
|
@@ -9191,6 +9735,7 @@ async function finalize_document(doc, options) {
|
|
|
9191
9735
|
report.add_transform_lines(scrub_timestamps(doc));
|
|
9192
9736
|
report.add_transform_lines(strip_custom_xml(doc));
|
|
9193
9737
|
report.add_transform_lines(strip_custom_properties(doc));
|
|
9738
|
+
report.add_transform_lines(strip_document_variables(doc));
|
|
9194
9739
|
report.add_transform_lines(strip_image_alt_text(doc));
|
|
9195
9740
|
const warnings = audit_hyperlinks(doc);
|
|
9196
9741
|
for (const w of warnings) report.warnings.push(w);
|
|
@@ -9274,6 +9819,12 @@ function verifySanitizedPackage(outBuffer) {
|
|
|
9274
9819
|
}
|
|
9275
9820
|
}
|
|
9276
9821
|
}
|
|
9822
|
+
if (names.includes("word/settings.xml")) {
|
|
9823
|
+
const settings = parseXml(strFromU82(unzipped["word/settings.xml"]));
|
|
9824
|
+
if (findDescendantsByLocalName(settings.documentElement, "docVar").length > 0) {
|
|
9825
|
+
problems.push("word/settings.xml still contains document variables (w:docVar)");
|
|
9826
|
+
}
|
|
9827
|
+
}
|
|
9277
9828
|
if (problems.length > 0) {
|
|
9278
9829
|
throw new Error(
|
|
9279
9830
|
"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."
|