@adeu/core 1.21.0 → 1.23.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 +1150 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +156 -12
- package/dist/index.d.ts +156 -12
- package/dist/index.js +1142 -88
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/diff.ts +500 -1
- package/src/docx/bridge.ts +13 -0
- package/src/domain.ts +68 -15
- package/src/engine.ts +391 -28
- package/src/index.ts +6 -5
- package/src/ingest.ts +77 -5
- package/src/mapper.ts +101 -12
- package/src/markup.test.ts +2 -1
- package/src/markup.ts +214 -20
- package/src/repro_qa_report_2026_07_18.test.ts +1244 -0
- package/src/repro_qa_report_v5.test.ts +399 -0
- package/src/sanitize/core.ts +3 -0
- package/src/sanitize/transforms.ts +66 -2
- package/src/utils/docx.ts +257 -16
- package/src/utils/safe-regex.ts +90 -0
package/dist/index.cjs
CHANGED
|
@@ -34,18 +34,25 @@ __export(index_exports, {
|
|
|
34
34
|
DocumentMapper: () => DocumentMapper,
|
|
35
35
|
DocumentObject: () => DocumentObject,
|
|
36
36
|
RedlineEngine: () => RedlineEngine,
|
|
37
|
+
RegexTimeoutError: () => RegexTimeoutError,
|
|
38
|
+
USER_PATTERN_TIMEOUT_MS: () => USER_PATTERN_TIMEOUT_MS,
|
|
37
39
|
_extractTextFromDoc: () => _extractTextFromDoc,
|
|
38
40
|
apply_edits_to_markdown: () => apply_edits_to_markdown,
|
|
39
41
|
create_unified_diff: () => create_unified_diff,
|
|
40
42
|
create_word_patch_diff: () => create_word_patch_diff,
|
|
43
|
+
describe_illegal_control_chars: () => describe_illegal_control_chars,
|
|
41
44
|
extractTextFromBuffer: () => extractTextFromBuffer,
|
|
42
45
|
extract_outline: () => extract_outline,
|
|
43
46
|
finalize_document: () => finalize_document,
|
|
44
47
|
generate_edits_from_text: () => generate_edits_from_text,
|
|
48
|
+
generate_structured_edits: () => generate_structured_edits,
|
|
45
49
|
identifyEngine: () => identifyEngine,
|
|
46
50
|
paginate: () => paginate,
|
|
47
51
|
split_structural_appendix: () => split_structural_appendix,
|
|
48
|
-
trim_common_context: () => trim_common_context
|
|
52
|
+
trim_common_context: () => trim_common_context,
|
|
53
|
+
userFindAllMatches: () => userFindAllMatches,
|
|
54
|
+
userSearch: () => userSearch,
|
|
55
|
+
validate_edit_strings: () => validate_edit_strings
|
|
49
56
|
});
|
|
50
57
|
module.exports = __toCommonJS(index_exports);
|
|
51
58
|
|
|
@@ -304,6 +311,13 @@ var DocumentObject = class _DocumentObject {
|
|
|
304
311
|
async save() {
|
|
305
312
|
for (const part of this.pkg.parts) {
|
|
306
313
|
let xmlStr = serializeXml(part._element.ownerDocument || part._element);
|
|
314
|
+
if (xmlStr.includes("w16du:") && !xmlStr.includes("xmlns:w16du=")) {
|
|
315
|
+
part._element.setAttribute(
|
|
316
|
+
"xmlns:w16du",
|
|
317
|
+
"http://schemas.microsoft.com/office/word/2023/wordml/word16du"
|
|
318
|
+
);
|
|
319
|
+
xmlStr = serializeXml(part._element.ownerDocument || part._element);
|
|
320
|
+
}
|
|
307
321
|
if (!xmlStr.startsWith("<?xml")) {
|
|
308
322
|
xmlStr = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' + xmlStr;
|
|
309
323
|
}
|
|
@@ -794,6 +808,62 @@ function extract_comments_data(pkg) {
|
|
|
794
808
|
return data;
|
|
795
809
|
}
|
|
796
810
|
|
|
811
|
+
// src/utils/safe-regex.ts
|
|
812
|
+
var vm = __toESM(require("vm"), 1);
|
|
813
|
+
var USER_PATTERN_TIMEOUT_MS = 2e3;
|
|
814
|
+
var RegexTimeoutError = class extends Error {
|
|
815
|
+
pattern;
|
|
816
|
+
constructor(pattern) {
|
|
817
|
+
super(
|
|
818
|
+
`Regular expression exceeded the ${USER_PATTERN_TIMEOUT_MS / 1e3}s matching time budget (catastrophic backtracking). Simplify the pattern \u2014 nested quantifiers like (a+)+ are the usual cause \u2014 or use a literal target instead.`
|
|
819
|
+
);
|
|
820
|
+
this.name = "RegexTimeoutError";
|
|
821
|
+
this.pattern = pattern;
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
function runBudgeted(pattern, script, sandbox) {
|
|
825
|
+
try {
|
|
826
|
+
return vm.runInNewContext(script, sandbox, {
|
|
827
|
+
timeout: USER_PATTERN_TIMEOUT_MS
|
|
828
|
+
});
|
|
829
|
+
} catch (e) {
|
|
830
|
+
if (e && e.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
|
|
831
|
+
throw new RegexTimeoutError(pattern);
|
|
832
|
+
}
|
|
833
|
+
throw e;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function userFindAllMatches(pattern, text, flags = "") {
|
|
837
|
+
const normalized = flags.includes("g") ? flags : flags + "g";
|
|
838
|
+
const re = new RegExp(pattern, normalized);
|
|
839
|
+
const raw = runBudgeted(
|
|
840
|
+
pattern,
|
|
841
|
+
`{
|
|
842
|
+
const out = [];
|
|
843
|
+
let m;
|
|
844
|
+
while ((m = re.exec(text)) !== null) {
|
|
845
|
+
out.push({ start: m.index, end: m.index + m[0].length });
|
|
846
|
+
if (m.index === re.lastIndex) re.lastIndex++;
|
|
847
|
+
}
|
|
848
|
+
out;
|
|
849
|
+
}`,
|
|
850
|
+
{ re, text }
|
|
851
|
+
);
|
|
852
|
+
return raw.map((r) => ({ start: r.start, end: r.end }));
|
|
853
|
+
}
|
|
854
|
+
function userSearch(pattern, text, flags = "") {
|
|
855
|
+
const re = new RegExp(pattern, flags.replace("g", ""));
|
|
856
|
+
const raw = runBudgeted(
|
|
857
|
+
pattern,
|
|
858
|
+
`{
|
|
859
|
+
const m = re.exec(text);
|
|
860
|
+
m ? { start: m.index, end: m.index + m[0].length } : null;
|
|
861
|
+
}`,
|
|
862
|
+
{ re, text }
|
|
863
|
+
);
|
|
864
|
+
return raw ? { start: raw.start, end: raw.end } : null;
|
|
865
|
+
}
|
|
866
|
+
|
|
797
867
|
// src/utils/docx.ts
|
|
798
868
|
var QN_W_P = "w:p";
|
|
799
869
|
var QN_W_R = "w:r";
|
|
@@ -835,7 +905,27 @@ var QN_W_OUTLINELVL = "w:outlineLvl";
|
|
|
835
905
|
var QN_W_NUMPR = "w:numPr";
|
|
836
906
|
var QN_W_NUMID = "w:numId";
|
|
837
907
|
var QN_W_ILVL = "w:ilvl";
|
|
908
|
+
var QN_W_DRAWING = "w:drawing";
|
|
909
|
+
var QN_W_OBJECT = "w:object";
|
|
910
|
+
var QN_W_PICT = "w:pict";
|
|
911
|
+
var QN_WP_DOCPR = "wp:docPr";
|
|
912
|
+
var QN_V_IMAGEDATA = "v:imagedata";
|
|
913
|
+
var QN_O_TITLE = "o:title";
|
|
838
914
|
var _CUSTOM_HEADING_NAME_RE = /Heading[ ]?([1-6])(?![0-9])/;
|
|
915
|
+
function _resolve_package(obj) {
|
|
916
|
+
let cur = obj;
|
|
917
|
+
const seen = /* @__PURE__ */ new Set();
|
|
918
|
+
while (cur && !seen.has(cur)) {
|
|
919
|
+
seen.add(cur);
|
|
920
|
+
if (cur.package) return cur.package;
|
|
921
|
+
if (cur.pkg) return cur.pkg;
|
|
922
|
+
if (cur.part && (cur.part.package || cur.part.pkg)) {
|
|
923
|
+
return cur.part.package || cur.part.pkg;
|
|
924
|
+
}
|
|
925
|
+
cur = cur._parent || cur.part || null;
|
|
926
|
+
}
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
839
929
|
function _get_style_cache(part) {
|
|
840
930
|
const pkg = part.package || part.pkg || (part.part ? part.part.pkg : null);
|
|
841
931
|
if (pkg && pkg._adeu_style_cache) {
|
|
@@ -869,6 +959,8 @@ function _get_style_cache(part) {
|
|
|
869
959
|
const based_on_el = findChild(s, "w:basedOn");
|
|
870
960
|
const based_on = based_on_el ? based_on_el.getAttribute("w:val") : null;
|
|
871
961
|
let outline_lvl = null;
|
|
962
|
+
let num_id = null;
|
|
963
|
+
let num_ilvl = null;
|
|
872
964
|
const pPr = findChild(s, "w:pPr");
|
|
873
965
|
if (pPr) {
|
|
874
966
|
const oLvl = findChild(pPr, "w:outlineLvl");
|
|
@@ -876,6 +968,19 @@ function _get_style_cache(part) {
|
|
|
876
968
|
const val = oLvl.getAttribute("w:val");
|
|
877
969
|
if (val && /^\d+$/.test(val)) outline_lvl = parseInt(val, 10);
|
|
878
970
|
}
|
|
971
|
+
const numPr = findChild(pPr, "w:numPr");
|
|
972
|
+
if (numPr) {
|
|
973
|
+
const numId_el = findChild(numPr, "w:numId");
|
|
974
|
+
if (numId_el) {
|
|
975
|
+
const n_val = numId_el.getAttribute("w:val");
|
|
976
|
+
if (n_val && n_val !== "0") num_id = n_val;
|
|
977
|
+
}
|
|
978
|
+
const ilvl_el = findChild(numPr, "w:ilvl");
|
|
979
|
+
if (ilvl_el) {
|
|
980
|
+
const i_val = ilvl_el.getAttribute("w:val");
|
|
981
|
+
if (i_val && /^\d+$/.test(i_val)) num_ilvl = parseInt(i_val, 10);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
879
984
|
}
|
|
880
985
|
let bold = null;
|
|
881
986
|
const rPr = findChild(s, "w:rPr");
|
|
@@ -886,23 +991,46 @@ function _get_style_cache(part) {
|
|
|
886
991
|
bold = val !== "0" && val !== "false" && val !== "off";
|
|
887
992
|
}
|
|
888
993
|
}
|
|
889
|
-
raw_styles[s_id] = {
|
|
994
|
+
raw_styles[s_id] = {
|
|
995
|
+
name,
|
|
996
|
+
based_on,
|
|
997
|
+
outline_level: outline_lvl,
|
|
998
|
+
bold,
|
|
999
|
+
num_id,
|
|
1000
|
+
num_ilvl
|
|
1001
|
+
};
|
|
890
1002
|
}
|
|
891
1003
|
const resolve_style = (s_id, visited) => {
|
|
892
1004
|
if (cache[s_id]) return cache[s_id];
|
|
893
1005
|
if (visited.has(s_id) || !raw_styles[s_id])
|
|
894
|
-
return {
|
|
1006
|
+
return {
|
|
1007
|
+
name: s_id,
|
|
1008
|
+
outline_level: null,
|
|
1009
|
+
bold: false,
|
|
1010
|
+
num_id: null,
|
|
1011
|
+
num_ilvl: null
|
|
1012
|
+
};
|
|
895
1013
|
visited.add(s_id);
|
|
896
1014
|
const raw = raw_styles[s_id];
|
|
897
1015
|
const based_on_id = raw.based_on;
|
|
898
1016
|
let o_lvl = raw.outline_level;
|
|
899
1017
|
let bold_val = raw.bold !== null ? raw.bold : false;
|
|
1018
|
+
let n_id = raw.num_id;
|
|
1019
|
+
let n_ilvl = raw.num_ilvl;
|
|
900
1020
|
if (based_on_id) {
|
|
901
1021
|
const parent = resolve_style(based_on_id, visited);
|
|
902
1022
|
if (o_lvl === null) o_lvl = parent.outline_level;
|
|
903
1023
|
if (raw.bold === null) bold_val = parent.bold;
|
|
904
|
-
|
|
905
|
-
|
|
1024
|
+
if (n_id === null) n_id = parent.num_id ?? null;
|
|
1025
|
+
if (n_ilvl === null) n_ilvl = parent.num_ilvl ?? null;
|
|
1026
|
+
}
|
|
1027
|
+
const resolved = {
|
|
1028
|
+
name: raw.name,
|
|
1029
|
+
outline_level: o_lvl,
|
|
1030
|
+
bold: bold_val,
|
|
1031
|
+
num_id: n_id,
|
|
1032
|
+
num_ilvl: n_ilvl
|
|
1033
|
+
};
|
|
906
1034
|
cache[s_id] = resolved;
|
|
907
1035
|
return resolved;
|
|
908
1036
|
};
|
|
@@ -911,6 +1039,68 @@ function _get_style_cache(part) {
|
|
|
911
1039
|
if (pkg) pkg._adeu_style_cache = result;
|
|
912
1040
|
return result;
|
|
913
1041
|
}
|
|
1042
|
+
function _get_numbering_cache(part) {
|
|
1043
|
+
const pkg = _resolve_package(part);
|
|
1044
|
+
if (!pkg) return {};
|
|
1045
|
+
if (pkg._adeu_numbering_cache) return pkg._adeu_numbering_cache;
|
|
1046
|
+
const cache = {};
|
|
1047
|
+
let numbering_root = null;
|
|
1048
|
+
try {
|
|
1049
|
+
const numberingPart = (pkg.parts || []).find(
|
|
1050
|
+
(p) => String(p.partname).endsWith("/numbering.xml")
|
|
1051
|
+
);
|
|
1052
|
+
if (numberingPart) numbering_root = numberingPart._element;
|
|
1053
|
+
} catch {
|
|
1054
|
+
numbering_root = null;
|
|
1055
|
+
}
|
|
1056
|
+
if (numbering_root) {
|
|
1057
|
+
const abstract_fmts = {};
|
|
1058
|
+
for (const abstract of findAllDescendants(numbering_root, "w:abstractNum")) {
|
|
1059
|
+
const a_id = abstract.getAttribute("w:abstractNumId");
|
|
1060
|
+
if (a_id === null) continue;
|
|
1061
|
+
const lvl_map = {};
|
|
1062
|
+
for (const lvl of findAllDescendants(abstract, "w:lvl")) {
|
|
1063
|
+
const ilvl_val = lvl.getAttribute("w:ilvl");
|
|
1064
|
+
const fmt_el = findChild(lvl, "w:numFmt");
|
|
1065
|
+
if (ilvl_val !== null && /^-?\d+$/.test(ilvl_val) && fmt_el) {
|
|
1066
|
+
const fmt = fmt_el.getAttribute("w:val");
|
|
1067
|
+
if (fmt) lvl_map[parseInt(ilvl_val, 10)] = fmt;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
abstract_fmts[a_id] = lvl_map;
|
|
1071
|
+
}
|
|
1072
|
+
for (const num of findAllDescendants(numbering_root, "w:num")) {
|
|
1073
|
+
const n_id = num.getAttribute("w:numId");
|
|
1074
|
+
const a_ref = findChild(num, "w:abstractNumId");
|
|
1075
|
+
if (n_id === null || !a_ref) continue;
|
|
1076
|
+
const a_id = a_ref.getAttribute("w:val");
|
|
1077
|
+
if (a_id !== null && abstract_fmts[a_id] !== void 0) {
|
|
1078
|
+
cache[n_id] = abstract_fmts[a_id];
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
pkg._adeu_numbering_cache = cache;
|
|
1083
|
+
return cache;
|
|
1084
|
+
}
|
|
1085
|
+
function get_list_marker(paragraph_part, num_id, ilvl) {
|
|
1086
|
+
let fmt = null;
|
|
1087
|
+
if (num_id !== null && num_id !== void 0) {
|
|
1088
|
+
const lvl_map = _get_numbering_cache(paragraph_part)[num_id];
|
|
1089
|
+
if (lvl_map && Object.keys(lvl_map).length > 0) {
|
|
1090
|
+
fmt = lvl_map[ilvl] !== void 0 ? lvl_map[ilvl] : null;
|
|
1091
|
+
if (fmt === null) {
|
|
1092
|
+
for (let lookup = ilvl; lookup >= 0; lookup--) {
|
|
1093
|
+
if (lvl_map[lookup] !== void 0) {
|
|
1094
|
+
fmt = lvl_map[lookup];
|
|
1095
|
+
break;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (fmt !== null && fmt !== "bullet") return "1. ";
|
|
1102
|
+
return "* ";
|
|
1103
|
+
}
|
|
914
1104
|
function _detect_heading_level_from_name(name) {
|
|
915
1105
|
if (!name) return null;
|
|
916
1106
|
const match = name.match(_CUSTOM_HEADING_NAME_RE);
|
|
@@ -988,21 +1178,48 @@ function get_paragraph_prefix(paragraph, style_cache, default_pstyle) {
|
|
|
988
1178
|
if (/^\d+$/.test(match)) return "#".repeat(parseInt(match, 10)) + " ";
|
|
989
1179
|
}
|
|
990
1180
|
if (style_name === "Title") return "# ";
|
|
1181
|
+
let list_num_id = null;
|
|
1182
|
+
let list_ilvl = null;
|
|
1183
|
+
let numbering_disabled = false;
|
|
991
1184
|
if (pPr) {
|
|
992
1185
|
const numPr = findChild(pPr, QN_W_NUMPR);
|
|
993
1186
|
if (numPr) {
|
|
994
1187
|
const numId = findChild(numPr, QN_W_NUMID);
|
|
995
|
-
if (numId
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1188
|
+
if (numId) {
|
|
1189
|
+
const val = numId.getAttribute(QN_W_VAL);
|
|
1190
|
+
if (val === "0") {
|
|
1191
|
+
numbering_disabled = true;
|
|
1192
|
+
} else if (val) {
|
|
1193
|
+
list_num_id = val;
|
|
1194
|
+
const ilvl = findChild(numPr, QN_W_ILVL);
|
|
1195
|
+
if (ilvl) {
|
|
1196
|
+
const valAttr = ilvl.getAttribute(QN_W_VAL);
|
|
1197
|
+
if (valAttr !== null && /^\d+$/.test(valAttr)) {
|
|
1198
|
+
list_ilvl = parseInt(valAttr, 10);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1001
1201
|
}
|
|
1002
|
-
return " ".repeat(level) + "* ";
|
|
1003
1202
|
}
|
|
1004
1203
|
}
|
|
1005
1204
|
}
|
|
1205
|
+
if (list_num_id === null && !numbering_disabled && style_info) {
|
|
1206
|
+
const style_num_id = style_info.num_id;
|
|
1207
|
+
if (style_num_id) {
|
|
1208
|
+
list_num_id = style_num_id;
|
|
1209
|
+
if (list_ilvl === null) {
|
|
1210
|
+
list_ilvl = style_info.num_ilvl !== void 0 ? style_info.num_ilvl : null;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
if (list_num_id !== null) {
|
|
1215
|
+
const level = list_ilvl !== null ? list_ilvl : 0;
|
|
1216
|
+
const marker = get_list_marker(
|
|
1217
|
+
paragraph._parent ? paragraph._parent.part || paragraph._parent : null,
|
|
1218
|
+
list_num_id,
|
|
1219
|
+
level
|
|
1220
|
+
);
|
|
1221
|
+
return " ".repeat(level) + marker;
|
|
1222
|
+
}
|
|
1006
1223
|
if (style_name && style_name !== "Normal") {
|
|
1007
1224
|
const custom_level = _detect_heading_level_from_name(style_name);
|
|
1008
1225
|
if (custom_level !== null) return "#".repeat(custom_level) + " ";
|
|
@@ -1100,6 +1317,9 @@ function* iter_block_items(parent) {
|
|
|
1100
1317
|
for (const child of notes) {
|
|
1101
1318
|
if (child.getAttribute("w:type") === "separator" || child.getAttribute("w:type") === "continuationSeparator")
|
|
1102
1319
|
continue;
|
|
1320
|
+
const note_id = child.getAttribute("w:id");
|
|
1321
|
+
if (note_id !== null && /^\s*[-+]?\d+\s*$/.test(note_id) && parseInt(note_id, 10) <= 0)
|
|
1322
|
+
continue;
|
|
1103
1323
|
yield new FootnoteItem(child, parent, parent.note_type);
|
|
1104
1324
|
}
|
|
1105
1325
|
return;
|
|
@@ -1115,19 +1335,24 @@ function* iter_block_items(parent) {
|
|
|
1115
1335
|
}
|
|
1116
1336
|
}
|
|
1117
1337
|
function* iter_document_parts(doc) {
|
|
1338
|
+
for (const [container] of iter_document_parts_with_kind(doc)) {
|
|
1339
|
+
yield container;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
function* iter_document_parts_with_kind(doc) {
|
|
1118
1343
|
const headers = doc.pkg.parts.filter(
|
|
1119
1344
|
(p) => p.contentType === "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"
|
|
1120
1345
|
);
|
|
1121
|
-
for (const h of headers) yield h;
|
|
1122
|
-
yield doc;
|
|
1346
|
+
for (const h of headers) yield [h, "header"];
|
|
1347
|
+
yield [doc, "body"];
|
|
1123
1348
|
const footers = doc.pkg.parts.filter(
|
|
1124
1349
|
(p) => p.contentType === "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"
|
|
1125
1350
|
);
|
|
1126
|
-
for (const f of footers) yield f;
|
|
1351
|
+
for (const f of footers) yield [f, "footer"];
|
|
1127
1352
|
const fnPart = doc.pkg.getPartByPath("word/footnotes.xml");
|
|
1128
1353
|
const enPart = doc.pkg.getPartByPath("word/endnotes.xml");
|
|
1129
|
-
if (fnPart) yield new NotesPart(fnPart, "fn");
|
|
1130
|
-
if (enPart) yield new NotesPart(enPart, "en");
|
|
1354
|
+
if (fnPart) yield [new NotesPart(fnPart, "fn"), "footnotes"];
|
|
1355
|
+
if (enPart) yield [new NotesPart(enPart, "en"), "endnotes"];
|
|
1131
1356
|
}
|
|
1132
1357
|
function _is_page_instr(instr) {
|
|
1133
1358
|
if (!instr) return false;
|
|
@@ -1165,7 +1390,22 @@ function* iter_paragraph_content(paragraph) {
|
|
|
1165
1390
|
const child = r_element.childNodes[i];
|
|
1166
1391
|
if (child.nodeType !== 1) continue;
|
|
1167
1392
|
const tag = child.tagName;
|
|
1168
|
-
if (tag ===
|
|
1393
|
+
if (tag === QN_W_DRAWING || tag === QN_W_OBJECT) {
|
|
1394
|
+
const doc_pr = findAllDescendants(child, QN_WP_DOCPR)[0] || null;
|
|
1395
|
+
let alt = "";
|
|
1396
|
+
let img_id = "0";
|
|
1397
|
+
if (doc_pr) {
|
|
1398
|
+
alt = doc_pr.getAttribute("descr") || doc_pr.getAttribute("title") || "";
|
|
1399
|
+
img_id = doc_pr.getAttribute("id") || "0";
|
|
1400
|
+
}
|
|
1401
|
+
yield { type: "image", id: img_id, date: alt };
|
|
1402
|
+
} else if (tag === QN_W_PICT) {
|
|
1403
|
+
const imagedata = findAllDescendants(child, QN_V_IMAGEDATA)[0] || null;
|
|
1404
|
+
if (imagedata) {
|
|
1405
|
+
const alt = imagedata.getAttribute(QN_O_TITLE) || "";
|
|
1406
|
+
yield { type: "image", id: "vml", date: alt };
|
|
1407
|
+
}
|
|
1408
|
+
} else if (tag === QN_W_COMMENTREFERENCE) {
|
|
1169
1409
|
const ref_id = child.getAttribute(QN_W_ID);
|
|
1170
1410
|
if (ref_id) yield { type: "ref", id: ref_id };
|
|
1171
1411
|
} else if (tag === QN_W_FOOTNOTEREFERENCE) {
|
|
@@ -1273,6 +1513,11 @@ var DocumentMapper = class {
|
|
|
1273
1513
|
full_text = "";
|
|
1274
1514
|
spans = [];
|
|
1275
1515
|
appendix_start_index = -1;
|
|
1516
|
+
// [start, end, kind] per projected part, in projection order. Spans carry
|
|
1517
|
+
// the matching index in .part_index. Together these let the engine refuse
|
|
1518
|
+
// or re-anchor edits at OPC part boundaries (QA 2026-07-18 C1).
|
|
1519
|
+
part_ranges = [];
|
|
1520
|
+
_current_part_index = 0;
|
|
1276
1521
|
_text_chunks = [];
|
|
1277
1522
|
_plain_projection = null;
|
|
1278
1523
|
constructor(doc, clean_view = false, original_view = false) {
|
|
@@ -1288,12 +1533,19 @@ var DocumentMapper = class {
|
|
|
1288
1533
|
this._text_chunks = [];
|
|
1289
1534
|
this.full_text = "";
|
|
1290
1535
|
this._plain_projection = null;
|
|
1291
|
-
|
|
1536
|
+
this.part_ranges = [];
|
|
1537
|
+
this._current_part_index = 0;
|
|
1538
|
+
let part_idx = 0;
|
|
1539
|
+
for (const [part, part_kind] of iter_document_parts_with_kind(this.doc)) {
|
|
1540
|
+
this._current_part_index = part_idx;
|
|
1541
|
+
const part_start = current_offset;
|
|
1292
1542
|
current_offset = this._map_blocks(part, current_offset);
|
|
1543
|
+
this.part_ranges.push([part_start, current_offset, part_kind]);
|
|
1293
1544
|
if (this.spans.length > 0 && this.spans[this.spans.length - 1].text !== "\n\n") {
|
|
1294
1545
|
this._add_virtual_text("\n\n", current_offset, null);
|
|
1295
1546
|
current_offset += 2;
|
|
1296
1547
|
}
|
|
1548
|
+
part_idx++;
|
|
1297
1549
|
}
|
|
1298
1550
|
while (this.spans.length > 0 && this.spans[this.spans.length - 1].text === "\n\n") {
|
|
1299
1551
|
this.spans.pop();
|
|
@@ -1302,6 +1554,47 @@ var DocumentMapper = class {
|
|
|
1302
1554
|
this.full_text = this._text_chunks.join("");
|
|
1303
1555
|
this.appendix_start_index = -1;
|
|
1304
1556
|
}
|
|
1557
|
+
/** [part_index, start, end, kind] for parts that projected any text. */
|
|
1558
|
+
_nonempty_part_ranges() {
|
|
1559
|
+
const out = [];
|
|
1560
|
+
for (let i = 0; i < this.part_ranges.length; i++) {
|
|
1561
|
+
const [s, e, k] = this.part_ranges[i];
|
|
1562
|
+
if (e > s) out.push([i, s, e, k]);
|
|
1563
|
+
}
|
|
1564
|
+
return out;
|
|
1565
|
+
}
|
|
1566
|
+
part_kind_of(part_index) {
|
|
1567
|
+
if (part_index >= 0 && part_index < this.part_ranges.length) {
|
|
1568
|
+
return this.part_ranges[part_index][2];
|
|
1569
|
+
}
|
|
1570
|
+
return null;
|
|
1571
|
+
}
|
|
1572
|
+
/** Kind of the part whose projected range contains `index`, or null. */
|
|
1573
|
+
part_kind_at(index) {
|
|
1574
|
+
for (const [, start, end, kind] of this._nonempty_part_ranges()) {
|
|
1575
|
+
if (start <= index && index <= end) return kind;
|
|
1576
|
+
}
|
|
1577
|
+
return null;
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* When `index` falls strictly AFTER one part's text and at-or-before the
|
|
1581
|
+
* start of the next part's text (i.e. inside the "\n\n" separator or
|
|
1582
|
+
* exactly at the next part's first character), returns
|
|
1583
|
+
* [previous_part_index, next_part_index]. Returns null everywhere else —
|
|
1584
|
+
* including index == previous part's end, which is an ordinary
|
|
1585
|
+
* end-of-part text position, not a boundary gap.
|
|
1586
|
+
*/
|
|
1587
|
+
part_boundary_at(index) {
|
|
1588
|
+
const ranges = this._nonempty_part_ranges();
|
|
1589
|
+
for (let j = 1; j < ranges.length; j++) {
|
|
1590
|
+
const [prev_i, , prev_end] = ranges[j - 1];
|
|
1591
|
+
const [next_i, next_start] = ranges[j];
|
|
1592
|
+
if (prev_end < index && index <= next_start) {
|
|
1593
|
+
return [prev_i, next_i];
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return null;
|
|
1597
|
+
}
|
|
1305
1598
|
_map_blocks(container, offset) {
|
|
1306
1599
|
let current = offset;
|
|
1307
1600
|
const c_type = container.constructor.name;
|
|
@@ -1431,7 +1724,8 @@ ${header}`;
|
|
|
1431
1724
|
end: current,
|
|
1432
1725
|
text: "",
|
|
1433
1726
|
run: null,
|
|
1434
|
-
paragraph
|
|
1727
|
+
paragraph,
|
|
1728
|
+
part_index: this._current_part_index
|
|
1435
1729
|
};
|
|
1436
1730
|
this.spans.push(span);
|
|
1437
1731
|
const active_ids = /* @__PURE__ */ new Set();
|
|
@@ -1463,7 +1757,8 @@ ${header}`;
|
|
|
1463
1757
|
ins_id: i_id || void 0,
|
|
1464
1758
|
del_id: d_id || void 0,
|
|
1465
1759
|
hyperlink_id: active_hyperlink_id || void 0,
|
|
1466
|
-
comment_ids: c_ids.length > 0 ? c_ids : void 0
|
|
1760
|
+
comment_ids: c_ids.length > 0 ? c_ids : void 0,
|
|
1761
|
+
part_index: this._current_part_index
|
|
1467
1762
|
};
|
|
1468
1763
|
this.spans.push(s);
|
|
1469
1764
|
this._text_chunks.push(txt);
|
|
@@ -1642,7 +1937,15 @@ ${header}`;
|
|
|
1642
1937
|
else if (ev.type === "del_end") delete active_del[ev.id];
|
|
1643
1938
|
else if (ev.type === "fmt_start") active_fmt[ev.id] = ev;
|
|
1644
1939
|
else if (ev.type === "fmt_end") delete active_fmt[ev.id];
|
|
1645
|
-
else if (ev.type === "
|
|
1940
|
+
else if (ev.type === "image") {
|
|
1941
|
+
const hidden = this.clean_view && Object.keys(active_del).length > 0 || this.original_view && Object.keys(active_ins).length > 0;
|
|
1942
|
+
if (!hidden) {
|
|
1943
|
+
const alt = (ev.date || "image").replace(/\]/g, ")").replace(/\n/g, " ");
|
|
1944
|
+
const txt = ``;
|
|
1945
|
+
this._add_virtual_text(txt, current, paragraph, null, true);
|
|
1946
|
+
current += txt.length;
|
|
1947
|
+
}
|
|
1948
|
+
} else if (ev.type === "footnote" || ev.type === "endnote") {
|
|
1646
1949
|
flush_pending_runs();
|
|
1647
1950
|
current_wrappers = ["", ""];
|
|
1648
1951
|
current_style = ["", ""];
|
|
@@ -1757,14 +2060,16 @@ ${header}`;
|
|
|
1757
2060
|
}
|
|
1758
2061
|
return [...change_lines, ...comment_lines].join("\n");
|
|
1759
2062
|
}
|
|
1760
|
-
_add_virtual_text(text, offset, context_paragraph, hyperlink_id = null) {
|
|
2063
|
+
_add_virtual_text(text, offset, context_paragraph, hyperlink_id = null, is_image_marker = false) {
|
|
1761
2064
|
const span = {
|
|
1762
2065
|
start: offset,
|
|
1763
2066
|
end: offset + text.length,
|
|
1764
2067
|
text,
|
|
1765
2068
|
run: null,
|
|
1766
2069
|
paragraph: context_paragraph,
|
|
1767
|
-
hyperlink_id: hyperlink_id || void 0
|
|
2070
|
+
hyperlink_id: hyperlink_id || void 0,
|
|
2071
|
+
part_index: this._current_part_index,
|
|
2072
|
+
is_image_marker: is_image_marker || void 0
|
|
1768
2073
|
};
|
|
1769
2074
|
this.spans.push(span);
|
|
1770
2075
|
this._text_chunks.push(text);
|
|
@@ -1868,10 +2173,10 @@ ${header}`;
|
|
|
1868
2173
|
find_match_index(target_text, is_regex = false) {
|
|
1869
2174
|
if (is_regex) {
|
|
1870
2175
|
try {
|
|
1871
|
-
const
|
|
1872
|
-
|
|
1873
|
-
if (match) return [match.index, match[0].length];
|
|
2176
|
+
const match = userSearch(target_text, this.full_text);
|
|
2177
|
+
if (match) return [match.start, match.end - match.start];
|
|
1874
2178
|
} catch (e) {
|
|
2179
|
+
if (e instanceof RegexTimeoutError) throw e;
|
|
1875
2180
|
}
|
|
1876
2181
|
return [-1, 0];
|
|
1877
2182
|
}
|
|
@@ -1900,11 +2205,10 @@ ${header}`;
|
|
|
1900
2205
|
if (!target_text) return [];
|
|
1901
2206
|
if (is_regex) {
|
|
1902
2207
|
try {
|
|
1903
|
-
const
|
|
1904
|
-
|
|
1905
|
-
if (matches2.length > 0)
|
|
1906
|
-
return matches2.map((m) => [m.index, m[0].length]);
|
|
2208
|
+
const matches2 = userFindAllMatches(target_text, this.full_text);
|
|
2209
|
+
if (matches2.length > 0) return matches2.map((m) => [m.start, m.end - m.start]);
|
|
1907
2210
|
} catch (e) {
|
|
2211
|
+
if (e instanceof RegexTimeoutError) throw e;
|
|
1908
2212
|
}
|
|
1909
2213
|
return [];
|
|
1910
2214
|
}
|
|
@@ -2365,6 +2669,327 @@ function generate_edits_from_text(original_text, modified_text) {
|
|
|
2365
2669
|
}
|
|
2366
2670
|
return edits;
|
|
2367
2671
|
}
|
|
2672
|
+
function _sequence_opcodes(a, b) {
|
|
2673
|
+
const n = a.length;
|
|
2674
|
+
const m = b.length;
|
|
2675
|
+
const dp = [];
|
|
2676
|
+
for (let i2 = 0; i2 <= n; i2++) dp.push(new Int32Array(m + 1));
|
|
2677
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
2678
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
2679
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
const ops = [];
|
|
2683
|
+
let i = 0;
|
|
2684
|
+
let j = 0;
|
|
2685
|
+
let pend_i1 = 0;
|
|
2686
|
+
let pend_j1 = 0;
|
|
2687
|
+
let pend_del = 0;
|
|
2688
|
+
let pend_ins = 0;
|
|
2689
|
+
const flushPending = () => {
|
|
2690
|
+
if (pend_del === 0 && pend_ins === 0) return;
|
|
2691
|
+
const tag = pend_del > 0 && pend_ins > 0 ? "replace" : pend_del > 0 ? "delete" : "insert";
|
|
2692
|
+
ops.push([tag, pend_i1, pend_i1 + pend_del, pend_j1, pend_j1 + pend_ins]);
|
|
2693
|
+
pend_del = 0;
|
|
2694
|
+
pend_ins = 0;
|
|
2695
|
+
};
|
|
2696
|
+
while (i < n || j < m) {
|
|
2697
|
+
if (i < n && j < m && a[i] === b[j]) {
|
|
2698
|
+
flushPending();
|
|
2699
|
+
const i1 = i;
|
|
2700
|
+
const j1 = j;
|
|
2701
|
+
while (i < n && j < m && a[i] === b[j]) {
|
|
2702
|
+
i++;
|
|
2703
|
+
j++;
|
|
2704
|
+
}
|
|
2705
|
+
ops.push(["equal", i1, i, j1, j]);
|
|
2706
|
+
} else {
|
|
2707
|
+
if (pend_del === 0 && pend_ins === 0) {
|
|
2708
|
+
pend_i1 = i;
|
|
2709
|
+
pend_j1 = j;
|
|
2710
|
+
}
|
|
2711
|
+
if (j < m && (i === n || dp[i][j + 1] >= dp[i + 1][j])) {
|
|
2712
|
+
pend_ins++;
|
|
2713
|
+
j++;
|
|
2714
|
+
} else {
|
|
2715
|
+
pend_del++;
|
|
2716
|
+
i++;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
flushPending();
|
|
2721
|
+
return ops;
|
|
2722
|
+
}
|
|
2723
|
+
var _IMAGE_MARKER_RE = /!\[[^\]]*\]\(docx-image:[^)]*\)/g;
|
|
2724
|
+
function _drop_image_marker_hunks(edits, warnings) {
|
|
2725
|
+
const _normalized = (s) => s.replace(_IMAGE_MARKER_RE, "").replace(/\s+/g, " ").trim();
|
|
2726
|
+
const kept = [];
|
|
2727
|
+
for (const e of edits) {
|
|
2728
|
+
const t_imgs = ((e.target_text || "").match(_IMAGE_MARKER_RE) || []).sort();
|
|
2729
|
+
const n_imgs = ((e.new_text || "").match(_IMAGE_MARKER_RE) || []).sort();
|
|
2730
|
+
if (JSON.stringify(t_imgs) === JSON.stringify(n_imgs)) {
|
|
2731
|
+
kept.push(e);
|
|
2732
|
+
continue;
|
|
2733
|
+
}
|
|
2734
|
+
if (_normalized(e.target_text || "") === _normalized(e.new_text || "")) {
|
|
2735
|
+
warnings.push(
|
|
2736
|
+
"An inline image was added or removed between the documents. Images cannot be transferred by text edits \u2014 the image-only difference was skipped; apply it manually in Word."
|
|
2737
|
+
);
|
|
2738
|
+
} else {
|
|
2739
|
+
warnings.push(
|
|
2740
|
+
"A text change overlapping an added/removed inline image was skipped \u2014 images cannot be transferred by text edits. Apply that section manually in Word."
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
return kept;
|
|
2745
|
+
}
|
|
2746
|
+
function _rows_are_plain(text, table) {
|
|
2747
|
+
for (const row of table.rows) {
|
|
2748
|
+
if (text.substring(row.start, row.end) !== row.cells.join(" | ")) {
|
|
2749
|
+
return false;
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
return true;
|
|
2753
|
+
}
|
|
2754
|
+
var _ROW_KEY_SEP = "";
|
|
2755
|
+
function _table_row_opcodes(rows_o, rows_m) {
|
|
2756
|
+
const keys_o = rows_o.map((r) => r.cells.join(_ROW_KEY_SEP));
|
|
2757
|
+
const keys_m = rows_m.map((r) => r.cells.join(_ROW_KEY_SEP));
|
|
2758
|
+
const opcodes = _sequence_opcodes(keys_o, keys_m);
|
|
2759
|
+
for (const [tag, i1, i2, j1, j2] of opcodes) {
|
|
2760
|
+
if (tag === "insert" || tag === "delete" || tag === "replace" && i2 - i1 !== j2 - j1) {
|
|
2761
|
+
return opcodes;
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
return null;
|
|
2765
|
+
}
|
|
2766
|
+
function _row_ops_for_table(table_o, table_m, opcodes, warnings) {
|
|
2767
|
+
const rows_o = table_o.rows;
|
|
2768
|
+
const rows_m = table_m.rows;
|
|
2769
|
+
const keys_o = rows_o.map((r) => r.cells.join(_ROW_KEY_SEP));
|
|
2770
|
+
const row_text = (r) => r.cells.join(" | ");
|
|
2771
|
+
if (new Set(keys_o).size !== keys_o.length) {
|
|
2772
|
+
warnings.push(
|
|
2773
|
+
"A table contains rows with identical text; the generated row operations anchor by row text and may be rejected as ambiguous at apply time. If that happens, apply the row changes with explicit insert_row/delete_row edits."
|
|
2774
|
+
);
|
|
2775
|
+
}
|
|
2776
|
+
const removed = /* @__PURE__ */ new Set();
|
|
2777
|
+
const replaced_new_text = {};
|
|
2778
|
+
for (const [tag, i1, i2, j1, j2] of opcodes) {
|
|
2779
|
+
if (tag === "delete") {
|
|
2780
|
+
for (let k = i1; k < i2; k++) removed.add(k);
|
|
2781
|
+
} else if (tag === "replace") {
|
|
2782
|
+
const pairs = Math.min(i2 - i1, j2 - j1);
|
|
2783
|
+
for (let k = i1 + pairs; k < i2; k++) removed.add(k);
|
|
2784
|
+
for (let k = 0; k < pairs; k++) {
|
|
2785
|
+
replaced_new_text[i1 + k] = row_text(rows_m[j1 + k]);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
const surviving = [];
|
|
2790
|
+
for (let k = 0; k < rows_o.length; k++) {
|
|
2791
|
+
if (!removed.has(k)) surviving.push(k);
|
|
2792
|
+
}
|
|
2793
|
+
const anchor_text = (orig_idx) => {
|
|
2794
|
+
return replaced_new_text[orig_idx] !== void 0 ? replaced_new_text[orig_idx] : row_text(rows_o[orig_idx]);
|
|
2795
|
+
};
|
|
2796
|
+
const insert_ops = (new_rows, at_orig_index) => {
|
|
2797
|
+
const ops2 = [];
|
|
2798
|
+
const before = surviving.filter((k) => k < at_orig_index);
|
|
2799
|
+
const after = surviving.filter((k) => k >= at_orig_index);
|
|
2800
|
+
if (before.length > 0) {
|
|
2801
|
+
const anchor_idx = before[before.length - 1];
|
|
2802
|
+
const anchor = anchor_text(anchor_idx);
|
|
2803
|
+
for (const r of [...new_rows].reverse()) {
|
|
2804
|
+
ops2.push({
|
|
2805
|
+
type: "insert_row",
|
|
2806
|
+
target_text: anchor,
|
|
2807
|
+
position: "below",
|
|
2808
|
+
cells: [...r.cells],
|
|
2809
|
+
// Pin to the anchor row's offset: text anchors alone are
|
|
2810
|
+
// ambiguous when tables share identical rows. Pins do not
|
|
2811
|
+
// survive JSON round-trips (the strict text match applies then,
|
|
2812
|
+
// failing closed with the duplicate-row warning above).
|
|
2813
|
+
_match_start_index: rows_o[anchor_idx].start
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
} else if (after.length > 0) {
|
|
2817
|
+
const anchor_idx = after[0];
|
|
2818
|
+
const anchor = anchor_text(anchor_idx);
|
|
2819
|
+
for (const r of new_rows) {
|
|
2820
|
+
ops2.push({
|
|
2821
|
+
type: "insert_row",
|
|
2822
|
+
target_text: anchor,
|
|
2823
|
+
position: "above",
|
|
2824
|
+
cells: [...r.cells],
|
|
2825
|
+
_match_start_index: rows_o[anchor_idx].start
|
|
2826
|
+
});
|
|
2827
|
+
}
|
|
2828
|
+
} else {
|
|
2829
|
+
warnings.push(
|
|
2830
|
+
"A table gained rows but no original row survives to anchor them; these row insertions were skipped \u2014 add them with explicit insert_row operations."
|
|
2831
|
+
);
|
|
2832
|
+
}
|
|
2833
|
+
return ops2;
|
|
2834
|
+
};
|
|
2835
|
+
const ops = [];
|
|
2836
|
+
for (const [tag, i1, i2, j1, j2] of opcodes) {
|
|
2837
|
+
if (tag === "equal") continue;
|
|
2838
|
+
if (tag === "replace") {
|
|
2839
|
+
const pairs = Math.min(i2 - i1, j2 - j1);
|
|
2840
|
+
for (let k = 0; k < pairs; k++) {
|
|
2841
|
+
const o_txt = row_text(rows_o[i1 + k]);
|
|
2842
|
+
const m_txt = row_text(rows_m[j1 + k]);
|
|
2843
|
+
if (o_txt !== m_txt) {
|
|
2844
|
+
ops.push({
|
|
2845
|
+
type: "modify",
|
|
2846
|
+
target_text: o_txt,
|
|
2847
|
+
new_text: m_txt,
|
|
2848
|
+
comment: "Diff: Table row modified"
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
for (let k = i1 + pairs; k < i2; k++) {
|
|
2853
|
+
ops.push({
|
|
2854
|
+
type: "delete_row",
|
|
2855
|
+
target_text: row_text(rows_o[k]),
|
|
2856
|
+
_match_start_index: rows_o[k].start
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
const surplus_new = rows_m.slice(j1 + pairs, j2);
|
|
2860
|
+
if (surplus_new.length > 0) {
|
|
2861
|
+
ops.push(...insert_ops(surplus_new, i2));
|
|
2862
|
+
}
|
|
2863
|
+
} else if (tag === "delete") {
|
|
2864
|
+
for (let k = i1; k < i2; k++) {
|
|
2865
|
+
ops.push({
|
|
2866
|
+
type: "delete_row",
|
|
2867
|
+
target_text: row_text(rows_o[k]),
|
|
2868
|
+
_match_start_index: rows_o[k].start
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
} else if (tag === "insert") {
|
|
2872
|
+
ops.push(...insert_ops(rows_m.slice(j1, j2), i1));
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
return ops;
|
|
2876
|
+
}
|
|
2877
|
+
function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod) {
|
|
2878
|
+
const warnings = [];
|
|
2879
|
+
const edits = [];
|
|
2880
|
+
const kinds_o = struct_orig.part_ranges.filter(([s, e]) => e > s).map(([, , k]) => k);
|
|
2881
|
+
const kinds_m = struct_mod.part_ranges.filter(([s, e]) => e > s).map(([, , k]) => k);
|
|
2882
|
+
if (JSON.stringify(kinds_o) !== JSON.stringify(kinds_m)) {
|
|
2883
|
+
warnings.push(
|
|
2884
|
+
`The documents have different part layouts (${kinds_o.join(" + ") || "none"} vs ${kinds_m.join(" + ") || "none"}); comparing flattened text instead. Header/footer additions or removals cannot be expressed as text edits.`
|
|
2885
|
+
);
|
|
2886
|
+
const flat = _drop_image_marker_hunks(
|
|
2887
|
+
generate_edits_from_text(text_orig, text_mod),
|
|
2888
|
+
warnings
|
|
2889
|
+
);
|
|
2890
|
+
return { edits: [...flat], warnings };
|
|
2891
|
+
}
|
|
2892
|
+
const ranges_o = struct_orig.part_ranges.filter(([s, e]) => e > s).map(([s, e]) => [s, e]);
|
|
2893
|
+
const ranges_m = struct_mod.part_ranges.filter(([s, e]) => e > s).map(([s, e]) => [s, e]);
|
|
2894
|
+
for (let p = 0; p < ranges_o.length; p++) {
|
|
2895
|
+
const [po_start, po_end] = ranges_o[p];
|
|
2896
|
+
const [pm_start, pm_end] = ranges_m[p];
|
|
2897
|
+
const tables_o = struct_orig.tables.filter(
|
|
2898
|
+
(t) => po_start <= t.start && t.end <= po_end
|
|
2899
|
+
);
|
|
2900
|
+
const tables_m = struct_mod.tables.filter(
|
|
2901
|
+
(t) => pm_start <= t.start && t.end <= pm_end
|
|
2902
|
+
);
|
|
2903
|
+
const tables_alignable = tables_o.length === tables_m.length && tables_o.every((t) => _rows_are_plain(text_orig, t)) && tables_m.every((t) => _rows_are_plain(text_mod, t));
|
|
2904
|
+
if (tables_o.length !== tables_m.length) {
|
|
2905
|
+
warnings.push(
|
|
2906
|
+
`A ${kinds_o[p]} part has ${tables_o.length} table(s) in the original but ${tables_m.length} in the modified document; its tables were compared as plain text. Adding or removing whole tables is not supported via diff/apply.`
|
|
2907
|
+
);
|
|
2908
|
+
}
|
|
2909
|
+
if (!tables_alignable) {
|
|
2910
|
+
const part_edits = generate_edits_from_text(
|
|
2911
|
+
text_orig.substring(po_start, po_end),
|
|
2912
|
+
text_mod.substring(pm_start, pm_end)
|
|
2913
|
+
);
|
|
2914
|
+
for (const e of part_edits) {
|
|
2915
|
+
e._match_start_index = (e._match_start_index || 0) + po_start;
|
|
2916
|
+
}
|
|
2917
|
+
edits.push(...part_edits);
|
|
2918
|
+
continue;
|
|
2919
|
+
}
|
|
2920
|
+
const boundaries_o = [
|
|
2921
|
+
[po_start, po_start],
|
|
2922
|
+
...tables_o.map((t) => [t.start, t.end]),
|
|
2923
|
+
[po_end, po_end]
|
|
2924
|
+
];
|
|
2925
|
+
const boundaries_m = [
|
|
2926
|
+
[pm_start, pm_start],
|
|
2927
|
+
...tables_m.map((t) => [t.start, t.end]),
|
|
2928
|
+
[pm_end, pm_end]
|
|
2929
|
+
];
|
|
2930
|
+
for (let seg_idx = 0; seg_idx < boundaries_o.length - 1; seg_idx++) {
|
|
2931
|
+
const seg_o_start = boundaries_o[seg_idx][1];
|
|
2932
|
+
const seg_o_end = boundaries_o[seg_idx + 1][0];
|
|
2933
|
+
const seg_m_start = boundaries_m[seg_idx][1];
|
|
2934
|
+
const seg_m_end = boundaries_m[seg_idx + 1][0];
|
|
2935
|
+
const seg_edits = generate_edits_from_text(
|
|
2936
|
+
text_orig.substring(seg_o_start, seg_o_end),
|
|
2937
|
+
text_mod.substring(seg_m_start, seg_m_end)
|
|
2938
|
+
);
|
|
2939
|
+
for (const e of seg_edits) {
|
|
2940
|
+
e._match_start_index = (e._match_start_index || 0) + seg_o_start;
|
|
2941
|
+
}
|
|
2942
|
+
edits.push(...seg_edits);
|
|
2943
|
+
if (seg_idx < tables_o.length) {
|
|
2944
|
+
const t_o = tables_o[seg_idx];
|
|
2945
|
+
const t_m = tables_m[seg_idx];
|
|
2946
|
+
const row_opcodes = _table_row_opcodes(t_o.rows, t_m.rows);
|
|
2947
|
+
if (row_opcodes !== null) {
|
|
2948
|
+
edits.push(..._row_ops_for_table(t_o, t_m, row_opcodes, warnings));
|
|
2949
|
+
} else {
|
|
2950
|
+
const tbl_edits = generate_edits_from_text(
|
|
2951
|
+
text_orig.substring(t_o.start, t_o.end),
|
|
2952
|
+
text_mod.substring(t_m.start, t_m.end)
|
|
2953
|
+
);
|
|
2954
|
+
for (const e of tbl_edits) {
|
|
2955
|
+
e._match_start_index = (e._match_start_index || 0) + t_o.start;
|
|
2956
|
+
}
|
|
2957
|
+
edits.push(...tbl_edits);
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
const countOccurrences = (haystack, needle) => {
|
|
2963
|
+
let count = 0;
|
|
2964
|
+
let from = 0;
|
|
2965
|
+
while (true) {
|
|
2966
|
+
const idx = haystack.indexOf(needle, from);
|
|
2967
|
+
if (idx === -1) break;
|
|
2968
|
+
count++;
|
|
2969
|
+
from = idx + needle.length;
|
|
2970
|
+
}
|
|
2971
|
+
return count;
|
|
2972
|
+
};
|
|
2973
|
+
let ambiguous_anchor_warned = false;
|
|
2974
|
+
for (const e of edits) {
|
|
2975
|
+
if ((e.type === "insert_row" || e.type === "delete_row") && !ambiguous_anchor_warned) {
|
|
2976
|
+
if (e.target_text && countOccurrences(text_orig, e.target_text) > 1) {
|
|
2977
|
+
warnings.push(
|
|
2978
|
+
`The row anchor "${e.target_text.substring(0, 60)}" appears more than once in the document. Applying this diff from its JSON output may be rejected as ambiguous \u2014 make the anchor rows unique, or apply the row changes with explicit insert_row/delete_row edits.`
|
|
2979
|
+
);
|
|
2980
|
+
ambiguous_anchor_warned = true;
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
const modify_edits = edits.filter(
|
|
2985
|
+
(e) => e.type === "modify"
|
|
2986
|
+
);
|
|
2987
|
+
const kept_modifies = new Set(_drop_image_marker_hunks(modify_edits, warnings));
|
|
2988
|
+
const final_edits = edits.filter(
|
|
2989
|
+
(e) => e.type !== "modify" || kept_modifies.has(e)
|
|
2990
|
+
);
|
|
2991
|
+
return { edits: final_edits, warnings };
|
|
2992
|
+
}
|
|
2368
2993
|
function create_unified_diff(original_text, modified_text, context_lines = 3) {
|
|
2369
2994
|
const dmp = new import_diff_match_patch.default.diff_match_patch();
|
|
2370
2995
|
dmp.Diff_Timeout = 2;
|
|
@@ -2672,6 +3297,30 @@ function _strip_balanced_markers(text) {
|
|
|
2672
3297
|
function _replace_smart_quotes(text) {
|
|
2673
3298
|
return text.replace(/“/g, '"').replace(/”/g, '"').replace(/‘/g, "'").replace(/’/g, "'");
|
|
2674
3299
|
}
|
|
3300
|
+
function _strip_markdown_for_matching(text) {
|
|
3301
|
+
const result = [];
|
|
3302
|
+
const position_map = [];
|
|
3303
|
+
let i = 0;
|
|
3304
|
+
while (i < text.length) {
|
|
3305
|
+
const pair = text.substring(i, i + 2);
|
|
3306
|
+
if (i < text.length - 1 && (pair === "**" || pair === "__")) {
|
|
3307
|
+
i += 2;
|
|
3308
|
+
continue;
|
|
3309
|
+
}
|
|
3310
|
+
if (text[i] === "*" || text[i] === "_") {
|
|
3311
|
+
const prev_char = i > 0 ? text[i - 1] : " ";
|
|
3312
|
+
const next_char = i < text.length - 1 ? text[i + 1] : " ";
|
|
3313
|
+
if ([" ", "\n", " "].includes(prev_char) || [" ", "\n", " "].includes(next_char)) {
|
|
3314
|
+
i += 1;
|
|
3315
|
+
continue;
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
position_map.push(i);
|
|
3319
|
+
result.push(text[i]);
|
|
3320
|
+
i += 1;
|
|
3321
|
+
}
|
|
3322
|
+
return [result.join(""), position_map];
|
|
3323
|
+
}
|
|
2675
3324
|
function _find_safe_boundaries(text, start, end) {
|
|
2676
3325
|
let new_start = start;
|
|
2677
3326
|
let new_end = end;
|
|
@@ -2775,31 +3424,63 @@ function _make_fuzzy_regex(target_text) {
|
|
|
2775
3424
|
if (remaining) parts.push(escapeRegExp(remaining));
|
|
2776
3425
|
return parts.join("");
|
|
2777
3426
|
}
|
|
2778
|
-
function
|
|
2779
|
-
if (!target) return [
|
|
2780
|
-
|
|
2781
|
-
|
|
3427
|
+
function _find_all_matches_in_text(text, target, is_regex = false) {
|
|
3428
|
+
if (!target) return [];
|
|
3429
|
+
if (is_regex) {
|
|
3430
|
+
try {
|
|
3431
|
+
return userFindAllMatches(target, text).map((m) => [m.start, m.end]);
|
|
3432
|
+
} catch (e) {
|
|
3433
|
+
if (e instanceof RegexTimeoutError) throw e;
|
|
3434
|
+
return [];
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
const findAllLiteral = (haystack, needle) => {
|
|
3438
|
+
const out = [];
|
|
3439
|
+
let from = 0;
|
|
3440
|
+
while (true) {
|
|
3441
|
+
const idx = haystack.indexOf(needle, from);
|
|
3442
|
+
if (idx === -1) break;
|
|
3443
|
+
out.push([idx, idx + needle.length]);
|
|
3444
|
+
from = idx + needle.length;
|
|
3445
|
+
}
|
|
3446
|
+
return out;
|
|
3447
|
+
};
|
|
3448
|
+
let spans = findAllLiteral(text, target);
|
|
3449
|
+
if (spans.length > 0) {
|
|
3450
|
+
return spans.map(([s, e]) => _find_safe_boundaries(text, s, e));
|
|
3451
|
+
}
|
|
2782
3452
|
const norm_text = _replace_smart_quotes(text);
|
|
2783
3453
|
const norm_target = _replace_smart_quotes(target);
|
|
2784
|
-
|
|
2785
|
-
if (
|
|
2786
|
-
return _find_safe_boundaries(text,
|
|
3454
|
+
spans = findAllLiteral(norm_text, norm_target);
|
|
3455
|
+
if (spans.length > 0) {
|
|
3456
|
+
return spans.map(([s, e]) => _find_safe_boundaries(text, s, e));
|
|
3457
|
+
}
|
|
3458
|
+
const [stripped_text, pos_map] = _strip_markdown_for_matching(norm_text);
|
|
3459
|
+
const [stripped_target] = _strip_markdown_for_matching(norm_target);
|
|
3460
|
+
if (stripped_target && (stripped_text !== norm_text || stripped_target !== norm_target)) {
|
|
3461
|
+
const results = [];
|
|
3462
|
+
for (const [p_start, p_end] of findAllLiteral(stripped_text, stripped_target)) {
|
|
3463
|
+
const raw_start = pos_map[p_start];
|
|
3464
|
+
const raw_end = pos_map[p_end - 1] + 1;
|
|
3465
|
+
results.push(_find_safe_boundaries(text, raw_start, raw_end));
|
|
3466
|
+
}
|
|
3467
|
+
if (results.length > 0) return results;
|
|
3468
|
+
}
|
|
2787
3469
|
try {
|
|
2788
|
-
const pattern = new RegExp(_make_fuzzy_regex(target));
|
|
2789
|
-
const
|
|
2790
|
-
|
|
2791
|
-
const raw_start = match.index;
|
|
2792
|
-
const raw_end = match.index + match[0].length;
|
|
3470
|
+
const pattern = new RegExp(_make_fuzzy_regex(target), "g");
|
|
3471
|
+
const results = [];
|
|
3472
|
+
for (const match of text.matchAll(pattern)) {
|
|
2793
3473
|
const [refined_start, refined_end] = _refine_match_boundaries(
|
|
2794
3474
|
text,
|
|
2795
|
-
|
|
2796
|
-
|
|
3475
|
+
match.index,
|
|
3476
|
+
match.index + match[0].length
|
|
2797
3477
|
);
|
|
2798
|
-
|
|
3478
|
+
results.push(_find_safe_boundaries(text, refined_start, refined_end));
|
|
2799
3479
|
}
|
|
3480
|
+
if (results.length > 0) return results;
|
|
2800
3481
|
} catch (e) {
|
|
2801
3482
|
}
|
|
2802
|
-
return [
|
|
3483
|
+
return [];
|
|
2803
3484
|
}
|
|
2804
3485
|
function _build_critic_markup(target_text, new_text, comment, edit_index, include_index, highlight_only) {
|
|
2805
3486
|
const parts = [];
|
|
@@ -2831,19 +3512,55 @@ function _build_critic_markup(target_text, new_text, comment, edit_index, includ
|
|
|
2831
3512
|
}
|
|
2832
3513
|
return parts.join("");
|
|
2833
3514
|
}
|
|
2834
|
-
function apply_edits_to_markdown(markdown_text, edits, include_index = false, highlight_only = false) {
|
|
3515
|
+
function apply_edits_to_markdown(markdown_text, edits, include_index = false, highlight_only = false, edit_reports) {
|
|
2835
3516
|
if (!edits || edits.length === 0) return markdown_text;
|
|
3517
|
+
const _report = (idx, status, error = null, occurrences = 0) => {
|
|
3518
|
+
if (edit_reports) edit_reports.push({ index: idx, status, error, occurrences });
|
|
3519
|
+
};
|
|
2836
3520
|
const matched_edits = [];
|
|
2837
3521
|
for (let idx = 0; idx < edits.length; idx++) {
|
|
2838
3522
|
const edit = edits[idx];
|
|
2839
3523
|
const target = edit.target_text || "";
|
|
3524
|
+
const match_mode = edit.match_mode || "strict";
|
|
3525
|
+
const is_regex = Boolean(edit.regex);
|
|
2840
3526
|
if (!target) {
|
|
3527
|
+
_report(
|
|
3528
|
+
idx,
|
|
3529
|
+
"failed",
|
|
3530
|
+
`- Edit ${idx + 1} Failed: target_text is empty. Pure insertions are expressed as a replacement: put the text immediately around the insertion point in target_text and repeat it (plus the new text) in new_text.`
|
|
3531
|
+
);
|
|
3532
|
+
continue;
|
|
3533
|
+
}
|
|
3534
|
+
let spans;
|
|
3535
|
+
try {
|
|
3536
|
+
spans = _find_all_matches_in_text(markdown_text, target, is_regex);
|
|
3537
|
+
} catch (e) {
|
|
3538
|
+
if (!(e instanceof RegexTimeoutError)) throw e;
|
|
3539
|
+
_report(idx, "failed", `- Edit ${idx + 1} Failed: ${e.message}`);
|
|
3540
|
+
continue;
|
|
3541
|
+
}
|
|
3542
|
+
if (spans.length === 0) {
|
|
3543
|
+
_report(
|
|
3544
|
+
idx,
|
|
3545
|
+
"failed",
|
|
3546
|
+
`- Edit ${idx + 1} Failed: Target text not found in document:
|
|
3547
|
+
"${target.substring(0, 80)}"`
|
|
3548
|
+
);
|
|
2841
3549
|
continue;
|
|
2842
3550
|
}
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
3551
|
+
if (spans.length > 1 && match_mode === "strict") {
|
|
3552
|
+
_report(
|
|
3553
|
+
idx,
|
|
3554
|
+
"failed",
|
|
3555
|
+
format_ambiguity_error(idx + 1, target, markdown_text, spans)
|
|
3556
|
+
);
|
|
3557
|
+
continue;
|
|
3558
|
+
}
|
|
3559
|
+
const selected = match_mode === "strict" || match_mode === "first" ? spans.slice(0, 1) : spans;
|
|
3560
|
+
for (const [start, end] of selected) {
|
|
3561
|
+
matched_edits.push([start, end, markdown_text.substring(start, end), edit, idx]);
|
|
3562
|
+
}
|
|
3563
|
+
_report(idx, "applied", null, selected.length);
|
|
2847
3564
|
}
|
|
2848
3565
|
const matched_edits_filtered = [];
|
|
2849
3566
|
const occupied_ranges = [];
|
|
@@ -2853,6 +3570,16 @@ function apply_edits_to_markdown(markdown_text, edits, include_index = false, hi
|
|
|
2853
3570
|
for (const [occ_start, occ_end] of occupied_ranges) {
|
|
2854
3571
|
if (start < occ_end && end > occ_start) {
|
|
2855
3572
|
overlaps = true;
|
|
3573
|
+
if (edit_reports) {
|
|
3574
|
+
const msg = `- Edit ${orig_idx + 1} Failed: overlaps with a previously matched edit.`;
|
|
3575
|
+
for (const r of edit_reports) {
|
|
3576
|
+
if (r.index === orig_idx) {
|
|
3577
|
+
r.status = "failed";
|
|
3578
|
+
r.error = msg;
|
|
3579
|
+
r.occurrences = 0;
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
2856
3583
|
break;
|
|
2857
3584
|
}
|
|
2858
3585
|
}
|
|
@@ -2882,7 +3609,9 @@ function apply_edits_to_markdown(markdown_text, edits, include_index = false, hi
|
|
|
2882
3609
|
isolated_target,
|
|
2883
3610
|
isolated_new,
|
|
2884
3611
|
edit.comment,
|
|
2885
|
-
|
|
3612
|
+
// 1-based, matching apply's "Edit N" reports and batch validation
|
|
3613
|
+
// errors (QA 2026-07-17 F10; mirrors Python).
|
|
3614
|
+
orig_idx + 1,
|
|
2886
3615
|
include_index,
|
|
2887
3616
|
highlight_only
|
|
2888
3617
|
);
|
|
@@ -3027,12 +3756,36 @@ function sequential_context_hint(applied_so_far) {
|
|
|
3027
3756
|
Note: ${applied_so_far} earlier edit(s) in this batch were already applied. Batches apply sequentially \u2014 each edit must target the document text as it reads AFTER the preceding edits (e.g. target the replacement text an earlier edit introduced, not the original wording).`;
|
|
3028
3757
|
}
|
|
3029
3758
|
var TRANSACTIONAL_NOT_APPLIED_ERROR = "Not applied: the batch is transactional and other edits failed validation (see their errors). Fix or remove those edits and re-run.";
|
|
3759
|
+
var XML_ILLEGAL_CHARS_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
|
|
3760
|
+
function describe_illegal_control_chars(text) {
|
|
3761
|
+
if (!text) return null;
|
|
3762
|
+
const found = text.match(XML_ILLEGAL_CHARS_RE);
|
|
3763
|
+
if (!found) return null;
|
|
3764
|
+
const codes = Array.from(new Set(found.map((c) => `0x${c.charCodeAt(0).toString(16).padStart(2, "0")}`))).sort();
|
|
3765
|
+
return codes.join(", ");
|
|
3766
|
+
}
|
|
3030
3767
|
function validate_edit_strings(edits, index_offset = 0) {
|
|
3031
3768
|
const errors = [];
|
|
3032
3769
|
for (let i = 0; i < edits.length; i++) {
|
|
3033
3770
|
const edit = edits[i];
|
|
3034
3771
|
const t_text = edit.target_text || "";
|
|
3035
3772
|
const n_text = edit.new_text || "";
|
|
3773
|
+
const checked_fields = [
|
|
3774
|
+
["target_text", t_text],
|
|
3775
|
+
["new_text", n_text]
|
|
3776
|
+
];
|
|
3777
|
+
if (edit.comment) checked_fields.push(["comment", edit.comment]);
|
|
3778
|
+
(edit.cells || []).forEach((cell, cell_idx) => {
|
|
3779
|
+
checked_fields.push([`cells[${cell_idx}]`, cell || ""]);
|
|
3780
|
+
});
|
|
3781
|
+
for (const [field_name, field_value] of checked_fields) {
|
|
3782
|
+
const described = describe_illegal_control_chars(field_value);
|
|
3783
|
+
if (described) {
|
|
3784
|
+
errors.push(
|
|
3785
|
+
`- Edit ${i + 1 + index_offset} Failed: \`${field_name}\` contains control character(s) (${described}) that cannot be stored in a DOCX. Remove them and re-submit.`
|
|
3786
|
+
);
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3036
3789
|
if (n_text.includes("{++") || n_text.includes("{--") || n_text.includes("{>>") || n_text.includes("{==")) {
|
|
3037
3790
|
errors.push(
|
|
3038
3791
|
`- Edit ${i + 1 + index_offset} Failed: Do not manually write CriticMarkup tags ({++, {--, {>>, {==) in \`new_text\`. The engine handles redlining automatically. To add a comment, use the \`comment\` parameter.`
|
|
@@ -3095,6 +3848,15 @@ function validate_edit_strings(edits, index_offset = 0) {
|
|
|
3095
3848
|
}
|
|
3096
3849
|
}
|
|
3097
3850
|
}
|
|
3851
|
+
if (t_text.includes("docx-image:") || n_text.includes("docx-image:")) {
|
|
3852
|
+
const t_imgs = (t_text.match(/!\[[^\]]*\]\(docx-image:[^)]*\)/g) || []).sort();
|
|
3853
|
+
const n_imgs = (n_text.match(/!\[[^\]]*\]\(docx-image:[^)]*\)/g) || []).sort();
|
|
3854
|
+
if (JSON.stringify(t_imgs) !== JSON.stringify(n_imgs)) {
|
|
3855
|
+
errors.push(
|
|
3856
|
+
`- Edit ${i + 1 + index_offset} Failed: image markers () are read-only projections of embedded images. They cannot be inserted, altered, or removed via text replacement \u2014 edit the text around the image instead.`
|
|
3857
|
+
);
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3098
3860
|
if (t_text.includes("{#") || n_text.includes("{#")) {
|
|
3099
3861
|
const t_anchors = t_text.match(/\{#[^\}]+\}/g) || [];
|
|
3100
3862
|
const n_anchors = n_text.match(/\{#[^\}]+\}/g) || [];
|
|
@@ -3339,8 +4101,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
3339
4101
|
* Snapshots the document text around a resolved edit BEFORE anything is
|
|
3340
4102
|
* applied. Previews rendered after the batch mutates the DOM cannot slice
|
|
3341
4103
|
* full_text at the stored offsets: applied edits shift offsets and inject
|
|
3342
|
-
* tracked-change markup,
|
|
3343
|
-
*
|
|
4104
|
+
* tracked-change markup, garbling previews with unrelated edits and
|
|
4105
|
+
* internal scaffolding (QA H1).
|
|
3344
4106
|
*/
|
|
3345
4107
|
_capture_preview_context(edit) {
|
|
3346
4108
|
if (edit.type !== "modify") return;
|
|
@@ -3441,8 +4203,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
3441
4203
|
}
|
|
3442
4204
|
/**
|
|
3443
4205
|
* The "new text" a batch report should show for an edit. InsertTableRow has
|
|
3444
|
-
* no new_text field — surface its cell contents
|
|
3445
|
-
* empty string
|
|
4206
|
+
* no new_text field — surface its cell contents rather than a misleading
|
|
4207
|
+
* empty string (QA M4).
|
|
3446
4208
|
*/
|
|
3447
4209
|
static _report_new_text(edit) {
|
|
3448
4210
|
if (edit && edit.type === "insert_row" && Array.isArray(edit.cells)) {
|
|
@@ -3818,6 +4580,43 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
3818
4580
|
element.setAttribute("xml:space", "preserve");
|
|
3819
4581
|
}
|
|
3820
4582
|
}
|
|
4583
|
+
/**
|
|
4584
|
+
* Walks `element` to its XML root element. Word (and LibreOffice, which
|
|
4585
|
+
* refuses to LOAD such files) only supports comment ranges in the main
|
|
4586
|
+
* document story ("w:document") — never in headers, footers, footnotes or
|
|
4587
|
+
* endnotes (QA 2026-07-18 H4/C1).
|
|
4588
|
+
*/
|
|
4589
|
+
_comment_anchor_in_main_story(element) {
|
|
4590
|
+
let root = element;
|
|
4591
|
+
while (root.parentNode && root.parentNode.nodeType === 1) {
|
|
4592
|
+
root = root.parentNode;
|
|
4593
|
+
}
|
|
4594
|
+
return root.tagName === "w:document";
|
|
4595
|
+
}
|
|
4596
|
+
/**
|
|
4597
|
+
* When the anchor lives outside the main document story, records a
|
|
4598
|
+
* user-visible warning and returns true (caller must skip the comment).
|
|
4599
|
+
* The tracked change itself still applies — only the bubble is dropped.
|
|
4600
|
+
*/
|
|
4601
|
+
_skip_comment_outside_main_story(element, text) {
|
|
4602
|
+
if (this._comment_anchor_in_main_story(element)) return false;
|
|
4603
|
+
let root = element;
|
|
4604
|
+
while (root.parentNode && root.parentNode.nodeType === 1) {
|
|
4605
|
+
root = root.parentNode;
|
|
4606
|
+
}
|
|
4607
|
+
const story = {
|
|
4608
|
+
"w:ftr": "footer",
|
|
4609
|
+
"w:hdr": "header",
|
|
4610
|
+
"w:footnotes": "footnote",
|
|
4611
|
+
"w:endnotes": "endnote"
|
|
4612
|
+
}[root.tagName] || "non-body";
|
|
4613
|
+
const msg = `- Warning: the comment "${text.substring(0, 60)}" was NOT attached: Word does not support comments inside a ${story} part, and writing one produces a document other applications cannot open. The tracked change itself was applied.`;
|
|
4614
|
+
this.skipped_details.push(msg);
|
|
4615
|
+
console.error(
|
|
4616
|
+
`Comment anchor outside main story; comment dropped (story=${story})`
|
|
4617
|
+
);
|
|
4618
|
+
return true;
|
|
4619
|
+
}
|
|
3821
4620
|
/**
|
|
3822
4621
|
* Attaches a comment that wraps a contiguous range within a single paragraph.
|
|
3823
4622
|
* start_element and end_element must both be direct children of parent_element
|
|
@@ -3826,6 +4625,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
3826
4625
|
*/
|
|
3827
4626
|
_attach_comment(parent_element, start_element, end_element, text) {
|
|
3828
4627
|
if (!text) return;
|
|
4628
|
+
if (!parent_element || !start_element || !end_element) return;
|
|
4629
|
+
if (this._skip_comment_outside_main_story(parent_element, text)) return;
|
|
3829
4630
|
const comment_id = this.comments_manager.addComment(this.author, text);
|
|
3830
4631
|
const xmlDoc = parent_element.ownerDocument;
|
|
3831
4632
|
const range_start = xmlDoc.createElement("w:commentRangeStart");
|
|
@@ -3859,6 +4660,9 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
3859
4660
|
*/
|
|
3860
4661
|
_attach_comment_spanning(start_p, start_el, end_p, end_el, text) {
|
|
3861
4662
|
if (!text) return;
|
|
4663
|
+
if (!start_p || !end_p) return;
|
|
4664
|
+
if (this._skip_comment_outside_main_story(start_p, text) || this._skip_comment_outside_main_story(end_p, text))
|
|
4665
|
+
return;
|
|
3862
4666
|
const comment_id = this.comments_manager.addComment(this.author, text);
|
|
3863
4667
|
const xmlDocStart = start_p.ownerDocument;
|
|
3864
4668
|
const xmlDocEnd = end_p.ownerDocument;
|
|
@@ -4470,6 +5274,51 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4470
5274
|
pfx,
|
|
4471
5275
|
(edit.new_text || "").length - sfx
|
|
4472
5276
|
);
|
|
5277
|
+
const multi_part_doc = target_mapper.part_ranges.filter((r) => r[1] > r[0]).length > 1;
|
|
5278
|
+
const raw_span_parts = multi_part_doc ? Array.from(
|
|
5279
|
+
new Set(
|
|
5280
|
+
target_mapper.spans.filter(
|
|
5281
|
+
(s) => s.run !== null && s.end > m_start && s.start < m_start + m_len
|
|
5282
|
+
).map((s) => s.part_index)
|
|
5283
|
+
)
|
|
5284
|
+
).sort((a, b) => a - b) : [];
|
|
5285
|
+
if (raw_span_parts.length > 1) {
|
|
5286
|
+
const kinds = raw_span_parts.map((pi) => target_mapper.part_kind_of(pi) || "?").join(" \u2192 ");
|
|
5287
|
+
errors.push(
|
|
5288
|
+
`- Edit ${i + 1 + index_offset} Failed: target_text spans a structural document-part boundary (${kinds}). Headers, body, footers and footnotes are separate Word parts \u2014 an edit cannot cross between them. Anchor the edit on text within a single part (split it into one edit per part if both sides must change).`
|
|
5289
|
+
);
|
|
5290
|
+
}
|
|
5291
|
+
const eff_start = m_start + pfx;
|
|
5292
|
+
const eff_end = m_start + m_len - sfx;
|
|
5293
|
+
if (eff_end > eff_start) {
|
|
5294
|
+
const overlapping = target_mapper.spans.filter(
|
|
5295
|
+
(s) => s.end > eff_start && s.start < eff_end && (s.run !== null || s.text.trim() !== "")
|
|
5296
|
+
);
|
|
5297
|
+
if (overlapping.some((s) => s.is_image_marker)) {
|
|
5298
|
+
errors.push(
|
|
5299
|
+
`- Edit ${i + 1 + index_offset} Failed: the target overlaps a read-only image marker (). Images cannot be edited or removed via text replacement \u2014 target the text around the image instead.`
|
|
5300
|
+
);
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
if (edit.comment && (edit.new_text || "") === (edit.target_text || "")) {
|
|
5304
|
+
const kind_here = target_mapper.part_kind_at(m_start);
|
|
5305
|
+
if (kind_here !== null && kind_here !== "body") {
|
|
5306
|
+
errors.push(
|
|
5307
|
+
`- Edit ${i + 1 + index_offset} Failed: comments cannot be anchored inside a ${kind_here} part \u2014 Word only supports comments in the main document body. Comment on the related body text instead.`
|
|
5308
|
+
);
|
|
5309
|
+
}
|
|
5310
|
+
}
|
|
5311
|
+
if (_RedlineEngine._introduces_table_row_text(
|
|
5312
|
+
target_mapper,
|
|
5313
|
+
m_start,
|
|
5314
|
+
m_len,
|
|
5315
|
+
final_target,
|
|
5316
|
+
final_new
|
|
5317
|
+
)) {
|
|
5318
|
+
errors.push(
|
|
5319
|
+
`- Edit ${i + 1 + index_offset} Failed: new_text introduces a pipe-delimited row line inside a table. Text replacement cannot create table rows \u2014 use the structured 'insert_row' operation instead (e.g. {"type": "insert_row", "target_text": "<anchor row text>", "cells": ["...", "..."]}).`
|
|
5320
|
+
);
|
|
5321
|
+
}
|
|
4473
5322
|
if (final_target.includes("\n\n")) {
|
|
4474
5323
|
const balanced = matched.split("\n\n").length === (edit.new_text || "").split("\n\n").length;
|
|
4475
5324
|
if (!balanced) {
|
|
@@ -4566,6 +5415,19 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4566
5415
|
* [start, start+length) in `mapper`, or null if that text is not inside a
|
|
4567
5416
|
* table row.
|
|
4568
5417
|
*/
|
|
5418
|
+
/**
|
|
5419
|
+
* True when a replacement anchored in a table would ADD line-separated
|
|
5420
|
+
* pipe-delimited content — the text shape of a table row. Writing that
|
|
5421
|
+
* into a cell renders a fake row inside one cell while the real grid
|
|
5422
|
+
* stays unchanged (QA 2026-07-18 C2); such edits must use insert_row.
|
|
5423
|
+
*/
|
|
5424
|
+
static _introduces_table_row_text(mapper, start, length, final_target, final_new) {
|
|
5425
|
+
if (!final_new.includes("\n") || !final_new.includes(" | ")) return false;
|
|
5426
|
+
const new_pipe_lines = final_new.split("\n").filter((line) => line.includes(" | ")).length;
|
|
5427
|
+
const old_pipe_lines = final_target.split("\n").filter((line) => line.includes(" | ")).length;
|
|
5428
|
+
if (new_pipe_lines <= old_pipe_lines) return false;
|
|
5429
|
+
return _RedlineEngine._column_count_at(mapper, start, Math.max(length, 1)) !== null;
|
|
5430
|
+
}
|
|
4569
5431
|
static _column_count_at(mapper, start, length) {
|
|
4570
5432
|
for (const s of mapper.spans) {
|
|
4571
5433
|
if (s.run === null || s.end <= start || s.start >= start + length) {
|
|
@@ -4727,7 +5589,13 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4727
5589
|
const reports_by_input = new Array(edits.length);
|
|
4728
5590
|
const validation_failed_idx = /* @__PURE__ */ new Set();
|
|
4729
5591
|
for (const { edit, i } of ordered_edits) {
|
|
4730
|
-
let single_errors
|
|
5592
|
+
let single_errors;
|
|
5593
|
+
try {
|
|
5594
|
+
single_errors = this.validate_edits([edit], i);
|
|
5595
|
+
} catch (e) {
|
|
5596
|
+
if (!(e instanceof RegexTimeoutError)) throw e;
|
|
5597
|
+
single_errors = [`- Edit ${i + 1} Failed: ${e.message}`];
|
|
5598
|
+
}
|
|
4731
5599
|
if (single_errors.length > 0) {
|
|
4732
5600
|
if (applied_edits > 0) {
|
|
4733
5601
|
const hint = sequential_context_hint(applied_edits);
|
|
@@ -4812,7 +5680,13 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4812
5680
|
const sequential_errors = [];
|
|
4813
5681
|
let applied_so_far = 0;
|
|
4814
5682
|
for (const { edit, i } of ordered_edits) {
|
|
4815
|
-
let single_errors
|
|
5683
|
+
let single_errors;
|
|
5684
|
+
try {
|
|
5685
|
+
single_errors = this.validate_edits([edit], i);
|
|
5686
|
+
} catch (e) {
|
|
5687
|
+
if (!(e instanceof RegexTimeoutError)) throw e;
|
|
5688
|
+
single_errors = [`- Edit ${i + 1} Failed: ${e.message}`];
|
|
5689
|
+
}
|
|
4816
5690
|
if (single_errors.length > 0) {
|
|
4817
5691
|
if (applied_so_far > 0) {
|
|
4818
5692
|
const hint = sequential_context_hint(applied_so_far);
|
|
@@ -4926,7 +5800,18 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4926
5800
|
edit._error_msg = msg;
|
|
4927
5801
|
}
|
|
4928
5802
|
} else {
|
|
4929
|
-
|
|
5803
|
+
let resolved;
|
|
5804
|
+
try {
|
|
5805
|
+
resolved = this._pre_resolve_heuristic_edit(edit);
|
|
5806
|
+
} catch (e) {
|
|
5807
|
+
if (!(e instanceof RegexTimeoutError)) throw e;
|
|
5808
|
+
skipped++;
|
|
5809
|
+
edit._applied_status = false;
|
|
5810
|
+
const msg = `- Failed to apply edit targeting: '${(edit.target_text || "").substring(0, 40)}...' (${e.message})`;
|
|
5811
|
+
this.skipped_details.push(msg);
|
|
5812
|
+
edit._error_msg = msg;
|
|
5813
|
+
continue;
|
|
5814
|
+
}
|
|
4930
5815
|
if (resolved) {
|
|
4931
5816
|
if (Array.isArray(resolved)) {
|
|
4932
5817
|
for (const r of resolved) {
|
|
@@ -4971,7 +5856,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4971
5856
|
const counted_split_groups = /* @__PURE__ */ new Set();
|
|
4972
5857
|
for (const [edit, orig_new] of resolved_edits) {
|
|
4973
5858
|
const start = edit._resolved_start_idx || 0;
|
|
4974
|
-
const end = start + (edit.target_text ? edit.target_text.length : 0);
|
|
5859
|
+
const end = edit.type === "insert_row" ? start : start + (edit.target_text ? edit.target_text.length : 0);
|
|
4975
5860
|
const overlaps = occupied_ranges.some(
|
|
4976
5861
|
([occ_start, occ_end]) => start < occ_end && end > occ_start
|
|
4977
5862
|
);
|
|
@@ -5085,6 +5970,16 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5085
5970
|
);
|
|
5086
5971
|
continue;
|
|
5087
5972
|
}
|
|
5973
|
+
const dup_authors = Array.from(
|
|
5974
|
+
new Set(all_nodes.map((n) => n.getAttribute("w:author") || "Unknown"))
|
|
5975
|
+
).sort();
|
|
5976
|
+
if (dup_authors.length > 1) {
|
|
5977
|
+
skipped++;
|
|
5978
|
+
this.skipped_details.push(
|
|
5979
|
+
`- Failed to apply action: ${type} on Chg:${target_id} is ambiguous. The document contains ${all_nodes.length} tracked-change elements sharing w:id=${target_id} from different authors (${dup_authors.join(", ")}) \u2014 duplicate revision IDs produced outside this engine (e.g. by a document merge or copy-paste). Acting on this ID would resolve all of them at once. Resolve these changes individually in Word, or apply the intended outcome as an explicit text edit instead.`
|
|
5980
|
+
);
|
|
5981
|
+
continue;
|
|
5982
|
+
}
|
|
5088
5983
|
for (const node of all_nodes) {
|
|
5089
5984
|
const is_ins = node.tagName === "w:ins";
|
|
5090
5985
|
const parent_tag = node.parentNode ? node.parentNode.tagName : "";
|
|
@@ -5651,13 +6546,49 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5651
6546
|
return true;
|
|
5652
6547
|
}
|
|
5653
6548
|
if (op === "INSERTION") {
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
);
|
|
6549
|
+
let final_new_text = edit.new_text || "";
|
|
6550
|
+
let boundary_anchor = null;
|
|
6551
|
+
const boundary = typeof active_mapper.part_boundary_at === "function" ? active_mapper.part_boundary_at(start_idx) : null;
|
|
6552
|
+
const is_machine_pure_insertion = !edit.target_text && (edit._parent_edit_ref === void 0 || edit._parent_edit_ref === null);
|
|
6553
|
+
if (boundary !== null && is_machine_pure_insertion) {
|
|
6554
|
+
const [prev_i, next_i] = boundary;
|
|
6555
|
+
const prev_kind = active_mapper.part_kind_of(prev_i);
|
|
6556
|
+
const next_kind = active_mapper.part_kind_of(next_i);
|
|
6557
|
+
if (prev_kind === "body" && next_kind !== "body") {
|
|
6558
|
+
const real_before = active_mapper.spans.filter(
|
|
6559
|
+
(s) => s.run !== null && s.part_index === prev_i
|
|
6560
|
+
);
|
|
6561
|
+
if (real_before.length > 0) {
|
|
6562
|
+
boundary_anchor = real_before[real_before.length - 1];
|
|
6563
|
+
}
|
|
6564
|
+
}
|
|
6565
|
+
}
|
|
6566
|
+
let anchor_run;
|
|
6567
|
+
let anchor_para;
|
|
6568
|
+
if (boundary_anchor !== null) {
|
|
6569
|
+
anchor_run = boundary_anchor.run;
|
|
6570
|
+
anchor_para = boundary_anchor.paragraph;
|
|
6571
|
+
if (!final_new_text.startsWith("\n")) {
|
|
6572
|
+
final_new_text = "\n\n" + final_new_text;
|
|
6573
|
+
}
|
|
6574
|
+
} else {
|
|
6575
|
+
[anchor_run, anchor_para] = active_mapper.get_insertion_anchor(
|
|
6576
|
+
start_idx,
|
|
6577
|
+
rebuild_map
|
|
6578
|
+
);
|
|
6579
|
+
}
|
|
5658
6580
|
if (!anchor_run && !anchor_para) return false;
|
|
5659
|
-
|
|
5660
|
-
|
|
6581
|
+
if (_RedlineEngine._introduces_table_row_text(
|
|
6582
|
+
active_mapper,
|
|
6583
|
+
start_idx,
|
|
6584
|
+
1,
|
|
6585
|
+
"",
|
|
6586
|
+
final_new_text
|
|
6587
|
+
)) {
|
|
6588
|
+
return false;
|
|
6589
|
+
}
|
|
6590
|
+
const _bug233_new = final_new_text;
|
|
6591
|
+
const _bug233_trailing_break = boundary_anchor === null && /\n\s*$/.test(_bug233_new);
|
|
5661
6592
|
let _bug233_target_para = null;
|
|
5662
6593
|
{
|
|
5663
6594
|
const startingSpans = active_mapper.spans.filter(
|
|
@@ -5727,7 +6658,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5727
6658
|
}
|
|
5728
6659
|
}
|
|
5729
6660
|
const result = this._track_insert_multiline(
|
|
5730
|
-
|
|
6661
|
+
final_new_text,
|
|
5731
6662
|
anchor_run,
|
|
5732
6663
|
anchor_para,
|
|
5733
6664
|
ins_id
|
|
@@ -5816,6 +6747,20 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5816
6747
|
}
|
|
5817
6748
|
return true;
|
|
5818
6749
|
}
|
|
6750
|
+
if ((op === "DELETION" || op === "MODIFICATION") && length && active_mapper.part_ranges.filter((r) => r[1] > r[0]).length > 1) {
|
|
6751
|
+
const crossed_parts = /* @__PURE__ */ new Set();
|
|
6752
|
+
for (const s of active_mapper.spans) {
|
|
6753
|
+
if (s.run !== null && s.end > start_idx && s.start < start_idx + length) {
|
|
6754
|
+
crossed_parts.add(s.part_index);
|
|
6755
|
+
}
|
|
6756
|
+
}
|
|
6757
|
+
if (crossed_parts.size > 1) {
|
|
6758
|
+
console.error(
|
|
6759
|
+
`Refusing edit that spans OPC part boundary (start=${start_idx}, parts=${Array.from(crossed_parts).sort().join(",")})`
|
|
6760
|
+
);
|
|
6761
|
+
return false;
|
|
6762
|
+
}
|
|
6763
|
+
}
|
|
5819
6764
|
const target_runs = active_mapper.find_target_runs_by_index(
|
|
5820
6765
|
start_idx,
|
|
5821
6766
|
length,
|
|
@@ -6400,6 +7345,27 @@ function scrub_doc_properties(doc) {
|
|
|
6400
7345
|
c.textContent = "";
|
|
6401
7346
|
}
|
|
6402
7347
|
});
|
|
7348
|
+
const titles = findDescendantsByLocalName(corePart._element, "title");
|
|
7349
|
+
titles.forEach((c) => {
|
|
7350
|
+
if (c.textContent) {
|
|
7351
|
+
lines.push(`Title kept (review manually): "${c.textContent}"`);
|
|
7352
|
+
}
|
|
7353
|
+
});
|
|
7354
|
+
const leakFields = [
|
|
7355
|
+
["category", "Category"],
|
|
7356
|
+
["keywords", "Keywords"],
|
|
7357
|
+
["subject", "Subject"],
|
|
7358
|
+
["contentStatus", "Content status"],
|
|
7359
|
+
["description", "Description/comments"]
|
|
7360
|
+
];
|
|
7361
|
+
for (const [local, label] of leakFields) {
|
|
7362
|
+
findDescendantsByLocalName(corePart._element, local).forEach((c) => {
|
|
7363
|
+
if (c.textContent) {
|
|
7364
|
+
lines.push(`${label}: ${c.textContent}`);
|
|
7365
|
+
c.textContent = "";
|
|
7366
|
+
}
|
|
7367
|
+
});
|
|
7368
|
+
}
|
|
6403
7369
|
const revisions = findDescendantsByLocalName(corePart._element, "revision");
|
|
6404
7370
|
revisions.forEach((c) => {
|
|
6405
7371
|
if (c.textContent && parseInt(c.textContent) > 1) {
|
|
@@ -6485,6 +7451,35 @@ function strip_image_alt_text(doc) {
|
|
|
6485
7451
|
}
|
|
6486
7452
|
return count ? [`Image alt text: ${count} auto-generated descriptions removed`] : [];
|
|
6487
7453
|
}
|
|
7454
|
+
function detect_watermarks(doc) {
|
|
7455
|
+
const warnings = [];
|
|
7456
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7457
|
+
for (const part of doc.pkg.parts) {
|
|
7458
|
+
const name = String(part.partname);
|
|
7459
|
+
let location;
|
|
7460
|
+
if (name.startsWith("/word/header")) location = "header";
|
|
7461
|
+
else if (name.startsWith("/word/footer")) location = "footer";
|
|
7462
|
+
else if (name === "/word/document.xml") location = "body";
|
|
7463
|
+
else continue;
|
|
7464
|
+
let textpaths;
|
|
7465
|
+
try {
|
|
7466
|
+
textpaths = findDescendantsByLocalName(part._element, "textpath");
|
|
7467
|
+
} catch {
|
|
7468
|
+
continue;
|
|
7469
|
+
}
|
|
7470
|
+
for (const tp of textpaths) {
|
|
7471
|
+
const text = (tp.getAttribute("string") || "").trim();
|
|
7472
|
+
const key = `${location}${text}`;
|
|
7473
|
+
if (seen.has(key)) continue;
|
|
7474
|
+
seen.add(key);
|
|
7475
|
+
const label = text ? `"${_truncate(text, 60)}"` : "(no text)";
|
|
7476
|
+
warnings.push(
|
|
7477
|
+
`Watermark-like text object in ${location}: ${label} \u2014 NOT removed. It remains in the document package and may render in other applications; review and remove it in Word if it must not reach the counterparty.`
|
|
7478
|
+
);
|
|
7479
|
+
}
|
|
7480
|
+
}
|
|
7481
|
+
return warnings;
|
|
7482
|
+
}
|
|
6488
7483
|
function audit_hyperlinks(doc) {
|
|
6489
7484
|
const internal = ["sharepoint.com", "onedrive.com", ".internal", "intranet", "localhost", "10.", "192.168.", "172.16."];
|
|
6490
7485
|
const warnings = [];
|
|
@@ -6537,25 +7532,53 @@ function _get_paragraph_text(p) {
|
|
|
6537
7532
|
}
|
|
6538
7533
|
return text;
|
|
6539
7534
|
}
|
|
7535
|
+
var _TERM_BODY = "[A-Z][A-Za-z0-9\\s\\-&'\u2019]{1,60}";
|
|
7536
|
+
var _LEADING_TERM_RE = new RegExp(
|
|
7537
|
+
`^(?:[\\d.\\-()a-zA-Z]+\\s*)?["\u201C](${_TERM_BODY})["\u201D]`,
|
|
7538
|
+
"d"
|
|
7539
|
+
);
|
|
7540
|
+
var _SENTENCE_TERM_RE = new RegExp(
|
|
7541
|
+
`(?<=[.;:!?])\\s+(?:[\\d.\\-()a-zA-Z]+\\s+)?["\u201C](${_TERM_BODY})["\u201D]`,
|
|
7542
|
+
"dg"
|
|
7543
|
+
);
|
|
7544
|
+
var _INLINE_TERM_RE = new RegExp(
|
|
7545
|
+
`\\([^)]*?["\u201C](${_TERM_BODY})["\u201D][^)]*?\\)`,
|
|
7546
|
+
"dg"
|
|
7547
|
+
);
|
|
7548
|
+
function extract_terms_from_paragraph(text) {
|
|
7549
|
+
const found = [];
|
|
7550
|
+
const leading = _LEADING_TERM_RE.exec(text);
|
|
7551
|
+
if (leading && leading.indices && leading.indices[1]) {
|
|
7552
|
+
found.push([leading.indices[1][0], leading[1].trim()]);
|
|
7553
|
+
}
|
|
7554
|
+
for (const m of text.matchAll(_SENTENCE_TERM_RE)) {
|
|
7555
|
+
const indices = m.indices;
|
|
7556
|
+
if (indices && indices[1]) found.push([indices[1][0], m[1].trim()]);
|
|
7557
|
+
}
|
|
7558
|
+
for (const m of text.matchAll(_INLINE_TERM_RE)) {
|
|
7559
|
+
const indices = m.indices;
|
|
7560
|
+
if (indices && indices[1]) found.push([indices[1][0], m[1].trim()]);
|
|
7561
|
+
}
|
|
7562
|
+
const terms = [];
|
|
7563
|
+
const seen_positions = /* @__PURE__ */ new Set();
|
|
7564
|
+
found.sort((a, b) => a[0] - b[0]);
|
|
7565
|
+
for (const [pos, term] of found) {
|
|
7566
|
+
if (seen_positions.has(pos)) continue;
|
|
7567
|
+
seen_positions.add(pos);
|
|
7568
|
+
terms.push(term);
|
|
7569
|
+
}
|
|
7570
|
+
return terms;
|
|
7571
|
+
}
|
|
6540
7572
|
function extract_all_domain_metadata(doc, base_text) {
|
|
6541
7573
|
const definitions = {};
|
|
6542
7574
|
const duplicates = /* @__PURE__ */ new Set();
|
|
6543
7575
|
const raw_anchors = {};
|
|
6544
7576
|
const raw_references = [];
|
|
6545
|
-
const leading_re = /^(?:[\d.\-()a-zA-Z]+\s*)?["“]([A-Z][A-Za-z0-9\s\-&'’]{1,60})["”]/;
|
|
6546
|
-
const inline_re = /\([^)]*?["“]([A-Z][A-Za-z0-9\s\-&'’]{1,60})["”][^)]*?\)/g;
|
|
6547
7577
|
for (const item of iter_block_items(doc)) {
|
|
6548
7578
|
if (!(item instanceof Paragraph)) continue;
|
|
6549
7579
|
const text = _get_paragraph_text(item).trim();
|
|
6550
7580
|
if (!text) continue;
|
|
6551
|
-
const
|
|
6552
|
-
const leading_match = text.match(leading_re);
|
|
6553
|
-
if (leading_match) extracted_terms.push(leading_match[1].trim());
|
|
6554
|
-
const inline_matches = text.matchAll(inline_re);
|
|
6555
|
-
for (const m of inline_matches) {
|
|
6556
|
-
extracted_terms.push(m[1].trim());
|
|
6557
|
-
}
|
|
6558
|
-
for (const term of extracted_terms) {
|
|
7581
|
+
for (const term of extract_terms_from_paragraph(text)) {
|
|
6559
7582
|
if (definitions[term]) duplicates.add(term);
|
|
6560
7583
|
else definitions[term] = { count: 0 };
|
|
6561
7584
|
}
|
|
@@ -6605,7 +7628,11 @@ function extract_all_domain_metadata(doc, base_text) {
|
|
|
6605
7628
|
for (const term of def_keys) {
|
|
6606
7629
|
if (definitions[term].count === 0) {
|
|
6607
7630
|
delete definitions[term];
|
|
6608
|
-
duplicates.
|
|
7631
|
+
if (!duplicates.has(term)) {
|
|
7632
|
+
diagnostics.push(
|
|
7633
|
+
`[Warning] Unused Definition: '${term}' is defined but never used.`
|
|
7634
|
+
);
|
|
7635
|
+
}
|
|
6609
7636
|
}
|
|
6610
7637
|
}
|
|
6611
7638
|
}
|
|
@@ -6779,27 +7806,32 @@ function build_structural_appendix(doc, base_text) {
|
|
|
6779
7806
|
}
|
|
6780
7807
|
|
|
6781
7808
|
// src/ingest.ts
|
|
6782
|
-
async function extractTextFromBuffer(buffer, cleanView = false) {
|
|
7809
|
+
async function extractTextFromBuffer(buffer, cleanView = false, includeAppendix = true) {
|
|
6783
7810
|
const doc = await DocumentObject.load(buffer);
|
|
6784
|
-
return _extractTextFromDoc(doc, cleanView);
|
|
7811
|
+
return _extractTextFromDoc(doc, cleanView, includeAppendix);
|
|
6785
7812
|
}
|
|
6786
|
-
function _extractTextFromDoc(doc, cleanView = false, includeAppendix = true, return_paragraph_offsets = false) {
|
|
7813
|
+
function _extractTextFromDoc(doc, cleanView = false, includeAppendix = true, return_paragraph_offsets = false, return_structure = false) {
|
|
6787
7814
|
const comments_map = extract_comments_data(doc.pkg);
|
|
6788
7815
|
const full_text = [];
|
|
6789
7816
|
const paragraph_offsets = /* @__PURE__ */ new Map();
|
|
7817
|
+
const structure = return_structure ? { part_ranges: [], tables: [] } : null;
|
|
6790
7818
|
let cursor = 0;
|
|
6791
|
-
for (const part of
|
|
7819
|
+
for (const [part, part_kind] of iter_document_parts_with_kind(doc)) {
|
|
6792
7820
|
const part_cursor = full_text.length > 0 ? cursor + 2 : cursor;
|
|
6793
7821
|
const part_text = _extract_blocks(
|
|
6794
7822
|
part,
|
|
6795
7823
|
comments_map,
|
|
6796
7824
|
cleanView,
|
|
6797
7825
|
part_cursor,
|
|
6798
|
-
return_paragraph_offsets ? paragraph_offsets : void 0
|
|
7826
|
+
return_paragraph_offsets ? paragraph_offsets : void 0,
|
|
7827
|
+
structure ? structure.tables : void 0
|
|
6799
7828
|
);
|
|
6800
7829
|
if (part_text) {
|
|
6801
7830
|
if (full_text.length > 0) cursor += 2;
|
|
6802
7831
|
full_text.push(part_text);
|
|
7832
|
+
if (structure) {
|
|
7833
|
+
structure.part_ranges.push([cursor, cursor + part_text.length, part_kind]);
|
|
7834
|
+
}
|
|
6803
7835
|
cursor += part_text.length;
|
|
6804
7836
|
}
|
|
6805
7837
|
}
|
|
@@ -6811,9 +7843,12 @@ function _extractTextFromDoc(doc, cleanView = false, includeAppendix = true, ret
|
|
|
6811
7843
|
if (return_paragraph_offsets) {
|
|
6812
7844
|
return { text: base_text, paragraph_offsets };
|
|
6813
7845
|
}
|
|
7846
|
+
if (structure) {
|
|
7847
|
+
return { text: base_text, structure };
|
|
7848
|
+
}
|
|
6814
7849
|
return base_text;
|
|
6815
7850
|
}
|
|
6816
|
-
function _extract_blocks(container, comments_map, cleanView, cursor, paragraph_offsets) {
|
|
7851
|
+
function _extract_blocks(container, comments_map, cleanView, cursor, paragraph_offsets, table_acc) {
|
|
6817
7852
|
const part = container.part || container;
|
|
6818
7853
|
const [style_cache, default_pstyle] = _get_style_cache(part);
|
|
6819
7854
|
const blocks = [];
|
|
@@ -6867,17 +7902,23 @@ ${header}`;
|
|
|
6867
7902
|
is_first_para = false;
|
|
6868
7903
|
is_first_block = false;
|
|
6869
7904
|
} else if (item instanceof Table) {
|
|
7905
|
+
const geometry = table_acc ? { start: block_start, end: block_start, rows: [] } : null;
|
|
6870
7906
|
const table_text = extract_table(
|
|
6871
7907
|
item,
|
|
6872
7908
|
comments_map,
|
|
6873
7909
|
cleanView,
|
|
6874
7910
|
block_start,
|
|
6875
|
-
paragraph_offsets
|
|
7911
|
+
paragraph_offsets,
|
|
7912
|
+
geometry
|
|
6876
7913
|
);
|
|
6877
7914
|
if (table_text) {
|
|
6878
7915
|
blocks.push(table_text);
|
|
6879
7916
|
local_cursor = block_start + table_text.length;
|
|
6880
7917
|
is_first_block = false;
|
|
7918
|
+
if (geometry && table_acc) {
|
|
7919
|
+
geometry.end = block_start + table_text.length;
|
|
7920
|
+
table_acc.push(geometry);
|
|
7921
|
+
}
|
|
6881
7922
|
} else if (!is_first_block) {
|
|
6882
7923
|
local_cursor -= 2;
|
|
6883
7924
|
}
|
|
@@ -6886,7 +7927,7 @@ ${header}`;
|
|
|
6886
7927
|
}
|
|
6887
7928
|
return blocks.join("\n\n");
|
|
6888
7929
|
}
|
|
6889
|
-
function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets) {
|
|
7930
|
+
function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets, geometry) {
|
|
6890
7931
|
const rows_text = [];
|
|
6891
7932
|
let rows_processed = 0;
|
|
6892
7933
|
let local_cursor = cursor;
|
|
@@ -6932,6 +7973,13 @@ function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets
|
|
|
6932
7973
|
}
|
|
6933
7974
|
rows_text.push(row_str);
|
|
6934
7975
|
local_cursor = row_start + row_str.length;
|
|
7976
|
+
if (geometry) {
|
|
7977
|
+
geometry.rows.push({
|
|
7978
|
+
start: row_start,
|
|
7979
|
+
end: local_cursor,
|
|
7980
|
+
cells: [...cell_texts]
|
|
7981
|
+
});
|
|
7982
|
+
}
|
|
6935
7983
|
rows_processed++;
|
|
6936
7984
|
}
|
|
6937
7985
|
return rows_text.join("\n");
|
|
@@ -7083,7 +8131,12 @@ function build_paragraph_text(paragraph, comments_map, cleanView, style_cache, d
|
|
|
7083
8131
|
else if (ev.type === "del_end") delete active_del[ev.id];
|
|
7084
8132
|
else if (ev.type === "fmt_start") active_fmt[ev.id] = ev;
|
|
7085
8133
|
else if (ev.type === "fmt_end") delete active_fmt[ev.id];
|
|
7086
|
-
else if (ev.type === "
|
|
8134
|
+
else if (ev.type === "image") {
|
|
8135
|
+
if (!(cleanView && Object.keys(active_del).length > 0)) {
|
|
8136
|
+
const alt = (ev.date || "image").replace(/\]/g, ")").replace(/\n/g, " ");
|
|
8137
|
+
parts.push(``);
|
|
8138
|
+
}
|
|
8139
|
+
} else if (ev.type === "footnote" || ev.type === "endnote") {
|
|
7087
8140
|
if (pending_text) {
|
|
7088
8141
|
parts.push(
|
|
7089
8142
|
`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
|
|
@@ -7885,6 +8938,7 @@ async function finalize_document(doc, options) {
|
|
|
7885
8938
|
report.add_transform_lines(strip_image_alt_text(doc));
|
|
7886
8939
|
const warnings = audit_hyperlinks(doc);
|
|
7887
8940
|
for (const w of warnings) report.warnings.push(w);
|
|
8941
|
+
for (const w of detect_watermarks(doc)) report.warnings.push(w);
|
|
7888
8942
|
report.add_transform_lines(normalize_change_dates(doc));
|
|
7889
8943
|
if (options.protection_mode === "read_only" || options.protection_mode === "encrypt") {
|
|
7890
8944
|
if (options.protection_mode === "encrypt") {
|
|
@@ -7941,17 +8995,24 @@ function identifyEngine() {
|
|
|
7941
8995
|
DocumentMapper,
|
|
7942
8996
|
DocumentObject,
|
|
7943
8997
|
RedlineEngine,
|
|
8998
|
+
RegexTimeoutError,
|
|
8999
|
+
USER_PATTERN_TIMEOUT_MS,
|
|
7944
9000
|
_extractTextFromDoc,
|
|
7945
9001
|
apply_edits_to_markdown,
|
|
7946
9002
|
create_unified_diff,
|
|
7947
9003
|
create_word_patch_diff,
|
|
9004
|
+
describe_illegal_control_chars,
|
|
7948
9005
|
extractTextFromBuffer,
|
|
7949
9006
|
extract_outline,
|
|
7950
9007
|
finalize_document,
|
|
7951
9008
|
generate_edits_from_text,
|
|
9009
|
+
generate_structured_edits,
|
|
7952
9010
|
identifyEngine,
|
|
7953
9011
|
paginate,
|
|
7954
9012
|
split_structural_appendix,
|
|
7955
|
-
trim_common_context
|
|
9013
|
+
trim_common_context,
|
|
9014
|
+
userFindAllMatches,
|
|
9015
|
+
userSearch,
|
|
9016
|
+
validate_edit_strings
|
|
7956
9017
|
});
|
|
7957
9018
|
//# sourceMappingURL=index.cjs.map
|