@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.js
CHANGED
|
@@ -253,6 +253,13 @@ var DocumentObject = class _DocumentObject {
|
|
|
253
253
|
async save() {
|
|
254
254
|
for (const part of this.pkg.parts) {
|
|
255
255
|
let xmlStr = serializeXml(part._element.ownerDocument || part._element);
|
|
256
|
+
if (xmlStr.includes("w16du:") && !xmlStr.includes("xmlns:w16du=")) {
|
|
257
|
+
part._element.setAttribute(
|
|
258
|
+
"xmlns:w16du",
|
|
259
|
+
"http://schemas.microsoft.com/office/word/2023/wordml/word16du"
|
|
260
|
+
);
|
|
261
|
+
xmlStr = serializeXml(part._element.ownerDocument || part._element);
|
|
262
|
+
}
|
|
256
263
|
if (!xmlStr.startsWith("<?xml")) {
|
|
257
264
|
xmlStr = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' + xmlStr;
|
|
258
265
|
}
|
|
@@ -743,6 +750,62 @@ function extract_comments_data(pkg) {
|
|
|
743
750
|
return data;
|
|
744
751
|
}
|
|
745
752
|
|
|
753
|
+
// src/utils/safe-regex.ts
|
|
754
|
+
import * as vm from "vm";
|
|
755
|
+
var USER_PATTERN_TIMEOUT_MS = 2e3;
|
|
756
|
+
var RegexTimeoutError = class extends Error {
|
|
757
|
+
pattern;
|
|
758
|
+
constructor(pattern) {
|
|
759
|
+
super(
|
|
760
|
+
`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.`
|
|
761
|
+
);
|
|
762
|
+
this.name = "RegexTimeoutError";
|
|
763
|
+
this.pattern = pattern;
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
function runBudgeted(pattern, script, sandbox) {
|
|
767
|
+
try {
|
|
768
|
+
return vm.runInNewContext(script, sandbox, {
|
|
769
|
+
timeout: USER_PATTERN_TIMEOUT_MS
|
|
770
|
+
});
|
|
771
|
+
} catch (e) {
|
|
772
|
+
if (e && e.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
|
|
773
|
+
throw new RegexTimeoutError(pattern);
|
|
774
|
+
}
|
|
775
|
+
throw e;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function userFindAllMatches(pattern, text, flags = "") {
|
|
779
|
+
const normalized = flags.includes("g") ? flags : flags + "g";
|
|
780
|
+
const re = new RegExp(pattern, normalized);
|
|
781
|
+
const raw = runBudgeted(
|
|
782
|
+
pattern,
|
|
783
|
+
`{
|
|
784
|
+
const out = [];
|
|
785
|
+
let m;
|
|
786
|
+
while ((m = re.exec(text)) !== null) {
|
|
787
|
+
out.push({ start: m.index, end: m.index + m[0].length });
|
|
788
|
+
if (m.index === re.lastIndex) re.lastIndex++;
|
|
789
|
+
}
|
|
790
|
+
out;
|
|
791
|
+
}`,
|
|
792
|
+
{ re, text }
|
|
793
|
+
);
|
|
794
|
+
return raw.map((r) => ({ start: r.start, end: r.end }));
|
|
795
|
+
}
|
|
796
|
+
function userSearch(pattern, text, flags = "") {
|
|
797
|
+
const re = new RegExp(pattern, flags.replace("g", ""));
|
|
798
|
+
const raw = runBudgeted(
|
|
799
|
+
pattern,
|
|
800
|
+
`{
|
|
801
|
+
const m = re.exec(text);
|
|
802
|
+
m ? { start: m.index, end: m.index + m[0].length } : null;
|
|
803
|
+
}`,
|
|
804
|
+
{ re, text }
|
|
805
|
+
);
|
|
806
|
+
return raw ? { start: raw.start, end: raw.end } : null;
|
|
807
|
+
}
|
|
808
|
+
|
|
746
809
|
// src/utils/docx.ts
|
|
747
810
|
var QN_W_P = "w:p";
|
|
748
811
|
var QN_W_R = "w:r";
|
|
@@ -784,7 +847,27 @@ var QN_W_OUTLINELVL = "w:outlineLvl";
|
|
|
784
847
|
var QN_W_NUMPR = "w:numPr";
|
|
785
848
|
var QN_W_NUMID = "w:numId";
|
|
786
849
|
var QN_W_ILVL = "w:ilvl";
|
|
850
|
+
var QN_W_DRAWING = "w:drawing";
|
|
851
|
+
var QN_W_OBJECT = "w:object";
|
|
852
|
+
var QN_W_PICT = "w:pict";
|
|
853
|
+
var QN_WP_DOCPR = "wp:docPr";
|
|
854
|
+
var QN_V_IMAGEDATA = "v:imagedata";
|
|
855
|
+
var QN_O_TITLE = "o:title";
|
|
787
856
|
var _CUSTOM_HEADING_NAME_RE = /Heading[ ]?([1-6])(?![0-9])/;
|
|
857
|
+
function _resolve_package(obj) {
|
|
858
|
+
let cur = obj;
|
|
859
|
+
const seen = /* @__PURE__ */ new Set();
|
|
860
|
+
while (cur && !seen.has(cur)) {
|
|
861
|
+
seen.add(cur);
|
|
862
|
+
if (cur.package) return cur.package;
|
|
863
|
+
if (cur.pkg) return cur.pkg;
|
|
864
|
+
if (cur.part && (cur.part.package || cur.part.pkg)) {
|
|
865
|
+
return cur.part.package || cur.part.pkg;
|
|
866
|
+
}
|
|
867
|
+
cur = cur._parent || cur.part || null;
|
|
868
|
+
}
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
788
871
|
function _get_style_cache(part) {
|
|
789
872
|
const pkg = part.package || part.pkg || (part.part ? part.part.pkg : null);
|
|
790
873
|
if (pkg && pkg._adeu_style_cache) {
|
|
@@ -818,6 +901,8 @@ function _get_style_cache(part) {
|
|
|
818
901
|
const based_on_el = findChild(s, "w:basedOn");
|
|
819
902
|
const based_on = based_on_el ? based_on_el.getAttribute("w:val") : null;
|
|
820
903
|
let outline_lvl = null;
|
|
904
|
+
let num_id = null;
|
|
905
|
+
let num_ilvl = null;
|
|
821
906
|
const pPr = findChild(s, "w:pPr");
|
|
822
907
|
if (pPr) {
|
|
823
908
|
const oLvl = findChild(pPr, "w:outlineLvl");
|
|
@@ -825,6 +910,19 @@ function _get_style_cache(part) {
|
|
|
825
910
|
const val = oLvl.getAttribute("w:val");
|
|
826
911
|
if (val && /^\d+$/.test(val)) outline_lvl = parseInt(val, 10);
|
|
827
912
|
}
|
|
913
|
+
const numPr = findChild(pPr, "w:numPr");
|
|
914
|
+
if (numPr) {
|
|
915
|
+
const numId_el = findChild(numPr, "w:numId");
|
|
916
|
+
if (numId_el) {
|
|
917
|
+
const n_val = numId_el.getAttribute("w:val");
|
|
918
|
+
if (n_val && n_val !== "0") num_id = n_val;
|
|
919
|
+
}
|
|
920
|
+
const ilvl_el = findChild(numPr, "w:ilvl");
|
|
921
|
+
if (ilvl_el) {
|
|
922
|
+
const i_val = ilvl_el.getAttribute("w:val");
|
|
923
|
+
if (i_val && /^\d+$/.test(i_val)) num_ilvl = parseInt(i_val, 10);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
828
926
|
}
|
|
829
927
|
let bold = null;
|
|
830
928
|
const rPr = findChild(s, "w:rPr");
|
|
@@ -835,23 +933,46 @@ function _get_style_cache(part) {
|
|
|
835
933
|
bold = val !== "0" && val !== "false" && val !== "off";
|
|
836
934
|
}
|
|
837
935
|
}
|
|
838
|
-
raw_styles[s_id] = {
|
|
936
|
+
raw_styles[s_id] = {
|
|
937
|
+
name,
|
|
938
|
+
based_on,
|
|
939
|
+
outline_level: outline_lvl,
|
|
940
|
+
bold,
|
|
941
|
+
num_id,
|
|
942
|
+
num_ilvl
|
|
943
|
+
};
|
|
839
944
|
}
|
|
840
945
|
const resolve_style = (s_id, visited) => {
|
|
841
946
|
if (cache[s_id]) return cache[s_id];
|
|
842
947
|
if (visited.has(s_id) || !raw_styles[s_id])
|
|
843
|
-
return {
|
|
948
|
+
return {
|
|
949
|
+
name: s_id,
|
|
950
|
+
outline_level: null,
|
|
951
|
+
bold: false,
|
|
952
|
+
num_id: null,
|
|
953
|
+
num_ilvl: null
|
|
954
|
+
};
|
|
844
955
|
visited.add(s_id);
|
|
845
956
|
const raw = raw_styles[s_id];
|
|
846
957
|
const based_on_id = raw.based_on;
|
|
847
958
|
let o_lvl = raw.outline_level;
|
|
848
959
|
let bold_val = raw.bold !== null ? raw.bold : false;
|
|
960
|
+
let n_id = raw.num_id;
|
|
961
|
+
let n_ilvl = raw.num_ilvl;
|
|
849
962
|
if (based_on_id) {
|
|
850
963
|
const parent = resolve_style(based_on_id, visited);
|
|
851
964
|
if (o_lvl === null) o_lvl = parent.outline_level;
|
|
852
965
|
if (raw.bold === null) bold_val = parent.bold;
|
|
853
|
-
|
|
854
|
-
|
|
966
|
+
if (n_id === null) n_id = parent.num_id ?? null;
|
|
967
|
+
if (n_ilvl === null) n_ilvl = parent.num_ilvl ?? null;
|
|
968
|
+
}
|
|
969
|
+
const resolved = {
|
|
970
|
+
name: raw.name,
|
|
971
|
+
outline_level: o_lvl,
|
|
972
|
+
bold: bold_val,
|
|
973
|
+
num_id: n_id,
|
|
974
|
+
num_ilvl: n_ilvl
|
|
975
|
+
};
|
|
855
976
|
cache[s_id] = resolved;
|
|
856
977
|
return resolved;
|
|
857
978
|
};
|
|
@@ -860,6 +981,68 @@ function _get_style_cache(part) {
|
|
|
860
981
|
if (pkg) pkg._adeu_style_cache = result;
|
|
861
982
|
return result;
|
|
862
983
|
}
|
|
984
|
+
function _get_numbering_cache(part) {
|
|
985
|
+
const pkg = _resolve_package(part);
|
|
986
|
+
if (!pkg) return {};
|
|
987
|
+
if (pkg._adeu_numbering_cache) return pkg._adeu_numbering_cache;
|
|
988
|
+
const cache = {};
|
|
989
|
+
let numbering_root = null;
|
|
990
|
+
try {
|
|
991
|
+
const numberingPart = (pkg.parts || []).find(
|
|
992
|
+
(p) => String(p.partname).endsWith("/numbering.xml")
|
|
993
|
+
);
|
|
994
|
+
if (numberingPart) numbering_root = numberingPart._element;
|
|
995
|
+
} catch {
|
|
996
|
+
numbering_root = null;
|
|
997
|
+
}
|
|
998
|
+
if (numbering_root) {
|
|
999
|
+
const abstract_fmts = {};
|
|
1000
|
+
for (const abstract of findAllDescendants(numbering_root, "w:abstractNum")) {
|
|
1001
|
+
const a_id = abstract.getAttribute("w:abstractNumId");
|
|
1002
|
+
if (a_id === null) continue;
|
|
1003
|
+
const lvl_map = {};
|
|
1004
|
+
for (const lvl of findAllDescendants(abstract, "w:lvl")) {
|
|
1005
|
+
const ilvl_val = lvl.getAttribute("w:ilvl");
|
|
1006
|
+
const fmt_el = findChild(lvl, "w:numFmt");
|
|
1007
|
+
if (ilvl_val !== null && /^-?\d+$/.test(ilvl_val) && fmt_el) {
|
|
1008
|
+
const fmt = fmt_el.getAttribute("w:val");
|
|
1009
|
+
if (fmt) lvl_map[parseInt(ilvl_val, 10)] = fmt;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
abstract_fmts[a_id] = lvl_map;
|
|
1013
|
+
}
|
|
1014
|
+
for (const num of findAllDescendants(numbering_root, "w:num")) {
|
|
1015
|
+
const n_id = num.getAttribute("w:numId");
|
|
1016
|
+
const a_ref = findChild(num, "w:abstractNumId");
|
|
1017
|
+
if (n_id === null || !a_ref) continue;
|
|
1018
|
+
const a_id = a_ref.getAttribute("w:val");
|
|
1019
|
+
if (a_id !== null && abstract_fmts[a_id] !== void 0) {
|
|
1020
|
+
cache[n_id] = abstract_fmts[a_id];
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
pkg._adeu_numbering_cache = cache;
|
|
1025
|
+
return cache;
|
|
1026
|
+
}
|
|
1027
|
+
function get_list_marker(paragraph_part, num_id, ilvl) {
|
|
1028
|
+
let fmt = null;
|
|
1029
|
+
if (num_id !== null && num_id !== void 0) {
|
|
1030
|
+
const lvl_map = _get_numbering_cache(paragraph_part)[num_id];
|
|
1031
|
+
if (lvl_map && Object.keys(lvl_map).length > 0) {
|
|
1032
|
+
fmt = lvl_map[ilvl] !== void 0 ? lvl_map[ilvl] : null;
|
|
1033
|
+
if (fmt === null) {
|
|
1034
|
+
for (let lookup = ilvl; lookup >= 0; lookup--) {
|
|
1035
|
+
if (lvl_map[lookup] !== void 0) {
|
|
1036
|
+
fmt = lvl_map[lookup];
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (fmt !== null && fmt !== "bullet") return "1. ";
|
|
1044
|
+
return "* ";
|
|
1045
|
+
}
|
|
863
1046
|
function _detect_heading_level_from_name(name) {
|
|
864
1047
|
if (!name) return null;
|
|
865
1048
|
const match = name.match(_CUSTOM_HEADING_NAME_RE);
|
|
@@ -937,21 +1120,48 @@ function get_paragraph_prefix(paragraph, style_cache, default_pstyle) {
|
|
|
937
1120
|
if (/^\d+$/.test(match)) return "#".repeat(parseInt(match, 10)) + " ";
|
|
938
1121
|
}
|
|
939
1122
|
if (style_name === "Title") return "# ";
|
|
1123
|
+
let list_num_id = null;
|
|
1124
|
+
let list_ilvl = null;
|
|
1125
|
+
let numbering_disabled = false;
|
|
940
1126
|
if (pPr) {
|
|
941
1127
|
const numPr = findChild(pPr, QN_W_NUMPR);
|
|
942
1128
|
if (numPr) {
|
|
943
1129
|
const numId = findChild(numPr, QN_W_NUMID);
|
|
944
|
-
if (numId
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1130
|
+
if (numId) {
|
|
1131
|
+
const val = numId.getAttribute(QN_W_VAL);
|
|
1132
|
+
if (val === "0") {
|
|
1133
|
+
numbering_disabled = true;
|
|
1134
|
+
} else if (val) {
|
|
1135
|
+
list_num_id = val;
|
|
1136
|
+
const ilvl = findChild(numPr, QN_W_ILVL);
|
|
1137
|
+
if (ilvl) {
|
|
1138
|
+
const valAttr = ilvl.getAttribute(QN_W_VAL);
|
|
1139
|
+
if (valAttr !== null && /^\d+$/.test(valAttr)) {
|
|
1140
|
+
list_ilvl = parseInt(valAttr, 10);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
950
1143
|
}
|
|
951
|
-
return " ".repeat(level) + "* ";
|
|
952
1144
|
}
|
|
953
1145
|
}
|
|
954
1146
|
}
|
|
1147
|
+
if (list_num_id === null && !numbering_disabled && style_info) {
|
|
1148
|
+
const style_num_id = style_info.num_id;
|
|
1149
|
+
if (style_num_id) {
|
|
1150
|
+
list_num_id = style_num_id;
|
|
1151
|
+
if (list_ilvl === null) {
|
|
1152
|
+
list_ilvl = style_info.num_ilvl !== void 0 ? style_info.num_ilvl : null;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
if (list_num_id !== null) {
|
|
1157
|
+
const level = list_ilvl !== null ? list_ilvl : 0;
|
|
1158
|
+
const marker = get_list_marker(
|
|
1159
|
+
paragraph._parent ? paragraph._parent.part || paragraph._parent : null,
|
|
1160
|
+
list_num_id,
|
|
1161
|
+
level
|
|
1162
|
+
);
|
|
1163
|
+
return " ".repeat(level) + marker;
|
|
1164
|
+
}
|
|
955
1165
|
if (style_name && style_name !== "Normal") {
|
|
956
1166
|
const custom_level = _detect_heading_level_from_name(style_name);
|
|
957
1167
|
if (custom_level !== null) return "#".repeat(custom_level) + " ";
|
|
@@ -1049,6 +1259,9 @@ function* iter_block_items(parent) {
|
|
|
1049
1259
|
for (const child of notes) {
|
|
1050
1260
|
if (child.getAttribute("w:type") === "separator" || child.getAttribute("w:type") === "continuationSeparator")
|
|
1051
1261
|
continue;
|
|
1262
|
+
const note_id = child.getAttribute("w:id");
|
|
1263
|
+
if (note_id !== null && /^\s*[-+]?\d+\s*$/.test(note_id) && parseInt(note_id, 10) <= 0)
|
|
1264
|
+
continue;
|
|
1052
1265
|
yield new FootnoteItem(child, parent, parent.note_type);
|
|
1053
1266
|
}
|
|
1054
1267
|
return;
|
|
@@ -1064,19 +1277,24 @@ function* iter_block_items(parent) {
|
|
|
1064
1277
|
}
|
|
1065
1278
|
}
|
|
1066
1279
|
function* iter_document_parts(doc) {
|
|
1280
|
+
for (const [container] of iter_document_parts_with_kind(doc)) {
|
|
1281
|
+
yield container;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
function* iter_document_parts_with_kind(doc) {
|
|
1067
1285
|
const headers = doc.pkg.parts.filter(
|
|
1068
1286
|
(p) => p.contentType === "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"
|
|
1069
1287
|
);
|
|
1070
|
-
for (const h of headers) yield h;
|
|
1071
|
-
yield doc;
|
|
1288
|
+
for (const h of headers) yield [h, "header"];
|
|
1289
|
+
yield [doc, "body"];
|
|
1072
1290
|
const footers = doc.pkg.parts.filter(
|
|
1073
1291
|
(p) => p.contentType === "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"
|
|
1074
1292
|
);
|
|
1075
|
-
for (const f of footers) yield f;
|
|
1293
|
+
for (const f of footers) yield [f, "footer"];
|
|
1076
1294
|
const fnPart = doc.pkg.getPartByPath("word/footnotes.xml");
|
|
1077
1295
|
const enPart = doc.pkg.getPartByPath("word/endnotes.xml");
|
|
1078
|
-
if (fnPart) yield new NotesPart(fnPart, "fn");
|
|
1079
|
-
if (enPart) yield new NotesPart(enPart, "en");
|
|
1296
|
+
if (fnPart) yield [new NotesPart(fnPart, "fn"), "footnotes"];
|
|
1297
|
+
if (enPart) yield [new NotesPart(enPart, "en"), "endnotes"];
|
|
1080
1298
|
}
|
|
1081
1299
|
function _is_page_instr(instr) {
|
|
1082
1300
|
if (!instr) return false;
|
|
@@ -1114,7 +1332,22 @@ function* iter_paragraph_content(paragraph) {
|
|
|
1114
1332
|
const child = r_element.childNodes[i];
|
|
1115
1333
|
if (child.nodeType !== 1) continue;
|
|
1116
1334
|
const tag = child.tagName;
|
|
1117
|
-
if (tag ===
|
|
1335
|
+
if (tag === QN_W_DRAWING || tag === QN_W_OBJECT) {
|
|
1336
|
+
const doc_pr = findAllDescendants(child, QN_WP_DOCPR)[0] || null;
|
|
1337
|
+
let alt = "";
|
|
1338
|
+
let img_id = "0";
|
|
1339
|
+
if (doc_pr) {
|
|
1340
|
+
alt = doc_pr.getAttribute("descr") || doc_pr.getAttribute("title") || "";
|
|
1341
|
+
img_id = doc_pr.getAttribute("id") || "0";
|
|
1342
|
+
}
|
|
1343
|
+
yield { type: "image", id: img_id, date: alt };
|
|
1344
|
+
} else if (tag === QN_W_PICT) {
|
|
1345
|
+
const imagedata = findAllDescendants(child, QN_V_IMAGEDATA)[0] || null;
|
|
1346
|
+
if (imagedata) {
|
|
1347
|
+
const alt = imagedata.getAttribute(QN_O_TITLE) || "";
|
|
1348
|
+
yield { type: "image", id: "vml", date: alt };
|
|
1349
|
+
}
|
|
1350
|
+
} else if (tag === QN_W_COMMENTREFERENCE) {
|
|
1118
1351
|
const ref_id = child.getAttribute(QN_W_ID);
|
|
1119
1352
|
if (ref_id) yield { type: "ref", id: ref_id };
|
|
1120
1353
|
} else if (tag === QN_W_FOOTNOTEREFERENCE) {
|
|
@@ -1222,6 +1455,11 @@ var DocumentMapper = class {
|
|
|
1222
1455
|
full_text = "";
|
|
1223
1456
|
spans = [];
|
|
1224
1457
|
appendix_start_index = -1;
|
|
1458
|
+
// [start, end, kind] per projected part, in projection order. Spans carry
|
|
1459
|
+
// the matching index in .part_index. Together these let the engine refuse
|
|
1460
|
+
// or re-anchor edits at OPC part boundaries (QA 2026-07-18 C1).
|
|
1461
|
+
part_ranges = [];
|
|
1462
|
+
_current_part_index = 0;
|
|
1225
1463
|
_text_chunks = [];
|
|
1226
1464
|
_plain_projection = null;
|
|
1227
1465
|
constructor(doc, clean_view = false, original_view = false) {
|
|
@@ -1237,12 +1475,19 @@ var DocumentMapper = class {
|
|
|
1237
1475
|
this._text_chunks = [];
|
|
1238
1476
|
this.full_text = "";
|
|
1239
1477
|
this._plain_projection = null;
|
|
1240
|
-
|
|
1478
|
+
this.part_ranges = [];
|
|
1479
|
+
this._current_part_index = 0;
|
|
1480
|
+
let part_idx = 0;
|
|
1481
|
+
for (const [part, part_kind] of iter_document_parts_with_kind(this.doc)) {
|
|
1482
|
+
this._current_part_index = part_idx;
|
|
1483
|
+
const part_start = current_offset;
|
|
1241
1484
|
current_offset = this._map_blocks(part, current_offset);
|
|
1485
|
+
this.part_ranges.push([part_start, current_offset, part_kind]);
|
|
1242
1486
|
if (this.spans.length > 0 && this.spans[this.spans.length - 1].text !== "\n\n") {
|
|
1243
1487
|
this._add_virtual_text("\n\n", current_offset, null);
|
|
1244
1488
|
current_offset += 2;
|
|
1245
1489
|
}
|
|
1490
|
+
part_idx++;
|
|
1246
1491
|
}
|
|
1247
1492
|
while (this.spans.length > 0 && this.spans[this.spans.length - 1].text === "\n\n") {
|
|
1248
1493
|
this.spans.pop();
|
|
@@ -1251,6 +1496,47 @@ var DocumentMapper = class {
|
|
|
1251
1496
|
this.full_text = this._text_chunks.join("");
|
|
1252
1497
|
this.appendix_start_index = -1;
|
|
1253
1498
|
}
|
|
1499
|
+
/** [part_index, start, end, kind] for parts that projected any text. */
|
|
1500
|
+
_nonempty_part_ranges() {
|
|
1501
|
+
const out = [];
|
|
1502
|
+
for (let i = 0; i < this.part_ranges.length; i++) {
|
|
1503
|
+
const [s, e, k] = this.part_ranges[i];
|
|
1504
|
+
if (e > s) out.push([i, s, e, k]);
|
|
1505
|
+
}
|
|
1506
|
+
return out;
|
|
1507
|
+
}
|
|
1508
|
+
part_kind_of(part_index) {
|
|
1509
|
+
if (part_index >= 0 && part_index < this.part_ranges.length) {
|
|
1510
|
+
return this.part_ranges[part_index][2];
|
|
1511
|
+
}
|
|
1512
|
+
return null;
|
|
1513
|
+
}
|
|
1514
|
+
/** Kind of the part whose projected range contains `index`, or null. */
|
|
1515
|
+
part_kind_at(index) {
|
|
1516
|
+
for (const [, start, end, kind] of this._nonempty_part_ranges()) {
|
|
1517
|
+
if (start <= index && index <= end) return kind;
|
|
1518
|
+
}
|
|
1519
|
+
return null;
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* When `index` falls strictly AFTER one part's text and at-or-before the
|
|
1523
|
+
* start of the next part's text (i.e. inside the "\n\n" separator or
|
|
1524
|
+
* exactly at the next part's first character), returns
|
|
1525
|
+
* [previous_part_index, next_part_index]. Returns null everywhere else —
|
|
1526
|
+
* including index == previous part's end, which is an ordinary
|
|
1527
|
+
* end-of-part text position, not a boundary gap.
|
|
1528
|
+
*/
|
|
1529
|
+
part_boundary_at(index) {
|
|
1530
|
+
const ranges = this._nonempty_part_ranges();
|
|
1531
|
+
for (let j = 1; j < ranges.length; j++) {
|
|
1532
|
+
const [prev_i, , prev_end] = ranges[j - 1];
|
|
1533
|
+
const [next_i, next_start] = ranges[j];
|
|
1534
|
+
if (prev_end < index && index <= next_start) {
|
|
1535
|
+
return [prev_i, next_i];
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1254
1540
|
_map_blocks(container, offset) {
|
|
1255
1541
|
let current = offset;
|
|
1256
1542
|
const c_type = container.constructor.name;
|
|
@@ -1380,7 +1666,8 @@ ${header}`;
|
|
|
1380
1666
|
end: current,
|
|
1381
1667
|
text: "",
|
|
1382
1668
|
run: null,
|
|
1383
|
-
paragraph
|
|
1669
|
+
paragraph,
|
|
1670
|
+
part_index: this._current_part_index
|
|
1384
1671
|
};
|
|
1385
1672
|
this.spans.push(span);
|
|
1386
1673
|
const active_ids = /* @__PURE__ */ new Set();
|
|
@@ -1412,7 +1699,8 @@ ${header}`;
|
|
|
1412
1699
|
ins_id: i_id || void 0,
|
|
1413
1700
|
del_id: d_id || void 0,
|
|
1414
1701
|
hyperlink_id: active_hyperlink_id || void 0,
|
|
1415
|
-
comment_ids: c_ids.length > 0 ? c_ids : void 0
|
|
1702
|
+
comment_ids: c_ids.length > 0 ? c_ids : void 0,
|
|
1703
|
+
part_index: this._current_part_index
|
|
1416
1704
|
};
|
|
1417
1705
|
this.spans.push(s);
|
|
1418
1706
|
this._text_chunks.push(txt);
|
|
@@ -1591,7 +1879,15 @@ ${header}`;
|
|
|
1591
1879
|
else if (ev.type === "del_end") delete active_del[ev.id];
|
|
1592
1880
|
else if (ev.type === "fmt_start") active_fmt[ev.id] = ev;
|
|
1593
1881
|
else if (ev.type === "fmt_end") delete active_fmt[ev.id];
|
|
1594
|
-
else if (ev.type === "
|
|
1882
|
+
else if (ev.type === "image") {
|
|
1883
|
+
const hidden = this.clean_view && Object.keys(active_del).length > 0 || this.original_view && Object.keys(active_ins).length > 0;
|
|
1884
|
+
if (!hidden) {
|
|
1885
|
+
const alt = (ev.date || "image").replace(/\]/g, ")").replace(/\n/g, " ");
|
|
1886
|
+
const txt = ``;
|
|
1887
|
+
this._add_virtual_text(txt, current, paragraph, null, true);
|
|
1888
|
+
current += txt.length;
|
|
1889
|
+
}
|
|
1890
|
+
} else if (ev.type === "footnote" || ev.type === "endnote") {
|
|
1595
1891
|
flush_pending_runs();
|
|
1596
1892
|
current_wrappers = ["", ""];
|
|
1597
1893
|
current_style = ["", ""];
|
|
@@ -1706,14 +2002,16 @@ ${header}`;
|
|
|
1706
2002
|
}
|
|
1707
2003
|
return [...change_lines, ...comment_lines].join("\n");
|
|
1708
2004
|
}
|
|
1709
|
-
_add_virtual_text(text, offset, context_paragraph, hyperlink_id = null) {
|
|
2005
|
+
_add_virtual_text(text, offset, context_paragraph, hyperlink_id = null, is_image_marker = false) {
|
|
1710
2006
|
const span = {
|
|
1711
2007
|
start: offset,
|
|
1712
2008
|
end: offset + text.length,
|
|
1713
2009
|
text,
|
|
1714
2010
|
run: null,
|
|
1715
2011
|
paragraph: context_paragraph,
|
|
1716
|
-
hyperlink_id: hyperlink_id || void 0
|
|
2012
|
+
hyperlink_id: hyperlink_id || void 0,
|
|
2013
|
+
part_index: this._current_part_index,
|
|
2014
|
+
is_image_marker: is_image_marker || void 0
|
|
1717
2015
|
};
|
|
1718
2016
|
this.spans.push(span);
|
|
1719
2017
|
this._text_chunks.push(text);
|
|
@@ -1817,10 +2115,10 @@ ${header}`;
|
|
|
1817
2115
|
find_match_index(target_text, is_regex = false) {
|
|
1818
2116
|
if (is_regex) {
|
|
1819
2117
|
try {
|
|
1820
|
-
const
|
|
1821
|
-
|
|
1822
|
-
if (match) return [match.index, match[0].length];
|
|
2118
|
+
const match = userSearch(target_text, this.full_text);
|
|
2119
|
+
if (match) return [match.start, match.end - match.start];
|
|
1823
2120
|
} catch (e) {
|
|
2121
|
+
if (e instanceof RegexTimeoutError) throw e;
|
|
1824
2122
|
}
|
|
1825
2123
|
return [-1, 0];
|
|
1826
2124
|
}
|
|
@@ -1849,11 +2147,10 @@ ${header}`;
|
|
|
1849
2147
|
if (!target_text) return [];
|
|
1850
2148
|
if (is_regex) {
|
|
1851
2149
|
try {
|
|
1852
|
-
const
|
|
1853
|
-
|
|
1854
|
-
if (matches2.length > 0)
|
|
1855
|
-
return matches2.map((m) => [m.index, m[0].length]);
|
|
2150
|
+
const matches2 = userFindAllMatches(target_text, this.full_text);
|
|
2151
|
+
if (matches2.length > 0) return matches2.map((m) => [m.start, m.end - m.start]);
|
|
1856
2152
|
} catch (e) {
|
|
2153
|
+
if (e instanceof RegexTimeoutError) throw e;
|
|
1857
2154
|
}
|
|
1858
2155
|
return [];
|
|
1859
2156
|
}
|
|
@@ -2314,6 +2611,327 @@ function generate_edits_from_text(original_text, modified_text) {
|
|
|
2314
2611
|
}
|
|
2315
2612
|
return edits;
|
|
2316
2613
|
}
|
|
2614
|
+
function _sequence_opcodes(a, b) {
|
|
2615
|
+
const n = a.length;
|
|
2616
|
+
const m = b.length;
|
|
2617
|
+
const dp = [];
|
|
2618
|
+
for (let i2 = 0; i2 <= n; i2++) dp.push(new Int32Array(m + 1));
|
|
2619
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
2620
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
2621
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
const ops = [];
|
|
2625
|
+
let i = 0;
|
|
2626
|
+
let j = 0;
|
|
2627
|
+
let pend_i1 = 0;
|
|
2628
|
+
let pend_j1 = 0;
|
|
2629
|
+
let pend_del = 0;
|
|
2630
|
+
let pend_ins = 0;
|
|
2631
|
+
const flushPending = () => {
|
|
2632
|
+
if (pend_del === 0 && pend_ins === 0) return;
|
|
2633
|
+
const tag = pend_del > 0 && pend_ins > 0 ? "replace" : pend_del > 0 ? "delete" : "insert";
|
|
2634
|
+
ops.push([tag, pend_i1, pend_i1 + pend_del, pend_j1, pend_j1 + pend_ins]);
|
|
2635
|
+
pend_del = 0;
|
|
2636
|
+
pend_ins = 0;
|
|
2637
|
+
};
|
|
2638
|
+
while (i < n || j < m) {
|
|
2639
|
+
if (i < n && j < m && a[i] === b[j]) {
|
|
2640
|
+
flushPending();
|
|
2641
|
+
const i1 = i;
|
|
2642
|
+
const j1 = j;
|
|
2643
|
+
while (i < n && j < m && a[i] === b[j]) {
|
|
2644
|
+
i++;
|
|
2645
|
+
j++;
|
|
2646
|
+
}
|
|
2647
|
+
ops.push(["equal", i1, i, j1, j]);
|
|
2648
|
+
} else {
|
|
2649
|
+
if (pend_del === 0 && pend_ins === 0) {
|
|
2650
|
+
pend_i1 = i;
|
|
2651
|
+
pend_j1 = j;
|
|
2652
|
+
}
|
|
2653
|
+
if (j < m && (i === n || dp[i][j + 1] >= dp[i + 1][j])) {
|
|
2654
|
+
pend_ins++;
|
|
2655
|
+
j++;
|
|
2656
|
+
} else {
|
|
2657
|
+
pend_del++;
|
|
2658
|
+
i++;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
flushPending();
|
|
2663
|
+
return ops;
|
|
2664
|
+
}
|
|
2665
|
+
var _IMAGE_MARKER_RE = /!\[[^\]]*\]\(docx-image:[^)]*\)/g;
|
|
2666
|
+
function _drop_image_marker_hunks(edits, warnings) {
|
|
2667
|
+
const _normalized = (s) => s.replace(_IMAGE_MARKER_RE, "").replace(/\s+/g, " ").trim();
|
|
2668
|
+
const kept = [];
|
|
2669
|
+
for (const e of edits) {
|
|
2670
|
+
const t_imgs = ((e.target_text || "").match(_IMAGE_MARKER_RE) || []).sort();
|
|
2671
|
+
const n_imgs = ((e.new_text || "").match(_IMAGE_MARKER_RE) || []).sort();
|
|
2672
|
+
if (JSON.stringify(t_imgs) === JSON.stringify(n_imgs)) {
|
|
2673
|
+
kept.push(e);
|
|
2674
|
+
continue;
|
|
2675
|
+
}
|
|
2676
|
+
if (_normalized(e.target_text || "") === _normalized(e.new_text || "")) {
|
|
2677
|
+
warnings.push(
|
|
2678
|
+
"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."
|
|
2679
|
+
);
|
|
2680
|
+
} else {
|
|
2681
|
+
warnings.push(
|
|
2682
|
+
"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."
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
return kept;
|
|
2687
|
+
}
|
|
2688
|
+
function _rows_are_plain(text, table) {
|
|
2689
|
+
for (const row of table.rows) {
|
|
2690
|
+
if (text.substring(row.start, row.end) !== row.cells.join(" | ")) {
|
|
2691
|
+
return false;
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
return true;
|
|
2695
|
+
}
|
|
2696
|
+
var _ROW_KEY_SEP = "";
|
|
2697
|
+
function _table_row_opcodes(rows_o, rows_m) {
|
|
2698
|
+
const keys_o = rows_o.map((r) => r.cells.join(_ROW_KEY_SEP));
|
|
2699
|
+
const keys_m = rows_m.map((r) => r.cells.join(_ROW_KEY_SEP));
|
|
2700
|
+
const opcodes = _sequence_opcodes(keys_o, keys_m);
|
|
2701
|
+
for (const [tag, i1, i2, j1, j2] of opcodes) {
|
|
2702
|
+
if (tag === "insert" || tag === "delete" || tag === "replace" && i2 - i1 !== j2 - j1) {
|
|
2703
|
+
return opcodes;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
return null;
|
|
2707
|
+
}
|
|
2708
|
+
function _row_ops_for_table(table_o, table_m, opcodes, warnings) {
|
|
2709
|
+
const rows_o = table_o.rows;
|
|
2710
|
+
const rows_m = table_m.rows;
|
|
2711
|
+
const keys_o = rows_o.map((r) => r.cells.join(_ROW_KEY_SEP));
|
|
2712
|
+
const row_text = (r) => r.cells.join(" | ");
|
|
2713
|
+
if (new Set(keys_o).size !== keys_o.length) {
|
|
2714
|
+
warnings.push(
|
|
2715
|
+
"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."
|
|
2716
|
+
);
|
|
2717
|
+
}
|
|
2718
|
+
const removed = /* @__PURE__ */ new Set();
|
|
2719
|
+
const replaced_new_text = {};
|
|
2720
|
+
for (const [tag, i1, i2, j1, j2] of opcodes) {
|
|
2721
|
+
if (tag === "delete") {
|
|
2722
|
+
for (let k = i1; k < i2; k++) removed.add(k);
|
|
2723
|
+
} else if (tag === "replace") {
|
|
2724
|
+
const pairs = Math.min(i2 - i1, j2 - j1);
|
|
2725
|
+
for (let k = i1 + pairs; k < i2; k++) removed.add(k);
|
|
2726
|
+
for (let k = 0; k < pairs; k++) {
|
|
2727
|
+
replaced_new_text[i1 + k] = row_text(rows_m[j1 + k]);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
const surviving = [];
|
|
2732
|
+
for (let k = 0; k < rows_o.length; k++) {
|
|
2733
|
+
if (!removed.has(k)) surviving.push(k);
|
|
2734
|
+
}
|
|
2735
|
+
const anchor_text = (orig_idx) => {
|
|
2736
|
+
return replaced_new_text[orig_idx] !== void 0 ? replaced_new_text[orig_idx] : row_text(rows_o[orig_idx]);
|
|
2737
|
+
};
|
|
2738
|
+
const insert_ops = (new_rows, at_orig_index) => {
|
|
2739
|
+
const ops2 = [];
|
|
2740
|
+
const before = surviving.filter((k) => k < at_orig_index);
|
|
2741
|
+
const after = surviving.filter((k) => k >= at_orig_index);
|
|
2742
|
+
if (before.length > 0) {
|
|
2743
|
+
const anchor_idx = before[before.length - 1];
|
|
2744
|
+
const anchor = anchor_text(anchor_idx);
|
|
2745
|
+
for (const r of [...new_rows].reverse()) {
|
|
2746
|
+
ops2.push({
|
|
2747
|
+
type: "insert_row",
|
|
2748
|
+
target_text: anchor,
|
|
2749
|
+
position: "below",
|
|
2750
|
+
cells: [...r.cells],
|
|
2751
|
+
// Pin to the anchor row's offset: text anchors alone are
|
|
2752
|
+
// ambiguous when tables share identical rows. Pins do not
|
|
2753
|
+
// survive JSON round-trips (the strict text match applies then,
|
|
2754
|
+
// failing closed with the duplicate-row warning above).
|
|
2755
|
+
_match_start_index: rows_o[anchor_idx].start
|
|
2756
|
+
});
|
|
2757
|
+
}
|
|
2758
|
+
} else if (after.length > 0) {
|
|
2759
|
+
const anchor_idx = after[0];
|
|
2760
|
+
const anchor = anchor_text(anchor_idx);
|
|
2761
|
+
for (const r of new_rows) {
|
|
2762
|
+
ops2.push({
|
|
2763
|
+
type: "insert_row",
|
|
2764
|
+
target_text: anchor,
|
|
2765
|
+
position: "above",
|
|
2766
|
+
cells: [...r.cells],
|
|
2767
|
+
_match_start_index: rows_o[anchor_idx].start
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
} else {
|
|
2771
|
+
warnings.push(
|
|
2772
|
+
"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."
|
|
2773
|
+
);
|
|
2774
|
+
}
|
|
2775
|
+
return ops2;
|
|
2776
|
+
};
|
|
2777
|
+
const ops = [];
|
|
2778
|
+
for (const [tag, i1, i2, j1, j2] of opcodes) {
|
|
2779
|
+
if (tag === "equal") continue;
|
|
2780
|
+
if (tag === "replace") {
|
|
2781
|
+
const pairs = Math.min(i2 - i1, j2 - j1);
|
|
2782
|
+
for (let k = 0; k < pairs; k++) {
|
|
2783
|
+
const o_txt = row_text(rows_o[i1 + k]);
|
|
2784
|
+
const m_txt = row_text(rows_m[j1 + k]);
|
|
2785
|
+
if (o_txt !== m_txt) {
|
|
2786
|
+
ops.push({
|
|
2787
|
+
type: "modify",
|
|
2788
|
+
target_text: o_txt,
|
|
2789
|
+
new_text: m_txt,
|
|
2790
|
+
comment: "Diff: Table row modified"
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
for (let k = i1 + pairs; k < i2; k++) {
|
|
2795
|
+
ops.push({
|
|
2796
|
+
type: "delete_row",
|
|
2797
|
+
target_text: row_text(rows_o[k]),
|
|
2798
|
+
_match_start_index: rows_o[k].start
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
const surplus_new = rows_m.slice(j1 + pairs, j2);
|
|
2802
|
+
if (surplus_new.length > 0) {
|
|
2803
|
+
ops.push(...insert_ops(surplus_new, i2));
|
|
2804
|
+
}
|
|
2805
|
+
} else if (tag === "delete") {
|
|
2806
|
+
for (let k = i1; k < i2; k++) {
|
|
2807
|
+
ops.push({
|
|
2808
|
+
type: "delete_row",
|
|
2809
|
+
target_text: row_text(rows_o[k]),
|
|
2810
|
+
_match_start_index: rows_o[k].start
|
|
2811
|
+
});
|
|
2812
|
+
}
|
|
2813
|
+
} else if (tag === "insert") {
|
|
2814
|
+
ops.push(...insert_ops(rows_m.slice(j1, j2), i1));
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
return ops;
|
|
2818
|
+
}
|
|
2819
|
+
function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod) {
|
|
2820
|
+
const warnings = [];
|
|
2821
|
+
const edits = [];
|
|
2822
|
+
const kinds_o = struct_orig.part_ranges.filter(([s, e]) => e > s).map(([, , k]) => k);
|
|
2823
|
+
const kinds_m = struct_mod.part_ranges.filter(([s, e]) => e > s).map(([, , k]) => k);
|
|
2824
|
+
if (JSON.stringify(kinds_o) !== JSON.stringify(kinds_m)) {
|
|
2825
|
+
warnings.push(
|
|
2826
|
+
`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.`
|
|
2827
|
+
);
|
|
2828
|
+
const flat = _drop_image_marker_hunks(
|
|
2829
|
+
generate_edits_from_text(text_orig, text_mod),
|
|
2830
|
+
warnings
|
|
2831
|
+
);
|
|
2832
|
+
return { edits: [...flat], warnings };
|
|
2833
|
+
}
|
|
2834
|
+
const ranges_o = struct_orig.part_ranges.filter(([s, e]) => e > s).map(([s, e]) => [s, e]);
|
|
2835
|
+
const ranges_m = struct_mod.part_ranges.filter(([s, e]) => e > s).map(([s, e]) => [s, e]);
|
|
2836
|
+
for (let p = 0; p < ranges_o.length; p++) {
|
|
2837
|
+
const [po_start, po_end] = ranges_o[p];
|
|
2838
|
+
const [pm_start, pm_end] = ranges_m[p];
|
|
2839
|
+
const tables_o = struct_orig.tables.filter(
|
|
2840
|
+
(t) => po_start <= t.start && t.end <= po_end
|
|
2841
|
+
);
|
|
2842
|
+
const tables_m = struct_mod.tables.filter(
|
|
2843
|
+
(t) => pm_start <= t.start && t.end <= pm_end
|
|
2844
|
+
);
|
|
2845
|
+
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));
|
|
2846
|
+
if (tables_o.length !== tables_m.length) {
|
|
2847
|
+
warnings.push(
|
|
2848
|
+
`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.`
|
|
2849
|
+
);
|
|
2850
|
+
}
|
|
2851
|
+
if (!tables_alignable) {
|
|
2852
|
+
const part_edits = generate_edits_from_text(
|
|
2853
|
+
text_orig.substring(po_start, po_end),
|
|
2854
|
+
text_mod.substring(pm_start, pm_end)
|
|
2855
|
+
);
|
|
2856
|
+
for (const e of part_edits) {
|
|
2857
|
+
e._match_start_index = (e._match_start_index || 0) + po_start;
|
|
2858
|
+
}
|
|
2859
|
+
edits.push(...part_edits);
|
|
2860
|
+
continue;
|
|
2861
|
+
}
|
|
2862
|
+
const boundaries_o = [
|
|
2863
|
+
[po_start, po_start],
|
|
2864
|
+
...tables_o.map((t) => [t.start, t.end]),
|
|
2865
|
+
[po_end, po_end]
|
|
2866
|
+
];
|
|
2867
|
+
const boundaries_m = [
|
|
2868
|
+
[pm_start, pm_start],
|
|
2869
|
+
...tables_m.map((t) => [t.start, t.end]),
|
|
2870
|
+
[pm_end, pm_end]
|
|
2871
|
+
];
|
|
2872
|
+
for (let seg_idx = 0; seg_idx < boundaries_o.length - 1; seg_idx++) {
|
|
2873
|
+
const seg_o_start = boundaries_o[seg_idx][1];
|
|
2874
|
+
const seg_o_end = boundaries_o[seg_idx + 1][0];
|
|
2875
|
+
const seg_m_start = boundaries_m[seg_idx][1];
|
|
2876
|
+
const seg_m_end = boundaries_m[seg_idx + 1][0];
|
|
2877
|
+
const seg_edits = generate_edits_from_text(
|
|
2878
|
+
text_orig.substring(seg_o_start, seg_o_end),
|
|
2879
|
+
text_mod.substring(seg_m_start, seg_m_end)
|
|
2880
|
+
);
|
|
2881
|
+
for (const e of seg_edits) {
|
|
2882
|
+
e._match_start_index = (e._match_start_index || 0) + seg_o_start;
|
|
2883
|
+
}
|
|
2884
|
+
edits.push(...seg_edits);
|
|
2885
|
+
if (seg_idx < tables_o.length) {
|
|
2886
|
+
const t_o = tables_o[seg_idx];
|
|
2887
|
+
const t_m = tables_m[seg_idx];
|
|
2888
|
+
const row_opcodes = _table_row_opcodes(t_o.rows, t_m.rows);
|
|
2889
|
+
if (row_opcodes !== null) {
|
|
2890
|
+
edits.push(..._row_ops_for_table(t_o, t_m, row_opcodes, warnings));
|
|
2891
|
+
} else {
|
|
2892
|
+
const tbl_edits = generate_edits_from_text(
|
|
2893
|
+
text_orig.substring(t_o.start, t_o.end),
|
|
2894
|
+
text_mod.substring(t_m.start, t_m.end)
|
|
2895
|
+
);
|
|
2896
|
+
for (const e of tbl_edits) {
|
|
2897
|
+
e._match_start_index = (e._match_start_index || 0) + t_o.start;
|
|
2898
|
+
}
|
|
2899
|
+
edits.push(...tbl_edits);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
const countOccurrences = (haystack, needle) => {
|
|
2905
|
+
let count = 0;
|
|
2906
|
+
let from = 0;
|
|
2907
|
+
while (true) {
|
|
2908
|
+
const idx = haystack.indexOf(needle, from);
|
|
2909
|
+
if (idx === -1) break;
|
|
2910
|
+
count++;
|
|
2911
|
+
from = idx + needle.length;
|
|
2912
|
+
}
|
|
2913
|
+
return count;
|
|
2914
|
+
};
|
|
2915
|
+
let ambiguous_anchor_warned = false;
|
|
2916
|
+
for (const e of edits) {
|
|
2917
|
+
if ((e.type === "insert_row" || e.type === "delete_row") && !ambiguous_anchor_warned) {
|
|
2918
|
+
if (e.target_text && countOccurrences(text_orig, e.target_text) > 1) {
|
|
2919
|
+
warnings.push(
|
|
2920
|
+
`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.`
|
|
2921
|
+
);
|
|
2922
|
+
ambiguous_anchor_warned = true;
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
const modify_edits = edits.filter(
|
|
2927
|
+
(e) => e.type === "modify"
|
|
2928
|
+
);
|
|
2929
|
+
const kept_modifies = new Set(_drop_image_marker_hunks(modify_edits, warnings));
|
|
2930
|
+
const final_edits = edits.filter(
|
|
2931
|
+
(e) => e.type !== "modify" || kept_modifies.has(e)
|
|
2932
|
+
);
|
|
2933
|
+
return { edits: final_edits, warnings };
|
|
2934
|
+
}
|
|
2317
2935
|
function create_unified_diff(original_text, modified_text, context_lines = 3) {
|
|
2318
2936
|
const dmp = new diff_match_patch.diff_match_patch();
|
|
2319
2937
|
dmp.Diff_Timeout = 2;
|
|
@@ -2621,6 +3239,30 @@ function _strip_balanced_markers(text) {
|
|
|
2621
3239
|
function _replace_smart_quotes(text) {
|
|
2622
3240
|
return text.replace(/“/g, '"').replace(/”/g, '"').replace(/‘/g, "'").replace(/’/g, "'");
|
|
2623
3241
|
}
|
|
3242
|
+
function _strip_markdown_for_matching(text) {
|
|
3243
|
+
const result = [];
|
|
3244
|
+
const position_map = [];
|
|
3245
|
+
let i = 0;
|
|
3246
|
+
while (i < text.length) {
|
|
3247
|
+
const pair = text.substring(i, i + 2);
|
|
3248
|
+
if (i < text.length - 1 && (pair === "**" || pair === "__")) {
|
|
3249
|
+
i += 2;
|
|
3250
|
+
continue;
|
|
3251
|
+
}
|
|
3252
|
+
if (text[i] === "*" || text[i] === "_") {
|
|
3253
|
+
const prev_char = i > 0 ? text[i - 1] : " ";
|
|
3254
|
+
const next_char = i < text.length - 1 ? text[i + 1] : " ";
|
|
3255
|
+
if ([" ", "\n", " "].includes(prev_char) || [" ", "\n", " "].includes(next_char)) {
|
|
3256
|
+
i += 1;
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
position_map.push(i);
|
|
3261
|
+
result.push(text[i]);
|
|
3262
|
+
i += 1;
|
|
3263
|
+
}
|
|
3264
|
+
return [result.join(""), position_map];
|
|
3265
|
+
}
|
|
2624
3266
|
function _find_safe_boundaries(text, start, end) {
|
|
2625
3267
|
let new_start = start;
|
|
2626
3268
|
let new_end = end;
|
|
@@ -2724,31 +3366,63 @@ function _make_fuzzy_regex(target_text) {
|
|
|
2724
3366
|
if (remaining) parts.push(escapeRegExp(remaining));
|
|
2725
3367
|
return parts.join("");
|
|
2726
3368
|
}
|
|
2727
|
-
function
|
|
2728
|
-
if (!target) return [
|
|
2729
|
-
|
|
2730
|
-
|
|
3369
|
+
function _find_all_matches_in_text(text, target, is_regex = false) {
|
|
3370
|
+
if (!target) return [];
|
|
3371
|
+
if (is_regex) {
|
|
3372
|
+
try {
|
|
3373
|
+
return userFindAllMatches(target, text).map((m) => [m.start, m.end]);
|
|
3374
|
+
} catch (e) {
|
|
3375
|
+
if (e instanceof RegexTimeoutError) throw e;
|
|
3376
|
+
return [];
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
const findAllLiteral = (haystack, needle) => {
|
|
3380
|
+
const out = [];
|
|
3381
|
+
let from = 0;
|
|
3382
|
+
while (true) {
|
|
3383
|
+
const idx = haystack.indexOf(needle, from);
|
|
3384
|
+
if (idx === -1) break;
|
|
3385
|
+
out.push([idx, idx + needle.length]);
|
|
3386
|
+
from = idx + needle.length;
|
|
3387
|
+
}
|
|
3388
|
+
return out;
|
|
3389
|
+
};
|
|
3390
|
+
let spans = findAllLiteral(text, target);
|
|
3391
|
+
if (spans.length > 0) {
|
|
3392
|
+
return spans.map(([s, e]) => _find_safe_boundaries(text, s, e));
|
|
3393
|
+
}
|
|
2731
3394
|
const norm_text = _replace_smart_quotes(text);
|
|
2732
3395
|
const norm_target = _replace_smart_quotes(target);
|
|
2733
|
-
|
|
2734
|
-
if (
|
|
2735
|
-
return _find_safe_boundaries(text,
|
|
3396
|
+
spans = findAllLiteral(norm_text, norm_target);
|
|
3397
|
+
if (spans.length > 0) {
|
|
3398
|
+
return spans.map(([s, e]) => _find_safe_boundaries(text, s, e));
|
|
3399
|
+
}
|
|
3400
|
+
const [stripped_text, pos_map] = _strip_markdown_for_matching(norm_text);
|
|
3401
|
+
const [stripped_target] = _strip_markdown_for_matching(norm_target);
|
|
3402
|
+
if (stripped_target && (stripped_text !== norm_text || stripped_target !== norm_target)) {
|
|
3403
|
+
const results = [];
|
|
3404
|
+
for (const [p_start, p_end] of findAllLiteral(stripped_text, stripped_target)) {
|
|
3405
|
+
const raw_start = pos_map[p_start];
|
|
3406
|
+
const raw_end = pos_map[p_end - 1] + 1;
|
|
3407
|
+
results.push(_find_safe_boundaries(text, raw_start, raw_end));
|
|
3408
|
+
}
|
|
3409
|
+
if (results.length > 0) return results;
|
|
3410
|
+
}
|
|
2736
3411
|
try {
|
|
2737
|
-
const pattern = new RegExp(_make_fuzzy_regex(target));
|
|
2738
|
-
const
|
|
2739
|
-
|
|
2740
|
-
const raw_start = match.index;
|
|
2741
|
-
const raw_end = match.index + match[0].length;
|
|
3412
|
+
const pattern = new RegExp(_make_fuzzy_regex(target), "g");
|
|
3413
|
+
const results = [];
|
|
3414
|
+
for (const match of text.matchAll(pattern)) {
|
|
2742
3415
|
const [refined_start, refined_end] = _refine_match_boundaries(
|
|
2743
3416
|
text,
|
|
2744
|
-
|
|
2745
|
-
|
|
3417
|
+
match.index,
|
|
3418
|
+
match.index + match[0].length
|
|
2746
3419
|
);
|
|
2747
|
-
|
|
3420
|
+
results.push(_find_safe_boundaries(text, refined_start, refined_end));
|
|
2748
3421
|
}
|
|
3422
|
+
if (results.length > 0) return results;
|
|
2749
3423
|
} catch (e) {
|
|
2750
3424
|
}
|
|
2751
|
-
return [
|
|
3425
|
+
return [];
|
|
2752
3426
|
}
|
|
2753
3427
|
function _build_critic_markup(target_text, new_text, comment, edit_index, include_index, highlight_only) {
|
|
2754
3428
|
const parts = [];
|
|
@@ -2780,19 +3454,55 @@ function _build_critic_markup(target_text, new_text, comment, edit_index, includ
|
|
|
2780
3454
|
}
|
|
2781
3455
|
return parts.join("");
|
|
2782
3456
|
}
|
|
2783
|
-
function apply_edits_to_markdown(markdown_text, edits, include_index = false, highlight_only = false) {
|
|
3457
|
+
function apply_edits_to_markdown(markdown_text, edits, include_index = false, highlight_only = false, edit_reports) {
|
|
2784
3458
|
if (!edits || edits.length === 0) return markdown_text;
|
|
3459
|
+
const _report = (idx, status, error = null, occurrences = 0) => {
|
|
3460
|
+
if (edit_reports) edit_reports.push({ index: idx, status, error, occurrences });
|
|
3461
|
+
};
|
|
2785
3462
|
const matched_edits = [];
|
|
2786
3463
|
for (let idx = 0; idx < edits.length; idx++) {
|
|
2787
3464
|
const edit = edits[idx];
|
|
2788
3465
|
const target = edit.target_text || "";
|
|
3466
|
+
const match_mode = edit.match_mode || "strict";
|
|
3467
|
+
const is_regex = Boolean(edit.regex);
|
|
2789
3468
|
if (!target) {
|
|
3469
|
+
_report(
|
|
3470
|
+
idx,
|
|
3471
|
+
"failed",
|
|
3472
|
+
`- 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.`
|
|
3473
|
+
);
|
|
3474
|
+
continue;
|
|
3475
|
+
}
|
|
3476
|
+
let spans;
|
|
3477
|
+
try {
|
|
3478
|
+
spans = _find_all_matches_in_text(markdown_text, target, is_regex);
|
|
3479
|
+
} catch (e) {
|
|
3480
|
+
if (!(e instanceof RegexTimeoutError)) throw e;
|
|
3481
|
+
_report(idx, "failed", `- Edit ${idx + 1} Failed: ${e.message}`);
|
|
3482
|
+
continue;
|
|
3483
|
+
}
|
|
3484
|
+
if (spans.length === 0) {
|
|
3485
|
+
_report(
|
|
3486
|
+
idx,
|
|
3487
|
+
"failed",
|
|
3488
|
+
`- Edit ${idx + 1} Failed: Target text not found in document:
|
|
3489
|
+
"${target.substring(0, 80)}"`
|
|
3490
|
+
);
|
|
2790
3491
|
continue;
|
|
2791
3492
|
}
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
3493
|
+
if (spans.length > 1 && match_mode === "strict") {
|
|
3494
|
+
_report(
|
|
3495
|
+
idx,
|
|
3496
|
+
"failed",
|
|
3497
|
+
format_ambiguity_error(idx + 1, target, markdown_text, spans)
|
|
3498
|
+
);
|
|
3499
|
+
continue;
|
|
3500
|
+
}
|
|
3501
|
+
const selected = match_mode === "strict" || match_mode === "first" ? spans.slice(0, 1) : spans;
|
|
3502
|
+
for (const [start, end] of selected) {
|
|
3503
|
+
matched_edits.push([start, end, markdown_text.substring(start, end), edit, idx]);
|
|
3504
|
+
}
|
|
3505
|
+
_report(idx, "applied", null, selected.length);
|
|
2796
3506
|
}
|
|
2797
3507
|
const matched_edits_filtered = [];
|
|
2798
3508
|
const occupied_ranges = [];
|
|
@@ -2802,6 +3512,16 @@ function apply_edits_to_markdown(markdown_text, edits, include_index = false, hi
|
|
|
2802
3512
|
for (const [occ_start, occ_end] of occupied_ranges) {
|
|
2803
3513
|
if (start < occ_end && end > occ_start) {
|
|
2804
3514
|
overlaps = true;
|
|
3515
|
+
if (edit_reports) {
|
|
3516
|
+
const msg = `- Edit ${orig_idx + 1} Failed: overlaps with a previously matched edit.`;
|
|
3517
|
+
for (const r of edit_reports) {
|
|
3518
|
+
if (r.index === orig_idx) {
|
|
3519
|
+
r.status = "failed";
|
|
3520
|
+
r.error = msg;
|
|
3521
|
+
r.occurrences = 0;
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
2805
3525
|
break;
|
|
2806
3526
|
}
|
|
2807
3527
|
}
|
|
@@ -2831,7 +3551,9 @@ function apply_edits_to_markdown(markdown_text, edits, include_index = false, hi
|
|
|
2831
3551
|
isolated_target,
|
|
2832
3552
|
isolated_new,
|
|
2833
3553
|
edit.comment,
|
|
2834
|
-
|
|
3554
|
+
// 1-based, matching apply's "Edit N" reports and batch validation
|
|
3555
|
+
// errors (QA 2026-07-17 F10; mirrors Python).
|
|
3556
|
+
orig_idx + 1,
|
|
2835
3557
|
include_index,
|
|
2836
3558
|
highlight_only
|
|
2837
3559
|
);
|
|
@@ -2976,12 +3698,36 @@ function sequential_context_hint(applied_so_far) {
|
|
|
2976
3698
|
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).`;
|
|
2977
3699
|
}
|
|
2978
3700
|
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.";
|
|
3701
|
+
var XML_ILLEGAL_CHARS_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
|
|
3702
|
+
function describe_illegal_control_chars(text) {
|
|
3703
|
+
if (!text) return null;
|
|
3704
|
+
const found = text.match(XML_ILLEGAL_CHARS_RE);
|
|
3705
|
+
if (!found) return null;
|
|
3706
|
+
const codes = Array.from(new Set(found.map((c) => `0x${c.charCodeAt(0).toString(16).padStart(2, "0")}`))).sort();
|
|
3707
|
+
return codes.join(", ");
|
|
3708
|
+
}
|
|
2979
3709
|
function validate_edit_strings(edits, index_offset = 0) {
|
|
2980
3710
|
const errors = [];
|
|
2981
3711
|
for (let i = 0; i < edits.length; i++) {
|
|
2982
3712
|
const edit = edits[i];
|
|
2983
3713
|
const t_text = edit.target_text || "";
|
|
2984
3714
|
const n_text = edit.new_text || "";
|
|
3715
|
+
const checked_fields = [
|
|
3716
|
+
["target_text", t_text],
|
|
3717
|
+
["new_text", n_text]
|
|
3718
|
+
];
|
|
3719
|
+
if (edit.comment) checked_fields.push(["comment", edit.comment]);
|
|
3720
|
+
(edit.cells || []).forEach((cell, cell_idx) => {
|
|
3721
|
+
checked_fields.push([`cells[${cell_idx}]`, cell || ""]);
|
|
3722
|
+
});
|
|
3723
|
+
for (const [field_name, field_value] of checked_fields) {
|
|
3724
|
+
const described = describe_illegal_control_chars(field_value);
|
|
3725
|
+
if (described) {
|
|
3726
|
+
errors.push(
|
|
3727
|
+
`- 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.`
|
|
3728
|
+
);
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
2985
3731
|
if (n_text.includes("{++") || n_text.includes("{--") || n_text.includes("{>>") || n_text.includes("{==")) {
|
|
2986
3732
|
errors.push(
|
|
2987
3733
|
`- 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.`
|
|
@@ -3044,6 +3790,15 @@ function validate_edit_strings(edits, index_offset = 0) {
|
|
|
3044
3790
|
}
|
|
3045
3791
|
}
|
|
3046
3792
|
}
|
|
3793
|
+
if (t_text.includes("docx-image:") || n_text.includes("docx-image:")) {
|
|
3794
|
+
const t_imgs = (t_text.match(/!\[[^\]]*\]\(docx-image:[^)]*\)/g) || []).sort();
|
|
3795
|
+
const n_imgs = (n_text.match(/!\[[^\]]*\]\(docx-image:[^)]*\)/g) || []).sort();
|
|
3796
|
+
if (JSON.stringify(t_imgs) !== JSON.stringify(n_imgs)) {
|
|
3797
|
+
errors.push(
|
|
3798
|
+
`- 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.`
|
|
3799
|
+
);
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3047
3802
|
if (t_text.includes("{#") || n_text.includes("{#")) {
|
|
3048
3803
|
const t_anchors = t_text.match(/\{#[^\}]+\}/g) || [];
|
|
3049
3804
|
const n_anchors = n_text.match(/\{#[^\}]+\}/g) || [];
|
|
@@ -3288,8 +4043,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
3288
4043
|
* Snapshots the document text around a resolved edit BEFORE anything is
|
|
3289
4044
|
* applied. Previews rendered after the batch mutates the DOM cannot slice
|
|
3290
4045
|
* full_text at the stored offsets: applied edits shift offsets and inject
|
|
3291
|
-
* tracked-change markup,
|
|
3292
|
-
*
|
|
4046
|
+
* tracked-change markup, garbling previews with unrelated edits and
|
|
4047
|
+
* internal scaffolding (QA H1).
|
|
3293
4048
|
*/
|
|
3294
4049
|
_capture_preview_context(edit) {
|
|
3295
4050
|
if (edit.type !== "modify") return;
|
|
@@ -3390,8 +4145,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
3390
4145
|
}
|
|
3391
4146
|
/**
|
|
3392
4147
|
* The "new text" a batch report should show for an edit. InsertTableRow has
|
|
3393
|
-
* no new_text field — surface its cell contents
|
|
3394
|
-
* empty string
|
|
4148
|
+
* no new_text field — surface its cell contents rather than a misleading
|
|
4149
|
+
* empty string (QA M4).
|
|
3395
4150
|
*/
|
|
3396
4151
|
static _report_new_text(edit) {
|
|
3397
4152
|
if (edit && edit.type === "insert_row" && Array.isArray(edit.cells)) {
|
|
@@ -3767,6 +4522,43 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
3767
4522
|
element.setAttribute("xml:space", "preserve");
|
|
3768
4523
|
}
|
|
3769
4524
|
}
|
|
4525
|
+
/**
|
|
4526
|
+
* Walks `element` to its XML root element. Word (and LibreOffice, which
|
|
4527
|
+
* refuses to LOAD such files) only supports comment ranges in the main
|
|
4528
|
+
* document story ("w:document") — never in headers, footers, footnotes or
|
|
4529
|
+
* endnotes (QA 2026-07-18 H4/C1).
|
|
4530
|
+
*/
|
|
4531
|
+
_comment_anchor_in_main_story(element) {
|
|
4532
|
+
let root = element;
|
|
4533
|
+
while (root.parentNode && root.parentNode.nodeType === 1) {
|
|
4534
|
+
root = root.parentNode;
|
|
4535
|
+
}
|
|
4536
|
+
return root.tagName === "w:document";
|
|
4537
|
+
}
|
|
4538
|
+
/**
|
|
4539
|
+
* When the anchor lives outside the main document story, records a
|
|
4540
|
+
* user-visible warning and returns true (caller must skip the comment).
|
|
4541
|
+
* The tracked change itself still applies — only the bubble is dropped.
|
|
4542
|
+
*/
|
|
4543
|
+
_skip_comment_outside_main_story(element, text) {
|
|
4544
|
+
if (this._comment_anchor_in_main_story(element)) return false;
|
|
4545
|
+
let root = element;
|
|
4546
|
+
while (root.parentNode && root.parentNode.nodeType === 1) {
|
|
4547
|
+
root = root.parentNode;
|
|
4548
|
+
}
|
|
4549
|
+
const story = {
|
|
4550
|
+
"w:ftr": "footer",
|
|
4551
|
+
"w:hdr": "header",
|
|
4552
|
+
"w:footnotes": "footnote",
|
|
4553
|
+
"w:endnotes": "endnote"
|
|
4554
|
+
}[root.tagName] || "non-body";
|
|
4555
|
+
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.`;
|
|
4556
|
+
this.skipped_details.push(msg);
|
|
4557
|
+
console.error(
|
|
4558
|
+
`Comment anchor outside main story; comment dropped (story=${story})`
|
|
4559
|
+
);
|
|
4560
|
+
return true;
|
|
4561
|
+
}
|
|
3770
4562
|
/**
|
|
3771
4563
|
* Attaches a comment that wraps a contiguous range within a single paragraph.
|
|
3772
4564
|
* start_element and end_element must both be direct children of parent_element
|
|
@@ -3775,6 +4567,8 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
3775
4567
|
*/
|
|
3776
4568
|
_attach_comment(parent_element, start_element, end_element, text) {
|
|
3777
4569
|
if (!text) return;
|
|
4570
|
+
if (!parent_element || !start_element || !end_element) return;
|
|
4571
|
+
if (this._skip_comment_outside_main_story(parent_element, text)) return;
|
|
3778
4572
|
const comment_id = this.comments_manager.addComment(this.author, text);
|
|
3779
4573
|
const xmlDoc = parent_element.ownerDocument;
|
|
3780
4574
|
const range_start = xmlDoc.createElement("w:commentRangeStart");
|
|
@@ -3808,6 +4602,9 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
3808
4602
|
*/
|
|
3809
4603
|
_attach_comment_spanning(start_p, start_el, end_p, end_el, text) {
|
|
3810
4604
|
if (!text) return;
|
|
4605
|
+
if (!start_p || !end_p) return;
|
|
4606
|
+
if (this._skip_comment_outside_main_story(start_p, text) || this._skip_comment_outside_main_story(end_p, text))
|
|
4607
|
+
return;
|
|
3811
4608
|
const comment_id = this.comments_manager.addComment(this.author, text);
|
|
3812
4609
|
const xmlDocStart = start_p.ownerDocument;
|
|
3813
4610
|
const xmlDocEnd = end_p.ownerDocument;
|
|
@@ -4419,6 +5216,51 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4419
5216
|
pfx,
|
|
4420
5217
|
(edit.new_text || "").length - sfx
|
|
4421
5218
|
);
|
|
5219
|
+
const multi_part_doc = target_mapper.part_ranges.filter((r) => r[1] > r[0]).length > 1;
|
|
5220
|
+
const raw_span_parts = multi_part_doc ? Array.from(
|
|
5221
|
+
new Set(
|
|
5222
|
+
target_mapper.spans.filter(
|
|
5223
|
+
(s) => s.run !== null && s.end > m_start && s.start < m_start + m_len
|
|
5224
|
+
).map((s) => s.part_index)
|
|
5225
|
+
)
|
|
5226
|
+
).sort((a, b) => a - b) : [];
|
|
5227
|
+
if (raw_span_parts.length > 1) {
|
|
5228
|
+
const kinds = raw_span_parts.map((pi) => target_mapper.part_kind_of(pi) || "?").join(" \u2192 ");
|
|
5229
|
+
errors.push(
|
|
5230
|
+
`- 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).`
|
|
5231
|
+
);
|
|
5232
|
+
}
|
|
5233
|
+
const eff_start = m_start + pfx;
|
|
5234
|
+
const eff_end = m_start + m_len - sfx;
|
|
5235
|
+
if (eff_end > eff_start) {
|
|
5236
|
+
const overlapping = target_mapper.spans.filter(
|
|
5237
|
+
(s) => s.end > eff_start && s.start < eff_end && (s.run !== null || s.text.trim() !== "")
|
|
5238
|
+
);
|
|
5239
|
+
if (overlapping.some((s) => s.is_image_marker)) {
|
|
5240
|
+
errors.push(
|
|
5241
|
+
`- 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.`
|
|
5242
|
+
);
|
|
5243
|
+
}
|
|
5244
|
+
}
|
|
5245
|
+
if (edit.comment && (edit.new_text || "") === (edit.target_text || "")) {
|
|
5246
|
+
const kind_here = target_mapper.part_kind_at(m_start);
|
|
5247
|
+
if (kind_here !== null && kind_here !== "body") {
|
|
5248
|
+
errors.push(
|
|
5249
|
+
`- 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.`
|
|
5250
|
+
);
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
if (_RedlineEngine._introduces_table_row_text(
|
|
5254
|
+
target_mapper,
|
|
5255
|
+
m_start,
|
|
5256
|
+
m_len,
|
|
5257
|
+
final_target,
|
|
5258
|
+
final_new
|
|
5259
|
+
)) {
|
|
5260
|
+
errors.push(
|
|
5261
|
+
`- 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": ["...", "..."]}).`
|
|
5262
|
+
);
|
|
5263
|
+
}
|
|
4422
5264
|
if (final_target.includes("\n\n")) {
|
|
4423
5265
|
const balanced = matched.split("\n\n").length === (edit.new_text || "").split("\n\n").length;
|
|
4424
5266
|
if (!balanced) {
|
|
@@ -4515,6 +5357,19 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4515
5357
|
* [start, start+length) in `mapper`, or null if that text is not inside a
|
|
4516
5358
|
* table row.
|
|
4517
5359
|
*/
|
|
5360
|
+
/**
|
|
5361
|
+
* True when a replacement anchored in a table would ADD line-separated
|
|
5362
|
+
* pipe-delimited content — the text shape of a table row. Writing that
|
|
5363
|
+
* into a cell renders a fake row inside one cell while the real grid
|
|
5364
|
+
* stays unchanged (QA 2026-07-18 C2); such edits must use insert_row.
|
|
5365
|
+
*/
|
|
5366
|
+
static _introduces_table_row_text(mapper, start, length, final_target, final_new) {
|
|
5367
|
+
if (!final_new.includes("\n") || !final_new.includes(" | ")) return false;
|
|
5368
|
+
const new_pipe_lines = final_new.split("\n").filter((line) => line.includes(" | ")).length;
|
|
5369
|
+
const old_pipe_lines = final_target.split("\n").filter((line) => line.includes(" | ")).length;
|
|
5370
|
+
if (new_pipe_lines <= old_pipe_lines) return false;
|
|
5371
|
+
return _RedlineEngine._column_count_at(mapper, start, Math.max(length, 1)) !== null;
|
|
5372
|
+
}
|
|
4518
5373
|
static _column_count_at(mapper, start, length) {
|
|
4519
5374
|
for (const s of mapper.spans) {
|
|
4520
5375
|
if (s.run === null || s.end <= start || s.start >= start + length) {
|
|
@@ -4676,7 +5531,13 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4676
5531
|
const reports_by_input = new Array(edits.length);
|
|
4677
5532
|
const validation_failed_idx = /* @__PURE__ */ new Set();
|
|
4678
5533
|
for (const { edit, i } of ordered_edits) {
|
|
4679
|
-
let single_errors
|
|
5534
|
+
let single_errors;
|
|
5535
|
+
try {
|
|
5536
|
+
single_errors = this.validate_edits([edit], i);
|
|
5537
|
+
} catch (e) {
|
|
5538
|
+
if (!(e instanceof RegexTimeoutError)) throw e;
|
|
5539
|
+
single_errors = [`- Edit ${i + 1} Failed: ${e.message}`];
|
|
5540
|
+
}
|
|
4680
5541
|
if (single_errors.length > 0) {
|
|
4681
5542
|
if (applied_edits > 0) {
|
|
4682
5543
|
const hint = sequential_context_hint(applied_edits);
|
|
@@ -4761,7 +5622,13 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4761
5622
|
const sequential_errors = [];
|
|
4762
5623
|
let applied_so_far = 0;
|
|
4763
5624
|
for (const { edit, i } of ordered_edits) {
|
|
4764
|
-
let single_errors
|
|
5625
|
+
let single_errors;
|
|
5626
|
+
try {
|
|
5627
|
+
single_errors = this.validate_edits([edit], i);
|
|
5628
|
+
} catch (e) {
|
|
5629
|
+
if (!(e instanceof RegexTimeoutError)) throw e;
|
|
5630
|
+
single_errors = [`- Edit ${i + 1} Failed: ${e.message}`];
|
|
5631
|
+
}
|
|
4765
5632
|
if (single_errors.length > 0) {
|
|
4766
5633
|
if (applied_so_far > 0) {
|
|
4767
5634
|
const hint = sequential_context_hint(applied_so_far);
|
|
@@ -4875,7 +5742,18 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4875
5742
|
edit._error_msg = msg;
|
|
4876
5743
|
}
|
|
4877
5744
|
} else {
|
|
4878
|
-
|
|
5745
|
+
let resolved;
|
|
5746
|
+
try {
|
|
5747
|
+
resolved = this._pre_resolve_heuristic_edit(edit);
|
|
5748
|
+
} catch (e) {
|
|
5749
|
+
if (!(e instanceof RegexTimeoutError)) throw e;
|
|
5750
|
+
skipped++;
|
|
5751
|
+
edit._applied_status = false;
|
|
5752
|
+
const msg = `- Failed to apply edit targeting: '${(edit.target_text || "").substring(0, 40)}...' (${e.message})`;
|
|
5753
|
+
this.skipped_details.push(msg);
|
|
5754
|
+
edit._error_msg = msg;
|
|
5755
|
+
continue;
|
|
5756
|
+
}
|
|
4879
5757
|
if (resolved) {
|
|
4880
5758
|
if (Array.isArray(resolved)) {
|
|
4881
5759
|
for (const r of resolved) {
|
|
@@ -4920,7 +5798,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
4920
5798
|
const counted_split_groups = /* @__PURE__ */ new Set();
|
|
4921
5799
|
for (const [edit, orig_new] of resolved_edits) {
|
|
4922
5800
|
const start = edit._resolved_start_idx || 0;
|
|
4923
|
-
const end = start + (edit.target_text ? edit.target_text.length : 0);
|
|
5801
|
+
const end = edit.type === "insert_row" ? start : start + (edit.target_text ? edit.target_text.length : 0);
|
|
4924
5802
|
const overlaps = occupied_ranges.some(
|
|
4925
5803
|
([occ_start, occ_end]) => start < occ_end && end > occ_start
|
|
4926
5804
|
);
|
|
@@ -5034,6 +5912,16 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5034
5912
|
);
|
|
5035
5913
|
continue;
|
|
5036
5914
|
}
|
|
5915
|
+
const dup_authors = Array.from(
|
|
5916
|
+
new Set(all_nodes.map((n) => n.getAttribute("w:author") || "Unknown"))
|
|
5917
|
+
).sort();
|
|
5918
|
+
if (dup_authors.length > 1) {
|
|
5919
|
+
skipped++;
|
|
5920
|
+
this.skipped_details.push(
|
|
5921
|
+
`- 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.`
|
|
5922
|
+
);
|
|
5923
|
+
continue;
|
|
5924
|
+
}
|
|
5037
5925
|
for (const node of all_nodes) {
|
|
5038
5926
|
const is_ins = node.tagName === "w:ins";
|
|
5039
5927
|
const parent_tag = node.parentNode ? node.parentNode.tagName : "";
|
|
@@ -5600,13 +6488,49 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5600
6488
|
return true;
|
|
5601
6489
|
}
|
|
5602
6490
|
if (op === "INSERTION") {
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
);
|
|
6491
|
+
let final_new_text = edit.new_text || "";
|
|
6492
|
+
let boundary_anchor = null;
|
|
6493
|
+
const boundary = typeof active_mapper.part_boundary_at === "function" ? active_mapper.part_boundary_at(start_idx) : null;
|
|
6494
|
+
const is_machine_pure_insertion = !edit.target_text && (edit._parent_edit_ref === void 0 || edit._parent_edit_ref === null);
|
|
6495
|
+
if (boundary !== null && is_machine_pure_insertion) {
|
|
6496
|
+
const [prev_i, next_i] = boundary;
|
|
6497
|
+
const prev_kind = active_mapper.part_kind_of(prev_i);
|
|
6498
|
+
const next_kind = active_mapper.part_kind_of(next_i);
|
|
6499
|
+
if (prev_kind === "body" && next_kind !== "body") {
|
|
6500
|
+
const real_before = active_mapper.spans.filter(
|
|
6501
|
+
(s) => s.run !== null && s.part_index === prev_i
|
|
6502
|
+
);
|
|
6503
|
+
if (real_before.length > 0) {
|
|
6504
|
+
boundary_anchor = real_before[real_before.length - 1];
|
|
6505
|
+
}
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
let anchor_run;
|
|
6509
|
+
let anchor_para;
|
|
6510
|
+
if (boundary_anchor !== null) {
|
|
6511
|
+
anchor_run = boundary_anchor.run;
|
|
6512
|
+
anchor_para = boundary_anchor.paragraph;
|
|
6513
|
+
if (!final_new_text.startsWith("\n")) {
|
|
6514
|
+
final_new_text = "\n\n" + final_new_text;
|
|
6515
|
+
}
|
|
6516
|
+
} else {
|
|
6517
|
+
[anchor_run, anchor_para] = active_mapper.get_insertion_anchor(
|
|
6518
|
+
start_idx,
|
|
6519
|
+
rebuild_map
|
|
6520
|
+
);
|
|
6521
|
+
}
|
|
5607
6522
|
if (!anchor_run && !anchor_para) return false;
|
|
5608
|
-
|
|
5609
|
-
|
|
6523
|
+
if (_RedlineEngine._introduces_table_row_text(
|
|
6524
|
+
active_mapper,
|
|
6525
|
+
start_idx,
|
|
6526
|
+
1,
|
|
6527
|
+
"",
|
|
6528
|
+
final_new_text
|
|
6529
|
+
)) {
|
|
6530
|
+
return false;
|
|
6531
|
+
}
|
|
6532
|
+
const _bug233_new = final_new_text;
|
|
6533
|
+
const _bug233_trailing_break = boundary_anchor === null && /\n\s*$/.test(_bug233_new);
|
|
5610
6534
|
let _bug233_target_para = null;
|
|
5611
6535
|
{
|
|
5612
6536
|
const startingSpans = active_mapper.spans.filter(
|
|
@@ -5676,7 +6600,7 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5676
6600
|
}
|
|
5677
6601
|
}
|
|
5678
6602
|
const result = this._track_insert_multiline(
|
|
5679
|
-
|
|
6603
|
+
final_new_text,
|
|
5680
6604
|
anchor_run,
|
|
5681
6605
|
anchor_para,
|
|
5682
6606
|
ins_id
|
|
@@ -5765,6 +6689,20 @@ var RedlineEngine = class _RedlineEngine {
|
|
|
5765
6689
|
}
|
|
5766
6690
|
return true;
|
|
5767
6691
|
}
|
|
6692
|
+
if ((op === "DELETION" || op === "MODIFICATION") && length && active_mapper.part_ranges.filter((r) => r[1] > r[0]).length > 1) {
|
|
6693
|
+
const crossed_parts = /* @__PURE__ */ new Set();
|
|
6694
|
+
for (const s of active_mapper.spans) {
|
|
6695
|
+
if (s.run !== null && s.end > start_idx && s.start < start_idx + length) {
|
|
6696
|
+
crossed_parts.add(s.part_index);
|
|
6697
|
+
}
|
|
6698
|
+
}
|
|
6699
|
+
if (crossed_parts.size > 1) {
|
|
6700
|
+
console.error(
|
|
6701
|
+
`Refusing edit that spans OPC part boundary (start=${start_idx}, parts=${Array.from(crossed_parts).sort().join(",")})`
|
|
6702
|
+
);
|
|
6703
|
+
return false;
|
|
6704
|
+
}
|
|
6705
|
+
}
|
|
5768
6706
|
const target_runs = active_mapper.find_target_runs_by_index(
|
|
5769
6707
|
start_idx,
|
|
5770
6708
|
length,
|
|
@@ -6349,6 +7287,27 @@ function scrub_doc_properties(doc) {
|
|
|
6349
7287
|
c.textContent = "";
|
|
6350
7288
|
}
|
|
6351
7289
|
});
|
|
7290
|
+
const titles = findDescendantsByLocalName(corePart._element, "title");
|
|
7291
|
+
titles.forEach((c) => {
|
|
7292
|
+
if (c.textContent) {
|
|
7293
|
+
lines.push(`Title kept (review manually): "${c.textContent}"`);
|
|
7294
|
+
}
|
|
7295
|
+
});
|
|
7296
|
+
const leakFields = [
|
|
7297
|
+
["category", "Category"],
|
|
7298
|
+
["keywords", "Keywords"],
|
|
7299
|
+
["subject", "Subject"],
|
|
7300
|
+
["contentStatus", "Content status"],
|
|
7301
|
+
["description", "Description/comments"]
|
|
7302
|
+
];
|
|
7303
|
+
for (const [local, label] of leakFields) {
|
|
7304
|
+
findDescendantsByLocalName(corePart._element, local).forEach((c) => {
|
|
7305
|
+
if (c.textContent) {
|
|
7306
|
+
lines.push(`${label}: ${c.textContent}`);
|
|
7307
|
+
c.textContent = "";
|
|
7308
|
+
}
|
|
7309
|
+
});
|
|
7310
|
+
}
|
|
6352
7311
|
const revisions = findDescendantsByLocalName(corePart._element, "revision");
|
|
6353
7312
|
revisions.forEach((c) => {
|
|
6354
7313
|
if (c.textContent && parseInt(c.textContent) > 1) {
|
|
@@ -6434,6 +7393,35 @@ function strip_image_alt_text(doc) {
|
|
|
6434
7393
|
}
|
|
6435
7394
|
return count ? [`Image alt text: ${count} auto-generated descriptions removed`] : [];
|
|
6436
7395
|
}
|
|
7396
|
+
function detect_watermarks(doc) {
|
|
7397
|
+
const warnings = [];
|
|
7398
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7399
|
+
for (const part of doc.pkg.parts) {
|
|
7400
|
+
const name = String(part.partname);
|
|
7401
|
+
let location;
|
|
7402
|
+
if (name.startsWith("/word/header")) location = "header";
|
|
7403
|
+
else if (name.startsWith("/word/footer")) location = "footer";
|
|
7404
|
+
else if (name === "/word/document.xml") location = "body";
|
|
7405
|
+
else continue;
|
|
7406
|
+
let textpaths;
|
|
7407
|
+
try {
|
|
7408
|
+
textpaths = findDescendantsByLocalName(part._element, "textpath");
|
|
7409
|
+
} catch {
|
|
7410
|
+
continue;
|
|
7411
|
+
}
|
|
7412
|
+
for (const tp of textpaths) {
|
|
7413
|
+
const text = (tp.getAttribute("string") || "").trim();
|
|
7414
|
+
const key = `${location}${text}`;
|
|
7415
|
+
if (seen.has(key)) continue;
|
|
7416
|
+
seen.add(key);
|
|
7417
|
+
const label = text ? `"${_truncate(text, 60)}"` : "(no text)";
|
|
7418
|
+
warnings.push(
|
|
7419
|
+
`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.`
|
|
7420
|
+
);
|
|
7421
|
+
}
|
|
7422
|
+
}
|
|
7423
|
+
return warnings;
|
|
7424
|
+
}
|
|
6437
7425
|
function audit_hyperlinks(doc) {
|
|
6438
7426
|
const internal = ["sharepoint.com", "onedrive.com", ".internal", "intranet", "localhost", "10.", "192.168.", "172.16."];
|
|
6439
7427
|
const warnings = [];
|
|
@@ -6486,25 +7474,53 @@ function _get_paragraph_text(p) {
|
|
|
6486
7474
|
}
|
|
6487
7475
|
return text;
|
|
6488
7476
|
}
|
|
7477
|
+
var _TERM_BODY = "[A-Z][A-Za-z0-9\\s\\-&'\u2019]{1,60}";
|
|
7478
|
+
var _LEADING_TERM_RE = new RegExp(
|
|
7479
|
+
`^(?:[\\d.\\-()a-zA-Z]+\\s*)?["\u201C](${_TERM_BODY})["\u201D]`,
|
|
7480
|
+
"d"
|
|
7481
|
+
);
|
|
7482
|
+
var _SENTENCE_TERM_RE = new RegExp(
|
|
7483
|
+
`(?<=[.;:!?])\\s+(?:[\\d.\\-()a-zA-Z]+\\s+)?["\u201C](${_TERM_BODY})["\u201D]`,
|
|
7484
|
+
"dg"
|
|
7485
|
+
);
|
|
7486
|
+
var _INLINE_TERM_RE = new RegExp(
|
|
7487
|
+
`\\([^)]*?["\u201C](${_TERM_BODY})["\u201D][^)]*?\\)`,
|
|
7488
|
+
"dg"
|
|
7489
|
+
);
|
|
7490
|
+
function extract_terms_from_paragraph(text) {
|
|
7491
|
+
const found = [];
|
|
7492
|
+
const leading = _LEADING_TERM_RE.exec(text);
|
|
7493
|
+
if (leading && leading.indices && leading.indices[1]) {
|
|
7494
|
+
found.push([leading.indices[1][0], leading[1].trim()]);
|
|
7495
|
+
}
|
|
7496
|
+
for (const m of text.matchAll(_SENTENCE_TERM_RE)) {
|
|
7497
|
+
const indices = m.indices;
|
|
7498
|
+
if (indices && indices[1]) found.push([indices[1][0], m[1].trim()]);
|
|
7499
|
+
}
|
|
7500
|
+
for (const m of text.matchAll(_INLINE_TERM_RE)) {
|
|
7501
|
+
const indices = m.indices;
|
|
7502
|
+
if (indices && indices[1]) found.push([indices[1][0], m[1].trim()]);
|
|
7503
|
+
}
|
|
7504
|
+
const terms = [];
|
|
7505
|
+
const seen_positions = /* @__PURE__ */ new Set();
|
|
7506
|
+
found.sort((a, b) => a[0] - b[0]);
|
|
7507
|
+
for (const [pos, term] of found) {
|
|
7508
|
+
if (seen_positions.has(pos)) continue;
|
|
7509
|
+
seen_positions.add(pos);
|
|
7510
|
+
terms.push(term);
|
|
7511
|
+
}
|
|
7512
|
+
return terms;
|
|
7513
|
+
}
|
|
6489
7514
|
function extract_all_domain_metadata(doc, base_text) {
|
|
6490
7515
|
const definitions = {};
|
|
6491
7516
|
const duplicates = /* @__PURE__ */ new Set();
|
|
6492
7517
|
const raw_anchors = {};
|
|
6493
7518
|
const raw_references = [];
|
|
6494
|
-
const leading_re = /^(?:[\d.\-()a-zA-Z]+\s*)?["“]([A-Z][A-Za-z0-9\s\-&'’]{1,60})["”]/;
|
|
6495
|
-
const inline_re = /\([^)]*?["“]([A-Z][A-Za-z0-9\s\-&'’]{1,60})["”][^)]*?\)/g;
|
|
6496
7519
|
for (const item of iter_block_items(doc)) {
|
|
6497
7520
|
if (!(item instanceof Paragraph)) continue;
|
|
6498
7521
|
const text = _get_paragraph_text(item).trim();
|
|
6499
7522
|
if (!text) continue;
|
|
6500
|
-
const
|
|
6501
|
-
const leading_match = text.match(leading_re);
|
|
6502
|
-
if (leading_match) extracted_terms.push(leading_match[1].trim());
|
|
6503
|
-
const inline_matches = text.matchAll(inline_re);
|
|
6504
|
-
for (const m of inline_matches) {
|
|
6505
|
-
extracted_terms.push(m[1].trim());
|
|
6506
|
-
}
|
|
6507
|
-
for (const term of extracted_terms) {
|
|
7523
|
+
for (const term of extract_terms_from_paragraph(text)) {
|
|
6508
7524
|
if (definitions[term]) duplicates.add(term);
|
|
6509
7525
|
else definitions[term] = { count: 0 };
|
|
6510
7526
|
}
|
|
@@ -6554,7 +7570,11 @@ function extract_all_domain_metadata(doc, base_text) {
|
|
|
6554
7570
|
for (const term of def_keys) {
|
|
6555
7571
|
if (definitions[term].count === 0) {
|
|
6556
7572
|
delete definitions[term];
|
|
6557
|
-
duplicates.
|
|
7573
|
+
if (!duplicates.has(term)) {
|
|
7574
|
+
diagnostics.push(
|
|
7575
|
+
`[Warning] Unused Definition: '${term}' is defined but never used.`
|
|
7576
|
+
);
|
|
7577
|
+
}
|
|
6558
7578
|
}
|
|
6559
7579
|
}
|
|
6560
7580
|
}
|
|
@@ -6728,27 +7748,32 @@ function build_structural_appendix(doc, base_text) {
|
|
|
6728
7748
|
}
|
|
6729
7749
|
|
|
6730
7750
|
// src/ingest.ts
|
|
6731
|
-
async function extractTextFromBuffer(buffer, cleanView = false) {
|
|
7751
|
+
async function extractTextFromBuffer(buffer, cleanView = false, includeAppendix = true) {
|
|
6732
7752
|
const doc = await DocumentObject.load(buffer);
|
|
6733
|
-
return _extractTextFromDoc(doc, cleanView);
|
|
7753
|
+
return _extractTextFromDoc(doc, cleanView, includeAppendix);
|
|
6734
7754
|
}
|
|
6735
|
-
function _extractTextFromDoc(doc, cleanView = false, includeAppendix = true, return_paragraph_offsets = false) {
|
|
7755
|
+
function _extractTextFromDoc(doc, cleanView = false, includeAppendix = true, return_paragraph_offsets = false, return_structure = false) {
|
|
6736
7756
|
const comments_map = extract_comments_data(doc.pkg);
|
|
6737
7757
|
const full_text = [];
|
|
6738
7758
|
const paragraph_offsets = /* @__PURE__ */ new Map();
|
|
7759
|
+
const structure = return_structure ? { part_ranges: [], tables: [] } : null;
|
|
6739
7760
|
let cursor = 0;
|
|
6740
|
-
for (const part of
|
|
7761
|
+
for (const [part, part_kind] of iter_document_parts_with_kind(doc)) {
|
|
6741
7762
|
const part_cursor = full_text.length > 0 ? cursor + 2 : cursor;
|
|
6742
7763
|
const part_text = _extract_blocks(
|
|
6743
7764
|
part,
|
|
6744
7765
|
comments_map,
|
|
6745
7766
|
cleanView,
|
|
6746
7767
|
part_cursor,
|
|
6747
|
-
return_paragraph_offsets ? paragraph_offsets : void 0
|
|
7768
|
+
return_paragraph_offsets ? paragraph_offsets : void 0,
|
|
7769
|
+
structure ? structure.tables : void 0
|
|
6748
7770
|
);
|
|
6749
7771
|
if (part_text) {
|
|
6750
7772
|
if (full_text.length > 0) cursor += 2;
|
|
6751
7773
|
full_text.push(part_text);
|
|
7774
|
+
if (structure) {
|
|
7775
|
+
structure.part_ranges.push([cursor, cursor + part_text.length, part_kind]);
|
|
7776
|
+
}
|
|
6752
7777
|
cursor += part_text.length;
|
|
6753
7778
|
}
|
|
6754
7779
|
}
|
|
@@ -6760,9 +7785,12 @@ function _extractTextFromDoc(doc, cleanView = false, includeAppendix = true, ret
|
|
|
6760
7785
|
if (return_paragraph_offsets) {
|
|
6761
7786
|
return { text: base_text, paragraph_offsets };
|
|
6762
7787
|
}
|
|
7788
|
+
if (structure) {
|
|
7789
|
+
return { text: base_text, structure };
|
|
7790
|
+
}
|
|
6763
7791
|
return base_text;
|
|
6764
7792
|
}
|
|
6765
|
-
function _extract_blocks(container, comments_map, cleanView, cursor, paragraph_offsets) {
|
|
7793
|
+
function _extract_blocks(container, comments_map, cleanView, cursor, paragraph_offsets, table_acc) {
|
|
6766
7794
|
const part = container.part || container;
|
|
6767
7795
|
const [style_cache, default_pstyle] = _get_style_cache(part);
|
|
6768
7796
|
const blocks = [];
|
|
@@ -6816,17 +7844,23 @@ ${header}`;
|
|
|
6816
7844
|
is_first_para = false;
|
|
6817
7845
|
is_first_block = false;
|
|
6818
7846
|
} else if (item instanceof Table) {
|
|
7847
|
+
const geometry = table_acc ? { start: block_start, end: block_start, rows: [] } : null;
|
|
6819
7848
|
const table_text = extract_table(
|
|
6820
7849
|
item,
|
|
6821
7850
|
comments_map,
|
|
6822
7851
|
cleanView,
|
|
6823
7852
|
block_start,
|
|
6824
|
-
paragraph_offsets
|
|
7853
|
+
paragraph_offsets,
|
|
7854
|
+
geometry
|
|
6825
7855
|
);
|
|
6826
7856
|
if (table_text) {
|
|
6827
7857
|
blocks.push(table_text);
|
|
6828
7858
|
local_cursor = block_start + table_text.length;
|
|
6829
7859
|
is_first_block = false;
|
|
7860
|
+
if (geometry && table_acc) {
|
|
7861
|
+
geometry.end = block_start + table_text.length;
|
|
7862
|
+
table_acc.push(geometry);
|
|
7863
|
+
}
|
|
6830
7864
|
} else if (!is_first_block) {
|
|
6831
7865
|
local_cursor -= 2;
|
|
6832
7866
|
}
|
|
@@ -6835,7 +7869,7 @@ ${header}`;
|
|
|
6835
7869
|
}
|
|
6836
7870
|
return blocks.join("\n\n");
|
|
6837
7871
|
}
|
|
6838
|
-
function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets) {
|
|
7872
|
+
function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets, geometry) {
|
|
6839
7873
|
const rows_text = [];
|
|
6840
7874
|
let rows_processed = 0;
|
|
6841
7875
|
let local_cursor = cursor;
|
|
@@ -6881,6 +7915,13 @@ function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets
|
|
|
6881
7915
|
}
|
|
6882
7916
|
rows_text.push(row_str);
|
|
6883
7917
|
local_cursor = row_start + row_str.length;
|
|
7918
|
+
if (geometry) {
|
|
7919
|
+
geometry.rows.push({
|
|
7920
|
+
start: row_start,
|
|
7921
|
+
end: local_cursor,
|
|
7922
|
+
cells: [...cell_texts]
|
|
7923
|
+
});
|
|
7924
|
+
}
|
|
6884
7925
|
rows_processed++;
|
|
6885
7926
|
}
|
|
6886
7927
|
return rows_text.join("\n");
|
|
@@ -7032,7 +8073,12 @@ function build_paragraph_text(paragraph, comments_map, cleanView, style_cache, d
|
|
|
7032
8073
|
else if (ev.type === "del_end") delete active_del[ev.id];
|
|
7033
8074
|
else if (ev.type === "fmt_start") active_fmt[ev.id] = ev;
|
|
7034
8075
|
else if (ev.type === "fmt_end") delete active_fmt[ev.id];
|
|
7035
|
-
else if (ev.type === "
|
|
8076
|
+
else if (ev.type === "image") {
|
|
8077
|
+
if (!(cleanView && Object.keys(active_del).length > 0)) {
|
|
8078
|
+
const alt = (ev.date || "image").replace(/\]/g, ")").replace(/\n/g, " ");
|
|
8079
|
+
parts.push(``);
|
|
8080
|
+
}
|
|
8081
|
+
} else if (ev.type === "footnote" || ev.type === "endnote") {
|
|
7036
8082
|
if (pending_text) {
|
|
7037
8083
|
parts.push(
|
|
7038
8084
|
`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
|
|
@@ -7834,6 +8880,7 @@ async function finalize_document(doc, options) {
|
|
|
7834
8880
|
report.add_transform_lines(strip_image_alt_text(doc));
|
|
7835
8881
|
const warnings = audit_hyperlinks(doc);
|
|
7836
8882
|
for (const w of warnings) report.warnings.push(w);
|
|
8883
|
+
for (const w of detect_watermarks(doc)) report.warnings.push(w);
|
|
7837
8884
|
report.add_transform_lines(normalize_change_dates(doc));
|
|
7838
8885
|
if (options.protection_mode === "read_only" || options.protection_mode === "encrypt") {
|
|
7839
8886
|
if (options.protection_mode === "encrypt") {
|
|
@@ -7889,17 +8936,24 @@ export {
|
|
|
7889
8936
|
DocumentMapper,
|
|
7890
8937
|
DocumentObject,
|
|
7891
8938
|
RedlineEngine,
|
|
8939
|
+
RegexTimeoutError,
|
|
8940
|
+
USER_PATTERN_TIMEOUT_MS,
|
|
7892
8941
|
_extractTextFromDoc,
|
|
7893
8942
|
apply_edits_to_markdown,
|
|
7894
8943
|
create_unified_diff,
|
|
7895
8944
|
create_word_patch_diff,
|
|
8945
|
+
describe_illegal_control_chars,
|
|
7896
8946
|
extractTextFromBuffer,
|
|
7897
8947
|
extract_outline,
|
|
7898
8948
|
finalize_document,
|
|
7899
8949
|
generate_edits_from_text,
|
|
8950
|
+
generate_structured_edits,
|
|
7900
8951
|
identifyEngine,
|
|
7901
8952
|
paginate,
|
|
7902
8953
|
split_structural_appendix,
|
|
7903
|
-
trim_common_context
|
|
8954
|
+
trim_common_context,
|
|
8955
|
+
userFindAllMatches,
|
|
8956
|
+
userSearch,
|
|
8957
|
+
validate_edit_strings
|
|
7904
8958
|
};
|
|
7905
8959
|
//# sourceMappingURL=index.js.map
|