@adeu/core 1.9.0 → 1.10.1
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 +639 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -2
- package/dist/index.d.ts +15 -2
- package/dist/index.js +639 -105
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/comments.ts +33 -14
- package/src/consistency.test.ts +62 -4
- package/src/diff.ts +42 -6
- package/src/docx/dom.ts +2 -2
- package/src/engine.bugs.test.ts +38 -0
- package/src/engine.feedback.test.ts +144 -0
- package/src/engine.issue23.test.ts +511 -0
- package/src/engine.ts +614 -82
- package/src/sanitize/core.ts +1 -0
- package/src/sanitize/sanitize.test.ts +48 -6
- package/src/sanitize/transforms.ts +88 -1
package/src/engine.ts
CHANGED
|
@@ -247,6 +247,47 @@ export class RedlineEngine {
|
|
|
247
247
|
this.comments_manager = new CommentsManager(this.doc);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
private _check_punctuation_warning(target_text: string): string | null {
|
|
251
|
+
if (!target_text) return null;
|
|
252
|
+
if (target_text.includes("_") || target_text.includes("-")) {
|
|
253
|
+
return `Warning: target_text '${target_text}' contains tokenization-splitting punctuation ('_' or '-'). This can trigger mid-word splits in the diff engine. Consider using a longer plain-prose anchor.`;
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private _build_edit_context_previews(
|
|
259
|
+
edit: any,
|
|
260
|
+
): [string | null, string | null] {
|
|
261
|
+
if (edit.type !== "modify") return [null, null];
|
|
262
|
+
if (edit._resolved_proxy_edit) {
|
|
263
|
+
edit = edit._resolved_proxy_edit;
|
|
264
|
+
}
|
|
265
|
+
const start_idx = edit._resolved_start_idx;
|
|
266
|
+
if (start_idx === undefined || start_idx === null) return [null, null];
|
|
267
|
+
const target_text = edit.target_text || "";
|
|
268
|
+
const new_text = edit.new_text || "";
|
|
269
|
+
const length = target_text.length;
|
|
270
|
+
const active_mapper = edit._active_mapper_ref || this.mapper;
|
|
271
|
+
const full_text = active_mapper.full_text;
|
|
272
|
+
if (!full_text) return [null, null];
|
|
273
|
+
|
|
274
|
+
const before_start = Math.max(0, start_idx - 30);
|
|
275
|
+
const context_before = full_text.substring(before_start, start_idx);
|
|
276
|
+
const context_after = full_text.substring(
|
|
277
|
+
start_idx + length,
|
|
278
|
+
start_idx + length + 30,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const critic_markup = `${context_before}{--${target_text}--}{++${new_text}++}${context_after}`;
|
|
282
|
+
|
|
283
|
+
let clean_text = critic_markup;
|
|
284
|
+
clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
|
|
285
|
+
clean_text = clean_text.replace(/\{--.*?--\}/gs, "");
|
|
286
|
+
clean_text = clean_text.replace(/\{\+\+(.*?)\+\+\}/gs, "$1");
|
|
287
|
+
|
|
288
|
+
return [critic_markup, clean_text];
|
|
289
|
+
}
|
|
290
|
+
|
|
250
291
|
private _scan_existing_ids(): number {
|
|
251
292
|
let maxId = 0;
|
|
252
293
|
for (const tag of ["w:ins", "w:del"]) {
|
|
@@ -301,7 +342,7 @@ export class RedlineEngine {
|
|
|
301
342
|
for (const tag of ["w:t", "w:tab", "w:br"]) {
|
|
302
343
|
for (const child of findAllDescendants(p, tag)) {
|
|
303
344
|
if (tag === "w:t" && !child.textContent) continue;
|
|
304
|
-
|
|
345
|
+
|
|
305
346
|
let is_deleted = false;
|
|
306
347
|
let curr = child.parentNode as Element | null;
|
|
307
348
|
while (curr && curr !== p) {
|
|
@@ -351,35 +392,113 @@ export class RedlineEngine {
|
|
|
351
392
|
}
|
|
352
393
|
}
|
|
353
394
|
|
|
354
|
-
// Final pass:
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
395
|
+
// Final pass: completely eject all comments, anchors, and parts
|
|
396
|
+
for (const root_element of parts_to_process) {
|
|
397
|
+
for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
|
|
398
|
+
for (const el of findAllDescendants(root_element, tag)) {
|
|
399
|
+
el.parentNode?.removeChild(el);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const refs = findAllDescendants(root_element, "w:commentReference");
|
|
404
|
+
for (const ref of refs) {
|
|
405
|
+
const parent = ref.parentNode as Element | null;
|
|
406
|
+
if (parent) {
|
|
407
|
+
if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
|
|
408
|
+
const nonRprChildren = Array.from(parent.childNodes).filter(
|
|
409
|
+
(c) =>
|
|
410
|
+
c.nodeType === 1 &&
|
|
411
|
+
(c as Element).tagName !== "w:rPr" &&
|
|
412
|
+
(c as Element).tagName !== "rPr",
|
|
413
|
+
);
|
|
414
|
+
if (nonRprChildren.length <= 1) {
|
|
415
|
+
parent.parentNode?.removeChild(parent);
|
|
416
|
+
} else {
|
|
417
|
+
parent.removeChild(ref);
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
parent.removeChild(ref);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
364
423
|
}
|
|
365
424
|
}
|
|
366
425
|
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
426
|
+
const pkg = this.doc.pkg;
|
|
427
|
+
const comment_partnames = new Set<string>();
|
|
428
|
+
for (const part of pkg.parts) {
|
|
429
|
+
if (part.partname.toLowerCase().includes("comments")) {
|
|
430
|
+
comment_partnames.add(part.partname);
|
|
431
|
+
const withSlash = part.partname.startsWith("/")
|
|
432
|
+
? part.partname
|
|
433
|
+
: "/" + part.partname;
|
|
434
|
+
const withoutSlash = part.partname.startsWith("/")
|
|
435
|
+
? part.partname.substring(1)
|
|
436
|
+
: part.partname;
|
|
437
|
+
comment_partnames.add(withSlash);
|
|
438
|
+
comment_partnames.add(withoutSlash);
|
|
376
439
|
}
|
|
377
440
|
}
|
|
378
441
|
|
|
379
|
-
|
|
380
|
-
|
|
442
|
+
if (comment_partnames.size > 0) {
|
|
443
|
+
// Sever relationships referencing comments
|
|
444
|
+
for (const part of pkg.parts) {
|
|
445
|
+
if (part.partname.endsWith(".rels")) {
|
|
446
|
+
const rels = findAllDescendants(part._element, "Relationship");
|
|
447
|
+
const toRemove: Element[] = [];
|
|
448
|
+
for (const rel of rels) {
|
|
449
|
+
const target = rel.getAttribute("Target") || "";
|
|
450
|
+
if (target.toLowerCase().includes("comments")) {
|
|
451
|
+
toRemove.push(rel);
|
|
452
|
+
|
|
453
|
+
const sourcePath = part.partname
|
|
454
|
+
.replace("/_rels/", "/")
|
|
455
|
+
.replace(".rels", "");
|
|
456
|
+
const sourcePart = pkg.getPartByPath(sourcePath);
|
|
457
|
+
if (sourcePart) {
|
|
458
|
+
const relId = rel.getAttribute("Id");
|
|
459
|
+
if (relId) sourcePart.rels.delete(relId);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
for (const relEl of toRemove) {
|
|
464
|
+
relEl.parentNode?.removeChild(relEl);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Remove overrides from [Content_Types].xml
|
|
470
|
+
const ctPart = pkg.getPartByPath("[Content_Types].xml");
|
|
471
|
+
if (ctPart) {
|
|
472
|
+
const overrides = findAllDescendants(ctPart._element, "Override");
|
|
473
|
+
const toRemove: Element[] = [];
|
|
474
|
+
for (const override of overrides) {
|
|
475
|
+
const partName = override.getAttribute("PartName") || "";
|
|
476
|
+
if (
|
|
477
|
+
comment_partnames.has(partName) ||
|
|
478
|
+
partName.toLowerCase().includes("comments")
|
|
479
|
+
) {
|
|
480
|
+
toRemove.push(override);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
for (const overrideEl of toRemove) {
|
|
484
|
+
overrideEl.parentNode?.removeChild(overrideEl);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Remove comment parts from pkg.parts
|
|
489
|
+
pkg.parts = pkg.parts.filter(
|
|
490
|
+
(p) => !p.partname.toLowerCase().includes("comments"),
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
// Remove comment files from pkg.unzipped
|
|
494
|
+
for (const key of Object.keys(pkg.unzipped)) {
|
|
495
|
+
if (key.toLowerCase().includes("comments")) {
|
|
496
|
+
delete pkg.unzipped[key];
|
|
497
|
+
}
|
|
498
|
+
}
|
|
381
499
|
}
|
|
382
500
|
}
|
|
501
|
+
|
|
383
502
|
private _getNextId(): string {
|
|
384
503
|
this.current_id++;
|
|
385
504
|
return this.current_id.toString();
|
|
@@ -505,7 +624,9 @@ export class RedlineEngine {
|
|
|
505
624
|
end_p.appendChild(range_end);
|
|
506
625
|
end_p.appendChild(ref_run);
|
|
507
626
|
}
|
|
508
|
-
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
509
630
|
* Inserts `text` as one or more tracked paragraphs anchored relative to
|
|
510
631
|
* either an existing run or a paragraph. Returns:
|
|
511
632
|
* { first_node, last_p, last_ins, used_block_mode }
|
|
@@ -734,8 +855,19 @@ export class RedlineEngine {
|
|
|
734
855
|
const anchor_rPr = findChild(anchor_run._element, "w:rPr");
|
|
735
856
|
if (anchor_rPr) {
|
|
736
857
|
const clone = anchor_rPr.cloneNode(true) as Element;
|
|
737
|
-
// Strip vanish / strike to avoid invisible inserts
|
|
738
|
-
|
|
858
|
+
// Strip vanish / strike to avoid invisible inserts, and emphasis
|
|
859
|
+
// (bold/italic) so inserted replacement text does not silently
|
|
860
|
+
// inherit the anchor run's character formatting (BUG-23-2). Explicit
|
|
861
|
+
// markdown emphasis is re-applied per-segment via _apply_run_props.
|
|
862
|
+
for (const tag of [
|
|
863
|
+
"w:vanish",
|
|
864
|
+
"w:strike",
|
|
865
|
+
"w:dstrike",
|
|
866
|
+
"w:i",
|
|
867
|
+
"w:iCs",
|
|
868
|
+
"w:b",
|
|
869
|
+
"w:bCs",
|
|
870
|
+
]) {
|
|
739
871
|
const found = findChild(clone, tag);
|
|
740
872
|
if (found) clone.removeChild(found);
|
|
741
873
|
}
|
|
@@ -750,6 +882,7 @@ export class RedlineEngine {
|
|
|
750
882
|
}
|
|
751
883
|
return ins;
|
|
752
884
|
}
|
|
885
|
+
|
|
753
886
|
private _parse_markdown_style(text: string): [string, string | null] {
|
|
754
887
|
const stripped_text = text.trimStart();
|
|
755
888
|
|
|
@@ -855,6 +988,7 @@ export class RedlineEngine {
|
|
|
855
988
|
}
|
|
856
989
|
}
|
|
857
990
|
}
|
|
991
|
+
|
|
858
992
|
/**
|
|
859
993
|
* Replaces (or creates) a paragraph's <w:pPr> with a single <w:pStyle> entry
|
|
860
994
|
* pointing at `style_name`. Strips any existing pPr to avoid layering a new
|
|
@@ -884,6 +1018,7 @@ export class RedlineEngine {
|
|
|
884
1018
|
// pPr is the first child of <w:p> per OOXML schema.
|
|
885
1019
|
p_element.insertBefore(pPr, p_element.firstChild);
|
|
886
1020
|
}
|
|
1021
|
+
|
|
887
1022
|
private _anchor_reply_comment(parent_id: string, new_id: string) {
|
|
888
1023
|
const docEl = this.doc.part._element.ownerDocument!;
|
|
889
1024
|
|
|
@@ -938,6 +1073,7 @@ export class RedlineEngine {
|
|
|
938
1073
|
|
|
939
1074
|
insertAfter(ref_run, new_end);
|
|
940
1075
|
}
|
|
1076
|
+
|
|
941
1077
|
private _clean_wrapping_comments(element: Element) {
|
|
942
1078
|
let first_node: Element = element;
|
|
943
1079
|
while (true) {
|
|
@@ -1046,6 +1182,7 @@ export class RedlineEngine {
|
|
|
1046
1182
|
}
|
|
1047
1183
|
}
|
|
1048
1184
|
}
|
|
1185
|
+
|
|
1049
1186
|
public validate_edits(edits: any[]): string[] {
|
|
1050
1187
|
const errors: string[] = [];
|
|
1051
1188
|
if (!this.mapper.full_text) this.mapper["_build_map"]();
|
|
@@ -1066,6 +1203,24 @@ export class RedlineEngine {
|
|
|
1066
1203
|
if (matches.length > 0) activeText = this.clean_mapper.full_text;
|
|
1067
1204
|
}
|
|
1068
1205
|
|
|
1206
|
+
// BUG-23-5: a copy of the target that lives entirely inside a tracked
|
|
1207
|
+
// deletion (<w:del>) is not a live, editable occurrence and must not
|
|
1208
|
+
// count toward ambiguity. Drop matches whose overlapping real text is
|
|
1209
|
+
// exclusively deleted. Only applies to the raw mapper (the clean mapper
|
|
1210
|
+
// already omits deleted text).
|
|
1211
|
+
if (activeText === this.mapper.full_text && matches.length > 1) {
|
|
1212
|
+
const liveMatches = matches.filter(([start, length]) => {
|
|
1213
|
+
const realSpans = this.mapper.spans.filter(
|
|
1214
|
+
(s) => s.run !== null && s.end > start && s.start < start + length,
|
|
1215
|
+
);
|
|
1216
|
+
if (realSpans.length === 0) return true; // virtual-only; keep
|
|
1217
|
+
// Keep only if at least one overlapping real span is live (not
|
|
1218
|
+
// part of a tracked deletion).
|
|
1219
|
+
return realSpans.some((s) => !s.del_id);
|
|
1220
|
+
});
|
|
1221
|
+
if (liveMatches.length > 0) matches = liveMatches;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1069
1224
|
if (matches.length === 0) {
|
|
1070
1225
|
errors.push(
|
|
1071
1226
|
`- Edit ${i + 1} Failed: Target text not found in document:\n "${edit.target_text}"`,
|
|
@@ -1085,6 +1240,46 @@ export class RedlineEngine {
|
|
|
1085
1240
|
);
|
|
1086
1241
|
}
|
|
1087
1242
|
|
|
1243
|
+
// BUG-23-4: when the effective (context-trimmed) target spans a
|
|
1244
|
+
// paragraph boundary with real body text on BOTH sides, we must reject
|
|
1245
|
+
// the modification to prevent silent corruption of the paragraph structure.
|
|
1246
|
+
if (matches.length === 1) {
|
|
1247
|
+
const [m_start, m_len] = matches[0];
|
|
1248
|
+
const matched = activeText.substring(m_start, m_start + m_len);
|
|
1249
|
+
const [pfx, sfx] = trim_common_context(matched, edit.new_text || "");
|
|
1250
|
+
const t_end = matched.length - sfx;
|
|
1251
|
+
const final_target = matched.substring(pfx, t_end);
|
|
1252
|
+
const final_new = (edit.new_text || "").substring(
|
|
1253
|
+
pfx,
|
|
1254
|
+
(edit.new_text || "").length - sfx,
|
|
1255
|
+
);
|
|
1256
|
+
if (final_target.includes("\n\n")) {
|
|
1257
|
+
if (final_new.includes("\n\n")) {
|
|
1258
|
+
const parts = matched.split("\n\n");
|
|
1259
|
+
if (
|
|
1260
|
+
parts.length >= 2 &&
|
|
1261
|
+
parts[0].trim() !== "" &&
|
|
1262
|
+
parts[parts.length - 1].trim() !== ""
|
|
1263
|
+
) {
|
|
1264
|
+
errors.push(
|
|
1265
|
+
`- 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.`,
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
} else {
|
|
1269
|
+
const parts = final_target.split("\n\n");
|
|
1270
|
+
if (
|
|
1271
|
+
parts.length >= 2 &&
|
|
1272
|
+
parts[0].trim() !== "" &&
|
|
1273
|
+
parts[parts.length - 1].trim() !== ""
|
|
1274
|
+
) {
|
|
1275
|
+
errors.push(
|
|
1276
|
+
`- 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.`,
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1088
1283
|
for (const [start, length] of matches) {
|
|
1089
1284
|
const spans = this.mapper.spans.filter(
|
|
1090
1285
|
(s) => s.end > start && s.start < start + length,
|
|
@@ -1111,6 +1306,7 @@ export class RedlineEngine {
|
|
|
1111
1306
|
}
|
|
1112
1307
|
return errors;
|
|
1113
1308
|
}
|
|
1309
|
+
|
|
1114
1310
|
public validate_review_actions(actions: any[]): string[] {
|
|
1115
1311
|
const errors: string[] = [];
|
|
1116
1312
|
for (let i = 0; i < actions.length; i++) {
|
|
@@ -1151,7 +1347,41 @@ export class RedlineEngine {
|
|
|
1151
1347
|
}
|
|
1152
1348
|
return errors;
|
|
1153
1349
|
}
|
|
1154
|
-
|
|
1350
|
+
|
|
1351
|
+
public process_batch(
|
|
1352
|
+
changes: DocumentChange[],
|
|
1353
|
+
dry_run: boolean = false,
|
|
1354
|
+
): any {
|
|
1355
|
+
if (dry_run) {
|
|
1356
|
+
const baselines = new Map<any, Element>();
|
|
1357
|
+
for (const part of this.doc.pkg.parts) {
|
|
1358
|
+
if (part._element) {
|
|
1359
|
+
baselines.set(part, part._element.cloneNode(true) as Element);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
try {
|
|
1363
|
+
return this._process_batch_internal(changes, true);
|
|
1364
|
+
} finally {
|
|
1365
|
+
for (const [part, originalEl] of baselines.entries()) {
|
|
1366
|
+
const doc = part._element.ownerDocument;
|
|
1367
|
+
if (doc && doc.documentElement) {
|
|
1368
|
+
doc.replaceChild(originalEl, doc.documentElement);
|
|
1369
|
+
}
|
|
1370
|
+
part._element = originalEl;
|
|
1371
|
+
}
|
|
1372
|
+
this.mapper = new DocumentMapper(this.doc);
|
|
1373
|
+
this.comments_manager = new CommentsManager(this.doc);
|
|
1374
|
+
this.clean_mapper = null;
|
|
1375
|
+
}
|
|
1376
|
+
} else {
|
|
1377
|
+
return this._process_batch_internal(changes, false);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
private _process_batch_internal(
|
|
1382
|
+
changes: DocumentChange[],
|
|
1383
|
+
dry_run_mode: boolean = false,
|
|
1384
|
+
): any {
|
|
1155
1385
|
this.skipped_details = [];
|
|
1156
1386
|
const actions = changes.filter((c) =>
|
|
1157
1387
|
["accept", "reject", "reply"].includes(c.type),
|
|
@@ -1160,37 +1390,131 @@ export class RedlineEngine {
|
|
|
1160
1390
|
(c) => !["accept", "reject", "reply"].includes(c.type),
|
|
1161
1391
|
);
|
|
1162
1392
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1393
|
+
// BUG-7: Unified single-pass validation in wet-run / standard mode
|
|
1394
|
+
if (!dry_run_mode) {
|
|
1395
|
+
const all_errors: string[] = [];
|
|
1396
|
+
if (actions.length > 0) {
|
|
1397
|
+
all_errors.push(...this.validate_review_actions(actions));
|
|
1398
|
+
}
|
|
1399
|
+
if (edits.length > 0) {
|
|
1400
|
+
all_errors.push(...this.validate_edits(edits));
|
|
1401
|
+
}
|
|
1402
|
+
if (all_errors.length > 0) {
|
|
1403
|
+
throw new BatchValidationError(all_errors);
|
|
1404
|
+
}
|
|
1405
|
+
} else {
|
|
1406
|
+
if (actions.length > 0) {
|
|
1407
|
+
const action_errors = this.validate_review_actions(actions);
|
|
1408
|
+
if (action_errors.length > 0) {
|
|
1409
|
+
throw new BatchValidationError(action_errors);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1174
1412
|
}
|
|
1175
1413
|
|
|
1176
|
-
let applied_actions = 0
|
|
1177
|
-
|
|
1414
|
+
let applied_actions = 0;
|
|
1415
|
+
let skipped_actions = 0;
|
|
1178
1416
|
if (actions.length > 0) {
|
|
1179
1417
|
const res = this.apply_review_actions(actions);
|
|
1180
1418
|
applied_actions = res[0];
|
|
1181
1419
|
skipped_actions = res[1];
|
|
1420
|
+
if (skipped_actions > 0) {
|
|
1421
|
+
throw new BatchValidationError(this.skipped_details);
|
|
1422
|
+
}
|
|
1182
1423
|
if (applied_actions > 0) {
|
|
1183
1424
|
this.mapper["_build_map"]();
|
|
1184
1425
|
if (this.clean_mapper) this.clean_mapper["_build_map"]();
|
|
1185
1426
|
}
|
|
1186
1427
|
}
|
|
1187
1428
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1429
|
+
const edits_reports: any[] = [];
|
|
1430
|
+
let applied_edits = 0;
|
|
1431
|
+
let skipped_edits = 0;
|
|
1432
|
+
|
|
1190
1433
|
if (edits.length > 0) {
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1434
|
+
if (dry_run_mode) {
|
|
1435
|
+
for (const edit of edits) {
|
|
1436
|
+
const single_errors = this.validate_edits([edit]);
|
|
1437
|
+
const warning = this._check_punctuation_warning(
|
|
1438
|
+
(edit as any).target_text || "",
|
|
1439
|
+
);
|
|
1440
|
+
if (single_errors.length > 0) {
|
|
1441
|
+
skipped_edits++;
|
|
1442
|
+
edits_reports.push({
|
|
1443
|
+
status: "failed",
|
|
1444
|
+
target_text: (edit as any).target_text || "",
|
|
1445
|
+
new_text: (edit as any).new_text || "",
|
|
1446
|
+
warning: warning,
|
|
1447
|
+
error: single_errors[0],
|
|
1448
|
+
critic_markup: null,
|
|
1449
|
+
clean_text: null,
|
|
1450
|
+
});
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
const res = this.apply_edits([edit]);
|
|
1454
|
+
const applied = res[0];
|
|
1455
|
+
if (applied > 0) {
|
|
1456
|
+
applied_edits++;
|
|
1457
|
+
const previews = this._build_edit_context_previews(edit);
|
|
1458
|
+
edits_reports.push({
|
|
1459
|
+
status: "applied",
|
|
1460
|
+
target_text: (edit as any).target_text || "",
|
|
1461
|
+
new_text: (edit as any).new_text || "",
|
|
1462
|
+
warning: warning,
|
|
1463
|
+
error: null,
|
|
1464
|
+
critic_markup: previews[0],
|
|
1465
|
+
clean_text: previews[1],
|
|
1466
|
+
});
|
|
1467
|
+
} else {
|
|
1468
|
+
skipped_edits++;
|
|
1469
|
+
const error_msg =
|
|
1470
|
+
this.skipped_details.length > 0
|
|
1471
|
+
? this.skipped_details[this.skipped_details.length - 1]
|
|
1472
|
+
: "Failed to apply edit";
|
|
1473
|
+
edits_reports.push({
|
|
1474
|
+
status: "failed",
|
|
1475
|
+
target_text: (edit as any).target_text || "",
|
|
1476
|
+
new_text: (edit as any).new_text || "",
|
|
1477
|
+
warning: warning,
|
|
1478
|
+
error: error_msg,
|
|
1479
|
+
critic_markup: null,
|
|
1480
|
+
clean_text: null,
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
} else {
|
|
1485
|
+
const errors = this.validate_edits(edits);
|
|
1486
|
+
if (errors.length > 0) {
|
|
1487
|
+
throw new BatchValidationError(errors);
|
|
1488
|
+
}
|
|
1489
|
+
const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
|
|
1490
|
+
const res = this.apply_edits(cloned_edits);
|
|
1491
|
+
applied_edits = res[0];
|
|
1492
|
+
skipped_edits = res[1];
|
|
1493
|
+
|
|
1494
|
+
for (const edit of cloned_edits) {
|
|
1495
|
+
const success = (edit as any)._applied_status || false;
|
|
1496
|
+
const error_msg = (edit as any)._error_msg || null;
|
|
1497
|
+
const warning = this._check_punctuation_warning(
|
|
1498
|
+
(edit as any).target_text || "",
|
|
1499
|
+
);
|
|
1500
|
+
let critic_markup = null;
|
|
1501
|
+
let clean_text = null;
|
|
1502
|
+
if (success) {
|
|
1503
|
+
const previews = this._build_edit_context_previews(edit);
|
|
1504
|
+
critic_markup = previews[0];
|
|
1505
|
+
clean_text = previews[1];
|
|
1506
|
+
}
|
|
1507
|
+
edits_reports.push({
|
|
1508
|
+
status: success ? "applied" : "failed",
|
|
1509
|
+
target_text: (edit as any).target_text || "",
|
|
1510
|
+
new_text: (edit as any).new_text || "",
|
|
1511
|
+
warning: warning,
|
|
1512
|
+
error: error_msg,
|
|
1513
|
+
critic_markup: critic_markup,
|
|
1514
|
+
clean_text: clean_text,
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1194
1518
|
}
|
|
1195
1519
|
|
|
1196
1520
|
return {
|
|
@@ -1199,6 +1523,9 @@ export class RedlineEngine {
|
|
|
1199
1523
|
edits_applied: applied_edits,
|
|
1200
1524
|
edits_skipped: skipped_edits,
|
|
1201
1525
|
skipped_details: this.skipped_details,
|
|
1526
|
+
edits: edits_reports,
|
|
1527
|
+
engine: "node",
|
|
1528
|
+
version: "1.10.0",
|
|
1202
1529
|
};
|
|
1203
1530
|
}
|
|
1204
1531
|
|
|
@@ -1207,47 +1534,90 @@ export class RedlineEngine {
|
|
|
1207
1534
|
let skipped = 0;
|
|
1208
1535
|
const resolved_edits: [any, string | null][] = [];
|
|
1209
1536
|
|
|
1537
|
+
for (const edit of edits) {
|
|
1538
|
+
edit._applied_status = false;
|
|
1539
|
+
edit._error_msg = null;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1210
1542
|
for (const edit of edits) {
|
|
1211
1543
|
if (
|
|
1544
|
+
edit._resolved_start_idx !== undefined &&
|
|
1545
|
+
edit._resolved_start_idx !== null
|
|
1546
|
+
) {
|
|
1547
|
+
resolved_edits.push([edit, edit.new_text || null]);
|
|
1548
|
+
} else if (
|
|
1212
1549
|
edit._match_start_index !== undefined &&
|
|
1213
1550
|
edit._match_start_index !== null
|
|
1214
1551
|
) {
|
|
1552
|
+
edit._resolved_start_idx = edit._match_start_index;
|
|
1215
1553
|
resolved_edits.push([edit, edit.new_text || null]);
|
|
1216
1554
|
} else if (edit.type === "insert_row" || edit.type === "delete_row") {
|
|
1217
|
-
|
|
1218
|
-
if (
|
|
1219
|
-
|
|
1555
|
+
let matches = this.mapper.find_all_match_indices(edit.target_text);
|
|
1556
|
+
if (matches.length === 0) {
|
|
1557
|
+
if (!this.clean_mapper) {
|
|
1558
|
+
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
1559
|
+
}
|
|
1560
|
+
matches = this.clean_mapper.find_all_match_indices(edit.target_text);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
if (matches.length > 0) {
|
|
1564
|
+
edit._resolved_start_idx = matches[0][0];
|
|
1220
1565
|
resolved_edits.push([edit, null]);
|
|
1221
1566
|
} else {
|
|
1222
1567
|
skipped++;
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1568
|
+
edit._applied_status = false;
|
|
1569
|
+
const target_snippet = (edit.target_text || "")
|
|
1570
|
+
.trim()
|
|
1571
|
+
.substring(0, 40);
|
|
1572
|
+
const msg = `- Failed to locate row target: '${target_snippet}...'`;
|
|
1573
|
+
this.skipped_details.push(msg);
|
|
1574
|
+
edit._error_msg = msg;
|
|
1226
1575
|
}
|
|
1227
1576
|
} else {
|
|
1228
1577
|
const resolved = this._pre_resolve_heuristic_edit(edit);
|
|
1229
1578
|
if (resolved) {
|
|
1230
1579
|
if (Array.isArray(resolved)) {
|
|
1231
|
-
for (const r of resolved)
|
|
1580
|
+
for (const r of resolved) {
|
|
1581
|
+
r._resolved_start_idx = r._match_start_index;
|
|
1582
|
+
r._parent_edit_ref = edit;
|
|
1583
|
+
if (
|
|
1584
|
+
edit._resolved_start_idx === undefined ||
|
|
1585
|
+
edit._resolved_start_idx === null
|
|
1586
|
+
) {
|
|
1587
|
+
edit._resolved_start_idx = r._resolved_start_idx;
|
|
1588
|
+
}
|
|
1589
|
+
if (!edit._resolved_proxy_edit) {
|
|
1590
|
+
edit._resolved_proxy_edit = r;
|
|
1591
|
+
}
|
|
1592
|
+
resolved_edits.push([r, r.new_text]);
|
|
1593
|
+
}
|
|
1232
1594
|
} else {
|
|
1595
|
+
resolved._resolved_start_idx = resolved._match_start_index;
|
|
1596
|
+
resolved._parent_edit_ref = edit;
|
|
1597
|
+
edit._resolved_start_idx = resolved._resolved_start_idx;
|
|
1598
|
+
edit._resolved_proxy_edit = resolved;
|
|
1233
1599
|
resolved_edits.push([resolved, (resolved as any).new_text]);
|
|
1234
1600
|
}
|
|
1235
1601
|
} else {
|
|
1236
1602
|
skipped++;
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
);
|
|
1603
|
+
edit._applied_status = false;
|
|
1604
|
+
const display_text = edit.target_text || "insertion";
|
|
1605
|
+
const target_snippet = display_text.trim().substring(0, 40);
|
|
1606
|
+
const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
|
|
1607
|
+
this.skipped_details.push(msg);
|
|
1608
|
+
edit._error_msg = msg;
|
|
1240
1609
|
}
|
|
1241
1610
|
}
|
|
1242
1611
|
}
|
|
1243
1612
|
|
|
1244
1613
|
resolved_edits.sort(
|
|
1245
|
-
(a, b) =>
|
|
1614
|
+
(a, b) =>
|
|
1615
|
+
(b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0),
|
|
1246
1616
|
);
|
|
1247
1617
|
const occupied_ranges: [number, number][] = [];
|
|
1248
1618
|
|
|
1249
1619
|
for (const [edit, orig_new] of resolved_edits) {
|
|
1250
|
-
const start = edit.
|
|
1620
|
+
const start = edit._resolved_start_idx || 0;
|
|
1251
1621
|
const end = start + (edit.target_text ? edit.target_text.length : 0);
|
|
1252
1622
|
|
|
1253
1623
|
const overlaps = occupied_ranges.some(
|
|
@@ -1255,9 +1625,17 @@ export class RedlineEngine {
|
|
|
1255
1625
|
);
|
|
1256
1626
|
if (overlaps) {
|
|
1257
1627
|
skipped++;
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1628
|
+
const display_text = edit.target_text || "insertion";
|
|
1629
|
+
const target_snippet = display_text.trim().substring(0, 40);
|
|
1630
|
+
const msg = `- Skipped overlapping edit targeting: '${target_snippet}...'`;
|
|
1631
|
+
this.skipped_details.push(msg);
|
|
1632
|
+
edit._applied_status = false;
|
|
1633
|
+
edit._error_msg = msg;
|
|
1634
|
+
const parent = edit._parent_edit_ref;
|
|
1635
|
+
if (parent) {
|
|
1636
|
+
parent._applied_status = false;
|
|
1637
|
+
parent._error_msg = msg;
|
|
1638
|
+
}
|
|
1261
1639
|
continue;
|
|
1262
1640
|
}
|
|
1263
1641
|
|
|
@@ -1271,11 +1649,26 @@ export class RedlineEngine {
|
|
|
1271
1649
|
if (success) {
|
|
1272
1650
|
applied++;
|
|
1273
1651
|
occupied_ranges.push([start, end]);
|
|
1652
|
+
edit._applied_status = true;
|
|
1653
|
+
const parent = edit._parent_edit_ref;
|
|
1654
|
+
if (parent) {
|
|
1655
|
+
parent._applied_status = true;
|
|
1656
|
+
}
|
|
1274
1657
|
} else {
|
|
1275
1658
|
skipped++;
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1659
|
+
const display_text = edit.target_text || "insertion";
|
|
1660
|
+
const target_snippet = display_text.trim().substring(0, 40);
|
|
1661
|
+
const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
|
|
1662
|
+
this.skipped_details.push(msg);
|
|
1663
|
+
edit._applied_status = false;
|
|
1664
|
+
edit._error_msg = msg;
|
|
1665
|
+
const parent = edit._parent_edit_ref;
|
|
1666
|
+
if (parent) {
|
|
1667
|
+
if (!parent._applied_status) {
|
|
1668
|
+
parent._applied_status = false;
|
|
1669
|
+
parent._error_msg = msg;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1279
1672
|
}
|
|
1280
1673
|
}
|
|
1281
1674
|
|
|
@@ -1378,7 +1771,11 @@ export class RedlineEngine {
|
|
|
1378
1771
|
}
|
|
1379
1772
|
|
|
1380
1773
|
private _apply_table_edit(edit: any, rebuild_map: boolean): boolean {
|
|
1381
|
-
const start_idx =
|
|
1774
|
+
const start_idx =
|
|
1775
|
+
edit._resolved_start_idx !== undefined &&
|
|
1776
|
+
edit._resolved_start_idx !== null
|
|
1777
|
+
? edit._resolved_start_idx
|
|
1778
|
+
: edit._match_start_index || 0;
|
|
1382
1779
|
const [anchor_run, anchor_para] = this.mapper.get_insertion_anchor(
|
|
1383
1780
|
start_idx,
|
|
1384
1781
|
rebuild_map,
|
|
@@ -1427,10 +1824,33 @@ export class RedlineEngine {
|
|
|
1427
1824
|
return false;
|
|
1428
1825
|
}
|
|
1429
1826
|
|
|
1827
|
+
/**
|
|
1828
|
+
* Returns the first match of `target_text` in the raw mapper that is NOT
|
|
1829
|
+
* entirely contained within a tracked deletion (<w:del>). Tracked-deleted
|
|
1830
|
+
* copies are not live, editable text, so an edit must resolve to a live
|
|
1831
|
+
* occurrence even when a dead copy appears earlier in the document
|
|
1832
|
+
* (BUG-23-5). Falls back to the plain first match when no live copy is
|
|
1833
|
+
* found (e.g. fuzzy/normalized matches the span filter cannot align).
|
|
1834
|
+
*/
|
|
1835
|
+
private _first_live_match(target_text: string): [number, number] {
|
|
1836
|
+
const all = this.mapper.find_all_match_indices(target_text);
|
|
1837
|
+
if (all.length <= 1) {
|
|
1838
|
+
return this.mapper.find_match_index(target_text);
|
|
1839
|
+
}
|
|
1840
|
+
for (const [start, length] of all) {
|
|
1841
|
+
const realSpans = this.mapper.spans.filter(
|
|
1842
|
+
(s) => s.run !== null && s.end > start && s.start < start + length,
|
|
1843
|
+
);
|
|
1844
|
+
if (realSpans.length === 0) return [start, length];
|
|
1845
|
+
if (realSpans.some((s) => !s.del_id)) return [start, length];
|
|
1846
|
+
}
|
|
1847
|
+
return this.mapper.find_match_index(target_text);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1430
1850
|
private _pre_resolve_heuristic_edit(edit: any): any {
|
|
1431
1851
|
if (!edit.target_text) return null;
|
|
1432
1852
|
|
|
1433
|
-
let [start_idx, match_len] = this.
|
|
1853
|
+
let [start_idx, match_len] = this._first_live_match(edit.target_text);
|
|
1434
1854
|
let use_clean_map = false;
|
|
1435
1855
|
|
|
1436
1856
|
if (start_idx === -1) {
|
|
@@ -1510,7 +1930,11 @@ export class RedlineEngine {
|
|
|
1510
1930
|
): boolean {
|
|
1511
1931
|
let op = edit._internal_op;
|
|
1512
1932
|
const active_mapper = edit._active_mapper_ref || this.mapper;
|
|
1513
|
-
const start_idx =
|
|
1933
|
+
const start_idx =
|
|
1934
|
+
edit._resolved_start_idx !== undefined &&
|
|
1935
|
+
edit._resolved_start_idx !== null
|
|
1936
|
+
? edit._resolved_start_idx
|
|
1937
|
+
: edit._match_start_index || 0;
|
|
1514
1938
|
const length = edit.target_text ? edit.target_text.length : 0;
|
|
1515
1939
|
|
|
1516
1940
|
const del_id = ["DELETION", "MODIFICATION"].includes(op)
|
|
@@ -1576,6 +2000,90 @@ export class RedlineEngine {
|
|
|
1576
2000
|
);
|
|
1577
2001
|
if (!anchor_run && !anchor_para) return false;
|
|
1578
2002
|
|
|
2003
|
+
// BUG-23-3: a prefix insertion whose new_text ends in a paragraph break
|
|
2004
|
+
// (e.g. "Summary\n\n" inserted before "Conclusion") must become a NEW
|
|
2005
|
+
// paragraph placed BEFORE the anchor paragraph, not inline text merged
|
|
2006
|
+
// into a neighbouring paragraph. _track_insert_multiline drops the
|
|
2007
|
+
// trailing break and inlines the remainder, which both loses the
|
|
2008
|
+
// paragraph boundary and mis-orders the content. Handle this case here.
|
|
2009
|
+
const _bug233_new = edit.new_text || "";
|
|
2010
|
+
const _bug233_trailing_break = /\n\s*$/.test(_bug233_new);
|
|
2011
|
+
let _bug233_target_para: Element | null = null;
|
|
2012
|
+
{
|
|
2013
|
+
const startingSpans = active_mapper.spans.filter(
|
|
2014
|
+
(s: TextSpan) => s.paragraph !== null && s.start === start_idx,
|
|
2015
|
+
);
|
|
2016
|
+
if (startingSpans.length > 0 && startingSpans[0].paragraph) {
|
|
2017
|
+
_bug233_target_para = startingSpans[0].paragraph._element;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
if (
|
|
2021
|
+
_bug233_trailing_break &&
|
|
2022
|
+
_bug233_target_para &&
|
|
2023
|
+
_bug233_target_para.parentNode
|
|
2024
|
+
) {
|
|
2025
|
+
const body = _bug233_target_para.parentNode as Element;
|
|
2026
|
+
const xmlDoc = this.doc.part._element.ownerDocument!;
|
|
2027
|
+
const lines = _bug233_new
|
|
2028
|
+
.split(/[\r\n]+/)
|
|
2029
|
+
.filter((l: string) => l !== "");
|
|
2030
|
+
let firstNew: Element | null = null;
|
|
2031
|
+
let lastNew: Element | null = null;
|
|
2032
|
+
let lastIns: Element | null = null;
|
|
2033
|
+
for (const raw_line of lines) {
|
|
2034
|
+
const [clean_text, style_name] = this._parse_markdown_style(raw_line);
|
|
2035
|
+
const new_p = xmlDoc.createElement("w:p");
|
|
2036
|
+
if (style_name) {
|
|
2037
|
+
this._set_paragraph_style(new_p, style_name);
|
|
2038
|
+
} else {
|
|
2039
|
+
const existing_pPr = findChild(_bug233_target_para, "w:pPr");
|
|
2040
|
+
if (existing_pPr) new_p.appendChild(existing_pPr.cloneNode(true));
|
|
2041
|
+
}
|
|
2042
|
+
let pPr = findChild(new_p, "w:pPr");
|
|
2043
|
+
if (!pPr) {
|
|
2044
|
+
pPr = xmlDoc.createElement("w:pPr");
|
|
2045
|
+
new_p.insertBefore(pPr, new_p.firstChild);
|
|
2046
|
+
}
|
|
2047
|
+
let rPr = findChild(pPr, "w:rPr");
|
|
2048
|
+
if (!rPr) {
|
|
2049
|
+
rPr = xmlDoc.createElement("w:rPr");
|
|
2050
|
+
pPr.appendChild(rPr);
|
|
2051
|
+
}
|
|
2052
|
+
rPr.appendChild(this._create_track_change_tag("w:ins", "", ins_id!));
|
|
2053
|
+
const content_ins = this._build_tracked_ins_for_line(
|
|
2054
|
+
clean_text,
|
|
2055
|
+
anchor_run,
|
|
2056
|
+
ins_id!,
|
|
2057
|
+
xmlDoc,
|
|
2058
|
+
);
|
|
2059
|
+
if (content_ins) new_p.appendChild(content_ins);
|
|
2060
|
+
body.insertBefore(new_p, _bug233_target_para);
|
|
2061
|
+
if (!firstNew) firstNew = new_p;
|
|
2062
|
+
lastNew = new_p;
|
|
2063
|
+
lastIns = content_ins;
|
|
2064
|
+
}
|
|
2065
|
+
if (firstNew) {
|
|
2066
|
+
if (edit.comment && lastNew && lastIns) {
|
|
2067
|
+
const ascend = (el: Element, p: Element): Element => {
|
|
2068
|
+
let cur: Element = el;
|
|
2069
|
+
while (cur.parentNode && cur.parentNode !== p)
|
|
2070
|
+
cur = cur.parentNode as Element;
|
|
2071
|
+
return cur;
|
|
2072
|
+
};
|
|
2073
|
+
const startIns =
|
|
2074
|
+
findAllDescendants(firstNew, "w:ins")[0] || firstNew;
|
|
2075
|
+
this._attach_comment_spanning(
|
|
2076
|
+
firstNew,
|
|
2077
|
+
ascend(startIns, firstNew),
|
|
2078
|
+
lastNew,
|
|
2079
|
+
ascend(lastIns, lastNew),
|
|
2080
|
+
edit.comment,
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
return true;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
1579
2087
|
const result = this._track_insert_multiline(
|
|
1580
2088
|
edit.new_text || "",
|
|
1581
2089
|
anchor_run,
|
|
@@ -1620,7 +2128,9 @@ export class RedlineEngine {
|
|
|
1620
2128
|
if (start_p) {
|
|
1621
2129
|
let first_anchor_target = result.first_node;
|
|
1622
2130
|
if (result.first_node.tagName === "w:p") {
|
|
1623
|
-
first_anchor_target =
|
|
2131
|
+
first_anchor_target =
|
|
2132
|
+
findAllDescendants(result.first_node, "w:ins")[0] ||
|
|
2133
|
+
result.first_node;
|
|
1624
2134
|
}
|
|
1625
2135
|
const start_anchor = ascend_to_paragraph_child(
|
|
1626
2136
|
first_anchor_target,
|
|
@@ -1637,22 +2147,27 @@ export class RedlineEngine {
|
|
|
1637
2147
|
end_anchor,
|
|
1638
2148
|
edit.comment,
|
|
1639
2149
|
);
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
2150
|
+
}
|
|
2151
|
+
} else {
|
|
2152
|
+
// Inline only: anchor around first_node in its host paragraph.
|
|
2153
|
+
let host_p: Element | null = result.first_node;
|
|
2154
|
+
while (host_p && host_p.tagName !== "w:p")
|
|
1645
2155
|
host_p = host_p.parentNode as Element;
|
|
1646
|
-
|
|
2156
|
+
if (host_p) {
|
|
1647
2157
|
let first_anchor_target = result.first_node;
|
|
1648
2158
|
if (result.first_node.tagName === "w:p") {
|
|
1649
|
-
first_anchor_target =
|
|
2159
|
+
first_anchor_target =
|
|
2160
|
+
findAllDescendants(result.first_node, "w:ins")[0] ||
|
|
2161
|
+
result.first_node;
|
|
1650
2162
|
}
|
|
1651
|
-
const anchor = ascend_to_paragraph_child(
|
|
2163
|
+
const anchor = ascend_to_paragraph_child(
|
|
2164
|
+
first_anchor_target,
|
|
2165
|
+
host_p,
|
|
2166
|
+
);
|
|
1652
2167
|
this._attach_comment(host_p, anchor, anchor, edit.comment);
|
|
1653
|
-
}
|
|
1654
2168
|
}
|
|
1655
2169
|
}
|
|
2170
|
+
}
|
|
1656
2171
|
return true;
|
|
1657
2172
|
}
|
|
1658
2173
|
|
|
@@ -1662,7 +2177,10 @@ export class RedlineEngine {
|
|
|
1662
2177
|
length,
|
|
1663
2178
|
rebuild_map,
|
|
1664
2179
|
);
|
|
1665
|
-
const virtual_spans = active_mapper.get_virtual_spans_in_range(
|
|
2180
|
+
const virtual_spans = active_mapper.get_virtual_spans_in_range(
|
|
2181
|
+
start_idx,
|
|
2182
|
+
length,
|
|
2183
|
+
);
|
|
1666
2184
|
|
|
1667
2185
|
if (target_runs.length === 0 && virtual_spans.length === 0) return false;
|
|
1668
2186
|
|
|
@@ -1738,18 +2256,26 @@ export class RedlineEngine {
|
|
|
1738
2256
|
|
|
1739
2257
|
// PHASE 2: OOXML Paragraph Merge Protocol
|
|
1740
2258
|
if (op === "DELETION" || op === "MODIFICATION") {
|
|
1741
|
-
if (
|
|
2259
|
+
if (
|
|
2260
|
+
op === "MODIFICATION" &&
|
|
2261
|
+
target_runs.length === 0 &&
|
|
2262
|
+
virtual_spans.length > 0 &&
|
|
2263
|
+
edit.new_text
|
|
2264
|
+
) {
|
|
1742
2265
|
const first_span = virtual_spans[0];
|
|
1743
2266
|
if (first_span.paragraph) {
|
|
1744
2267
|
const p1_el = first_span.paragraph._element;
|
|
1745
2268
|
const last_runs = findAllDescendants(p1_el, "w:r");
|
|
1746
|
-
const anchor =
|
|
1747
|
-
|
|
2269
|
+
const anchor =
|
|
2270
|
+
last_runs.length > 0
|
|
2271
|
+
? new Run(last_runs[last_runs.length - 1], first_span.paragraph)
|
|
2272
|
+
: null;
|
|
2273
|
+
|
|
1748
2274
|
const result = this._track_insert_multiline(
|
|
1749
2275
|
edit.new_text,
|
|
1750
2276
|
anchor,
|
|
1751
2277
|
first_span.paragraph,
|
|
1752
|
-
ins_id
|
|
2278
|
+
ins_id!,
|
|
1753
2279
|
);
|
|
1754
2280
|
if (result.first_node) {
|
|
1755
2281
|
p1_el.appendChild(result.first_node);
|
|
@@ -1769,7 +2295,10 @@ export class RedlineEngine {
|
|
|
1769
2295
|
let pPr = findChild(p1_element, "w:pPr");
|
|
1770
2296
|
if (!pPr) {
|
|
1771
2297
|
pPr = p1_element.ownerDocument!.createElement("w:pPr") as Element;
|
|
1772
|
-
p1_element.insertBefore(
|
|
2298
|
+
p1_element.insertBefore(
|
|
2299
|
+
pPr,
|
|
2300
|
+
p1_element.firstChild as Node | null,
|
|
2301
|
+
);
|
|
1773
2302
|
}
|
|
1774
2303
|
let rPr = findChild(pPr!, "w:rPr");
|
|
1775
2304
|
if (!rPr) {
|
|
@@ -1781,7 +2310,10 @@ export class RedlineEngine {
|
|
|
1781
2310
|
|
|
1782
2311
|
const children = Array.from(p2_element.childNodes);
|
|
1783
2312
|
for (const child of children) {
|
|
1784
|
-
if (
|
|
2313
|
+
if (
|
|
2314
|
+
child.nodeType === 1 &&
|
|
2315
|
+
(child as Element).tagName === "w:pPr"
|
|
2316
|
+
) {
|
|
1785
2317
|
continue;
|
|
1786
2318
|
}
|
|
1787
2319
|
p1_element.appendChild(child);
|