@adeu/core 1.17.1 → 1.17.2
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 +345 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +345 -84
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/engine.qa.test.ts +4 -4
- package/src/engine.tables.test.ts +138 -36
- package/src/engine.ts +433 -105
- package/src/ingest.ts +16 -1
- package/src/mapper.ts +23 -1
- package/src/markup.ts +1 -1
- package/src/repro.feedback.test.ts +160 -0
- package/src/repro.report.test.ts +130 -0
- package/src/repro_qa_report_v3.test.ts +194 -0
package/src/engine.ts
CHANGED
|
@@ -65,6 +65,81 @@ function insertAtIndex(parent: Element, index: number, child: Node) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
|
|
69
|
+
function safeCloneEdit(val: any, seen: WeakMap<any, any> = new WeakMap()): any {
|
|
70
|
+
if (val === null || typeof val !== "object") {
|
|
71
|
+
return val;
|
|
72
|
+
}
|
|
73
|
+
if (seen.has(val)) {
|
|
74
|
+
return seen.get(val);
|
|
75
|
+
}
|
|
76
|
+
if (val.nodeType !== undefined || typeof val.cloneNode === "function") {
|
|
77
|
+
return val;
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(val)) {
|
|
80
|
+
const copy: any[] = [];
|
|
81
|
+
seen.set(val, copy);
|
|
82
|
+
for (let i = 0; i < val.length; i++) {
|
|
83
|
+
copy.push(safeCloneEdit(val[i], seen));
|
|
84
|
+
}
|
|
85
|
+
return copy;
|
|
86
|
+
}
|
|
87
|
+
const copy: any = {};
|
|
88
|
+
seen.set(val, copy);
|
|
89
|
+
for (const key of Object.keys(val)) {
|
|
90
|
+
copy[key] = safeCloneEdit(val[key], seen);
|
|
91
|
+
}
|
|
92
|
+
return copy;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function takeSnapshot(doc: any): any {
|
|
96
|
+
const parts = [...doc.pkg.parts];
|
|
97
|
+
const unzipped = { ...doc.pkg.unzipped };
|
|
98
|
+
const rels = new Map<any, Map<string, any>>();
|
|
99
|
+
const elements = new Map<any, Element>();
|
|
100
|
+
for (const part of parts) {
|
|
101
|
+
rels.set(part, new Map(part.rels));
|
|
102
|
+
if (part._element) {
|
|
103
|
+
elements.set(part, part._element.cloneNode(true) as Element);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { parts, unzipped, rels, elements };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function restoreSnapshot(doc: any, snapshot: any): void {
|
|
110
|
+
doc.pkg.parts = [...snapshot.parts];
|
|
111
|
+
for (const key of Object.keys(doc.pkg.unzipped)) {
|
|
112
|
+
delete doc.pkg.unzipped[key];
|
|
113
|
+
}
|
|
114
|
+
for (const [key, val] of Object.entries(snapshot.unzipped)) {
|
|
115
|
+
doc.pkg.unzipped[key] = val;
|
|
116
|
+
}
|
|
117
|
+
for (const part of snapshot.parts) {
|
|
118
|
+
part.rels = new Map(snapshot.rels.get(part)!);
|
|
119
|
+
const originalEl = snapshot.elements.get(part);
|
|
120
|
+
if (originalEl && part._element) {
|
|
121
|
+
const xmlDoc = part._element.ownerDocument;
|
|
122
|
+
if (xmlDoc && xmlDoc.documentElement) {
|
|
123
|
+
xmlDoc.replaceChild(originalEl, xmlDoc.documentElement);
|
|
124
|
+
}
|
|
125
|
+
part._element = originalEl;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function stripMatchingHeadingHashes(target: string, newText: string): [string, string] {
|
|
131
|
+
if (!target || !newText) return [target, newText];
|
|
132
|
+
const targetMatch = target.match(/^(#+)\s+/);
|
|
133
|
+
const newMatch = newText.match(/^(#+)\s+/);
|
|
134
|
+
if (targetMatch && newMatch && targetMatch[1] === newMatch[1]) {
|
|
135
|
+
const hashes = targetMatch[1];
|
|
136
|
+
const targetClean = target.substring(hashes.length).trimStart();
|
|
137
|
+
const newClean = newText.substring(hashes.length).trimStart();
|
|
138
|
+
return [targetClean, newClean];
|
|
139
|
+
}
|
|
140
|
+
return [target, newText];
|
|
141
|
+
}
|
|
142
|
+
|
|
68
143
|
// --- Validation ---
|
|
69
144
|
export class BatchValidationError extends Error {
|
|
70
145
|
public errors: string[];
|
|
@@ -75,7 +150,7 @@ export class BatchValidationError extends Error {
|
|
|
75
150
|
}
|
|
76
151
|
}
|
|
77
152
|
|
|
78
|
-
export function validate_edit_strings(edits: any[]): string[] {
|
|
153
|
+
export function validate_edit_strings(edits: any[], index_offset: number = 0): string[] {
|
|
79
154
|
const errors: string[] = [];
|
|
80
155
|
|
|
81
156
|
for (let i = 0; i < edits.length; i++) {
|
|
@@ -90,7 +165,7 @@ export function validate_edit_strings(edits: any[]): string[] {
|
|
|
90
165
|
n_text.includes("{==")
|
|
91
166
|
) {
|
|
92
167
|
errors.push(
|
|
93
|
-
`- Edit ${i + 1} Failed: Do not manually write CriticMarkup tags ({++, {--, {>>, {==) in \`new_text\`. The engine handles redlining automatically. To add a comment, use the \`comment\` parameter.`,
|
|
168
|
+
`- 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.`,
|
|
94
169
|
);
|
|
95
170
|
}
|
|
96
171
|
|
|
@@ -107,11 +182,11 @@ export function validate_edit_strings(edits: any[]): string[] {
|
|
|
107
182
|
)
|
|
108
183
|
) {
|
|
109
184
|
errors.push(
|
|
110
|
-
`- Edit ${i + 1} Failed: Cannot insert footnote/endnote markers via text replace. Markers like \`[^fn-N]\` are read-only projections. Use Word's References menu.`,
|
|
185
|
+
`- Edit ${i + 1 + index_offset} Failed: Cannot insert footnote/endnote markers via text replace. Markers like \`[^fn-N]\` are read-only projections. Use Word's References menu.`,
|
|
111
186
|
);
|
|
112
187
|
} else {
|
|
113
188
|
errors.push(
|
|
114
|
-
`- Edit ${i + 1} Failed: Cannot delete footnote/endnote references via text replace. The marker corresponds to a structural XML element.`,
|
|
189
|
+
`- Edit ${i + 1 + index_offset} Failed: Cannot delete footnote/endnote references via text replace. The marker corresponds to a structural XML element.`,
|
|
115
190
|
);
|
|
116
191
|
}
|
|
117
192
|
}
|
|
@@ -123,11 +198,11 @@ export function validate_edit_strings(edits: any[]): string[] {
|
|
|
123
198
|
if (t_links.length !== n_links.length) {
|
|
124
199
|
if (n_links.length > t_links.length) {
|
|
125
200
|
errors.push(
|
|
126
|
-
`- Edit ${i + 1} Failed: Cannot insert hyperlinks via text replace. Use a dedicated structural operation.`,
|
|
201
|
+
`- Edit ${i + 1 + index_offset} Failed: Cannot insert hyperlinks via text replace. Use a dedicated structural operation.`,
|
|
127
202
|
);
|
|
128
203
|
} else {
|
|
129
204
|
errors.push(
|
|
130
|
-
`- Edit ${i + 1} Failed: Cannot delete hyperlinks via text replace. The marker corresponds to a structural XML element.`,
|
|
205
|
+
`- Edit ${i + 1 + index_offset} Failed: Cannot delete hyperlinks via text replace. The marker corresponds to a structural XML element.`,
|
|
131
206
|
);
|
|
132
207
|
}
|
|
133
208
|
} else if (
|
|
@@ -135,7 +210,7 @@ export function validate_edit_strings(edits: any[]): string[] {
|
|
|
135
210
|
JSON.stringify(t_links) !== JSON.stringify(n_links)
|
|
136
211
|
) {
|
|
137
212
|
errors.push(
|
|
138
|
-
`- Edit ${i + 1} Failed: Can only edit or retarget one hyperlink per text replacement. Please split into multiple edits.`,
|
|
213
|
+
`- Edit ${i + 1 + index_offset} Failed: Can only edit or retarget one hyperlink per text replacement. Please split into multiple edits.`,
|
|
139
214
|
);
|
|
140
215
|
}
|
|
141
216
|
}
|
|
@@ -146,18 +221,18 @@ export function validate_edit_strings(edits: any[]): string[] {
|
|
|
146
221
|
if (t_xrefs.length !== n_xrefs.length) {
|
|
147
222
|
if (n_xrefs.length > t_xrefs.length) {
|
|
148
223
|
errors.push(
|
|
149
|
-
`- Edit ${i + 1} Failed: Cannot insert cross-references via text replace. Markers are read-only projections.`,
|
|
224
|
+
`- Edit ${i + 1 + index_offset} Failed: Cannot insert cross-references via text replace. Markers are read-only projections.`,
|
|
150
225
|
);
|
|
151
226
|
} else {
|
|
152
227
|
errors.push(
|
|
153
|
-
`- Edit ${i + 1} Failed: Cannot delete cross-references via text replace. The marker corresponds to a structural XML element.`,
|
|
228
|
+
`- Edit ${i + 1 + index_offset} Failed: Cannot delete cross-references via text replace. The marker corresponds to a structural XML element.`,
|
|
154
229
|
);
|
|
155
230
|
}
|
|
156
231
|
} else {
|
|
157
232
|
// Advanced XREF validation simplified for port scope
|
|
158
233
|
if (JSON.stringify(t_xrefs) !== JSON.stringify(n_xrefs)) {
|
|
159
234
|
errors.push(
|
|
160
|
-
`- Edit ${i + 1} Failed: Modifying or retargeting cross-reference markers is disallowed to prevent dependency corruption.`,
|
|
235
|
+
`- Edit ${i + 1 + index_offset} Failed: Modifying or retargeting cross-reference markers is disallowed to prevent dependency corruption.`,
|
|
161
236
|
);
|
|
162
237
|
}
|
|
163
238
|
}
|
|
@@ -172,7 +247,7 @@ export function validate_edit_strings(edits: any[]): string[] {
|
|
|
172
247
|
t_anchors.filter((x: string) => x === a).length
|
|
173
248
|
) {
|
|
174
249
|
errors.push(
|
|
175
|
-
`- Edit ${i + 1} Failed: Cannot modify or insert internal anchor markers (\`{#...}\`). These represent structural XML bookmarks.`,
|
|
250
|
+
`- Edit ${i + 1 + index_offset} Failed: Cannot modify or insert internal anchor markers (\`{#...}\`). These represent structural XML bookmarks.`,
|
|
176
251
|
);
|
|
177
252
|
break;
|
|
178
253
|
}
|
|
@@ -190,7 +265,7 @@ export function validate_edit_strings(edits: any[]): string[] {
|
|
|
190
265
|
stripped.substring(level) === ""
|
|
191
266
|
) {
|
|
192
267
|
errors.push(
|
|
193
|
-
`- Edit ${i + 1} Failed: Heading level ${level} is not supported (maximum is 6).`,
|
|
268
|
+
`- Edit ${i + 1 + index_offset} Failed: Heading level ${level} is not supported (maximum is 6).`,
|
|
194
269
|
);
|
|
195
270
|
break;
|
|
196
271
|
}
|
|
@@ -202,10 +277,12 @@ export function validate_edit_strings(edits: any[]): string[] {
|
|
|
202
277
|
t_text.includes("READONLY_BOUNDARY_START") ||
|
|
203
278
|
n_text.includes("READONLY_BOUNDARY_START") ||
|
|
204
279
|
t_text.includes("# Document Structure (Read-Only)") ||
|
|
205
|
-
n_text.includes("# Document Structure (Read-Only)")
|
|
280
|
+
n_text.includes("# Document Structure (Read-Only)") ||
|
|
281
|
+
t_text.includes("Document Structure (Read-Only)") ||
|
|
282
|
+
n_text.includes("Document Structure (Read-Only)")
|
|
206
283
|
) {
|
|
207
284
|
errors.push(
|
|
208
|
-
`- Edit ${i + 1} Failed: Modification targets the read-only boundary (Structural Appendix). This section cannot be edited.`,
|
|
285
|
+
`- Edit ${i + 1 + index_offset} Failed: Modification targets the read-only boundary (Structural Appendix). This section cannot be edited.`,
|
|
209
286
|
);
|
|
210
287
|
}
|
|
211
288
|
}
|
|
@@ -233,11 +310,7 @@ export class RedlineEngine {
|
|
|
233
310
|
const w16du_ns =
|
|
234
311
|
"http://schemas.microsoft.com/office/word/2023/wordml/word16du";
|
|
235
312
|
for (const part of this.doc.pkg.parts) {
|
|
236
|
-
if (
|
|
237
|
-
part === this.doc.part ||
|
|
238
|
-
(part.contentType.includes("wordprocessingml") &&
|
|
239
|
-
part.contentType.endsWith("+xml"))
|
|
240
|
-
) {
|
|
313
|
+
if (part === this.doc.part) {
|
|
241
314
|
if (!part._element.hasAttribute("xmlns:w16du")) {
|
|
242
315
|
part._element.setAttribute("xmlns:w16du", w16du_ns);
|
|
243
316
|
}
|
|
@@ -256,7 +329,51 @@ export class RedlineEngine {
|
|
|
256
329
|
}
|
|
257
330
|
return null;
|
|
258
331
|
}
|
|
259
|
-
|
|
332
|
+
/**
|
|
333
|
+
* Best-effort "did you mean" hint for a failed target. The common loop trap
|
|
334
|
+
* (observed in the field) is an anchored regex like `^\( x \)$` against a
|
|
335
|
+
* mid-document string: ^/$ bind to the whole full_text, so it never matches
|
|
336
|
+
* even though the literal `( x )` is present. We strip regex anchoring/escapes
|
|
337
|
+
* and probe full_text for a literal occurrence; if found, we tell the model
|
|
338
|
+
* the exact literal that WOULD match so it drops the anchors instead of
|
|
339
|
+
* escalating the regex further.
|
|
340
|
+
*/
|
|
341
|
+
private _nearest_match_hint(
|
|
342
|
+
target_text: string | undefined,
|
|
343
|
+
is_regex: boolean,
|
|
344
|
+
): string {
|
|
345
|
+
if (!target_text) return "";
|
|
346
|
+
let probe = target_text;
|
|
347
|
+
if (is_regex) {
|
|
348
|
+
// Strip leading/trailing anchors and surrounding \s* the model tends to add.
|
|
349
|
+
probe = probe.replace(/^\^/, "").replace(/\$$/, "");
|
|
350
|
+
probe = probe.replace(/^\\s\*/, "").replace(/\\s\*$/, "");
|
|
351
|
+
// Unescape the common literal escapes so "\( x \)" -> "( x )".
|
|
352
|
+
probe = probe.replace(/\\([.^$*+?()[\]{}|\\/])/g, "$1");
|
|
353
|
+
}
|
|
354
|
+
probe = probe.trim();
|
|
355
|
+
if (!probe || probe === target_text) {
|
|
356
|
+
// No anchors to strip, or nothing changed: nothing useful to suggest.
|
|
357
|
+
if (!is_regex) return "";
|
|
358
|
+
}
|
|
359
|
+
const idx = this.mapper.full_text.indexOf(probe);
|
|
360
|
+
if (idx !== -1) {
|
|
361
|
+
const ctx_start = Math.max(0, idx - 15);
|
|
362
|
+
const ctx_end = Math.min(
|
|
363
|
+
this.mapper.full_text.length,
|
|
364
|
+
idx + probe.length + 15,
|
|
365
|
+
);
|
|
366
|
+
const ctx = this.mapper.full_text
|
|
367
|
+
.substring(ctx_start, ctx_end)
|
|
368
|
+
.replace(/\n/g, " ");
|
|
369
|
+
return (
|
|
370
|
+
`\n Did you mean the literal "${probe}"? It appears in the document` +
|
|
371
|
+
` (…${ctx}…). If you used a regex, drop the ^/$ anchors — they match` +
|
|
372
|
+
` the start/end of the entire document, not a line.`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return "";
|
|
376
|
+
}
|
|
260
377
|
private _build_edit_context_previews(
|
|
261
378
|
edit: any,
|
|
262
379
|
): [string | null, string | null] {
|
|
@@ -264,10 +381,23 @@ export class RedlineEngine {
|
|
|
264
381
|
if (edit._resolved_proxy_edit) {
|
|
265
382
|
edit = edit._resolved_proxy_edit;
|
|
266
383
|
}
|
|
267
|
-
|
|
384
|
+
let start_idx = edit._resolved_start_idx;
|
|
268
385
|
if (start_idx === undefined || start_idx === null) return [null, null];
|
|
269
|
-
|
|
270
|
-
|
|
386
|
+
let target_text = edit.target_text || "";
|
|
387
|
+
let new_text = edit.new_text || "";
|
|
388
|
+
|
|
389
|
+
const [clean_target, target_style] = this._parse_markdown_style(target_text);
|
|
390
|
+
if (target_style && target_style.startsWith("Heading")) {
|
|
391
|
+
const prefix_len = target_text.length - clean_target.length;
|
|
392
|
+
start_idx += prefix_len;
|
|
393
|
+
target_text = clean_target;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const [clean_new, new_style] = this._parse_markdown_style(new_text);
|
|
397
|
+
if (new_style && new_style.startsWith("Heading")) {
|
|
398
|
+
new_text = clean_new;
|
|
399
|
+
}
|
|
400
|
+
|
|
271
401
|
const length = target_text.length;
|
|
272
402
|
const active_mapper = edit._active_mapper_ref || this.mapper;
|
|
273
403
|
const full_text = active_mapper.full_text;
|
|
@@ -303,7 +433,11 @@ export class RedlineEngine {
|
|
|
303
433
|
return maxId;
|
|
304
434
|
}
|
|
305
435
|
|
|
306
|
-
private _get_heading_path_and_page(
|
|
436
|
+
private _get_heading_path_and_page(
|
|
437
|
+
start_idx: number,
|
|
438
|
+
text: string,
|
|
439
|
+
page_offsets: number[],
|
|
440
|
+
): [string, number] {
|
|
307
441
|
let page = 1;
|
|
308
442
|
for (let i = 0; i < page_offsets.length; i++) {
|
|
309
443
|
if (start_idx >= page_offsets[i]) {
|
|
@@ -317,14 +451,17 @@ export class RedlineEngine {
|
|
|
317
451
|
const lines = textBefore.split("\n");
|
|
318
452
|
const path: string[] = [];
|
|
319
453
|
let current_level = 999;
|
|
320
|
-
|
|
454
|
+
|
|
321
455
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
322
456
|
const line = lines[i];
|
|
323
457
|
const m = line.match(/^(#{1,6})\s+(.*)/);
|
|
324
458
|
if (m) {
|
|
325
459
|
const level = m[1].length;
|
|
326
460
|
if (level < current_level) {
|
|
327
|
-
let cleanHeading = m[2]
|
|
461
|
+
let cleanHeading = m[2]
|
|
462
|
+
.replace(/\*\*|__|[*_]/g, "")
|
|
463
|
+
.replace(/\{#[^}]+\}/g, "")
|
|
464
|
+
.trim();
|
|
328
465
|
if (cleanHeading.length > 80) {
|
|
329
466
|
cleanHeading = cleanHeading.substring(0, 80) + "...";
|
|
330
467
|
}
|
|
@@ -1220,33 +1357,39 @@ export class RedlineEngine {
|
|
|
1220
1357
|
}
|
|
1221
1358
|
}
|
|
1222
1359
|
|
|
1223
|
-
public validate_edits(edits: any[]): string[] {
|
|
1360
|
+
public validate_edits(edits: any[], index_offset: number = 0): string[] {
|
|
1224
1361
|
const errors: string[] = [];
|
|
1225
1362
|
if (!this.mapper.full_text) this.mapper["_build_map"]();
|
|
1226
1363
|
|
|
1227
|
-
errors.push(...validate_edit_strings(edits));
|
|
1364
|
+
errors.push(...validate_edit_strings(edits, index_offset));
|
|
1228
1365
|
|
|
1229
1366
|
for (let i = 0; i < edits.length; i++) {
|
|
1230
1367
|
const edit = edits[i];
|
|
1231
1368
|
if (typeof edit !== "object" || edit === null) {
|
|
1232
1369
|
errors.push(
|
|
1233
|
-
`- Edit ${i + 1} Failed: Invalid change format. Expected a JSON object, but received a primitive ${typeof edit}. Do not pass raw strings
|
|
1370
|
+
`- Edit ${i + 1 + index_offset} Failed: Invalid change format. Expected a JSON object, but received a primitive ${typeof edit}. Do not pass raw strings.`,
|
|
1234
1371
|
);
|
|
1235
1372
|
continue;
|
|
1236
1373
|
}
|
|
1237
1374
|
if (!edit.target_text) continue;
|
|
1238
|
-
|
|
1375
|
+
|
|
1239
1376
|
const is_regex = (edit as any).regex || false;
|
|
1240
1377
|
const match_mode = (edit as any).match_mode || "strict";
|
|
1241
1378
|
|
|
1242
|
-
let matches = this.mapper.find_all_match_indices(
|
|
1379
|
+
let matches = this.mapper.find_all_match_indices(
|
|
1380
|
+
edit.target_text,
|
|
1381
|
+
is_regex,
|
|
1382
|
+
);
|
|
1243
1383
|
let activeText = this.mapper.full_text;
|
|
1244
1384
|
let target_mapper = this.mapper;
|
|
1245
1385
|
|
|
1246
1386
|
if (matches.length === 0) {
|
|
1247
1387
|
if (!this.clean_mapper)
|
|
1248
1388
|
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
1249
|
-
matches = this.clean_mapper.find_all_match_indices(
|
|
1389
|
+
matches = this.clean_mapper.find_all_match_indices(
|
|
1390
|
+
edit.target_text,
|
|
1391
|
+
is_regex,
|
|
1392
|
+
);
|
|
1250
1393
|
if (matches.length > 0) {
|
|
1251
1394
|
activeText = this.clean_mapper.full_text;
|
|
1252
1395
|
target_mapper = this.clean_mapper;
|
|
@@ -1278,7 +1421,10 @@ export class RedlineEngine {
|
|
|
1278
1421
|
if (!this.original_mapper) {
|
|
1279
1422
|
this.original_mapper = new DocumentMapper(this.doc, false, true);
|
|
1280
1423
|
}
|
|
1281
|
-
const orig_matches = this.original_mapper.find_all_match_indices(
|
|
1424
|
+
const orig_matches = this.original_mapper.find_all_match_indices(
|
|
1425
|
+
edit.target_text,
|
|
1426
|
+
is_regex,
|
|
1427
|
+
);
|
|
1282
1428
|
if (orig_matches.length > 0) {
|
|
1283
1429
|
is_deleted_text = true;
|
|
1284
1430
|
for (const [start, length] of orig_matches) {
|
|
@@ -1289,7 +1435,10 @@ export class RedlineEngine {
|
|
|
1289
1435
|
if (s.run !== null) {
|
|
1290
1436
|
let parent = s.run._element as Node | null;
|
|
1291
1437
|
while (parent) {
|
|
1292
|
-
if (
|
|
1438
|
+
if (
|
|
1439
|
+
parent.nodeType === 1 &&
|
|
1440
|
+
(parent as Element).tagName === "w:del"
|
|
1441
|
+
) {
|
|
1293
1442
|
const auth = (parent as Element).getAttribute("w:author");
|
|
1294
1443
|
if (auth) {
|
|
1295
1444
|
deleted_authors.add(auth);
|
|
@@ -1306,30 +1455,36 @@ export class RedlineEngine {
|
|
|
1306
1455
|
|
|
1307
1456
|
if (matches.length === 0) {
|
|
1308
1457
|
if (is_deleted_text) {
|
|
1309
|
-
const author_phrase =
|
|
1310
|
-
|
|
1311
|
-
|
|
1458
|
+
const author_phrase =
|
|
1459
|
+
deleted_authors.size > 0
|
|
1460
|
+
? `by ${Array.from(deleted_authors).sort().join(", ")}`
|
|
1461
|
+
: "by an existing revision";
|
|
1312
1462
|
errors.push(
|
|
1313
|
-
`- Edit ${i + 1} Failed: Target text matches text inside a tracked deletion ${author_phrase}. Reject/accept that change first or target the active replacement text instead.`,
|
|
1463
|
+
`- Edit ${i + 1 + index_offset} Failed: Target text matches text inside a tracked deletion ${author_phrase}. Reject/accept that change first or target the active replacement text instead.`,
|
|
1314
1464
|
);
|
|
1315
1465
|
} else {
|
|
1466
|
+
const hint = this._nearest_match_hint(edit.target_text, is_regex);
|
|
1316
1467
|
errors.push(
|
|
1317
|
-
`- Edit ${i + 1} Failed: Target text not found in document:\n "${edit.target_text}"`,
|
|
1468
|
+
`- Edit ${i + 1 + index_offset} Failed: Target text not found in document:\n "${edit.target_text}"${hint}`,
|
|
1318
1469
|
);
|
|
1319
1470
|
}
|
|
1320
1471
|
} else if (matches.length > 1 && match_mode === "strict") {
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1472
|
+
if (edit.target_text.includes("|")) {
|
|
1473
|
+
matches = matches.slice(0, 1);
|
|
1474
|
+
} else {
|
|
1475
|
+
const positions: [number, number][] = matches.map(([start, length]) => [
|
|
1476
|
+
start,
|
|
1477
|
+
start + length,
|
|
1478
|
+
]);
|
|
1479
|
+
errors.push(
|
|
1480
|
+
format_ambiguity_error(
|
|
1481
|
+
i + 1 + index_offset,
|
|
1482
|
+
edit.target_text,
|
|
1483
|
+
activeText,
|
|
1484
|
+
positions,
|
|
1485
|
+
),
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1333
1488
|
}
|
|
1334
1489
|
|
|
1335
1490
|
// BUG-23-4: when the effective (context-trimmed) target spans a
|
|
@@ -1354,7 +1509,7 @@ export class RedlineEngine {
|
|
|
1354
1509
|
parts[parts.length - 1].trim() !== ""
|
|
1355
1510
|
) {
|
|
1356
1511
|
errors.push(
|
|
1357
|
-
`- Edit ${i + 1} Failed: target_text spans a paragraph boundary with body text on both sides. The paragraph break is a structural element, not literal text, so it cannot be replaced as a single span without corrupting the document. Split this into one edit per paragraph.`,
|
|
1512
|
+
`- Edit ${i + 1 + index_offset} Failed: target_text spans a paragraph boundary with body text on both sides. The paragraph break is a structural element, not literal text, so it cannot be replaced as a single span without corrupting the document. Split this into one edit per paragraph.`,
|
|
1358
1513
|
);
|
|
1359
1514
|
}
|
|
1360
1515
|
} else {
|
|
@@ -1365,7 +1520,7 @@ export class RedlineEngine {
|
|
|
1365
1520
|
parts[parts.length - 1].trim() !== ""
|
|
1366
1521
|
) {
|
|
1367
1522
|
errors.push(
|
|
1368
|
-
`- Edit ${i + 1} Failed: target_text spans a paragraph boundary with body text on both sides. The paragraph break is a structural element, not literal text, so it cannot be replaced as a single span without corrupting the document. Split this into one edit per paragraph.`,
|
|
1523
|
+
`- Edit ${i + 1 + index_offset} Failed: target_text spans a paragraph boundary with body text on both sides. The paragraph break is a structural element, not literal text, so it cannot be replaced as a single span without corrupting the document. Split this into one edit per paragraph.`,
|
|
1369
1524
|
);
|
|
1370
1525
|
}
|
|
1371
1526
|
}
|
|
@@ -1385,7 +1540,13 @@ export class RedlineEngine {
|
|
|
1385
1540
|
).filter((n) => n.getAttribute("w:id") === s.ins_id);
|
|
1386
1541
|
if (insNodes.length > 0) {
|
|
1387
1542
|
const auth = insNodes[0].getAttribute("w:author");
|
|
1388
|
-
if (auth && auth !== this.author)
|
|
1543
|
+
if (auth && auth !== this.author) {
|
|
1544
|
+
const is_fully_contained_in_ins = start >= s.start && (start + length) <= s.end;
|
|
1545
|
+
const is_lockout = is_fully_contained_in_ins || match_mode === "all";
|
|
1546
|
+
if (is_lockout) {
|
|
1547
|
+
nestedAuthors.add(auth);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1389
1550
|
}
|
|
1390
1551
|
}
|
|
1391
1552
|
if (s.comment_ids) {
|
|
@@ -1399,7 +1560,7 @@ export class RedlineEngine {
|
|
|
1399
1560
|
}
|
|
1400
1561
|
if (nestedAuthors.size > 0) {
|
|
1401
1562
|
errors.push(
|
|
1402
|
-
`- Edit ${i + 1} Failed: Modification targets an active insertion from another author (${Array.from(nestedAuthors).join(", ")}). Accept that change first or scope your edit outside of it.`,
|
|
1563
|
+
`- Edit ${i + 1 + index_offset} Failed: Modification targets an active insertion from another author (${Array.from(nestedAuthors).join(", ")}). Accept that change first or scope your edit outside of it.`,
|
|
1403
1564
|
);
|
|
1404
1565
|
}
|
|
1405
1566
|
}
|
|
@@ -1481,22 +1642,13 @@ export class RedlineEngine {
|
|
|
1481
1642
|
}
|
|
1482
1643
|
|
|
1483
1644
|
if (dry_run) {
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1486
|
-
if (part._element) {
|
|
1487
|
-
baselines.set(part, part._element.cloneNode(true) as Element);
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1645
|
+
const snapshot = takeSnapshot(this.doc);
|
|
1646
|
+
const originalCurrentId = this.current_id;
|
|
1490
1647
|
try {
|
|
1491
1648
|
return this._process_batch_internal(changes, true);
|
|
1492
1649
|
} finally {
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
if (doc && doc.documentElement) {
|
|
1496
|
-
doc.replaceChild(originalEl, doc.documentElement);
|
|
1497
|
-
}
|
|
1498
|
-
part._element = originalEl;
|
|
1499
|
-
}
|
|
1650
|
+
restoreSnapshot(this.doc, snapshot);
|
|
1651
|
+
this.current_id = originalCurrentId;
|
|
1500
1652
|
this.mapper = new DocumentMapper(this.doc);
|
|
1501
1653
|
this.comments_manager = new CommentsManager(this.doc);
|
|
1502
1654
|
this.clean_mapper = null;
|
|
@@ -1510,23 +1662,95 @@ export class RedlineEngine {
|
|
|
1510
1662
|
changes: DocumentChange[],
|
|
1511
1663
|
dry_run_mode: boolean = false,
|
|
1512
1664
|
): any {
|
|
1665
|
+
// Pre-process edits: strip identical leading heading hashes from target_text and new_text
|
|
1666
|
+
for (const c of changes) {
|
|
1667
|
+
if (c && typeof c === "object" && (c as any).type === "modify" && (c as any).target_text && (c as any).new_text) {
|
|
1668
|
+
const [strippedTarget, strippedNew] = stripMatchingHeadingHashes((c as any).target_text, (c as any).new_text);
|
|
1669
|
+
(c as any).target_text = strippedTarget;
|
|
1670
|
+
(c as any).new_text = strippedNew;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1513
1674
|
this.skipped_details = [];
|
|
1514
|
-
|
|
1515
|
-
|
|
1675
|
+
|
|
1676
|
+
const actions = changes.filter(
|
|
1677
|
+
(c) =>
|
|
1678
|
+
c !== null &&
|
|
1679
|
+
typeof c === "object" &&
|
|
1680
|
+
["accept", "reject", "reply"].includes(c.type),
|
|
1516
1681
|
);
|
|
1517
1682
|
const edits = changes.filter(
|
|
1518
|
-
(c) =>
|
|
1683
|
+
(c) =>
|
|
1684
|
+
c === null ||
|
|
1685
|
+
typeof c !== "object" ||
|
|
1686
|
+
!["accept", "reject", "reply"].includes(c.type),
|
|
1519
1687
|
);
|
|
1520
1688
|
|
|
1521
|
-
//
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
if (
|
|
1525
|
-
|
|
1689
|
+
// Run edits_for_merge logic to unwrap collaborative active insertions
|
|
1690
|
+
let mapper_dirty = false;
|
|
1691
|
+
for (const edit of edits as any[]) {
|
|
1692
|
+
if (typeof edit !== "object" || edit === null || !edit.target_text) continue;
|
|
1693
|
+
const is_regex = edit.regex || false;
|
|
1694
|
+
let matches = this.mapper.find_all_match_indices(edit.target_text, is_regex);
|
|
1695
|
+
let target_mapper = this.mapper;
|
|
1696
|
+
if (matches.length === 0) {
|
|
1697
|
+
if (!this.clean_mapper) {
|
|
1698
|
+
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
1699
|
+
}
|
|
1700
|
+
matches = this.clean_mapper.find_all_match_indices(edit.target_text, is_regex);
|
|
1701
|
+
target_mapper = this.clean_mapper!;
|
|
1526
1702
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1703
|
+
for (const [start, length] of matches) {
|
|
1704
|
+
const spans = target_mapper.spans.filter(
|
|
1705
|
+
(s) => s.end > start && s.start < start + length,
|
|
1706
|
+
);
|
|
1707
|
+
for (const s of spans) {
|
|
1708
|
+
if (s.ins_id) {
|
|
1709
|
+
const insNodes = findAllDescendants(this.doc.element, "w:ins").filter(
|
|
1710
|
+
(n) => n.getAttribute("w:id") === s.ins_id,
|
|
1711
|
+
);
|
|
1712
|
+
if (insNodes.length > 0) {
|
|
1713
|
+
const auth = insNodes[0].getAttribute("w:author");
|
|
1714
|
+
if (auth && auth !== this.author) {
|
|
1715
|
+
const is_fully_contained_in_ins = start >= s.start && (start + length) <= s.end;
|
|
1716
|
+
const match_mode = edit.match_mode || "strict";
|
|
1717
|
+
if (!is_fully_contained_in_ins && match_mode !== "all") {
|
|
1718
|
+
const node = insNodes[0];
|
|
1719
|
+
this._clean_wrapping_comments(node);
|
|
1720
|
+
const parent = node.parentNode as Element | null;
|
|
1721
|
+
if (parent) {
|
|
1722
|
+
if (parent.tagName === "w:trPr") {
|
|
1723
|
+
parent.removeChild(node);
|
|
1724
|
+
} else {
|
|
1725
|
+
while (node.firstChild) {
|
|
1726
|
+
parent.insertBefore(node.firstChild, node);
|
|
1727
|
+
}
|
|
1728
|
+
parent.removeChild(node);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
mapper_dirty = true;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1529
1737
|
}
|
|
1738
|
+
}
|
|
1739
|
+
if (mapper_dirty) {
|
|
1740
|
+
this.mapper = new DocumentMapper(this.doc);
|
|
1741
|
+
this.clean_mapper = null;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// BUG-7: Unified single-pass validation in wet-run / standard mode.
|
|
1745
|
+
if (!dry_run_mode) {
|
|
1746
|
+
const action_errors =
|
|
1747
|
+
actions.length > 0 ? this.validate_review_actions(actions) : [];
|
|
1748
|
+
const validate_edits_now = edits.length > 0 && action_errors.length > 0;
|
|
1749
|
+
const edit_errors = validate_edits_now ? this.validate_edits(edits) : [];
|
|
1750
|
+
const all_errors = [
|
|
1751
|
+
...action_errors,
|
|
1752
|
+
...edit_errors,
|
|
1753
|
+
];
|
|
1530
1754
|
if (all_errors.length > 0) {
|
|
1531
1755
|
throw new BatchValidationError(all_errors);
|
|
1532
1756
|
}
|
|
@@ -1564,8 +1788,9 @@ export class RedlineEngine {
|
|
|
1564
1788
|
|
|
1565
1789
|
if (edits.length > 0) {
|
|
1566
1790
|
if (dry_run_mode) {
|
|
1567
|
-
for (
|
|
1568
|
-
const
|
|
1791
|
+
for (let i = 0; i < edits.length; i++) {
|
|
1792
|
+
const edit = edits[i];
|
|
1793
|
+
const single_errors = this.validate_edits([edit], i);
|
|
1569
1794
|
const warning = this._check_punctuation_warning(
|
|
1570
1795
|
(edit as any).target_text || "",
|
|
1571
1796
|
);
|
|
@@ -1599,6 +1824,8 @@ export class RedlineEngine {
|
|
|
1599
1824
|
occurrences_modified: (edit as any)._occurrences_modified || 0,
|
|
1600
1825
|
match_mode: (edit as any).match_mode || "strict",
|
|
1601
1826
|
});
|
|
1827
|
+
this.mapper = new DocumentMapper(this.doc);
|
|
1828
|
+
this.clean_mapper = null;
|
|
1602
1829
|
} else {
|
|
1603
1830
|
skipped_edits++;
|
|
1604
1831
|
const error_msg =
|
|
@@ -1617,16 +1844,40 @@ export class RedlineEngine {
|
|
|
1617
1844
|
}
|
|
1618
1845
|
}
|
|
1619
1846
|
} else {
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1847
|
+
// Simulated dry-run sequentially for wet-run validation parity
|
|
1848
|
+
const snapshot = takeSnapshot(this.doc);
|
|
1849
|
+
const originalCurrentId = this.current_id;
|
|
1850
|
+
try {
|
|
1851
|
+
const sequential_errors: string[] = [];
|
|
1852
|
+
for (let i = 0; i < edits.length; i++) {
|
|
1853
|
+
const edit = edits[i];
|
|
1854
|
+
const single_errors = this.validate_edits([edit], i);
|
|
1855
|
+
if (single_errors.length > 0) {
|
|
1856
|
+
sequential_errors.push(...single_errors);
|
|
1857
|
+
} else {
|
|
1858
|
+
this.apply_edits([edit], page_offsets);
|
|
1859
|
+
this.mapper = new DocumentMapper(this.doc);
|
|
1860
|
+
this.clean_mapper = null;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
if (sequential_errors.length > 0) {
|
|
1864
|
+
throw new BatchValidationError(sequential_errors);
|
|
1865
|
+
}
|
|
1866
|
+
} catch (err) {
|
|
1867
|
+
restoreSnapshot(this.doc, snapshot);
|
|
1868
|
+
this.current_id = originalCurrentId;
|
|
1869
|
+
this.mapper = new DocumentMapper(this.doc);
|
|
1870
|
+
this.comments_manager = new CommentsManager(this.doc);
|
|
1871
|
+
this.clean_mapper = null;
|
|
1872
|
+
throw err;
|
|
1623
1873
|
}
|
|
1624
|
-
const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
|
|
1625
|
-
const res = this.apply_edits(cloned_edits, page_offsets);
|
|
1626
|
-
applied_edits = cloned_edits.filter((e) => (e as any)._applied_status).length;
|
|
1627
|
-
skipped_edits = cloned_edits.length - applied_edits;
|
|
1628
1874
|
|
|
1629
|
-
|
|
1875
|
+
applied_edits = edits.filter(
|
|
1876
|
+
(e) => (e as any)._applied_status,
|
|
1877
|
+
).length;
|
|
1878
|
+
skipped_edits = edits.length - applied_edits;
|
|
1879
|
+
|
|
1880
|
+
for (const edit of edits) {
|
|
1630
1881
|
const success = (edit as any)._applied_status || false;
|
|
1631
1882
|
const error_msg = (edit as any)._error_msg || null;
|
|
1632
1883
|
const warning = this._check_punctuation_warning(
|
|
@@ -1655,7 +1906,6 @@ export class RedlineEngine {
|
|
|
1655
1906
|
}
|
|
1656
1907
|
}
|
|
1657
1908
|
}
|
|
1658
|
-
|
|
1659
1909
|
return {
|
|
1660
1910
|
actions_applied: applied_actions,
|
|
1661
1911
|
actions_skipped: skipped_actions,
|
|
@@ -1668,7 +1918,10 @@ export class RedlineEngine {
|
|
|
1668
1918
|
};
|
|
1669
1919
|
}
|
|
1670
1920
|
|
|
1671
|
-
public apply_edits(
|
|
1921
|
+
public apply_edits(
|
|
1922
|
+
edits: any[],
|
|
1923
|
+
page_offsets: number[] = [],
|
|
1924
|
+
): [number, number] {
|
|
1672
1925
|
let applied = 0;
|
|
1673
1926
|
let skipped = 0;
|
|
1674
1927
|
|
|
@@ -1803,15 +2056,24 @@ export class RedlineEngine {
|
|
|
1803
2056
|
const parent = edit._parent_edit_ref;
|
|
1804
2057
|
if (parent) {
|
|
1805
2058
|
parent._applied_status = true;
|
|
1806
|
-
parent._occurrences_modified =
|
|
1807
|
-
|
|
2059
|
+
parent._occurrences_modified =
|
|
2060
|
+
(parent._occurrences_modified || 0) + 1;
|
|
2061
|
+
const [path, page] = this._get_heading_path_and_page(
|
|
2062
|
+
start,
|
|
2063
|
+
this.mapper.full_text,
|
|
2064
|
+
page_offsets,
|
|
2065
|
+
);
|
|
1808
2066
|
const pages: number[] = parent._pages || [];
|
|
1809
2067
|
if (!pages.includes(page)) pages.unshift(page);
|
|
1810
2068
|
parent._pages = pages;
|
|
1811
2069
|
parent._heading_path = path;
|
|
1812
2070
|
} else {
|
|
1813
2071
|
edit._occurrences_modified = (edit._occurrences_modified || 0) + 1;
|
|
1814
|
-
const [path, page] = this._get_heading_path_and_page(
|
|
2072
|
+
const [path, page] = this._get_heading_path_and_page(
|
|
2073
|
+
start,
|
|
2074
|
+
this.mapper.full_text,
|
|
2075
|
+
page_offsets,
|
|
2076
|
+
);
|
|
1815
2077
|
const pages: number[] = edit._pages || [];
|
|
1816
2078
|
if (!pages.includes(page)) pages.unshift(page);
|
|
1817
2079
|
edit._pages = pages;
|
|
@@ -1993,13 +2255,19 @@ export class RedlineEngine {
|
|
|
1993
2255
|
const is_regex = edit.regex || false;
|
|
1994
2256
|
const match_mode = edit.match_mode || "strict";
|
|
1995
2257
|
|
|
1996
|
-
let matches = this.mapper.find_all_match_indices(
|
|
2258
|
+
let matches = this.mapper.find_all_match_indices(
|
|
2259
|
+
edit.target_text,
|
|
2260
|
+
is_regex,
|
|
2261
|
+
);
|
|
1997
2262
|
let use_clean_map = false;
|
|
1998
2263
|
|
|
1999
2264
|
if (matches.length === 0) {
|
|
2000
2265
|
if (!this.clean_mapper)
|
|
2001
2266
|
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
2002
|
-
matches = this.clean_mapper.find_all_match_indices(
|
|
2267
|
+
matches = this.clean_mapper.find_all_match_indices(
|
|
2268
|
+
edit.target_text,
|
|
2269
|
+
is_regex,
|
|
2270
|
+
);
|
|
2003
2271
|
if (matches.length > 0) use_clean_map = true;
|
|
2004
2272
|
else return null;
|
|
2005
2273
|
}
|
|
@@ -2009,7 +2277,8 @@ export class RedlineEngine {
|
|
|
2009
2277
|
let live_matches: [number, number][] = [];
|
|
2010
2278
|
for (const [s, match_len] of matches) {
|
|
2011
2279
|
const realSpans = active_mapper.spans.filter(
|
|
2012
|
-
(span) =>
|
|
2280
|
+
(span) =>
|
|
2281
|
+
span.run !== null && span.end > s && span.start < s + match_len,
|
|
2013
2282
|
);
|
|
2014
2283
|
if (realSpans.length === 0 || realSpans.some((span) => !span.del_id)) {
|
|
2015
2284
|
live_matches.push([s, match_len]);
|
|
@@ -2031,23 +2300,72 @@ export class RedlineEngine {
|
|
|
2031
2300
|
);
|
|
2032
2301
|
let current_effective_new_text = edit.new_text || "";
|
|
2033
2302
|
|
|
2303
|
+
// Cell anchors ({#cell:<paraId>}) are pure position markers with no real
|
|
2304
|
+
// content — they let the model address an empty (or any) table cell that
|
|
2305
|
+
// has no run to diff against. Treat such a target as a clean INSERTION at
|
|
2306
|
+
// the anchor's paragraph: never delete the marker, never run trim_common_context
|
|
2307
|
+
// (which refuses to split inside {#...} markup and yields a no-op MODIFICATION).
|
|
2308
|
+
// Strip any echoed anchor from new_text so the model can send either
|
|
2309
|
+
// "June 22, 2026" or "June 22, 2026{#cell:...}" and get the same result.
|
|
2310
|
+
if (/^\{#cell:[^}]+\}$/.test(actual_doc_text.trim())) {
|
|
2311
|
+
let ins_text = current_effective_new_text;
|
|
2312
|
+
// Drop a leading/trailing copy of the same anchor token if echoed.
|
|
2313
|
+
ins_text = ins_text.split(actual_doc_text.trim()).join("");
|
|
2314
|
+
if (ins_text) {
|
|
2315
|
+
all_sub_edits.push({
|
|
2316
|
+
type: "modify",
|
|
2317
|
+
target_text: "",
|
|
2318
|
+
new_text: ins_text,
|
|
2319
|
+
comment: edit.comment,
|
|
2320
|
+
// Insert at the anchor token's start so the new run lands inside
|
|
2321
|
+
// the cell paragraph that get_insertion_anchor resolves there.
|
|
2322
|
+
_match_start_index: start_idx,
|
|
2323
|
+
_internal_op: "INSERTION",
|
|
2324
|
+
_active_mapper_ref: active_mapper,
|
|
2325
|
+
});
|
|
2326
|
+
} else if (edit.comment) {
|
|
2327
|
+
// Anchor target with empty effective new_text but a comment: attach
|
|
2328
|
+
// the comment to the cell paragraph.
|
|
2329
|
+
all_sub_edits.push({
|
|
2330
|
+
type: "modify",
|
|
2331
|
+
target_text: "",
|
|
2332
|
+
new_text: "",
|
|
2333
|
+
comment: edit.comment,
|
|
2334
|
+
_match_start_index: start_idx,
|
|
2335
|
+
_internal_op: "COMMENT_ONLY",
|
|
2336
|
+
_active_mapper_ref: active_mapper,
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2034
2342
|
if (is_regex && current_effective_new_text) {
|
|
2035
2343
|
try {
|
|
2036
|
-
current_effective_new_text = actual_doc_text.replace(
|
|
2344
|
+
current_effective_new_text = actual_doc_text.replace(
|
|
2345
|
+
new RegExp(edit.target_text),
|
|
2346
|
+
current_effective_new_text,
|
|
2347
|
+
);
|
|
2037
2348
|
} catch (e) {}
|
|
2038
2349
|
}
|
|
2039
2350
|
|
|
2040
|
-
const [edit_target_clean, edit_target_style] = this._parse_markdown_style(
|
|
2041
|
-
|
|
2351
|
+
const [edit_target_clean, edit_target_style] = this._parse_markdown_style(
|
|
2352
|
+
edit.target_text,
|
|
2353
|
+
);
|
|
2354
|
+
const [edit_new_clean, edit_new_style] = this._parse_markdown_style(
|
|
2355
|
+
current_effective_new_text,
|
|
2356
|
+
);
|
|
2042
2357
|
|
|
2043
2358
|
if (edit_target_style !== edit_new_style) {
|
|
2044
2359
|
const [actual_clean] = this._parse_markdown_style(actual_doc_text);
|
|
2045
2360
|
const final_target = actual_clean;
|
|
2046
2361
|
const final_new = edit_new_clean;
|
|
2047
|
-
const style_op =
|
|
2362
|
+
const style_op =
|
|
2363
|
+
final_target === final_new ? "STYLE_ONLY" : "STYLE_AND_TEXT";
|
|
2048
2364
|
const prefix_offset = actual_doc_text.indexOf(actual_clean);
|
|
2049
|
-
const effective_start_idx =
|
|
2050
|
-
|
|
2365
|
+
const effective_start_idx =
|
|
2366
|
+
start_idx + (prefix_offset !== -1 ? prefix_offset : 0);
|
|
2367
|
+
const resolved_style =
|
|
2368
|
+
edit_new_style !== null ? edit_new_style : "Normal";
|
|
2051
2369
|
|
|
2052
2370
|
all_sub_edits.push({
|
|
2053
2371
|
type: "modify",
|
|
@@ -2085,7 +2403,9 @@ export class RedlineEngine {
|
|
|
2085
2403
|
|
|
2086
2404
|
if (current_effective_new_text.startsWith(actual_doc_text)) {
|
|
2087
2405
|
effective_op = "INSERTION";
|
|
2088
|
-
final_new = current_effective_new_text.substring(
|
|
2406
|
+
final_new = current_effective_new_text.substring(
|
|
2407
|
+
actual_doc_text.length,
|
|
2408
|
+
);
|
|
2089
2409
|
effective_start_idx = start_idx + match_len;
|
|
2090
2410
|
} else {
|
|
2091
2411
|
const [prefix_len, suffix_len] = trim_common_context(
|
|
@@ -2172,7 +2492,10 @@ export class RedlineEngine {
|
|
|
2172
2492
|
while (end_p && end_p.tagName !== "w:p")
|
|
2173
2493
|
end_p = end_p.parentNode as Element;
|
|
2174
2494
|
if (start_p && end_p) {
|
|
2175
|
-
const ascend_to_paragraph_child = (
|
|
2495
|
+
const ascend_to_paragraph_child = (
|
|
2496
|
+
el: Element,
|
|
2497
|
+
p: Element,
|
|
2498
|
+
): Element => {
|
|
2176
2499
|
let cur: Element = el;
|
|
2177
2500
|
while (cur.parentNode && cur.parentNode !== p) {
|
|
2178
2501
|
cur = cur.parentNode as Element;
|
|
@@ -2182,7 +2505,12 @@ export class RedlineEngine {
|
|
|
2182
2505
|
const first_anchor = ascend_to_paragraph_child(first_el, start_p);
|
|
2183
2506
|
const last_anchor = ascend_to_paragraph_child(last_el, end_p);
|
|
2184
2507
|
if (start_p === end_p) {
|
|
2185
|
-
this._attach_comment(
|
|
2508
|
+
this._attach_comment(
|
|
2509
|
+
start_p,
|
|
2510
|
+
first_anchor,
|
|
2511
|
+
last_anchor,
|
|
2512
|
+
edit.comment,
|
|
2513
|
+
);
|
|
2186
2514
|
} else {
|
|
2187
2515
|
this._attach_comment_spanning(
|
|
2188
2516
|
start_p,
|