@adeu/core 1.13.0 → 1.15.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 +2215 -1789
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -12
- package/dist/index.d.ts +7 -12
- package/dist/index.js +2215 -1789
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/comment_dedup.test.ts +269 -0
- package/src/domain.test.ts +280 -77
- package/src/domain.ts +146 -46
- package/src/engine.bugs.test.ts +20 -0
- package/src/engine.qa.test.ts +162 -0
- package/src/engine.ts +302 -89
- package/src/ingest.ts +283 -133
- package/src/mapper.ts +425 -196
- package/src/models.ts +2 -0
- package/src/repro_heading_bug.test.ts +174 -0
- package/src/search_write_engine.test.ts +112 -0
package/src/engine.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "./models.js";
|
|
14
14
|
import { trim_common_context } from "./diff.js";
|
|
15
15
|
import { findChild, findAllDescendants, serializeXml } from "./docx/dom.js";
|
|
16
|
+
import { split_structural_appendix, paginate } from "./pagination.js";
|
|
16
17
|
import {
|
|
17
18
|
is_heading_paragraph,
|
|
18
19
|
is_native_heading,
|
|
@@ -279,7 +280,8 @@ export class RedlineEngine {
|
|
|
279
280
|
start_idx + length + 30,
|
|
280
281
|
);
|
|
281
282
|
|
|
282
|
-
const
|
|
283
|
+
const insertion = new_text ? `{++${new_text}++}` : "";
|
|
284
|
+
const critic_markup = `${context_before}{--${target_text}--}${insertion}${context_after}`;
|
|
283
285
|
|
|
284
286
|
let clean_text = critic_markup;
|
|
285
287
|
clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
|
|
@@ -301,6 +303,40 @@ export class RedlineEngine {
|
|
|
301
303
|
return maxId;
|
|
302
304
|
}
|
|
303
305
|
|
|
306
|
+
private _get_heading_path_and_page(start_idx: number, text: string, page_offsets: number[]): [string, number] {
|
|
307
|
+
let page = 1;
|
|
308
|
+
for (let i = 0; i < page_offsets.length; i++) {
|
|
309
|
+
if (start_idx >= page_offsets[i]) {
|
|
310
|
+
page = i + 1;
|
|
311
|
+
} else {
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const textBefore = text.substring(0, start_idx);
|
|
317
|
+
const lines = textBefore.split("\n");
|
|
318
|
+
const path: string[] = [];
|
|
319
|
+
let current_level = 999;
|
|
320
|
+
|
|
321
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
322
|
+
const line = lines[i];
|
|
323
|
+
const m = line.match(/^(#{1,6})\s+(.*)/);
|
|
324
|
+
if (m) {
|
|
325
|
+
const level = m[1].length;
|
|
326
|
+
if (level < current_level) {
|
|
327
|
+
let cleanHeading = m[2].replace(/\*\*|__|[*_]/g, "").replace(/\{#[^}]+\}/g, "").trim();
|
|
328
|
+
if (cleanHeading.length > 80) {
|
|
329
|
+
cleanHeading = cleanHeading.substring(0, 80) + "...";
|
|
330
|
+
}
|
|
331
|
+
path.unshift(cleanHeading);
|
|
332
|
+
current_level = level;
|
|
333
|
+
if (level === 1) break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return [path.join(" > "), page];
|
|
338
|
+
}
|
|
339
|
+
|
|
304
340
|
public accept_all_revisions() {
|
|
305
341
|
const parts_to_process: Element[] = [this.doc.element];
|
|
306
342
|
|
|
@@ -1193,15 +1229,18 @@ export class RedlineEngine {
|
|
|
1193
1229
|
for (let i = 0; i < edits.length; i++) {
|
|
1194
1230
|
const edit = edits[i];
|
|
1195
1231
|
if (!edit.target_text) continue;
|
|
1232
|
+
|
|
1233
|
+
const is_regex = (edit as any).regex || false;
|
|
1234
|
+
const match_mode = (edit as any).match_mode || "strict";
|
|
1196
1235
|
|
|
1197
|
-
let matches = this.mapper.find_all_match_indices(edit.target_text);
|
|
1236
|
+
let matches = this.mapper.find_all_match_indices(edit.target_text, is_regex);
|
|
1198
1237
|
let activeText = this.mapper.full_text;
|
|
1199
1238
|
let target_mapper = this.mapper;
|
|
1200
1239
|
|
|
1201
1240
|
if (matches.length === 0) {
|
|
1202
1241
|
if (!this.clean_mapper)
|
|
1203
1242
|
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
1204
|
-
matches = this.clean_mapper.find_all_match_indices(edit.target_text);
|
|
1243
|
+
matches = this.clean_mapper.find_all_match_indices(edit.target_text, is_regex);
|
|
1205
1244
|
if (matches.length > 0) {
|
|
1206
1245
|
activeText = this.clean_mapper.full_text;
|
|
1207
1246
|
target_mapper = this.clean_mapper;
|
|
@@ -1233,7 +1272,7 @@ export class RedlineEngine {
|
|
|
1233
1272
|
if (!this.original_mapper) {
|
|
1234
1273
|
this.original_mapper = new DocumentMapper(this.doc, false, true);
|
|
1235
1274
|
}
|
|
1236
|
-
const orig_matches = this.original_mapper.find_all_match_indices(edit.target_text);
|
|
1275
|
+
const orig_matches = this.original_mapper.find_all_match_indices(edit.target_text, is_regex);
|
|
1237
1276
|
if (orig_matches.length > 0) {
|
|
1238
1277
|
is_deleted_text = true;
|
|
1239
1278
|
for (const [start, length] of orig_matches) {
|
|
@@ -1272,7 +1311,7 @@ export class RedlineEngine {
|
|
|
1272
1311
|
`- Edit ${i + 1} Failed: Target text not found in document:\n "${edit.target_text}"`,
|
|
1273
1312
|
);
|
|
1274
1313
|
}
|
|
1275
|
-
} else if (matches.length > 1) {
|
|
1314
|
+
} else if (matches.length > 1 && match_mode === "strict") {
|
|
1276
1315
|
const positions: [number, number][] = matches.map(([start, length]) => [
|
|
1277
1316
|
start,
|
|
1278
1317
|
start + length,
|
|
@@ -1343,6 +1382,14 @@ export class RedlineEngine {
|
|
|
1343
1382
|
if (auth && auth !== this.author) nestedAuthors.add(auth);
|
|
1344
1383
|
}
|
|
1345
1384
|
}
|
|
1385
|
+
if (s.comment_ids) {
|
|
1386
|
+
for (const cid of s.comment_ids) {
|
|
1387
|
+
const c_data = this.mapper.comments_map[cid];
|
|
1388
|
+
if (c_data && c_data.author && c_data.author !== this.author) {
|
|
1389
|
+
nestedAuthors.add(c_data.author);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1346
1393
|
}
|
|
1347
1394
|
if (nestedAuthors.size > 0) {
|
|
1348
1395
|
errors.push(
|
|
@@ -1399,6 +1446,34 @@ export class RedlineEngine {
|
|
|
1399
1446
|
changes: DocumentChange[],
|
|
1400
1447
|
dry_run: boolean = false,
|
|
1401
1448
|
): any {
|
|
1449
|
+
// Defensive sanitization: some LLM clients "double-serialize" nested
|
|
1450
|
+
// arrays, delivering each element of `changes` as a JSON string instead of
|
|
1451
|
+
// a parsed object. Downstream code mutates state trackers (e.g.
|
|
1452
|
+
// `edit._applied_status`) and reads `change.type` on these elements, which
|
|
1453
|
+
// throws a TypeError on string primitives. Parse stringified elements back
|
|
1454
|
+
// into objects here, leaving genuine objects (and unparseable strings)
|
|
1455
|
+
// untouched so validation can surface a clear error rather than crashing.
|
|
1456
|
+
if (Array.isArray(changes)) {
|
|
1457
|
+
changes = changes.map((item: any) => {
|
|
1458
|
+
if (typeof item === "string") {
|
|
1459
|
+
try {
|
|
1460
|
+
const parsed = JSON.parse(item);
|
|
1461
|
+
// Only swap in the parsed value if it is an object; a string that
|
|
1462
|
+
// parses to a scalar (e.g. "42") is not a valid change.
|
|
1463
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
1464
|
+
return parsed;
|
|
1465
|
+
}
|
|
1466
|
+
return item;
|
|
1467
|
+
} catch {
|
|
1468
|
+
// Leave malformed strings as-is; the validation pass downstream
|
|
1469
|
+
// will report them rather than crashing on a raw TypeError.
|
|
1470
|
+
return item;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return item;
|
|
1474
|
+
}) as DocumentChange[];
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1402
1477
|
if (dry_run) {
|
|
1403
1478
|
const baselines = new Map<any, Element>();
|
|
1404
1479
|
for (const part of this.doc.pkg.parts) {
|
|
@@ -1473,6 +1548,10 @@ export class RedlineEngine {
|
|
|
1473
1548
|
}
|
|
1474
1549
|
}
|
|
1475
1550
|
|
|
1551
|
+
const [body_text] = split_structural_appendix(this.mapper.full_text);
|
|
1552
|
+
const pag_res = paginate(body_text, "");
|
|
1553
|
+
const page_offsets = pag_res.body_page_offsets;
|
|
1554
|
+
|
|
1476
1555
|
const edits_reports: any[] = [];
|
|
1477
1556
|
let applied_edits = 0;
|
|
1478
1557
|
let skipped_edits = 0;
|
|
@@ -1497,9 +1576,8 @@ export class RedlineEngine {
|
|
|
1497
1576
|
});
|
|
1498
1577
|
continue;
|
|
1499
1578
|
}
|
|
1500
|
-
const res = this.apply_edits([edit]);
|
|
1501
|
-
|
|
1502
|
-
if (applied > 0) {
|
|
1579
|
+
const res = this.apply_edits([edit], page_offsets);
|
|
1580
|
+
if ((edit as any)._applied_status) {
|
|
1503
1581
|
applied_edits++;
|
|
1504
1582
|
const previews = this._build_edit_context_previews(edit);
|
|
1505
1583
|
edits_reports.push({
|
|
@@ -1510,6 +1588,10 @@ export class RedlineEngine {
|
|
|
1510
1588
|
error: null,
|
|
1511
1589
|
critic_markup: previews[0],
|
|
1512
1590
|
clean_text: previews[1],
|
|
1591
|
+
pages: (edit as any)._pages || [],
|
|
1592
|
+
heading_path: (edit as any)._heading_path || "",
|
|
1593
|
+
occurrences_modified: (edit as any)._occurrences_modified || 0,
|
|
1594
|
+
match_mode: (edit as any).match_mode || "strict",
|
|
1513
1595
|
});
|
|
1514
1596
|
} else {
|
|
1515
1597
|
skipped_edits++;
|
|
@@ -1534,9 +1616,9 @@ export class RedlineEngine {
|
|
|
1534
1616
|
throw new BatchValidationError(errors);
|
|
1535
1617
|
}
|
|
1536
1618
|
const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
|
|
1537
|
-
const res = this.apply_edits(cloned_edits);
|
|
1538
|
-
applied_edits =
|
|
1539
|
-
skipped_edits =
|
|
1619
|
+
const res = this.apply_edits(cloned_edits, page_offsets);
|
|
1620
|
+
applied_edits = cloned_edits.filter((e) => (e as any)._applied_status).length;
|
|
1621
|
+
skipped_edits = cloned_edits.length - applied_edits;
|
|
1540
1622
|
|
|
1541
1623
|
for (const edit of cloned_edits) {
|
|
1542
1624
|
const success = (edit as any)._applied_status || false;
|
|
@@ -1559,6 +1641,10 @@ export class RedlineEngine {
|
|
|
1559
1641
|
error: error_msg,
|
|
1560
1642
|
critic_markup: critic_markup,
|
|
1561
1643
|
clean_text: clean_text,
|
|
1644
|
+
pages: (edit as any)._pages || [],
|
|
1645
|
+
heading_path: (edit as any)._heading_path || "",
|
|
1646
|
+
occurrences_modified: (edit as any)._occurrences_modified || 0,
|
|
1647
|
+
match_mode: (edit as any).match_mode || "strict",
|
|
1562
1648
|
});
|
|
1563
1649
|
}
|
|
1564
1650
|
}
|
|
@@ -1576,9 +1662,14 @@ export class RedlineEngine {
|
|
|
1576
1662
|
};
|
|
1577
1663
|
}
|
|
1578
1664
|
|
|
1579
|
-
public apply_edits(edits: any[]): [number, number] {
|
|
1665
|
+
public apply_edits(edits: any[], page_offsets: number[] = []): [number, number] {
|
|
1580
1666
|
let applied = 0;
|
|
1581
1667
|
let skipped = 0;
|
|
1668
|
+
|
|
1669
|
+
if (!page_offsets || page_offsets.length === 0) {
|
|
1670
|
+
const [body_text] = split_structural_appendix(this.mapper.full_text);
|
|
1671
|
+
page_offsets = paginate(body_text, "").body_page_offsets;
|
|
1672
|
+
}
|
|
1582
1673
|
const resolved_edits: [any, string | null][] = [];
|
|
1583
1674
|
|
|
1584
1675
|
for (const edit of edits) {
|
|
@@ -1700,6 +1791,19 @@ export class RedlineEngine {
|
|
|
1700
1791
|
const parent = edit._parent_edit_ref;
|
|
1701
1792
|
if (parent) {
|
|
1702
1793
|
parent._applied_status = true;
|
|
1794
|
+
parent._occurrences_modified = (parent._occurrences_modified || 0) + 1;
|
|
1795
|
+
const [path, page] = this._get_heading_path_and_page(start, this.mapper.full_text, page_offsets);
|
|
1796
|
+
const pages: number[] = parent._pages || [];
|
|
1797
|
+
if (!pages.includes(page)) pages.unshift(page);
|
|
1798
|
+
parent._pages = pages;
|
|
1799
|
+
parent._heading_path = path;
|
|
1800
|
+
} else {
|
|
1801
|
+
edit._occurrences_modified = (edit._occurrences_modified || 0) + 1;
|
|
1802
|
+
const [path, page] = this._get_heading_path_and_page(start, this.mapper.full_text, page_offsets);
|
|
1803
|
+
const pages: number[] = edit._pages || [];
|
|
1804
|
+
if (!pages.includes(page)) pages.unshift(page);
|
|
1805
|
+
edit._pages = pages;
|
|
1806
|
+
edit._heading_path = path;
|
|
1703
1807
|
}
|
|
1704
1808
|
} else {
|
|
1705
1809
|
skipped++;
|
|
@@ -1871,103 +1975,138 @@ export class RedlineEngine {
|
|
|
1871
1975
|
return false;
|
|
1872
1976
|
}
|
|
1873
1977
|
|
|
1874
|
-
/**
|
|
1875
|
-
* Returns the first match of `target_text` in the raw mapper that is NOT
|
|
1876
|
-
* entirely contained within a tracked deletion (<w:del>). Tracked-deleted
|
|
1877
|
-
* copies are not live, editable text, so an edit must resolve to a live
|
|
1878
|
-
* occurrence even when a dead copy appears earlier in the document
|
|
1879
|
-
* (BUG-23-5). Falls back to the plain first match when no live copy is
|
|
1880
|
-
* found (e.g. fuzzy/normalized matches the span filter cannot align).
|
|
1881
|
-
*/
|
|
1882
|
-
private _first_live_match(target_text: string): [number, number] {
|
|
1883
|
-
const all = this.mapper.find_all_match_indices(target_text);
|
|
1884
|
-
if (all.length <= 1) {
|
|
1885
|
-
return this.mapper.find_match_index(target_text);
|
|
1886
|
-
}
|
|
1887
|
-
for (const [start, length] of all) {
|
|
1888
|
-
const realSpans = this.mapper.spans.filter(
|
|
1889
|
-
(s) => s.run !== null && s.end > start && s.start < start + length,
|
|
1890
|
-
);
|
|
1891
|
-
if (realSpans.length === 0) return [start, length];
|
|
1892
|
-
if (realSpans.some((s) => !s.del_id)) return [start, length];
|
|
1893
|
-
}
|
|
1894
|
-
return this.mapper.find_match_index(target_text);
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
1978
|
private _pre_resolve_heuristic_edit(edit: any): any {
|
|
1898
1979
|
if (!edit.target_text) return null;
|
|
1899
1980
|
|
|
1900
|
-
|
|
1981
|
+
const is_regex = edit.regex || false;
|
|
1982
|
+
const match_mode = edit.match_mode || "strict";
|
|
1983
|
+
|
|
1984
|
+
let matches = this.mapper.find_all_match_indices(edit.target_text, is_regex);
|
|
1901
1985
|
let use_clean_map = false;
|
|
1902
1986
|
|
|
1903
|
-
if (
|
|
1987
|
+
if (matches.length === 0) {
|
|
1904
1988
|
if (!this.clean_mapper)
|
|
1905
1989
|
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
);
|
|
1909
|
-
if (start_idx !== -1) use_clean_map = true;
|
|
1990
|
+
matches = this.clean_mapper.find_all_match_indices(edit.target_text, is_regex);
|
|
1991
|
+
if (matches.length > 0) use_clean_map = true;
|
|
1910
1992
|
else return null;
|
|
1911
1993
|
}
|
|
1912
1994
|
|
|
1913
1995
|
const active_mapper = use_clean_map ? this.clean_mapper! : this.mapper;
|
|
1914
|
-
const effective_new_text = edit.new_text || "";
|
|
1915
|
-
const actual_doc_text = this.mapper.full_text.substring(
|
|
1916
|
-
start_idx,
|
|
1917
|
-
start_idx + match_len,
|
|
1918
|
-
);
|
|
1919
1996
|
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
comment: edit.comment,
|
|
1929
|
-
_match_start_index: start_idx,
|
|
1930
|
-
_internal_op: "COMMENT_ONLY",
|
|
1931
|
-
_active_mapper_ref: active_mapper,
|
|
1932
|
-
};
|
|
1997
|
+
let live_matches: [number, number][] = [];
|
|
1998
|
+
for (const [s, match_len] of matches) {
|
|
1999
|
+
const realSpans = active_mapper.spans.filter(
|
|
2000
|
+
(span) => span.run !== null && span.end > s && span.start < s + match_len
|
|
2001
|
+
);
|
|
2002
|
+
if (realSpans.length === 0 || realSpans.some((span) => !span.del_id)) {
|
|
2003
|
+
live_matches.push([s, match_len]);
|
|
2004
|
+
}
|
|
1933
2005
|
}
|
|
1934
2006
|
|
|
1935
|
-
|
|
1936
|
-
let final_target = "";
|
|
1937
|
-
let final_new = "";
|
|
1938
|
-
let effective_start_idx = start_idx;
|
|
2007
|
+
if (live_matches.length === 0) return null;
|
|
1939
2008
|
|
|
1940
|
-
if (
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2009
|
+
if (match_mode === "strict" || match_mode === "first") {
|
|
2010
|
+
live_matches = live_matches.slice(0, 1);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
const all_sub_edits: any[] = [];
|
|
2014
|
+
|
|
2015
|
+
for (const [start_idx, match_len] of live_matches) {
|
|
2016
|
+
const actual_doc_text = active_mapper.full_text.substring(
|
|
2017
|
+
start_idx,
|
|
2018
|
+
start_idx + match_len,
|
|
1948
2019
|
);
|
|
1949
|
-
|
|
1950
|
-
|
|
2020
|
+
let current_effective_new_text = edit.new_text || "";
|
|
2021
|
+
|
|
2022
|
+
if (is_regex && current_effective_new_text) {
|
|
2023
|
+
try {
|
|
2024
|
+
current_effective_new_text = actual_doc_text.replace(new RegExp(edit.target_text), current_effective_new_text);
|
|
2025
|
+
} catch (e) {}
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
const [edit_target_clean, edit_target_style] = this._parse_markdown_style(edit.target_text);
|
|
2029
|
+
const [edit_new_clean, edit_new_style] = this._parse_markdown_style(current_effective_new_text);
|
|
2030
|
+
|
|
2031
|
+
if (edit_target_style !== edit_new_style) {
|
|
2032
|
+
const [actual_clean] = this._parse_markdown_style(actual_doc_text);
|
|
2033
|
+
const final_target = actual_clean;
|
|
2034
|
+
const final_new = edit_new_clean;
|
|
2035
|
+
const style_op = final_target === final_new ? "STYLE_ONLY" : "STYLE_AND_TEXT";
|
|
2036
|
+
const prefix_offset = actual_doc_text.indexOf(actual_clean);
|
|
2037
|
+
const effective_start_idx = start_idx + (prefix_offset !== -1 ? prefix_offset : 0);
|
|
2038
|
+
const resolved_style = edit_new_style !== null ? edit_new_style : "Normal";
|
|
2039
|
+
|
|
2040
|
+
all_sub_edits.push({
|
|
2041
|
+
type: "modify",
|
|
2042
|
+
target_text: final_target,
|
|
2043
|
+
new_text: final_new,
|
|
2044
|
+
comment: edit.comment,
|
|
2045
|
+
_match_start_index: effective_start_idx,
|
|
2046
|
+
_internal_op: style_op,
|
|
2047
|
+
_new_style: resolved_style,
|
|
2048
|
+
_active_mapper_ref: active_mapper,
|
|
2049
|
+
});
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
if (
|
|
2054
|
+
actual_doc_text === current_effective_new_text ||
|
|
2055
|
+
edit.target_text === current_effective_new_text
|
|
2056
|
+
) {
|
|
2057
|
+
all_sub_edits.push({
|
|
2058
|
+
type: "modify",
|
|
2059
|
+
target_text: actual_doc_text,
|
|
2060
|
+
new_text: actual_doc_text,
|
|
2061
|
+
comment: edit.comment,
|
|
2062
|
+
_match_start_index: start_idx,
|
|
2063
|
+
_internal_op: "COMMENT_ONLY",
|
|
2064
|
+
_active_mapper_ref: active_mapper,
|
|
2065
|
+
});
|
|
2066
|
+
continue;
|
|
2067
|
+
}
|
|
1951
2068
|
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2069
|
+
let effective_op = "";
|
|
2070
|
+
let final_target = "";
|
|
2071
|
+
let final_new = "";
|
|
2072
|
+
let effective_start_idx = start_idx;
|
|
1955
2073
|
|
|
1956
|
-
if (
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2074
|
+
if (current_effective_new_text.startsWith(actual_doc_text)) {
|
|
2075
|
+
effective_op = "INSERTION";
|
|
2076
|
+
final_new = current_effective_new_text.substring(actual_doc_text.length);
|
|
2077
|
+
effective_start_idx = start_idx + match_len;
|
|
2078
|
+
} else {
|
|
2079
|
+
const [prefix_len, suffix_len] = trim_common_context(
|
|
2080
|
+
actual_doc_text,
|
|
2081
|
+
current_effective_new_text,
|
|
2082
|
+
);
|
|
2083
|
+
const t_end = actual_doc_text.length - suffix_len;
|
|
2084
|
+
const n_end = current_effective_new_text.length - suffix_len;
|
|
2085
|
+
|
|
2086
|
+
final_target = actual_doc_text.substring(prefix_len, t_end);
|
|
2087
|
+
final_new = current_effective_new_text.substring(prefix_len, n_end);
|
|
2088
|
+
effective_start_idx = start_idx + prefix_len;
|
|
2089
|
+
|
|
2090
|
+
if (!final_target && final_new) effective_op = "INSERTION";
|
|
2091
|
+
else if (final_target && !final_new) effective_op = "DELETION";
|
|
2092
|
+
else if (final_target && final_new) effective_op = "MODIFICATION";
|
|
2093
|
+
else effective_op = "COMMENT_ONLY";
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
all_sub_edits.push({
|
|
2097
|
+
type: "modify",
|
|
2098
|
+
target_text: final_target,
|
|
2099
|
+
new_text: final_new,
|
|
2100
|
+
comment: edit.comment,
|
|
2101
|
+
_match_start_index: effective_start_idx,
|
|
2102
|
+
_internal_op: effective_op,
|
|
2103
|
+
_active_mapper_ref: active_mapper,
|
|
2104
|
+
});
|
|
1960
2105
|
}
|
|
1961
2106
|
|
|
1962
|
-
return
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
new_text: final_new,
|
|
1966
|
-
comment: edit.comment,
|
|
1967
|
-
_match_start_index: effective_start_idx,
|
|
1968
|
-
_internal_op: effective_op,
|
|
1969
|
-
_active_mapper_ref: active_mapper,
|
|
1970
|
-
};
|
|
2107
|
+
if (all_sub_edits.length === 0) return null;
|
|
2108
|
+
if (match_mode === "all" || all_sub_edits.length > 1) return all_sub_edits;
|
|
2109
|
+
return all_sub_edits[0];
|
|
1971
2110
|
}
|
|
1972
2111
|
|
|
1973
2112
|
private _apply_single_edit_indexed(
|
|
@@ -1984,6 +2123,80 @@ export class RedlineEngine {
|
|
|
1984
2123
|
: edit._match_start_index || 0;
|
|
1985
2124
|
const length = edit.target_text ? edit.target_text.length : 0;
|
|
1986
2125
|
|
|
2126
|
+
if (op === "STYLE_ONLY" || op === "STYLE_AND_TEXT") {
|
|
2127
|
+
const [anchor_run, anchor_para] = active_mapper.get_insertion_anchor(
|
|
2128
|
+
start_idx,
|
|
2129
|
+
rebuild_map,
|
|
2130
|
+
);
|
|
2131
|
+
let target_para_el: Element | null = null;
|
|
2132
|
+
if (anchor_para) {
|
|
2133
|
+
target_para_el = anchor_para._element;
|
|
2134
|
+
} else if (anchor_run) {
|
|
2135
|
+
let walker: Element | null = anchor_run._element;
|
|
2136
|
+
while (walker && walker.tagName !== "w:p") {
|
|
2137
|
+
walker = walker.parentNode as Element | null;
|
|
2138
|
+
}
|
|
2139
|
+
target_para_el = walker;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
if (target_para_el && edit._new_style) {
|
|
2143
|
+
this._set_paragraph_style(target_para_el, edit._new_style);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
if (op === "STYLE_ONLY") {
|
|
2147
|
+
if (edit.comment) {
|
|
2148
|
+
const target_runs = active_mapper.find_target_runs_by_index(
|
|
2149
|
+
start_idx,
|
|
2150
|
+
length,
|
|
2151
|
+
rebuild_map,
|
|
2152
|
+
);
|
|
2153
|
+
if (target_runs.length > 0) {
|
|
2154
|
+
const first_el = target_runs[0]._element;
|
|
2155
|
+
const last_el = target_runs[target_runs.length - 1]._element;
|
|
2156
|
+
let start_p: Element | null = first_el;
|
|
2157
|
+
while (start_p && start_p.tagName !== "w:p")
|
|
2158
|
+
start_p = start_p.parentNode as Element;
|
|
2159
|
+
let end_p: Element | null = last_el;
|
|
2160
|
+
while (end_p && end_p.tagName !== "w:p")
|
|
2161
|
+
end_p = end_p.parentNode as Element;
|
|
2162
|
+
if (start_p && end_p) {
|
|
2163
|
+
const ascend_to_paragraph_child = (el: Element, p: Element): Element => {
|
|
2164
|
+
let cur: Element = el;
|
|
2165
|
+
while (cur.parentNode && cur.parentNode !== p) {
|
|
2166
|
+
cur = cur.parentNode as Element;
|
|
2167
|
+
}
|
|
2168
|
+
return cur;
|
|
2169
|
+
};
|
|
2170
|
+
const first_anchor = ascend_to_paragraph_child(first_el, start_p);
|
|
2171
|
+
const last_anchor = ascend_to_paragraph_child(last_el, end_p);
|
|
2172
|
+
if (start_p === end_p) {
|
|
2173
|
+
this._attach_comment(start_p, first_anchor, last_anchor, edit.comment);
|
|
2174
|
+
} else {
|
|
2175
|
+
this._attach_comment_spanning(
|
|
2176
|
+
start_p,
|
|
2177
|
+
first_anchor,
|
|
2178
|
+
end_p,
|
|
2179
|
+
last_anchor,
|
|
2180
|
+
edit.comment,
|
|
2181
|
+
);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
return true;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
if (edit.target_text && edit.new_text) {
|
|
2190
|
+
op = "MODIFICATION";
|
|
2191
|
+
} else if (!edit.target_text && edit.new_text) {
|
|
2192
|
+
op = "INSERTION";
|
|
2193
|
+
} else if (edit.target_text && !edit.new_text) {
|
|
2194
|
+
op = "DELETION";
|
|
2195
|
+
} else {
|
|
2196
|
+
op = "COMMENT_ONLY";
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
|
|
1987
2200
|
const del_id = ["DELETION", "MODIFICATION"].includes(op)
|
|
1988
2201
|
? this._getNextId()
|
|
1989
2202
|
: null;
|