@adeu/core 1.27.0 → 1.29.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 +696 -78
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -2
- package/dist/index.d.ts +63 -2
- package/dist/index.js +696 -78
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/diff.test.ts +8 -1
- package/src/diff.ts +242 -3
- package/src/engine.atomic.test.ts +5 -1
- package/src/engine.batch.test.ts +16 -6
- package/src/engine.bugs.test.ts +49 -0
- package/src/engine.tables.test.ts +31 -0
- package/src/engine.ts +443 -66
- package/src/ingest.test.ts +3 -1
- package/src/ingest.ts +35 -5
- package/src/mapper.ts +94 -11
- package/src/repro_agy_bug.test.ts +108 -0
- package/src/repro_qa_mcp_issues_2026_07_20.test.ts +224 -0
- package/src/repro_qa_report_v9.test.ts +392 -0
- package/src/sanitize/core.ts +12 -1
- package/src/sanitize/report.ts +9 -7
- package/src/sanitize/sanitize.test.ts +1 -0
- 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 "";
|
|
@@ -1640,11 +1696,34 @@ ${header}`;
|
|
|
1640
1696
|
const cell_start = current;
|
|
1641
1697
|
current = this._map_blocks(cell, current);
|
|
1642
1698
|
if (!this.clean_view && !this.original_view) {
|
|
1643
|
-
|
|
1644
|
-
|
|
1699
|
+
let firstP = cell._element.getElementsByTagName("w:p")[0];
|
|
1700
|
+
let paraId = firstP ? firstP.getAttribute("w14:paraId") : null;
|
|
1701
|
+
const is_empty = current === cell_start;
|
|
1702
|
+
if (!paraId && is_empty) {
|
|
1703
|
+
if (!firstP) {
|
|
1704
|
+
const xmlDoc = cell._element.ownerDocument;
|
|
1705
|
+
firstP = xmlDoc.createElement("w:p");
|
|
1706
|
+
cell._element.appendChild(firstP);
|
|
1707
|
+
}
|
|
1708
|
+
const allPs = Array.from(cell._element.ownerDocument.getElementsByTagName("w:p"));
|
|
1709
|
+
const index = allPs.indexOf(firstP);
|
|
1710
|
+
let hash = 2166136261;
|
|
1711
|
+
const str = `fallback-paraId-${index}`;
|
|
1712
|
+
for (let i = 0; i < str.length; i++) {
|
|
1713
|
+
hash ^= str.charCodeAt(i);
|
|
1714
|
+
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
1715
|
+
}
|
|
1716
|
+
paraId = (hash >>> 0).toString(16).toUpperCase().padStart(8, "0");
|
|
1717
|
+
firstP.setAttribute("w14:paraId", paraId);
|
|
1718
|
+
}
|
|
1645
1719
|
if (paraId && firstP) {
|
|
1646
1720
|
const cellPara = new Paragraph(firstP, cell);
|
|
1647
1721
|
this._add_virtual_text("", current, cellPara);
|
|
1722
|
+
const has_content = current > cell_start;
|
|
1723
|
+
if (has_content) {
|
|
1724
|
+
this._add_virtual_text(" ", current, cellPara);
|
|
1725
|
+
current += 1;
|
|
1726
|
+
}
|
|
1648
1727
|
const anchor = `{#cell:${paraId}}`;
|
|
1649
1728
|
this._add_virtual_text(anchor, current, cellPara);
|
|
1650
1729
|
current += anchor.length;
|
|
@@ -1995,6 +2074,8 @@ ${header}`;
|
|
|
1995
2074
|
const change_lines = [];
|
|
1996
2075
|
const comment_lines = [];
|
|
1997
2076
|
const seen_sigs = /* @__PURE__ */ new Set();
|
|
2077
|
+
const pair_map = compute_change_pair_map(states_list);
|
|
2078
|
+
const pairSuffix = (uid) => pair_map[uid] ? ` (pairs with ${pair_map[uid]})` : "";
|
|
1998
2079
|
for (const [ins_map, del_map, comments_set, fmt_map] of states_list) {
|
|
1999
2080
|
for (const [uid, meta] of Object.entries(
|
|
2000
2081
|
ins_map
|
|
@@ -2002,7 +2083,7 @@ ${header}`;
|
|
|
2002
2083
|
const sig = `Chg:${uid}`;
|
|
2003
2084
|
if (!seen_sigs.has(sig)) {
|
|
2004
2085
|
const auth = meta.author || "Unknown";
|
|
2005
|
-
change_lines.push(`[${sig} insert] ${auth}`);
|
|
2086
|
+
change_lines.push(`[${sig} insert] ${auth}${pairSuffix(uid)}`);
|
|
2006
2087
|
seen_sigs.add(sig);
|
|
2007
2088
|
}
|
|
2008
2089
|
}
|
|
@@ -2012,7 +2093,7 @@ ${header}`;
|
|
|
2012
2093
|
const sig = `Chg:${uid}`;
|
|
2013
2094
|
if (!seen_sigs.has(sig)) {
|
|
2014
2095
|
const auth = meta.author || "Unknown";
|
|
2015
|
-
change_lines.push(`[${sig} delete] ${auth}`);
|
|
2096
|
+
change_lines.push(`[${sig} delete] ${auth}${pairSuffix(uid)}`);
|
|
2016
2097
|
seen_sigs.add(sig);
|
|
2017
2098
|
}
|
|
2018
2099
|
}
|
|
@@ -2152,21 +2233,60 @@ ${header}`;
|
|
|
2152
2233
|
}
|
|
2153
2234
|
return results;
|
|
2154
2235
|
}
|
|
2236
|
+
/**
|
|
2237
|
+
* True when no run-backed span overlaps [start, start+length): the range
|
|
2238
|
+
* covers only virtual projection text — meta bubbles (change/comment
|
|
2239
|
+
* headers, timestamps), style markers, list prefixes. Such text does not
|
|
2240
|
+
* exist in the document, so it can neither satisfy a match nor count
|
|
2241
|
+
* toward ambiguity (QA 2026-07-19 ADEU-QA-002 C): an edit targeting "4"
|
|
2242
|
+
* used to be rejected as "appears 8 times" because a comment bubble's
|
|
2243
|
+
* timestamp matched.
|
|
2244
|
+
*
|
|
2245
|
+
* Anchor tokens ({#Bookmark}, {#cell:paraId}) are the exception: they are
|
|
2246
|
+
* deliberate virtual TARGETING surfaces (empty-cell writes, bookmark
|
|
2247
|
+
* anchors) and must stay matchable.
|
|
2248
|
+
*/
|
|
2249
|
+
range_is_virtual_only(start, length) {
|
|
2250
|
+
const end = start + length;
|
|
2251
|
+
const overlapping = this.spans.filter(
|
|
2252
|
+
(s) => s.end > start && s.start < end
|
|
2253
|
+
);
|
|
2254
|
+
if (overlapping.some((s) => s.run !== null)) return false;
|
|
2255
|
+
return !overlapping.some(
|
|
2256
|
+
(s) => s.run === null && s.text.startsWith("{#")
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
/** Filters find_all_match_indices output down to matches that touch at
|
|
2260
|
+
* least one run-backed span. See range_is_virtual_only. */
|
|
2261
|
+
drop_virtual_only_matches(matches) {
|
|
2262
|
+
return matches.filter(
|
|
2263
|
+
([start, length]) => !this.range_is_virtual_only(start, length)
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2155
2266
|
find_match_index(target_text, is_regex = false) {
|
|
2156
2267
|
if (is_regex) {
|
|
2157
2268
|
try {
|
|
2158
2269
|
const match = userSearch(target_text, this.full_text);
|
|
2159
|
-
if (match
|
|
2270
|
+
if (match && !this.range_is_virtual_only(match.start, match.end - match.start)) {
|
|
2271
|
+
return [match.start, match.end - match.start];
|
|
2272
|
+
}
|
|
2160
2273
|
} catch (e) {
|
|
2161
2274
|
if (e instanceof RegexTimeoutError) throw e;
|
|
2162
2275
|
}
|
|
2163
2276
|
return [-1, 0];
|
|
2164
2277
|
}
|
|
2165
|
-
let
|
|
2166
|
-
|
|
2278
|
+
let from = 0;
|
|
2279
|
+
while (true) {
|
|
2280
|
+
const idx = this.full_text.indexOf(target_text, from);
|
|
2281
|
+
if (idx === -1) break;
|
|
2282
|
+
if (!this.range_is_virtual_only(idx, target_text.length)) {
|
|
2283
|
+
return [idx, target_text.length];
|
|
2284
|
+
}
|
|
2285
|
+
from = idx + 1;
|
|
2286
|
+
}
|
|
2167
2287
|
const norm_full = this._replace_smart_quotes(this.full_text);
|
|
2168
2288
|
const norm_target = this._replace_smart_quotes(target_text);
|
|
2169
|
-
start_idx = norm_full.indexOf(norm_target);
|
|
2289
|
+
let start_idx = norm_full.indexOf(norm_target);
|
|
2170
2290
|
if (start_idx !== -1) return [start_idx, target_text.length];
|
|
2171
2291
|
const stripped_target = this._strip_markdown_formatting(target_text);
|
|
2172
2292
|
if (this.full_text.includes(stripped_target)) {
|
|
@@ -2176,9 +2296,12 @@ ${header}`;
|
|
|
2176
2296
|
const plain_first = this._find_plain_projection_matches(target_text);
|
|
2177
2297
|
if (plain_first.length > 0) return plain_first[0];
|
|
2178
2298
|
try {
|
|
2179
|
-
const pattern = new RegExp(this._make_fuzzy_regex(target_text));
|
|
2180
|
-
const match
|
|
2181
|
-
|
|
2299
|
+
const pattern = new RegExp(this._make_fuzzy_regex(target_text), "g");
|
|
2300
|
+
for (const match of this.full_text.matchAll(pattern)) {
|
|
2301
|
+
if (!this.range_is_virtual_only(match.index, match[0].length)) {
|
|
2302
|
+
return [match.index, match[0].length];
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2182
2305
|
} catch (e) {
|
|
2183
2306
|
}
|
|
2184
2307
|
return [-1, 0];
|
|
@@ -2707,6 +2830,157 @@ function _sequence_opcodes(a, b) {
|
|
|
2707
2830
|
flushPending();
|
|
2708
2831
|
return ops;
|
|
2709
2832
|
}
|
|
2833
|
+
function _is_table_blob(block) {
|
|
2834
|
+
const lines = block.split("\n");
|
|
2835
|
+
return lines.length > 0 && lines.every((line) => line.includes(" | "));
|
|
2836
|
+
}
|
|
2837
|
+
function _table_blob_row_edits(orig_blob, mod_blob) {
|
|
2838
|
+
const rows_o = orig_blob.split("\n");
|
|
2839
|
+
const rows_m = mod_blob.split("\n");
|
|
2840
|
+
if (rows_o.length !== rows_m.length) return null;
|
|
2841
|
+
const row_edits = [];
|
|
2842
|
+
for (let k = 0; k < rows_o.length; k++) {
|
|
2843
|
+
if (rows_o[k] === rows_m[k]) continue;
|
|
2844
|
+
row_edits.push({
|
|
2845
|
+
type: "modify",
|
|
2846
|
+
target_text: rows_o[k],
|
|
2847
|
+
new_text: rows_m[k],
|
|
2848
|
+
comment: "Diff: Table row modified",
|
|
2849
|
+
_is_table_edit: true
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
return row_edits;
|
|
2853
|
+
}
|
|
2854
|
+
function _split_cross_paragraph_hunks(edits) {
|
|
2855
|
+
const out = [];
|
|
2856
|
+
for (const e of edits) {
|
|
2857
|
+
const target = e.target_text || "";
|
|
2858
|
+
const newText = e.new_text || "";
|
|
2859
|
+
const idx = e._match_start_index;
|
|
2860
|
+
if (idx === void 0 || idx === null || !target.includes("\n\n") || target.split("\n\n").length - 1 === newText.split("\n\n").length - 1) {
|
|
2861
|
+
out.push(e);
|
|
2862
|
+
continue;
|
|
2863
|
+
}
|
|
2864
|
+
const parts = target.split("\n\n");
|
|
2865
|
+
if (parts.length < 2 || !parts[0].trim() || !parts[parts.length - 1].trim()) {
|
|
2866
|
+
out.push(e);
|
|
2867
|
+
continue;
|
|
2868
|
+
}
|
|
2869
|
+
let offset = idx;
|
|
2870
|
+
for (const piece of parts.slice(0, -1)) {
|
|
2871
|
+
out.push({
|
|
2872
|
+
type: "modify",
|
|
2873
|
+
target_text: piece + "\n\n",
|
|
2874
|
+
new_text: "",
|
|
2875
|
+
comment: e.comment || "Diff: Text deleted",
|
|
2876
|
+
_match_start_index: offset
|
|
2877
|
+
});
|
|
2878
|
+
offset += piece.length + 2;
|
|
2879
|
+
}
|
|
2880
|
+
out.push({
|
|
2881
|
+
type: "modify",
|
|
2882
|
+
target_text: parts[parts.length - 1],
|
|
2883
|
+
new_text: newText,
|
|
2884
|
+
comment: e.comment,
|
|
2885
|
+
_match_start_index: offset
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
return out;
|
|
2889
|
+
}
|
|
2890
|
+
function generate_edits_via_paragraph_alignment(original_text, modified_text) {
|
|
2891
|
+
const orig_stripped = original_text.trimEnd();
|
|
2892
|
+
const orig_ws = original_text.slice(orig_stripped.length);
|
|
2893
|
+
const mod_stripped = modified_text.trimEnd();
|
|
2894
|
+
modified_text = mod_stripped + orig_ws;
|
|
2895
|
+
const orig_paragraphs = original_text.split("\n\n");
|
|
2896
|
+
const mod_paragraphs = modified_text.split("\n\n");
|
|
2897
|
+
const orig_offsets = [];
|
|
2898
|
+
let current_offset = 0;
|
|
2899
|
+
for (const p of orig_paragraphs) {
|
|
2900
|
+
orig_offsets.push(current_offset);
|
|
2901
|
+
current_offset += p.length + 2;
|
|
2902
|
+
}
|
|
2903
|
+
const opcodes = _sequence_opcodes(orig_paragraphs, mod_paragraphs);
|
|
2904
|
+
const edits = [];
|
|
2905
|
+
for (const [tag, i1, i2, j1, j2] of opcodes) {
|
|
2906
|
+
if (tag === "equal") continue;
|
|
2907
|
+
const offset = i1 < orig_offsets.length ? orig_offsets[i1] : original_text.length;
|
|
2908
|
+
if (tag === "delete") {
|
|
2909
|
+
if (i2 < orig_paragraphs.length) {
|
|
2910
|
+
let piece_offset = offset;
|
|
2911
|
+
for (let k = i1; k < i2; k++) {
|
|
2912
|
+
const piece = orig_paragraphs[k] + "\n\n";
|
|
2913
|
+
edits.push({
|
|
2914
|
+
type: "modify",
|
|
2915
|
+
target_text: piece,
|
|
2916
|
+
new_text: "",
|
|
2917
|
+
comment: "Diff: Text deleted",
|
|
2918
|
+
_match_start_index: piece_offset
|
|
2919
|
+
});
|
|
2920
|
+
piece_offset += piece.length;
|
|
2921
|
+
}
|
|
2922
|
+
} else {
|
|
2923
|
+
let deleted_text = orig_paragraphs.slice(i1, i2).join("\n\n");
|
|
2924
|
+
let del_offset = offset;
|
|
2925
|
+
if (i1 > 0) {
|
|
2926
|
+
deleted_text = "\n\n" + deleted_text;
|
|
2927
|
+
del_offset -= 2;
|
|
2928
|
+
}
|
|
2929
|
+
edits.push({
|
|
2930
|
+
type: "modify",
|
|
2931
|
+
target_text: deleted_text,
|
|
2932
|
+
new_text: "",
|
|
2933
|
+
comment: "Diff: Text deleted",
|
|
2934
|
+
_match_start_index: del_offset
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2937
|
+
} else if (tag === "insert") {
|
|
2938
|
+
let inserted_text = mod_paragraphs.slice(j1, j2).join("\n\n");
|
|
2939
|
+
if (i1 < orig_paragraphs.length) {
|
|
2940
|
+
inserted_text = inserted_text + "\n\n";
|
|
2941
|
+
} else {
|
|
2942
|
+
inserted_text = "\n\n" + inserted_text;
|
|
2943
|
+
}
|
|
2944
|
+
edits.push({
|
|
2945
|
+
type: "modify",
|
|
2946
|
+
target_text: "",
|
|
2947
|
+
new_text: inserted_text,
|
|
2948
|
+
comment: "Diff: Text inserted",
|
|
2949
|
+
_match_start_index: offset
|
|
2950
|
+
});
|
|
2951
|
+
} else if (tag === "replace") {
|
|
2952
|
+
if (i2 - i1 === j2 - j1 && Array.from({ length: i2 - i1 }).some(
|
|
2953
|
+
(_, k) => _is_table_blob(orig_paragraphs[i1 + k]) || _is_table_blob(mod_paragraphs[j1 + k])
|
|
2954
|
+
)) {
|
|
2955
|
+
for (let k = 0; k < i2 - i1; k++) {
|
|
2956
|
+
const orig_p = orig_paragraphs[i1 + k];
|
|
2957
|
+
const mod_p = mod_paragraphs[j1 + k];
|
|
2958
|
+
if (orig_p === mod_p) continue;
|
|
2959
|
+
const pair_offset = orig_offsets[i1 + k];
|
|
2960
|
+
const row_edits = _is_table_blob(orig_p) && _is_table_blob(mod_p) ? _table_blob_row_edits(orig_p, mod_p) : null;
|
|
2961
|
+
if (row_edits !== null) {
|
|
2962
|
+
edits.push(...row_edits);
|
|
2963
|
+
continue;
|
|
2964
|
+
}
|
|
2965
|
+
const pair_edits = generate_edits_from_text(orig_p, mod_p);
|
|
2966
|
+
for (const ce of pair_edits) {
|
|
2967
|
+
ce._match_start_index = (ce._match_start_index || 0) + pair_offset;
|
|
2968
|
+
edits.push(ce);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
continue;
|
|
2972
|
+
}
|
|
2973
|
+
const orig_chunk = orig_paragraphs.slice(i1, i2).join("\n\n");
|
|
2974
|
+
const mod_chunk = mod_paragraphs.slice(j1, j2).join("\n\n");
|
|
2975
|
+
const chunk_edits = generate_edits_from_text(orig_chunk, mod_chunk);
|
|
2976
|
+
for (const ce of chunk_edits) {
|
|
2977
|
+
ce._match_start_index = (ce._match_start_index || 0) + offset;
|
|
2978
|
+
edits.push(ce);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
return _split_cross_paragraph_hunks(edits);
|
|
2983
|
+
}
|
|
2710
2984
|
var _IMAGE_MARKER_RE = /!\[[^\]]*\]\(docx-image:[^)]*\)/g;
|
|
2711
2985
|
function _drop_image_marker_hunks(edits, warnings) {
|
|
2712
2986
|
const _normalized = (s) => s.replace(_IMAGE_MARKER_RE, "").replace(/\s+/g, " ").trim();
|
|
@@ -2904,7 +3178,7 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2904
3178
|
);
|
|
2905
3179
|
const flat = _drop_marker_interior_hunks(
|
|
2906
3180
|
_drop_image_marker_hunks(
|
|
2907
|
-
|
|
3181
|
+
generate_edits_via_paragraph_alignment(text_orig, text_mod),
|
|
2908
3182
|
warnings
|
|
2909
3183
|
),
|
|
2910
3184
|
text_orig,
|
|
@@ -2930,7 +3204,7 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2930
3204
|
);
|
|
2931
3205
|
}
|
|
2932
3206
|
if (!tables_alignable) {
|
|
2933
|
-
const part_edits =
|
|
3207
|
+
const part_edits = generate_edits_via_paragraph_alignment(
|
|
2934
3208
|
text_orig.substring(po_start, po_end),
|
|
2935
3209
|
text_mod.substring(pm_start, pm_end)
|
|
2936
3210
|
);
|
|
@@ -2955,7 +3229,7 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
|
|
|
2955
3229
|
const seg_o_end = boundaries_o[seg_idx + 1][0];
|
|
2956
3230
|
const seg_m_start = boundaries_m[seg_idx][1];
|
|
2957
3231
|
const seg_m_end = boundaries_m[seg_idx + 1][0];
|
|
2958
|
-
const seg_edits =
|
|
3232
|
+
const seg_edits = generate_edits_via_paragraph_alignment(
|
|
2959
3233
|
text_orig.substring(seg_o_start, seg_o_end),
|
|
2960
3234
|
text_mod.substring(seg_m_start, seg_m_end)
|
|
2961
3235
|
);
|
|
@@ -4403,10 +4677,21 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4403
4677
|
parts_to_process.push(part._element);
|
|
4404
4678
|
}
|
|
4405
4679
|
}
|
|
4680
|
+
let accepted_insertions = 0;
|
|
4681
|
+
let accepted_deletions = 0;
|
|
4682
|
+
let accepted_formatting = 0;
|
|
4683
|
+
for (const root_element of parts_to_process) {
|
|
4684
|
+
accepted_insertions += findAllDescendants(root_element, "w:ins").length;
|
|
4685
|
+
accepted_deletions += findAllDescendants(root_element, "w:del").length;
|
|
4686
|
+
for (const tag of ["w:rPrChange", "w:pPrChange", "w:sectPrChange"]) {
|
|
4687
|
+
accepted_formatting += findAllDescendants(root_element, tag).length;
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
let removed_comments = 0;
|
|
4406
4691
|
for (const root_element of parts_to_process) {
|
|
4407
4692
|
const insNodes = findAllDescendants(root_element, "w:ins");
|
|
4408
4693
|
for (const ins of insNodes) {
|
|
4409
|
-
this._clean_wrapping_comments(ins);
|
|
4694
|
+
removed_comments += this._clean_wrapping_comments(ins);
|
|
4410
4695
|
const parent = ins.parentNode;
|
|
4411
4696
|
if (!parent) continue;
|
|
4412
4697
|
if (parent.tagName === "w:trPr") {
|
|
@@ -4450,8 +4735,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4450
4735
|
if (has_content) {
|
|
4451
4736
|
rPr.removeChild(delMark);
|
|
4452
4737
|
} else {
|
|
4453
|
-
this._clean_wrapping_comments(p);
|
|
4454
|
-
this._delete_comments_in_element(p);
|
|
4738
|
+
removed_comments += this._clean_wrapping_comments(p);
|
|
4739
|
+
removed_comments += this._delete_comments_in_element(p);
|
|
4455
4740
|
if (p.parentNode) {
|
|
4456
4741
|
p.parentNode.removeChild(p);
|
|
4457
4742
|
}
|
|
@@ -4461,8 +4746,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4461
4746
|
}
|
|
4462
4747
|
const delNodes = findAllDescendants(root_element, "w:del");
|
|
4463
4748
|
for (const d of delNodes) {
|
|
4464
|
-
this._clean_wrapping_comments(d);
|
|
4465
|
-
this._delete_comments_in_element(d);
|
|
4749
|
+
removed_comments += this._clean_wrapping_comments(d);
|
|
4750
|
+
removed_comments += this._delete_comments_in_element(d);
|
|
4466
4751
|
const parent = d.parentNode;
|
|
4467
4752
|
if (parent) {
|
|
4468
4753
|
if (parent.tagName === "w:trPr") {
|
|
@@ -4557,6 +4842,12 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4557
4842
|
}
|
|
4558
4843
|
}
|
|
4559
4844
|
}
|
|
4845
|
+
return {
|
|
4846
|
+
accepted_insertions,
|
|
4847
|
+
accepted_deletions,
|
|
4848
|
+
accepted_formatting,
|
|
4849
|
+
removed_comments
|
|
4850
|
+
};
|
|
4560
4851
|
}
|
|
4561
4852
|
/**
|
|
4562
4853
|
* Revert every tracked change, returning the document to the state it had
|
|
@@ -4809,6 +5100,24 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4809
5100
|
*
|
|
4810
5101
|
* Does NOT attach comments; callers handle that.
|
|
4811
5102
|
*/
|
|
5103
|
+
_clone_pPr_scrubbing_headings(existing_pPr) {
|
|
5104
|
+
const pPr_clone = existing_pPr.cloneNode(true);
|
|
5105
|
+
const pStyle_el = findChild(pPr_clone, "w:pStyle");
|
|
5106
|
+
if (pStyle_el) {
|
|
5107
|
+
const style_val = pStyle_el.getAttribute("w:val");
|
|
5108
|
+
if (style_val) {
|
|
5109
|
+
const is_heading = style_val.startsWith("Heading") || style_val === "Title" || style_val.replace(/\s+/g, "").startsWith("Heading");
|
|
5110
|
+
if (is_heading) {
|
|
5111
|
+
pPr_clone.removeChild(pStyle_el);
|
|
5112
|
+
}
|
|
5113
|
+
}
|
|
5114
|
+
}
|
|
5115
|
+
const outlineLvl_el = findChild(pPr_clone, "w:outlineLvl");
|
|
5116
|
+
if (outlineLvl_el) {
|
|
5117
|
+
pPr_clone.removeChild(outlineLvl_el);
|
|
5118
|
+
}
|
|
5119
|
+
return pPr_clone;
|
|
5120
|
+
}
|
|
4812
5121
|
_track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id, positional_anchor_el = null, suppress_emphasis = false, insert_before = false) {
|
|
4813
5122
|
if (!text) {
|
|
4814
5123
|
return {
|
|
@@ -4930,7 +5239,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4930
5239
|
} else {
|
|
4931
5240
|
const existing_pPr = findChild(current_p, "w:pPr");
|
|
4932
5241
|
if (existing_pPr) {
|
|
4933
|
-
new_p.appendChild(
|
|
5242
|
+
new_p.appendChild(this._clone_pPr_scrubbing_headings(existing_pPr));
|
|
4934
5243
|
}
|
|
4935
5244
|
}
|
|
4936
5245
|
let pPr = findChild(new_p, "w:pPr");
|
|
@@ -5195,7 +5504,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5195
5504
|
}
|
|
5196
5505
|
return null;
|
|
5197
5506
|
}
|
|
5507
|
+
/** Returns how many comment BODIES were actually deleted (see below: only
|
|
5508
|
+
* our own are; foreign ones keep their body and lose only the anchor). */
|
|
5198
5509
|
_clean_wrapping_comments(element) {
|
|
5510
|
+
let deleted = 0;
|
|
5199
5511
|
let first_node = element;
|
|
5200
5512
|
while (true) {
|
|
5201
5513
|
const prev2 = getPreviousElement(first_node);
|
|
@@ -5263,6 +5575,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5263
5575
|
const is_own = author !== null && author === this.author;
|
|
5264
5576
|
if (is_own) {
|
|
5265
5577
|
this.comments_manager.deleteComment(c_id);
|
|
5578
|
+
deleted++;
|
|
5266
5579
|
}
|
|
5267
5580
|
if (s.parentNode) s.parentNode.removeChild(s);
|
|
5268
5581
|
for (const e of ends_to_remove) {
|
|
@@ -5280,13 +5593,17 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5280
5593
|
}
|
|
5281
5594
|
}
|
|
5282
5595
|
}
|
|
5596
|
+
return deleted;
|
|
5283
5597
|
}
|
|
5598
|
+
/** Returns how many comment bodies were deleted. */
|
|
5284
5599
|
_delete_comments_in_element(element) {
|
|
5600
|
+
let deleted = 0;
|
|
5285
5601
|
const refs = findAllDescendants(element, "w:commentReference");
|
|
5286
5602
|
for (const ref of refs) {
|
|
5287
5603
|
const c_id = ref.getAttribute("w:id");
|
|
5288
5604
|
if (c_id) {
|
|
5289
5605
|
this.comments_manager.deleteComment(c_id);
|
|
5606
|
+
deleted++;
|
|
5290
5607
|
for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
|
|
5291
5608
|
const nodes = findAllDescendants(this.doc.element, tag);
|
|
5292
5609
|
for (const node of nodes) {
|
|
@@ -5297,6 +5614,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5297
5614
|
}
|
|
5298
5615
|
}
|
|
5299
5616
|
}
|
|
5617
|
+
return deleted;
|
|
5300
5618
|
}
|
|
5301
5619
|
validate_edits(edits, index_offset = 0) {
|
|
5302
5620
|
const errors = [];
|
|
@@ -5325,18 +5643,16 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5325
5643
|
continue;
|
|
5326
5644
|
}
|
|
5327
5645
|
}
|
|
5328
|
-
let matches = this.mapper.
|
|
5329
|
-
edit.target_text,
|
|
5330
|
-
is_regex
|
|
5646
|
+
let matches = this.mapper.drop_virtual_only_matches(
|
|
5647
|
+
this.mapper.find_all_match_indices(edit.target_text, is_regex)
|
|
5331
5648
|
);
|
|
5332
5649
|
let activeText = this.mapper.full_text;
|
|
5333
5650
|
let target_mapper = this.mapper;
|
|
5334
5651
|
if (matches.length === 0) {
|
|
5335
5652
|
if (!this.clean_mapper)
|
|
5336
5653
|
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
5337
|
-
matches = this.clean_mapper.
|
|
5338
|
-
edit.target_text,
|
|
5339
|
-
is_regex
|
|
5654
|
+
matches = this.clean_mapper.drop_virtual_only_matches(
|
|
5655
|
+
this.clean_mapper.find_all_match_indices(edit.target_text, is_regex)
|
|
5340
5656
|
);
|
|
5341
5657
|
if (matches.length > 0) {
|
|
5342
5658
|
activeText = this.clean_mapper.full_text;
|
|
@@ -5359,9 +5675,11 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5359
5675
|
if (!this.original_mapper) {
|
|
5360
5676
|
this.original_mapper = new DocumentMapper(this.doc, false, true);
|
|
5361
5677
|
}
|
|
5362
|
-
const orig_matches = this.original_mapper.
|
|
5363
|
-
|
|
5364
|
-
|
|
5678
|
+
const orig_matches = this.original_mapper.drop_virtual_only_matches(
|
|
5679
|
+
this.original_mapper.find_all_match_indices(
|
|
5680
|
+
edit.target_text,
|
|
5681
|
+
is_regex
|
|
5682
|
+
)
|
|
5365
5683
|
);
|
|
5366
5684
|
if (orig_matches.length > 0) {
|
|
5367
5685
|
is_deleted_text = true;
|
|
@@ -5583,10 +5901,15 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5583
5901
|
}
|
|
5584
5902
|
static _column_count_at(mapper, start, length) {
|
|
5585
5903
|
for (const s of mapper.spans) {
|
|
5586
|
-
if (s.
|
|
5904
|
+
if (s.end <= start || s.start >= start + length) {
|
|
5587
5905
|
continue;
|
|
5588
5906
|
}
|
|
5589
|
-
let curr =
|
|
5907
|
+
let curr = null;
|
|
5908
|
+
if (s.run !== null) {
|
|
5909
|
+
curr = s.run._element;
|
|
5910
|
+
} else if (s.paragraph !== null) {
|
|
5911
|
+
curr = s.paragraph._element;
|
|
5912
|
+
}
|
|
5590
5913
|
while (curr) {
|
|
5591
5914
|
if (curr.nodeType === 1 && curr.tagName === "w:tr") {
|
|
5592
5915
|
return findAllDescendants(curr, "w:tc").filter(
|
|
@@ -5726,7 +6049,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5726
6049
|
(c) => c === null || typeof c !== "object" || !["accept", "reject", "reply"].includes(c.type)
|
|
5727
6050
|
);
|
|
5728
6051
|
if (!dry_run_mode) {
|
|
5729
|
-
|
|
6052
|
+
let action_errors = actions.length > 0 ? this.validate_review_actions(actions) : [];
|
|
6053
|
+
if (actions.length > 0 && action_errors.length === 0) {
|
|
6054
|
+
action_errors = this.validate_action_pairing(actions);
|
|
6055
|
+
}
|
|
5730
6056
|
const validate_edits_now = edits.length > 0 && action_errors.length > 0;
|
|
5731
6057
|
const edit_errors = validate_edits_now ? this.validate_edits(edits) : [];
|
|
5732
6058
|
const all_errors = [...action_errors, ...edit_errors];
|
|
@@ -5735,7 +6061,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5735
6061
|
}
|
|
5736
6062
|
} else {
|
|
5737
6063
|
if (actions.length > 0) {
|
|
5738
|
-
|
|
6064
|
+
let action_errors = this.validate_review_actions(actions);
|
|
6065
|
+
if (action_errors.length === 0) {
|
|
6066
|
+
action_errors = this.validate_action_pairing(actions);
|
|
6067
|
+
}
|
|
5739
6068
|
if (action_errors.length > 0) {
|
|
5740
6069
|
throw new BatchValidationError(action_errors);
|
|
5741
6070
|
}
|
|
@@ -5743,10 +6072,12 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5743
6072
|
}
|
|
5744
6073
|
let applied_actions = 0;
|
|
5745
6074
|
let skipped_actions = 0;
|
|
6075
|
+
let already_resolved_actions = 0;
|
|
5746
6076
|
if (actions.length > 0) {
|
|
5747
6077
|
const res = this.apply_review_actions(actions);
|
|
5748
6078
|
applied_actions = res[0];
|
|
5749
6079
|
skipped_actions = res[1];
|
|
6080
|
+
already_resolved_actions = res[2];
|
|
5750
6081
|
if (skipped_actions > 0) {
|
|
5751
6082
|
throw new BatchValidationError(this.skipped_details);
|
|
5752
6083
|
}
|
|
@@ -5800,6 +6131,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5800
6131
|
);
|
|
5801
6132
|
reports_by_input[i] = {
|
|
5802
6133
|
status: "failed",
|
|
6134
|
+
type: edit.type || "modify",
|
|
5803
6135
|
target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
|
|
5804
6136
|
new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
|
|
5805
6137
|
warning,
|
|
@@ -5815,6 +6147,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5815
6147
|
const previews = this._build_edit_context_previews(edit);
|
|
5816
6148
|
reports_by_input[i] = {
|
|
5817
6149
|
status: "applied",
|
|
6150
|
+
type: edit.type || "modify",
|
|
5818
6151
|
target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
|
|
5819
6152
|
new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
|
|
5820
6153
|
warning: null,
|
|
@@ -5836,6 +6169,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5836
6169
|
);
|
|
5837
6170
|
reports_by_input[i] = {
|
|
5838
6171
|
status: "failed",
|
|
6172
|
+
type: edit.type || "modify",
|
|
5839
6173
|
target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
|
|
5840
6174
|
new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
|
|
5841
6175
|
warning,
|
|
@@ -5854,6 +6188,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5854
6188
|
if (report.status === "applied") {
|
|
5855
6189
|
reports_by_input[i] = {
|
|
5856
6190
|
status: "failed",
|
|
6191
|
+
type: report.type || "modify",
|
|
5857
6192
|
target_text: report.target_text,
|
|
5858
6193
|
new_text: report.new_text,
|
|
5859
6194
|
warning: null,
|
|
@@ -5924,6 +6259,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5924
6259
|
}
|
|
5925
6260
|
edits_reports.push({
|
|
5926
6261
|
status: success ? "applied" : "failed",
|
|
6262
|
+
type: edit.type || "modify",
|
|
5927
6263
|
target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
|
|
5928
6264
|
new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
|
|
5929
6265
|
warning,
|
|
@@ -5941,6 +6277,11 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5941
6277
|
return {
|
|
5942
6278
|
actions_applied: applied_actions,
|
|
5943
6279
|
actions_skipped: skipped_actions,
|
|
6280
|
+
// Actions whose target was already resolved by an earlier action of
|
|
6281
|
+
// this batch (via its replacement pair): consistent no-ops, never
|
|
6282
|
+
// counted as applied — every reported "applied" action causes an
|
|
6283
|
+
// observable state transition (ADEU-QA-004).
|
|
6284
|
+
actions_already_resolved: already_resolved_actions,
|
|
5944
6285
|
edits_applied: applied_edits,
|
|
5945
6286
|
edits_skipped: skipped_edits,
|
|
5946
6287
|
// edits_applied counts change OBJECTS; this is the total number of
|
|
@@ -5982,19 +6323,66 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5982
6323
|
edit._resolved_start_idx = edit._match_start_index;
|
|
5983
6324
|
resolved_edits.push([edit, edit.new_text || null]);
|
|
5984
6325
|
} else if (edit.type === "insert_row" || edit.type === "delete_row") {
|
|
5985
|
-
let matches = this.mapper.
|
|
6326
|
+
let matches = this.mapper.drop_virtual_only_matches(
|
|
6327
|
+
this.mapper.find_all_match_indices(edit.target_text)
|
|
6328
|
+
);
|
|
5986
6329
|
let resolved_mapper = this.mapper;
|
|
5987
6330
|
if (matches.length === 0) {
|
|
5988
6331
|
if (!this.clean_mapper) {
|
|
5989
6332
|
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
5990
6333
|
}
|
|
5991
|
-
matches = this.clean_mapper.
|
|
6334
|
+
matches = this.clean_mapper.drop_virtual_only_matches(
|
|
6335
|
+
this.clean_mapper.find_all_match_indices(edit.target_text)
|
|
6336
|
+
);
|
|
5992
6337
|
resolved_mapper = this.clean_mapper;
|
|
5993
6338
|
}
|
|
5994
6339
|
if (matches.length > 0) {
|
|
5995
|
-
edit.
|
|
5996
|
-
|
|
5997
|
-
|
|
6340
|
+
const match_mode = edit.match_mode || "strict";
|
|
6341
|
+
const unique_matches = [];
|
|
6342
|
+
const seen_trs = /* @__PURE__ */ new Set();
|
|
6343
|
+
for (const match of matches) {
|
|
6344
|
+
const start_idx = match[0];
|
|
6345
|
+
const [anchor_run, anchor_para] = resolved_mapper.get_insertion_anchor(start_idx, false);
|
|
6346
|
+
let target_element = null;
|
|
6347
|
+
if (anchor_run) target_element = anchor_run._element;
|
|
6348
|
+
else if (anchor_para) target_element = anchor_para._element;
|
|
6349
|
+
let tr = target_element;
|
|
6350
|
+
while (tr && tr.tagName !== "w:tr") tr = tr.parentNode;
|
|
6351
|
+
if (tr) {
|
|
6352
|
+
if (!seen_trs.has(tr)) {
|
|
6353
|
+
seen_trs.add(tr);
|
|
6354
|
+
unique_matches.push(match);
|
|
6355
|
+
}
|
|
6356
|
+
}
|
|
6357
|
+
}
|
|
6358
|
+
if (unique_matches.length > 0) {
|
|
6359
|
+
let matches_to_apply = unique_matches;
|
|
6360
|
+
if (match_mode === "strict" || match_mode === "first") {
|
|
6361
|
+
matches_to_apply = unique_matches.slice(0, 1);
|
|
6362
|
+
}
|
|
6363
|
+
if (match_mode === "all" || matches_to_apply.length > 1) {
|
|
6364
|
+
for (const m of matches_to_apply) {
|
|
6365
|
+
const sub_edit = {
|
|
6366
|
+
...edit,
|
|
6367
|
+
_resolved_start_idx: m[0],
|
|
6368
|
+
_active_mapper_ref: resolved_mapper,
|
|
6369
|
+
_parent_edit_ref: edit
|
|
6370
|
+
};
|
|
6371
|
+
resolved_edits.push([sub_edit, null]);
|
|
6372
|
+
}
|
|
6373
|
+
} else {
|
|
6374
|
+
edit._resolved_start_idx = matches_to_apply[0][0];
|
|
6375
|
+
edit._active_mapper_ref = resolved_mapper;
|
|
6376
|
+
resolved_edits.push([edit, null]);
|
|
6377
|
+
}
|
|
6378
|
+
} else {
|
|
6379
|
+
skipped++;
|
|
6380
|
+
edit._applied_status = false;
|
|
6381
|
+
const target_snippet = (edit.target_text || "").trim().substring(0, 40);
|
|
6382
|
+
const msg = `- Failed to locate row target: '${target_snippet}...'`;
|
|
6383
|
+
this.skipped_details.push(msg);
|
|
6384
|
+
edit._error_msg = msg;
|
|
6385
|
+
}
|
|
5998
6386
|
} else {
|
|
5999
6387
|
skipped++;
|
|
6000
6388
|
edit._applied_status = false;
|
|
@@ -6159,10 +6547,156 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6159
6547
|
}
|
|
6160
6548
|
return [applied_logical, skipped_logical];
|
|
6161
6549
|
}
|
|
6550
|
+
/**
|
|
6551
|
+
* True when the paragraph still carries visible content (w:t text, w:tab,
|
|
6552
|
+
* w:br) that is NOT wrapped in a tracked deletion — i.e. the paragraph
|
|
6553
|
+
* would render non-empty in the accepted document.
|
|
6554
|
+
*/
|
|
6555
|
+
_paragraph_has_visible_content(p_elem) {
|
|
6556
|
+
for (const tag of ["w:t", "w:tab", "w:br"]) {
|
|
6557
|
+
const nodes = findAllDescendants(p_elem, tag);
|
|
6558
|
+
for (const node of nodes) {
|
|
6559
|
+
let is_deleted = false;
|
|
6560
|
+
let curr = node.parentNode;
|
|
6561
|
+
while (curr && curr !== p_elem.parentNode) {
|
|
6562
|
+
if (curr.tagName === "w:del") {
|
|
6563
|
+
is_deleted = true;
|
|
6564
|
+
break;
|
|
6565
|
+
}
|
|
6566
|
+
curr = curr.parentNode;
|
|
6567
|
+
}
|
|
6568
|
+
if (!is_deleted) {
|
|
6569
|
+
if (tag === "w:t" && !node.textContent) continue;
|
|
6570
|
+
return true;
|
|
6571
|
+
}
|
|
6572
|
+
}
|
|
6573
|
+
}
|
|
6574
|
+
return false;
|
|
6575
|
+
}
|
|
6576
|
+
/**
|
|
6577
|
+
* All contiguous same-author w:ins/w:del siblings that form one logical
|
|
6578
|
+
* modification block with `node` (a replacement's del+ins pair). Mirrors
|
|
6579
|
+
* the Python engine's _get_paired_nodes: comment range markers and
|
|
6580
|
+
* rPr/pPr are transparent; a different author or any other element breaks
|
|
6581
|
+
* the group.
|
|
6582
|
+
*/
|
|
6583
|
+
_get_paired_nodes(node) {
|
|
6584
|
+
const pairs = [];
|
|
6585
|
+
const author = node.getAttribute("w:author");
|
|
6586
|
+
const transparent = /* @__PURE__ */ new Set([
|
|
6587
|
+
"w:commentRangeStart",
|
|
6588
|
+
"w:commentRangeEnd",
|
|
6589
|
+
"w:commentReference",
|
|
6590
|
+
"w:rPr",
|
|
6591
|
+
"w:pPr"
|
|
6592
|
+
]);
|
|
6593
|
+
const walk = (start, dir) => {
|
|
6594
|
+
let cur = dir === "next" ? start.nextSibling : start.previousSibling;
|
|
6595
|
+
while (cur) {
|
|
6596
|
+
if (cur.nodeType !== 1) {
|
|
6597
|
+
cur = dir === "next" ? cur.nextSibling : cur.previousSibling;
|
|
6598
|
+
continue;
|
|
6599
|
+
}
|
|
6600
|
+
const el = cur;
|
|
6601
|
+
if (transparent.has(el.tagName)) {
|
|
6602
|
+
cur = dir === "next" ? cur.nextSibling : cur.previousSibling;
|
|
6603
|
+
continue;
|
|
6604
|
+
}
|
|
6605
|
+
if ((el.tagName === "w:ins" || el.tagName === "w:del") && el.getAttribute("w:author") === author) {
|
|
6606
|
+
pairs.push(el);
|
|
6607
|
+
cur = dir === "next" ? cur.nextSibling : cur.previousSibling;
|
|
6608
|
+
continue;
|
|
6609
|
+
}
|
|
6610
|
+
break;
|
|
6611
|
+
}
|
|
6612
|
+
};
|
|
6613
|
+
walk(node, "next");
|
|
6614
|
+
walk(node, "prev");
|
|
6615
|
+
return pairs;
|
|
6616
|
+
}
|
|
6617
|
+
/**
|
|
6618
|
+
* All revision ids that resolve as ONE unit with `target_id`: the ids of
|
|
6619
|
+
* every contiguous same-author w:ins/w:del sibling of its elements (a
|
|
6620
|
+
* replacement's del+ins pair), plus the id itself.
|
|
6621
|
+
*/
|
|
6622
|
+
_resolution_group_ids(target_id) {
|
|
6623
|
+
const nodes = [
|
|
6624
|
+
...findAllDescendants(this.doc.element, "w:ins"),
|
|
6625
|
+
...findAllDescendants(this.doc.element, "w:del")
|
|
6626
|
+
].filter((n) => n.getAttribute("w:id") === target_id);
|
|
6627
|
+
const group = /* @__PURE__ */ new Set();
|
|
6628
|
+
if (nodes.length === 0) return group;
|
|
6629
|
+
group.add(target_id);
|
|
6630
|
+
for (const node of nodes) {
|
|
6631
|
+
for (const paired of this._get_paired_nodes(node)) {
|
|
6632
|
+
const pid = paired.getAttribute("w:id");
|
|
6633
|
+
if (pid) group.add(pid);
|
|
6634
|
+
}
|
|
6635
|
+
}
|
|
6636
|
+
return group;
|
|
6637
|
+
}
|
|
6638
|
+
/**
|
|
6639
|
+
* Document-aware validation (QA 2026-07-19 ADEU-QA-004): a replacement's
|
|
6640
|
+
* del+ins pair carries two distinct ids but resolves as one unit, so a
|
|
6641
|
+
* batch that accepts one side and rejects the other is contradictory.
|
|
6642
|
+
* Rejecting it up front — before any action mutates the document — keeps
|
|
6643
|
+
* the batch transactional.
|
|
6644
|
+
*/
|
|
6645
|
+
validate_action_pairing(actions) {
|
|
6646
|
+
const errors = [];
|
|
6647
|
+
const group_first = /* @__PURE__ */ new Map();
|
|
6648
|
+
for (let pos = 0; pos < actions.length; pos++) {
|
|
6649
|
+
const act = actions[pos];
|
|
6650
|
+
if (act.type !== "accept" && act.type !== "reject") continue;
|
|
6651
|
+
const raw_id = String(act.target_id ?? "");
|
|
6652
|
+
if (raw_id.startsWith("Com:")) continue;
|
|
6653
|
+
const target_id = raw_id.startsWith("Chg:") ? raw_id.slice(4) : raw_id;
|
|
6654
|
+
const group = this._resolution_group_ids(target_id);
|
|
6655
|
+
if (group.size === 0) continue;
|
|
6656
|
+
let conflict = null;
|
|
6657
|
+
for (const gid of group) {
|
|
6658
|
+
const prior = group_first.get(gid);
|
|
6659
|
+
if (prior !== void 0 && prior[1] !== act.type) {
|
|
6660
|
+
conflict = prior;
|
|
6661
|
+
break;
|
|
6662
|
+
}
|
|
6663
|
+
}
|
|
6664
|
+
if (conflict !== null) {
|
|
6665
|
+
const [first_pos, first_type, first_id] = conflict;
|
|
6666
|
+
errors.push(
|
|
6667
|
+
`- 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.`
|
|
6668
|
+
);
|
|
6669
|
+
continue;
|
|
6670
|
+
}
|
|
6671
|
+
for (const gid of group) {
|
|
6672
|
+
if (!group_first.has(gid)) {
|
|
6673
|
+
group_first.set(gid, [pos, act.type, target_id]);
|
|
6674
|
+
}
|
|
6675
|
+
}
|
|
6676
|
+
}
|
|
6677
|
+
return errors;
|
|
6678
|
+
}
|
|
6679
|
+
/**
|
|
6680
|
+
* Returns [applied, skipped, already_resolved]. `applied` counts actions
|
|
6681
|
+
* that caused an observable state transition; an action naming an id an
|
|
6682
|
+
* earlier action of this batch already resolved (via its replacement pair)
|
|
6683
|
+
* is counted in `already_resolved` instead — never as applied
|
|
6684
|
+
* (QA 2026-07-19 ADEU-QA-004).
|
|
6685
|
+
*/
|
|
6162
6686
|
apply_review_actions(actions) {
|
|
6163
6687
|
let applied = 0;
|
|
6164
6688
|
let skipped = 0;
|
|
6165
|
-
|
|
6689
|
+
let already_resolved = 0;
|
|
6690
|
+
const resolved_history = /* @__PURE__ */ new Map();
|
|
6691
|
+
const sortedActions = actions.map((action, pos) => ({ action, pos })).sort((a, b) => {
|
|
6692
|
+
const aPri = a.action.type === "reply" ? 0 : 1;
|
|
6693
|
+
const bPri = b.action.type === "reply" ? 0 : 1;
|
|
6694
|
+
if (aPri !== bPri) {
|
|
6695
|
+
return aPri - bPri;
|
|
6696
|
+
}
|
|
6697
|
+
return a.pos - b.pos;
|
|
6698
|
+
});
|
|
6699
|
+
for (const { action, pos } of sortedActions) {
|
|
6166
6700
|
const type = action.type;
|
|
6167
6701
|
if (type === "reply") {
|
|
6168
6702
|
const cid = action.target_id.replace("Com:", "");
|
|
@@ -6176,6 +6710,21 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6176
6710
|
continue;
|
|
6177
6711
|
}
|
|
6178
6712
|
const target_id = action.target_id.replace("Chg:", "");
|
|
6713
|
+
const prior_type = resolved_history.get(target_id);
|
|
6714
|
+
if (prior_type !== void 0) {
|
|
6715
|
+
if (prior_type === type) {
|
|
6716
|
+
already_resolved++;
|
|
6717
|
+
this.skipped_details.push(
|
|
6718
|
+
`- 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.`
|
|
6719
|
+
);
|
|
6720
|
+
continue;
|
|
6721
|
+
}
|
|
6722
|
+
this.skipped_details.push(
|
|
6723
|
+
`- 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.`
|
|
6724
|
+
);
|
|
6725
|
+
skipped++;
|
|
6726
|
+
continue;
|
|
6727
|
+
}
|
|
6179
6728
|
const all_ins = findAllDescendants(this.doc.element, "w:ins").filter(
|
|
6180
6729
|
(n) => n.getAttribute("w:id") === target_id
|
|
6181
6730
|
);
|
|
@@ -6200,7 +6749,18 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6200
6749
|
);
|
|
6201
6750
|
continue;
|
|
6202
6751
|
}
|
|
6752
|
+
const group_nodes = new Set(all_nodes);
|
|
6203
6753
|
for (const node of all_nodes) {
|
|
6754
|
+
for (const paired of this._get_paired_nodes(node)) {
|
|
6755
|
+
group_nodes.add(paired);
|
|
6756
|
+
}
|
|
6757
|
+
}
|
|
6758
|
+
const resolved_now = /* @__PURE__ */ new Set();
|
|
6759
|
+
for (const node of group_nodes) {
|
|
6760
|
+
const rid = node.getAttribute("w:id");
|
|
6761
|
+
if (rid) resolved_now.add(rid);
|
|
6762
|
+
}
|
|
6763
|
+
for (const node of group_nodes) {
|
|
6204
6764
|
const is_ins = node.tagName === "w:ins";
|
|
6205
6765
|
const parent_tag = node.parentNode ? node.parentNode.tagName : "";
|
|
6206
6766
|
const is_trPr = parent_tag === "w:trPr";
|
|
@@ -6252,9 +6812,12 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6252
6812
|
}
|
|
6253
6813
|
}
|
|
6254
6814
|
}
|
|
6815
|
+
for (const rid of resolved_now) {
|
|
6816
|
+
resolved_history.set(rid, type);
|
|
6817
|
+
}
|
|
6255
6818
|
applied++;
|
|
6256
6819
|
}
|
|
6257
|
-
return [applied, skipped];
|
|
6820
|
+
return [applied, skipped, already_resolved];
|
|
6258
6821
|
}
|
|
6259
6822
|
_apply_table_edit(edit, rebuild_map) {
|
|
6260
6823
|
const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
|
|
@@ -6314,17 +6877,15 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6314
6877
|
if (!edit.target_text) return null;
|
|
6315
6878
|
const is_regex = edit.regex || false;
|
|
6316
6879
|
const match_mode = edit.match_mode || "strict";
|
|
6317
|
-
let matches = this.mapper.
|
|
6318
|
-
edit.target_text,
|
|
6319
|
-
is_regex
|
|
6880
|
+
let matches = this.mapper.drop_virtual_only_matches(
|
|
6881
|
+
this.mapper.find_all_match_indices(edit.target_text, is_regex)
|
|
6320
6882
|
);
|
|
6321
6883
|
let use_clean_map = false;
|
|
6322
6884
|
if (matches.length === 0) {
|
|
6323
6885
|
if (!this.clean_mapper)
|
|
6324
6886
|
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
6325
|
-
matches = this.clean_mapper.
|
|
6326
|
-
edit.target_text,
|
|
6327
|
-
is_regex
|
|
6887
|
+
matches = this.clean_mapper.drop_virtual_only_matches(
|
|
6888
|
+
this.clean_mapper.find_all_match_indices(edit.target_text, is_regex)
|
|
6328
6889
|
);
|
|
6329
6890
|
if (matches.length > 0) use_clean_map = true;
|
|
6330
6891
|
else return null;
|
|
@@ -6834,7 +7395,9 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
6834
7395
|
this._set_paragraph_style(new_p, style_name);
|
|
6835
7396
|
} else {
|
|
6836
7397
|
const existing_pPr = findChild(_bug233_target_para, "w:pPr");
|
|
6837
|
-
if (existing_pPr)
|
|
7398
|
+
if (existing_pPr) {
|
|
7399
|
+
new_p.appendChild(this._clone_pPr_scrubbing_headings(existing_pPr));
|
|
7400
|
+
}
|
|
6838
7401
|
}
|
|
6839
7402
|
let pPr = findChild(new_p, "w:pPr");
|
|
6840
7403
|
if (!pPr) {
|
|
@@ -7092,7 +7655,24 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
7092
7655
|
p2_element = getNextElement(p2_element);
|
|
7093
7656
|
}
|
|
7094
7657
|
if (p2_element && p2_element.tagName === "w:p") {
|
|
7658
|
+
const p1_fully_deleted = !this._paragraph_has_visible_content(p1_element);
|
|
7095
7659
|
let pPr = findChild(p1_element, "w:pPr");
|
|
7660
|
+
if (p1_fully_deleted) {
|
|
7661
|
+
const p2_pPr = findChild(p2_element, "w:pPr");
|
|
7662
|
+
const adopted = p2_pPr ? p2_pPr.cloneNode(true) : p1_element.ownerDocument.createElement("w:pPr");
|
|
7663
|
+
if (pPr) {
|
|
7664
|
+
const sect = findChild(pPr, "w:sectPr");
|
|
7665
|
+
if (sect && !findChild(adopted, "w:sectPr")) {
|
|
7666
|
+
adopted.appendChild(sect.cloneNode(true));
|
|
7667
|
+
}
|
|
7668
|
+
p1_element.removeChild(pPr);
|
|
7669
|
+
}
|
|
7670
|
+
p1_element.insertBefore(
|
|
7671
|
+
adopted,
|
|
7672
|
+
p1_element.firstChild
|
|
7673
|
+
);
|
|
7674
|
+
pPr = adopted;
|
|
7675
|
+
}
|
|
7096
7676
|
if (!pPr) {
|
|
7097
7677
|
pPr = p1_element.ownerDocument.createElement("w:pPr");
|
|
7098
7678
|
p1_element.insertBefore(
|
|
@@ -7105,8 +7685,10 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
7105
7685
|
rPr = p1_element.ownerDocument.createElement("w:rPr");
|
|
7106
7686
|
pPr.appendChild(rPr);
|
|
7107
7687
|
}
|
|
7108
|
-
|
|
7109
|
-
|
|
7688
|
+
if (!findChild(rPr, "w:del")) {
|
|
7689
|
+
const del_mark = this._create_track_change_tag("w:del");
|
|
7690
|
+
rPr.appendChild(del_mark);
|
|
7691
|
+
}
|
|
7110
7692
|
const children = Array.from(p2_element.childNodes);
|
|
7111
7693
|
for (const child of children) {
|
|
7112
7694
|
if (child.nodeType === 1 && child.tagName === "w:pPr") {
|
|
@@ -7160,27 +7742,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
7160
7742
|
}
|
|
7161
7743
|
}
|
|
7162
7744
|
for (const p_elem of affected_ps) {
|
|
7163
|
-
|
|
7164
|
-
for (const tag of ["w:t", "w:tab", "w:br"]) {
|
|
7165
|
-
const nodes = findAllDescendants(p_elem, tag);
|
|
7166
|
-
for (const node of nodes) {
|
|
7167
|
-
let is_deleted = false;
|
|
7168
|
-
let curr = node.parentNode;
|
|
7169
|
-
while (curr && curr !== p_elem.parentNode) {
|
|
7170
|
-
if (curr.tagName === "w:del") {
|
|
7171
|
-
is_deleted = true;
|
|
7172
|
-
break;
|
|
7173
|
-
}
|
|
7174
|
-
curr = curr.parentNode;
|
|
7175
|
-
}
|
|
7176
|
-
if (!is_deleted) {
|
|
7177
|
-
if (tag === "w:t" && !node.textContent) continue;
|
|
7178
|
-
has_visible = true;
|
|
7179
|
-
break;
|
|
7180
|
-
}
|
|
7181
|
-
}
|
|
7182
|
-
if (has_visible) break;
|
|
7183
|
-
}
|
|
7745
|
+
const has_visible = this._paragraph_has_visible_content(p_elem);
|
|
7184
7746
|
if (!has_visible) {
|
|
7185
7747
|
let pPr = findChild(p_elem, "w:pPr");
|
|
7186
7748
|
if (!pPr) {
|
|
@@ -7739,6 +8301,31 @@ function strip_custom_properties(doc) {
|
|
|
7739
8301
|
const count = Math.max(lines.length, 1);
|
|
7740
8302
|
return [`Custom document properties: ${count} removed (docProps/custom.xml)`].concat(lines);
|
|
7741
8303
|
}
|
|
8304
|
+
function strip_document_variables(doc) {
|
|
8305
|
+
const settingsPart = doc.pkg.getPartByPath("word/settings.xml");
|
|
8306
|
+
if (!settingsPart) return [];
|
|
8307
|
+
const names = [];
|
|
8308
|
+
let removedAny = false;
|
|
8309
|
+
for (const container of findDescendantsByLocalName(settingsPart._element, "docVars")) {
|
|
8310
|
+
for (const child of Array.from(container.childNodes)) {
|
|
8311
|
+
const el = child;
|
|
8312
|
+
const tag = el.tagName || "";
|
|
8313
|
+
if (tag === "docVar" || tag.endsWith(":docVar")) {
|
|
8314
|
+
names.push(el.getAttribute("w:name") || el.getAttribute("name") || "(unnamed)");
|
|
8315
|
+
}
|
|
8316
|
+
}
|
|
8317
|
+
container.parentNode?.removeChild(container);
|
|
8318
|
+
removedAny = true;
|
|
8319
|
+
}
|
|
8320
|
+
for (const stray of findDescendantsByLocalName(settingsPart._element, "docVar")) {
|
|
8321
|
+
names.push(stray.getAttribute("w:name") || stray.getAttribute("name") || "(unnamed)");
|
|
8322
|
+
stray.parentNode?.removeChild(stray);
|
|
8323
|
+
removedAny = true;
|
|
8324
|
+
}
|
|
8325
|
+
if (!removedAny) return [];
|
|
8326
|
+
const shown = Array.from(new Set(names)).sort().join(", ") || "(unnamed)";
|
|
8327
|
+
return [`Document variables: ${names.length} removed (${shown})`];
|
|
8328
|
+
}
|
|
7742
8329
|
function strip_image_alt_text(doc) {
|
|
7743
8330
|
let count = 0;
|
|
7744
8331
|
for (const docPr of findDescendantsByLocalName(doc.element, "docPr")) {
|
|
@@ -8257,10 +8844,28 @@ function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets
|
|
|
8257
8844
|
paragraph_offsets
|
|
8258
8845
|
);
|
|
8259
8846
|
if (!cleanView) {
|
|
8260
|
-
|
|
8261
|
-
|
|
8847
|
+
let firstP = cell._element.getElementsByTagName("w:p")[0];
|
|
8848
|
+
let paraId = firstP ? firstP.getAttribute("w14:paraId") : null;
|
|
8849
|
+
if (!paraId && (!cell_content || cell_content.trim() === "")) {
|
|
8850
|
+
if (!firstP) {
|
|
8851
|
+
const xmlDoc = cell._element.ownerDocument;
|
|
8852
|
+
firstP = xmlDoc.createElement("w:p");
|
|
8853
|
+
cell._element.appendChild(firstP);
|
|
8854
|
+
}
|
|
8855
|
+
const allPs = Array.from(cell._element.ownerDocument.getElementsByTagName("w:p"));
|
|
8856
|
+
const index = allPs.indexOf(firstP);
|
|
8857
|
+
let hash = 2166136261;
|
|
8858
|
+
const str = `fallback-paraId-${index}`;
|
|
8859
|
+
for (let i = 0; i < str.length; i++) {
|
|
8860
|
+
hash ^= str.charCodeAt(i);
|
|
8861
|
+
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
8862
|
+
}
|
|
8863
|
+
paraId = (hash >>> 0).toString(16).toUpperCase().padStart(8, "0");
|
|
8864
|
+
firstP.setAttribute("w14:paraId", paraId);
|
|
8865
|
+
}
|
|
8262
8866
|
if (paraId) {
|
|
8263
|
-
const
|
|
8867
|
+
const space_pad = cell_content ? " " : "";
|
|
8868
|
+
const anchor = `${space_pad}{#cell:${paraId}}`;
|
|
8264
8869
|
cell_content = cell_content + anchor;
|
|
8265
8870
|
}
|
|
8266
8871
|
}
|
|
@@ -8525,6 +9130,8 @@ function _build_merged_meta_block(states_list, comments_map) {
|
|
|
8525
9130
|
const change_lines = [];
|
|
8526
9131
|
const comment_lines = [];
|
|
8527
9132
|
const seen_sigs = /* @__PURE__ */ new Set();
|
|
9133
|
+
const pair_map = compute_change_pair_map(states_list);
|
|
9134
|
+
const pairSuffix = (uid) => pair_map[uid] ? ` (pairs with ${pair_map[uid]})` : "";
|
|
8528
9135
|
for (const [ins_map, del_map, comments_set, fmt_map] of states_list) {
|
|
8529
9136
|
let render_comment2 = function(cid) {
|
|
8530
9137
|
if (!comments_map[cid]) return;
|
|
@@ -8553,7 +9160,9 @@ function _build_merged_meta_block(states_list, comments_map) {
|
|
|
8553
9160
|
)) {
|
|
8554
9161
|
const sig = `Chg:${uid}`;
|
|
8555
9162
|
if (!seen_sigs.has(sig)) {
|
|
8556
|
-
change_lines.push(
|
|
9163
|
+
change_lines.push(
|
|
9164
|
+
`[${sig} insert] ${meta.author || "Unknown"}${pairSuffix(uid)}`
|
|
9165
|
+
);
|
|
8557
9166
|
seen_sigs.add(sig);
|
|
8558
9167
|
}
|
|
8559
9168
|
}
|
|
@@ -8562,7 +9171,9 @@ function _build_merged_meta_block(states_list, comments_map) {
|
|
|
8562
9171
|
)) {
|
|
8563
9172
|
const sig = `Chg:${uid}`;
|
|
8564
9173
|
if (!seen_sigs.has(sig)) {
|
|
8565
|
-
change_lines.push(
|
|
9174
|
+
change_lines.push(
|
|
9175
|
+
`[${sig} delete] ${meta.author || "Unknown"}${pairSuffix(uid)}`
|
|
9176
|
+
);
|
|
8566
9177
|
seen_sigs.add(sig);
|
|
8567
9178
|
}
|
|
8568
9179
|
}
|
|
@@ -9124,14 +9735,14 @@ var SanitizeReport = class {
|
|
|
9124
9735
|
const lower = line.toLowerCase();
|
|
9125
9736
|
if (lower.includes("tracked change") || lower.includes("insertion") || lower.includes("deletion") || lower.includes("accepted")) {
|
|
9126
9737
|
this.change_lines.push(line);
|
|
9738
|
+
} 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")) {
|
|
9739
|
+
this.metadata_lines.push(line);
|
|
9127
9740
|
} else if (lower.includes("comment") || lower.includes("[open]") || lower.includes("[resolved]")) {
|
|
9128
9741
|
if (lower.includes("kept") || lower.includes("visible")) {
|
|
9129
9742
|
this.kept_comment_lines.push(line);
|
|
9130
9743
|
} else {
|
|
9131
9744
|
this.removed_comment_lines.push(line);
|
|
9132
9745
|
}
|
|
9133
|
-
} 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")) {
|
|
9134
|
-
this.metadata_lines.push(line);
|
|
9135
9746
|
} else if (lower.includes("hyperlink") || lower.includes("warning")) {
|
|
9136
9747
|
this.warnings.push(line);
|
|
9137
9748
|
} else {
|
|
@@ -9210,7 +9821,7 @@ async function finalize_document(doc, options) {
|
|
|
9210
9821
|
report.tracked_changes_found = total;
|
|
9211
9822
|
if (total > 0 && !options.accept_all) {
|
|
9212
9823
|
report.status = "blocked";
|
|
9213
|
-
report.blocked_reason = `Document contains ${total} unresolved tracked changes (${counts[0]} insertions, ${counts[1]} deletions, ${counts[2]} formatting). Review in Word first, or set
|
|
9824
|
+
report.blocked_reason = `Document contains ${total} unresolved tracked changes (${counts[0]} insertions, ${counts[1]} deletions, ${counts[2]} formatting). Review in Word first, set accept_all=true, or set sanitize_mode='keep-markup'.`;
|
|
9214
9825
|
return { reportText: report.render() };
|
|
9215
9826
|
}
|
|
9216
9827
|
if (total > 0) {
|
|
@@ -9244,6 +9855,7 @@ async function finalize_document(doc, options) {
|
|
|
9244
9855
|
report.add_transform_lines(scrub_timestamps(doc));
|
|
9245
9856
|
report.add_transform_lines(strip_custom_xml(doc));
|
|
9246
9857
|
report.add_transform_lines(strip_custom_properties(doc));
|
|
9858
|
+
report.add_transform_lines(strip_document_variables(doc));
|
|
9247
9859
|
report.add_transform_lines(strip_image_alt_text(doc));
|
|
9248
9860
|
const warnings = audit_hyperlinks(doc);
|
|
9249
9861
|
for (const w of warnings) report.warnings.push(w);
|
|
@@ -9327,6 +9939,12 @@ function verifySanitizedPackage(outBuffer) {
|
|
|
9327
9939
|
}
|
|
9328
9940
|
}
|
|
9329
9941
|
}
|
|
9942
|
+
if (names.includes("word/settings.xml")) {
|
|
9943
|
+
const settings = parseXml(strFromU82(unzipped["word/settings.xml"]));
|
|
9944
|
+
if (findDescendantsByLocalName(settings.documentElement, "docVar").length > 0) {
|
|
9945
|
+
problems.push("word/settings.xml still contains document variables (w:docVar)");
|
|
9946
|
+
}
|
|
9947
|
+
}
|
|
9330
9948
|
if (problems.length > 0) {
|
|
9331
9949
|
throw new Error(
|
|
9332
9950
|
"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."
|