@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/dist/index.js
CHANGED
|
@@ -452,6 +452,21 @@ var CommentsManager = class {
|
|
|
452
452
|
return part;
|
|
453
453
|
}
|
|
454
454
|
_ensureNamespaces() {
|
|
455
|
+
const root = this._commentsPart?._element;
|
|
456
|
+
if (!root) return;
|
|
457
|
+
const required = [
|
|
458
|
+
["xmlns:w", NS.w],
|
|
459
|
+
["xmlns:w14", NS.w14],
|
|
460
|
+
["xmlns:w15", NS.w15],
|
|
461
|
+
["xmlns:w16cid", NS.w16cid],
|
|
462
|
+
["xmlns:w16cex", NS.w16cex],
|
|
463
|
+
["xmlns:mc", NS.mc]
|
|
464
|
+
];
|
|
465
|
+
for (const [attr, uri] of required) {
|
|
466
|
+
if (!root.getAttribute(attr)) {
|
|
467
|
+
root.setAttribute(attr, uri);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
455
470
|
}
|
|
456
471
|
_getNextCommentId() {
|
|
457
472
|
const ids = [0];
|
|
@@ -577,9 +592,9 @@ var CommentsManager = class {
|
|
|
577
592
|
return commentId;
|
|
578
593
|
}
|
|
579
594
|
deleteComment(commentId) {
|
|
580
|
-
if (!this.
|
|
595
|
+
if (!this.commentsPart) return;
|
|
581
596
|
let commentEl = null;
|
|
582
|
-
for (const c of findAllDescendants(this.
|
|
597
|
+
for (const c of findAllDescendants(this.commentsPart._element, "w:comment")) {
|
|
583
598
|
if (c.getAttribute("w:id") === commentId) {
|
|
584
599
|
commentEl = c;
|
|
585
600
|
break;
|
|
@@ -603,7 +618,7 @@ var CommentsManager = class {
|
|
|
603
618
|
if (child.getAttribute("w15:paraIdParent") === paraId) {
|
|
604
619
|
const childParaId = child.getAttribute("w15:paraId");
|
|
605
620
|
if (childParaId) {
|
|
606
|
-
for (const c of findAllDescendants(this.
|
|
621
|
+
for (const c of findAllDescendants(this.commentsPart._element, "w:comment")) {
|
|
607
622
|
for (const p of findAllDescendants(c, "w:p")) {
|
|
608
623
|
if (p.getAttribute("w14:paraId") === childParaId) {
|
|
609
624
|
const cid = c.getAttribute("w:id");
|
|
@@ -1837,6 +1852,29 @@ ${header}`;
|
|
|
1837
1852
|
|
|
1838
1853
|
// src/diff.ts
|
|
1839
1854
|
import diff_match_patch from "diff-match-patch";
|
|
1855
|
+
function _count_standalone_underscores(s) {
|
|
1856
|
+
let count = 0;
|
|
1857
|
+
let i = 0;
|
|
1858
|
+
const n = s.length;
|
|
1859
|
+
const isAlnum = (char) => /[a-zA-Z0-9]/.test(char);
|
|
1860
|
+
while (i < n) {
|
|
1861
|
+
if (s[i] === "_") {
|
|
1862
|
+
let is_double = false;
|
|
1863
|
+
if (i > 0 && s[i - 1] === "_" || i < n - 1 && s[i + 1] === "_") {
|
|
1864
|
+
is_double = true;
|
|
1865
|
+
}
|
|
1866
|
+
let is_intra = false;
|
|
1867
|
+
if (i > 0 && isAlnum(s[i - 1]) && i < n - 1 && isAlnum(s[i + 1])) {
|
|
1868
|
+
is_intra = true;
|
|
1869
|
+
}
|
|
1870
|
+
if (!is_double && !is_intra) {
|
|
1871
|
+
count++;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
i++;
|
|
1875
|
+
}
|
|
1876
|
+
return count;
|
|
1877
|
+
}
|
|
1840
1878
|
function trim_common_context(target, new_val) {
|
|
1841
1879
|
if (!target || !new_val) return [0, 0];
|
|
1842
1880
|
const isSpace = (char) => /\s/.test(char);
|
|
@@ -1867,7 +1905,7 @@ function trim_common_context(target, new_val) {
|
|
|
1867
1905
|
const left = target.substring(0, prefix_len);
|
|
1868
1906
|
const b_count = (left.match(/\*\*/g) || []).length;
|
|
1869
1907
|
const u2_count = (left.match(/__/g) || []).length;
|
|
1870
|
-
const u1_count = (left
|
|
1908
|
+
const u1_count = _count_standalone_underscores(left);
|
|
1871
1909
|
if (b_count % 2 !== 0) {
|
|
1872
1910
|
prefix_len = left.lastIndexOf("**");
|
|
1873
1911
|
continue;
|
|
@@ -1878,10 +1916,14 @@ function trim_common_context(target, new_val) {
|
|
|
1878
1916
|
}
|
|
1879
1917
|
if (u1_count % 2 !== 0) {
|
|
1880
1918
|
let idx = left.length - 1;
|
|
1919
|
+
const isAlnum = (char) => /[a-zA-Z0-9]/.test(char);
|
|
1881
1920
|
while (idx >= 0) {
|
|
1882
1921
|
if (left[idx] === "_" && (idx === 0 || left[idx - 1] !== "_") && (idx === left.length - 1 || left[idx + 1] !== "_")) {
|
|
1883
|
-
|
|
1884
|
-
|
|
1922
|
+
const is_intra = idx > 0 && isAlnum(left[idx - 1]) && idx < left.length - 1 && isAlnum(left[idx + 1]);
|
|
1923
|
+
if (!is_intra) {
|
|
1924
|
+
prefix_len = idx;
|
|
1925
|
+
break;
|
|
1926
|
+
}
|
|
1885
1927
|
}
|
|
1886
1928
|
idx--;
|
|
1887
1929
|
}
|
|
@@ -1941,7 +1983,7 @@ function trim_common_context(target, new_val) {
|
|
|
1941
1983
|
const right = target.substring(target.length - suffix_len);
|
|
1942
1984
|
const b_count = (right.match(/\*\*/g) || []).length;
|
|
1943
1985
|
const u2_count = (right.match(/__/g) || []).length;
|
|
1944
|
-
const u1_count = (right
|
|
1986
|
+
const u1_count = _count_standalone_underscores(right);
|
|
1945
1987
|
if (b_count % 2 !== 0) {
|
|
1946
1988
|
suffix_len -= right.indexOf("**") + 2;
|
|
1947
1989
|
continue;
|
|
@@ -1952,10 +1994,14 @@ function trim_common_context(target, new_val) {
|
|
|
1952
1994
|
}
|
|
1953
1995
|
if (u1_count % 2 !== 0) {
|
|
1954
1996
|
let idx_in_right = 0;
|
|
1997
|
+
const isAlnum = (char) => /[a-zA-Z0-9]/.test(char);
|
|
1955
1998
|
while (idx_in_right < right.length) {
|
|
1956
1999
|
if (right[idx_in_right] === "_" && (idx_in_right === 0 || right[idx_in_right - 1] !== "_") && (idx_in_right === right.length - 1 || right[idx_in_right + 1] !== "_")) {
|
|
1957
|
-
|
|
1958
|
-
|
|
2000
|
+
const is_intra = idx_in_right > 0 && isAlnum(right[idx_in_right - 1]) && idx_in_right < right.length - 1 && isAlnum(right[idx_in_right + 1]);
|
|
2001
|
+
if (!is_intra) {
|
|
2002
|
+
suffix_len -= idx_in_right + 1;
|
|
2003
|
+
break;
|
|
2004
|
+
}
|
|
1959
2005
|
}
|
|
1960
2006
|
idx_in_right++;
|
|
1961
2007
|
}
|
|
@@ -2602,6 +2648,39 @@ var RedlineEngine = class {
|
|
|
2602
2648
|
this.mapper = new DocumentMapper(this.doc);
|
|
2603
2649
|
this.comments_manager = new CommentsManager(this.doc);
|
|
2604
2650
|
}
|
|
2651
|
+
_check_punctuation_warning(target_text) {
|
|
2652
|
+
if (!target_text) return null;
|
|
2653
|
+
if (target_text.includes("_") || target_text.includes("-")) {
|
|
2654
|
+
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.`;
|
|
2655
|
+
}
|
|
2656
|
+
return null;
|
|
2657
|
+
}
|
|
2658
|
+
_build_edit_context_previews(edit) {
|
|
2659
|
+
if (edit.type !== "modify") return [null, null];
|
|
2660
|
+
if (edit._resolved_proxy_edit) {
|
|
2661
|
+
edit = edit._resolved_proxy_edit;
|
|
2662
|
+
}
|
|
2663
|
+
const start_idx = edit._resolved_start_idx;
|
|
2664
|
+
if (start_idx === void 0 || start_idx === null) return [null, null];
|
|
2665
|
+
const target_text = edit.target_text || "";
|
|
2666
|
+
const new_text = edit.new_text || "";
|
|
2667
|
+
const length = target_text.length;
|
|
2668
|
+
const active_mapper = edit._active_mapper_ref || this.mapper;
|
|
2669
|
+
const full_text = active_mapper.full_text;
|
|
2670
|
+
if (!full_text) return [null, null];
|
|
2671
|
+
const before_start = Math.max(0, start_idx - 30);
|
|
2672
|
+
const context_before = full_text.substring(before_start, start_idx);
|
|
2673
|
+
const context_after = full_text.substring(
|
|
2674
|
+
start_idx + length,
|
|
2675
|
+
start_idx + length + 30
|
|
2676
|
+
);
|
|
2677
|
+
const critic_markup = `${context_before}{--${target_text}--}{++${new_text}++}${context_after}`;
|
|
2678
|
+
let clean_text = critic_markup;
|
|
2679
|
+
clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
|
|
2680
|
+
clean_text = clean_text.replace(/\{--.*?--\}/gs, "");
|
|
2681
|
+
clean_text = clean_text.replace(/\{\+\+(.*?)\+\+\}/gs, "$1");
|
|
2682
|
+
return [critic_markup, clean_text];
|
|
2683
|
+
}
|
|
2605
2684
|
_scan_existing_ids() {
|
|
2606
2685
|
let maxId = 0;
|
|
2607
2686
|
for (const tag of ["w:ins", "w:del"]) {
|
|
@@ -2694,28 +2773,86 @@ var RedlineEngine = class {
|
|
|
2694
2773
|
}
|
|
2695
2774
|
}
|
|
2696
2775
|
}
|
|
2697
|
-
const
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
]) {
|
|
2703
|
-
for (const node of findAllDescendants(this.doc.element, tag)) {
|
|
2704
|
-
const cid = node.getAttribute("w:id");
|
|
2705
|
-
if (cid) comment_ids.add(cid);
|
|
2776
|
+
for (const root_element of parts_to_process) {
|
|
2777
|
+
for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
|
|
2778
|
+
for (const el of findAllDescendants(root_element, tag)) {
|
|
2779
|
+
el.parentNode?.removeChild(el);
|
|
2780
|
+
}
|
|
2706
2781
|
}
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2782
|
+
const refs = findAllDescendants(root_element, "w:commentReference");
|
|
2783
|
+
for (const ref of refs) {
|
|
2784
|
+
const parent = ref.parentNode;
|
|
2785
|
+
if (parent) {
|
|
2786
|
+
if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
|
|
2787
|
+
const nonRprChildren = Array.from(parent.childNodes).filter(
|
|
2788
|
+
(c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
|
|
2789
|
+
);
|
|
2790
|
+
if (nonRprChildren.length <= 1) {
|
|
2791
|
+
parent.parentNode?.removeChild(parent);
|
|
2792
|
+
} else {
|
|
2793
|
+
parent.removeChild(ref);
|
|
2794
|
+
}
|
|
2795
|
+
} else {
|
|
2796
|
+
parent.removeChild(ref);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2715
2799
|
}
|
|
2716
2800
|
}
|
|
2717
|
-
|
|
2718
|
-
|
|
2801
|
+
const pkg = this.doc.pkg;
|
|
2802
|
+
const comment_partnames = /* @__PURE__ */ new Set();
|
|
2803
|
+
for (const part of pkg.parts) {
|
|
2804
|
+
if (part.partname.toLowerCase().includes("comments")) {
|
|
2805
|
+
comment_partnames.add(part.partname);
|
|
2806
|
+
const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
|
|
2807
|
+
const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
|
|
2808
|
+
comment_partnames.add(withSlash);
|
|
2809
|
+
comment_partnames.add(withoutSlash);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
if (comment_partnames.size > 0) {
|
|
2813
|
+
for (const part of pkg.parts) {
|
|
2814
|
+
if (part.partname.endsWith(".rels")) {
|
|
2815
|
+
const rels = findAllDescendants(part._element, "Relationship");
|
|
2816
|
+
const toRemove = [];
|
|
2817
|
+
for (const rel of rels) {
|
|
2818
|
+
const target = rel.getAttribute("Target") || "";
|
|
2819
|
+
if (target.toLowerCase().includes("comments")) {
|
|
2820
|
+
toRemove.push(rel);
|
|
2821
|
+
const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
|
|
2822
|
+
const sourcePart = pkg.getPartByPath(sourcePath);
|
|
2823
|
+
if (sourcePart) {
|
|
2824
|
+
const relId = rel.getAttribute("Id");
|
|
2825
|
+
if (relId) sourcePart.rels.delete(relId);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
for (const relEl of toRemove) {
|
|
2830
|
+
relEl.parentNode?.removeChild(relEl);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
const ctPart = pkg.getPartByPath("[Content_Types].xml");
|
|
2835
|
+
if (ctPart) {
|
|
2836
|
+
const overrides = findAllDescendants(ctPart._element, "Override");
|
|
2837
|
+
const toRemove = [];
|
|
2838
|
+
for (const override of overrides) {
|
|
2839
|
+
const partName = override.getAttribute("PartName") || "";
|
|
2840
|
+
if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
|
|
2841
|
+
toRemove.push(override);
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
for (const overrideEl of toRemove) {
|
|
2845
|
+
overrideEl.parentNode?.removeChild(overrideEl);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
pkg.parts = pkg.parts.filter(
|
|
2849
|
+
(p) => !p.partname.toLowerCase().includes("comments")
|
|
2850
|
+
);
|
|
2851
|
+
for (const key of Object.keys(pkg.unzipped)) {
|
|
2852
|
+
if (key.toLowerCase().includes("comments")) {
|
|
2853
|
+
delete pkg.unzipped[key];
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2719
2856
|
}
|
|
2720
2857
|
}
|
|
2721
2858
|
_getNextId() {
|
|
@@ -2806,40 +2943,40 @@ var RedlineEngine = class {
|
|
|
2806
2943
|
}
|
|
2807
2944
|
}
|
|
2808
2945
|
/**
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2946
|
+
* Inserts `text` as one or more tracked paragraphs anchored relative to
|
|
2947
|
+
* either an existing run or a paragraph. Returns:
|
|
2948
|
+
* { first_node, last_p, last_ins, used_block_mode }
|
|
2949
|
+
* where:
|
|
2950
|
+
* - first_node: the first <w:ins> (for inline mode) OR the first new <w:p>
|
|
2951
|
+
* (for block mode). The caller uses this for splicing into the DOM and
|
|
2952
|
+
* for anchoring comments.
|
|
2953
|
+
* - last_p: the last new <w:p> created, if any. null when entirely inline.
|
|
2954
|
+
* - last_ins: the last <w:ins> created (inside the last new <w:p>, or the
|
|
2955
|
+
* sole inline ins). Used as the comment's end anchor.
|
|
2956
|
+
* - used_block_mode: true when the first line carried a heading/list style
|
|
2957
|
+
* marker and we created a new paragraph for it (rather than inlining it).
|
|
2958
|
+
*
|
|
2959
|
+
* Multi-paragraph rules (only when text contains '\n'):
|
|
2960
|
+
* - Each additional line becomes a new <w:p>, inserted after the anchor
|
|
2961
|
+
* paragraph in document order.
|
|
2962
|
+
* - Each new <w:p> gets a copy of the anchor paragraph's <w:pPr> (so list
|
|
2963
|
+
* numbering / indentation are preserved) unless the line itself starts
|
|
2964
|
+
* with a markdown heading or list marker, which overrides the style.
|
|
2965
|
+
* - Each new <w:p> carries a tracked paragraph-break marker
|
|
2966
|
+
* (<w:pPr><w:rPr><w:ins/></w:rPr></w:pPr>) so Word natively tracks the
|
|
2967
|
+
* paragraph break.
|
|
2968
|
+
* - Each new <w:p>'s content is wrapped in a <w:ins>, with inline bold/
|
|
2969
|
+
* italic markdown parsed via _parse_inline_markdown.
|
|
2970
|
+
*
|
|
2971
|
+
* The first line:
|
|
2972
|
+
* - If it carries a heading / list marker AND we have a paragraph anchor,
|
|
2973
|
+
* we drop into "block mode": no inline <w:ins>; the first line itself
|
|
2974
|
+
* becomes the first new <w:p>.
|
|
2975
|
+
* - Otherwise we emit a single inline <w:ins> for the first line (current
|
|
2976
|
+
* behaviour) and treat the remaining lines as block extensions.
|
|
2977
|
+
*
|
|
2978
|
+
* Does NOT attach comments; callers handle that.
|
|
2979
|
+
*/
|
|
2843
2980
|
_track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id) {
|
|
2844
2981
|
if (!text) {
|
|
2845
2982
|
return {
|
|
@@ -2979,7 +3116,15 @@ var RedlineEngine = class {
|
|
|
2979
3116
|
const anchor_rPr = findChild(anchor_run._element, "w:rPr");
|
|
2980
3117
|
if (anchor_rPr) {
|
|
2981
3118
|
const clone = anchor_rPr.cloneNode(true);
|
|
2982
|
-
for (const tag of [
|
|
3119
|
+
for (const tag of [
|
|
3120
|
+
"w:vanish",
|
|
3121
|
+
"w:strike",
|
|
3122
|
+
"w:dstrike",
|
|
3123
|
+
"w:i",
|
|
3124
|
+
"w:iCs",
|
|
3125
|
+
"w:b",
|
|
3126
|
+
"w:bCs"
|
|
3127
|
+
]) {
|
|
2983
3128
|
const found = findChild(clone, tag);
|
|
2984
3129
|
if (found) clone.removeChild(found);
|
|
2985
3130
|
}
|
|
@@ -3253,6 +3398,16 @@ var RedlineEngine = class {
|
|
|
3253
3398
|
matches = this.clean_mapper.find_all_match_indices(edit.target_text);
|
|
3254
3399
|
if (matches.length > 0) activeText = this.clean_mapper.full_text;
|
|
3255
3400
|
}
|
|
3401
|
+
if (activeText === this.mapper.full_text && matches.length > 1) {
|
|
3402
|
+
const liveMatches = matches.filter(([start, length]) => {
|
|
3403
|
+
const realSpans = this.mapper.spans.filter(
|
|
3404
|
+
(s) => s.run !== null && s.end > start && s.start < start + length
|
|
3405
|
+
);
|
|
3406
|
+
if (realSpans.length === 0) return true;
|
|
3407
|
+
return realSpans.some((s) => !s.del_id);
|
|
3408
|
+
});
|
|
3409
|
+
if (liveMatches.length > 0) matches = liveMatches;
|
|
3410
|
+
}
|
|
3256
3411
|
if (matches.length === 0) {
|
|
3257
3412
|
errors.push(
|
|
3258
3413
|
`- Edit ${i + 1} Failed: Target text not found in document:
|
|
@@ -3272,6 +3427,34 @@ var RedlineEngine = class {
|
|
|
3272
3427
|
)
|
|
3273
3428
|
);
|
|
3274
3429
|
}
|
|
3430
|
+
if (matches.length === 1) {
|
|
3431
|
+
const [m_start, m_len] = matches[0];
|
|
3432
|
+
const matched = activeText.substring(m_start, m_start + m_len);
|
|
3433
|
+
const [pfx, sfx] = trim_common_context(matched, edit.new_text || "");
|
|
3434
|
+
const t_end = matched.length - sfx;
|
|
3435
|
+
const final_target = matched.substring(pfx, t_end);
|
|
3436
|
+
const final_new = (edit.new_text || "").substring(
|
|
3437
|
+
pfx,
|
|
3438
|
+
(edit.new_text || "").length - sfx
|
|
3439
|
+
);
|
|
3440
|
+
if (final_target.includes("\n\n")) {
|
|
3441
|
+
if (final_new.includes("\n\n")) {
|
|
3442
|
+
const parts = matched.split("\n\n");
|
|
3443
|
+
if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
|
|
3444
|
+
errors.push(
|
|
3445
|
+
`- 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.`
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3448
|
+
} else {
|
|
3449
|
+
const parts = final_target.split("\n\n");
|
|
3450
|
+
if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
|
|
3451
|
+
errors.push(
|
|
3452
|
+
`- 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.`
|
|
3453
|
+
);
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3275
3458
|
for (const [start, length] of matches) {
|
|
3276
3459
|
const spans = this.mapper.spans.filter(
|
|
3277
3460
|
(s) => s.end > start && s.start < start + length
|
|
@@ -3335,7 +3518,33 @@ var RedlineEngine = class {
|
|
|
3335
3518
|
}
|
|
3336
3519
|
return errors;
|
|
3337
3520
|
}
|
|
3338
|
-
process_batch(changes) {
|
|
3521
|
+
process_batch(changes, dry_run = false) {
|
|
3522
|
+
if (dry_run) {
|
|
3523
|
+
const baselines = /* @__PURE__ */ new Map();
|
|
3524
|
+
for (const part of this.doc.pkg.parts) {
|
|
3525
|
+
if (part._element) {
|
|
3526
|
+
baselines.set(part, part._element.cloneNode(true));
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
try {
|
|
3530
|
+
return this._process_batch_internal(changes, true);
|
|
3531
|
+
} finally {
|
|
3532
|
+
for (const [part, originalEl] of baselines.entries()) {
|
|
3533
|
+
const doc = part._element.ownerDocument;
|
|
3534
|
+
if (doc && doc.documentElement) {
|
|
3535
|
+
doc.replaceChild(originalEl, doc.documentElement);
|
|
3536
|
+
}
|
|
3537
|
+
part._element = originalEl;
|
|
3538
|
+
}
|
|
3539
|
+
this.mapper = new DocumentMapper(this.doc);
|
|
3540
|
+
this.comments_manager = new CommentsManager(this.doc);
|
|
3541
|
+
this.clean_mapper = null;
|
|
3542
|
+
}
|
|
3543
|
+
} else {
|
|
3544
|
+
return this._process_batch_internal(changes, false);
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
_process_batch_internal(changes, dry_run_mode = false) {
|
|
3339
3548
|
this.skipped_details = [];
|
|
3340
3549
|
const actions = changes.filter(
|
|
3341
3550
|
(c) => ["accept", "reject", "reply"].includes(c.type)
|
|
@@ -3343,38 +3552,133 @@ var RedlineEngine = class {
|
|
|
3343
3552
|
const edits = changes.filter(
|
|
3344
3553
|
(c) => !["accept", "reject", "reply"].includes(c.type)
|
|
3345
3554
|
);
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3555
|
+
if (!dry_run_mode) {
|
|
3556
|
+
const all_errors = [];
|
|
3557
|
+
if (actions.length > 0) {
|
|
3558
|
+
all_errors.push(...this.validate_review_actions(actions));
|
|
3559
|
+
}
|
|
3560
|
+
if (edits.length > 0) {
|
|
3561
|
+
all_errors.push(...this.validate_edits(edits));
|
|
3562
|
+
}
|
|
3563
|
+
if (all_errors.length > 0) {
|
|
3564
|
+
throw new BatchValidationError(all_errors);
|
|
3565
|
+
}
|
|
3566
|
+
} else {
|
|
3567
|
+
if (actions.length > 0) {
|
|
3568
|
+
const action_errors = this.validate_review_actions(actions);
|
|
3569
|
+
if (action_errors.length > 0) {
|
|
3570
|
+
throw new BatchValidationError(action_errors);
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3355
3573
|
}
|
|
3356
|
-
let applied_actions = 0
|
|
3574
|
+
let applied_actions = 0;
|
|
3575
|
+
let skipped_actions = 0;
|
|
3357
3576
|
if (actions.length > 0) {
|
|
3358
3577
|
const res = this.apply_review_actions(actions);
|
|
3359
3578
|
applied_actions = res[0];
|
|
3360
3579
|
skipped_actions = res[1];
|
|
3580
|
+
if (skipped_actions > 0) {
|
|
3581
|
+
throw new BatchValidationError(this.skipped_details);
|
|
3582
|
+
}
|
|
3361
3583
|
if (applied_actions > 0) {
|
|
3362
3584
|
this.mapper["_build_map"]();
|
|
3363
3585
|
if (this.clean_mapper) this.clean_mapper["_build_map"]();
|
|
3364
3586
|
}
|
|
3365
3587
|
}
|
|
3366
|
-
|
|
3588
|
+
const edits_reports = [];
|
|
3589
|
+
let applied_edits = 0;
|
|
3590
|
+
let skipped_edits = 0;
|
|
3367
3591
|
if (edits.length > 0) {
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3592
|
+
if (dry_run_mode) {
|
|
3593
|
+
for (const edit of edits) {
|
|
3594
|
+
const single_errors = this.validate_edits([edit]);
|
|
3595
|
+
const warning = this._check_punctuation_warning(
|
|
3596
|
+
edit.target_text || ""
|
|
3597
|
+
);
|
|
3598
|
+
if (single_errors.length > 0) {
|
|
3599
|
+
skipped_edits++;
|
|
3600
|
+
edits_reports.push({
|
|
3601
|
+
status: "failed",
|
|
3602
|
+
target_text: edit.target_text || "",
|
|
3603
|
+
new_text: edit.new_text || "",
|
|
3604
|
+
warning,
|
|
3605
|
+
error: single_errors[0],
|
|
3606
|
+
critic_markup: null,
|
|
3607
|
+
clean_text: null
|
|
3608
|
+
});
|
|
3609
|
+
continue;
|
|
3610
|
+
}
|
|
3611
|
+
const res = this.apply_edits([edit]);
|
|
3612
|
+
const applied = res[0];
|
|
3613
|
+
if (applied > 0) {
|
|
3614
|
+
applied_edits++;
|
|
3615
|
+
const previews = this._build_edit_context_previews(edit);
|
|
3616
|
+
edits_reports.push({
|
|
3617
|
+
status: "applied",
|
|
3618
|
+
target_text: edit.target_text || "",
|
|
3619
|
+
new_text: edit.new_text || "",
|
|
3620
|
+
warning,
|
|
3621
|
+
error: null,
|
|
3622
|
+
critic_markup: previews[0],
|
|
3623
|
+
clean_text: previews[1]
|
|
3624
|
+
});
|
|
3625
|
+
} else {
|
|
3626
|
+
skipped_edits++;
|
|
3627
|
+
const error_msg = this.skipped_details.length > 0 ? this.skipped_details[this.skipped_details.length - 1] : "Failed to apply edit";
|
|
3628
|
+
edits_reports.push({
|
|
3629
|
+
status: "failed",
|
|
3630
|
+
target_text: edit.target_text || "",
|
|
3631
|
+
new_text: edit.new_text || "",
|
|
3632
|
+
warning,
|
|
3633
|
+
error: error_msg,
|
|
3634
|
+
critic_markup: null,
|
|
3635
|
+
clean_text: null
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
} else {
|
|
3640
|
+
const errors = this.validate_edits(edits);
|
|
3641
|
+
if (errors.length > 0) {
|
|
3642
|
+
throw new BatchValidationError(errors);
|
|
3643
|
+
}
|
|
3644
|
+
const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
|
|
3645
|
+
const res = this.apply_edits(cloned_edits);
|
|
3646
|
+
applied_edits = res[0];
|
|
3647
|
+
skipped_edits = res[1];
|
|
3648
|
+
for (const edit of cloned_edits) {
|
|
3649
|
+
const success = edit._applied_status || false;
|
|
3650
|
+
const error_msg = edit._error_msg || null;
|
|
3651
|
+
const warning = this._check_punctuation_warning(
|
|
3652
|
+
edit.target_text || ""
|
|
3653
|
+
);
|
|
3654
|
+
let critic_markup = null;
|
|
3655
|
+
let clean_text = null;
|
|
3656
|
+
if (success) {
|
|
3657
|
+
const previews = this._build_edit_context_previews(edit);
|
|
3658
|
+
critic_markup = previews[0];
|
|
3659
|
+
clean_text = previews[1];
|
|
3660
|
+
}
|
|
3661
|
+
edits_reports.push({
|
|
3662
|
+
status: success ? "applied" : "failed",
|
|
3663
|
+
target_text: edit.target_text || "",
|
|
3664
|
+
new_text: edit.new_text || "",
|
|
3665
|
+
warning,
|
|
3666
|
+
error: error_msg,
|
|
3667
|
+
critic_markup,
|
|
3668
|
+
clean_text
|
|
3669
|
+
});
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3371
3672
|
}
|
|
3372
3673
|
return {
|
|
3373
3674
|
actions_applied: applied_actions,
|
|
3374
3675
|
actions_skipped: skipped_actions,
|
|
3375
3676
|
edits_applied: applied_edits,
|
|
3376
3677
|
edits_skipped: skipped_edits,
|
|
3377
|
-
skipped_details: this.skipped_details
|
|
3678
|
+
skipped_details: this.skipped_details,
|
|
3679
|
+
edits: edits_reports,
|
|
3680
|
+
engine: "node",
|
|
3681
|
+
version: "1.10.0"
|
|
3378
3682
|
};
|
|
3379
3683
|
}
|
|
3380
3684
|
apply_edits(edits) {
|
|
@@ -3382,50 +3686,90 @@ var RedlineEngine = class {
|
|
|
3382
3686
|
let skipped = 0;
|
|
3383
3687
|
const resolved_edits = [];
|
|
3384
3688
|
for (const edit of edits) {
|
|
3385
|
-
|
|
3689
|
+
edit._applied_status = false;
|
|
3690
|
+
edit._error_msg = null;
|
|
3691
|
+
}
|
|
3692
|
+
for (const edit of edits) {
|
|
3693
|
+
if (edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null) {
|
|
3694
|
+
resolved_edits.push([edit, edit.new_text || null]);
|
|
3695
|
+
} else if (edit._match_start_index !== void 0 && edit._match_start_index !== null) {
|
|
3696
|
+
edit._resolved_start_idx = edit._match_start_index;
|
|
3386
3697
|
resolved_edits.push([edit, edit.new_text || null]);
|
|
3387
3698
|
} else if (edit.type === "insert_row" || edit.type === "delete_row") {
|
|
3388
|
-
|
|
3389
|
-
if (
|
|
3390
|
-
|
|
3699
|
+
let matches = this.mapper.find_all_match_indices(edit.target_text);
|
|
3700
|
+
if (matches.length === 0) {
|
|
3701
|
+
if (!this.clean_mapper) {
|
|
3702
|
+
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
3703
|
+
}
|
|
3704
|
+
matches = this.clean_mapper.find_all_match_indices(edit.target_text);
|
|
3705
|
+
}
|
|
3706
|
+
if (matches.length > 0) {
|
|
3707
|
+
edit._resolved_start_idx = matches[0][0];
|
|
3391
3708
|
resolved_edits.push([edit, null]);
|
|
3392
3709
|
} else {
|
|
3393
3710
|
skipped++;
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3711
|
+
edit._applied_status = false;
|
|
3712
|
+
const target_snippet = (edit.target_text || "").trim().substring(0, 40);
|
|
3713
|
+
const msg = `- Failed to locate row target: '${target_snippet}...'`;
|
|
3714
|
+
this.skipped_details.push(msg);
|
|
3715
|
+
edit._error_msg = msg;
|
|
3397
3716
|
}
|
|
3398
3717
|
} else {
|
|
3399
3718
|
const resolved = this._pre_resolve_heuristic_edit(edit);
|
|
3400
3719
|
if (resolved) {
|
|
3401
3720
|
if (Array.isArray(resolved)) {
|
|
3402
|
-
for (const r of resolved)
|
|
3721
|
+
for (const r of resolved) {
|
|
3722
|
+
r._resolved_start_idx = r._match_start_index;
|
|
3723
|
+
r._parent_edit_ref = edit;
|
|
3724
|
+
if (edit._resolved_start_idx === void 0 || edit._resolved_start_idx === null) {
|
|
3725
|
+
edit._resolved_start_idx = r._resolved_start_idx;
|
|
3726
|
+
}
|
|
3727
|
+
if (!edit._resolved_proxy_edit) {
|
|
3728
|
+
edit._resolved_proxy_edit = r;
|
|
3729
|
+
}
|
|
3730
|
+
resolved_edits.push([r, r.new_text]);
|
|
3731
|
+
}
|
|
3403
3732
|
} else {
|
|
3733
|
+
resolved._resolved_start_idx = resolved._match_start_index;
|
|
3734
|
+
resolved._parent_edit_ref = edit;
|
|
3735
|
+
edit._resolved_start_idx = resolved._resolved_start_idx;
|
|
3736
|
+
edit._resolved_proxy_edit = resolved;
|
|
3404
3737
|
resolved_edits.push([resolved, resolved.new_text]);
|
|
3405
3738
|
}
|
|
3406
3739
|
} else {
|
|
3407
3740
|
skipped++;
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
);
|
|
3741
|
+
edit._applied_status = false;
|
|
3742
|
+
const display_text = edit.target_text || "insertion";
|
|
3743
|
+
const target_snippet = display_text.trim().substring(0, 40);
|
|
3744
|
+
const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
|
|
3745
|
+
this.skipped_details.push(msg);
|
|
3746
|
+
edit._error_msg = msg;
|
|
3411
3747
|
}
|
|
3412
3748
|
}
|
|
3413
3749
|
}
|
|
3414
3750
|
resolved_edits.sort(
|
|
3415
|
-
(a, b) => (b[0].
|
|
3751
|
+
(a, b) => (b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0)
|
|
3416
3752
|
);
|
|
3417
3753
|
const occupied_ranges = [];
|
|
3418
3754
|
for (const [edit, orig_new] of resolved_edits) {
|
|
3419
|
-
const start = edit.
|
|
3755
|
+
const start = edit._resolved_start_idx || 0;
|
|
3420
3756
|
const end = start + (edit.target_text ? edit.target_text.length : 0);
|
|
3421
3757
|
const overlaps = occupied_ranges.some(
|
|
3422
3758
|
([occ_start, occ_end]) => start < occ_end && end > occ_start
|
|
3423
3759
|
);
|
|
3424
3760
|
if (overlaps) {
|
|
3425
3761
|
skipped++;
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3762
|
+
const display_text = edit.target_text || "insertion";
|
|
3763
|
+
const target_snippet = display_text.trim().substring(0, 40);
|
|
3764
|
+
const msg = `- Skipped overlapping edit targeting: '${target_snippet}...'`;
|
|
3765
|
+
this.skipped_details.push(msg);
|
|
3766
|
+
edit._applied_status = false;
|
|
3767
|
+
edit._error_msg = msg;
|
|
3768
|
+
const parent = edit._parent_edit_ref;
|
|
3769
|
+
if (parent) {
|
|
3770
|
+
parent._applied_status = false;
|
|
3771
|
+
parent._error_msg = msg;
|
|
3772
|
+
}
|
|
3429
3773
|
continue;
|
|
3430
3774
|
}
|
|
3431
3775
|
let success = false;
|
|
@@ -3437,11 +3781,26 @@ var RedlineEngine = class {
|
|
|
3437
3781
|
if (success) {
|
|
3438
3782
|
applied++;
|
|
3439
3783
|
occupied_ranges.push([start, end]);
|
|
3784
|
+
edit._applied_status = true;
|
|
3785
|
+
const parent = edit._parent_edit_ref;
|
|
3786
|
+
if (parent) {
|
|
3787
|
+
parent._applied_status = true;
|
|
3788
|
+
}
|
|
3440
3789
|
} else {
|
|
3441
3790
|
skipped++;
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3791
|
+
const display_text = edit.target_text || "insertion";
|
|
3792
|
+
const target_snippet = display_text.trim().substring(0, 40);
|
|
3793
|
+
const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
|
|
3794
|
+
this.skipped_details.push(msg);
|
|
3795
|
+
edit._applied_status = false;
|
|
3796
|
+
edit._error_msg = msg;
|
|
3797
|
+
const parent = edit._parent_edit_ref;
|
|
3798
|
+
if (parent) {
|
|
3799
|
+
if (!parent._applied_status) {
|
|
3800
|
+
parent._applied_status = false;
|
|
3801
|
+
parent._error_msg = msg;
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3445
3804
|
}
|
|
3446
3805
|
}
|
|
3447
3806
|
return [applied, skipped];
|
|
@@ -3534,7 +3893,7 @@ var RedlineEngine = class {
|
|
|
3534
3893
|
return [applied, skipped];
|
|
3535
3894
|
}
|
|
3536
3895
|
_apply_table_edit(edit, rebuild_map) {
|
|
3537
|
-
const start_idx = edit._match_start_index || 0;
|
|
3896
|
+
const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
|
|
3538
3897
|
const [anchor_run, anchor_para] = this.mapper.get_insertion_anchor(
|
|
3539
3898
|
start_idx,
|
|
3540
3899
|
rebuild_map
|
|
@@ -3578,9 +3937,31 @@ var RedlineEngine = class {
|
|
|
3578
3937
|
}
|
|
3579
3938
|
return false;
|
|
3580
3939
|
}
|
|
3940
|
+
/**
|
|
3941
|
+
* Returns the first match of `target_text` in the raw mapper that is NOT
|
|
3942
|
+
* entirely contained within a tracked deletion (<w:del>). Tracked-deleted
|
|
3943
|
+
* copies are not live, editable text, so an edit must resolve to a live
|
|
3944
|
+
* occurrence even when a dead copy appears earlier in the document
|
|
3945
|
+
* (BUG-23-5). Falls back to the plain first match when no live copy is
|
|
3946
|
+
* found (e.g. fuzzy/normalized matches the span filter cannot align).
|
|
3947
|
+
*/
|
|
3948
|
+
_first_live_match(target_text) {
|
|
3949
|
+
const all = this.mapper.find_all_match_indices(target_text);
|
|
3950
|
+
if (all.length <= 1) {
|
|
3951
|
+
return this.mapper.find_match_index(target_text);
|
|
3952
|
+
}
|
|
3953
|
+
for (const [start, length] of all) {
|
|
3954
|
+
const realSpans = this.mapper.spans.filter(
|
|
3955
|
+
(s) => s.run !== null && s.end > start && s.start < start + length
|
|
3956
|
+
);
|
|
3957
|
+
if (realSpans.length === 0) return [start, length];
|
|
3958
|
+
if (realSpans.some((s) => !s.del_id)) return [start, length];
|
|
3959
|
+
}
|
|
3960
|
+
return this.mapper.find_match_index(target_text);
|
|
3961
|
+
}
|
|
3581
3962
|
_pre_resolve_heuristic_edit(edit) {
|
|
3582
3963
|
if (!edit.target_text) return null;
|
|
3583
|
-
let [start_idx, match_len] = this.
|
|
3964
|
+
let [start_idx, match_len] = this._first_live_match(edit.target_text);
|
|
3584
3965
|
let use_clean_map = false;
|
|
3585
3966
|
if (start_idx === -1) {
|
|
3586
3967
|
if (!this.clean_mapper)
|
|
@@ -3644,7 +4025,7 @@ var RedlineEngine = class {
|
|
|
3644
4025
|
_apply_single_edit_indexed(edit, orig_new, rebuild_map) {
|
|
3645
4026
|
let op = edit._internal_op;
|
|
3646
4027
|
const active_mapper = edit._active_mapper_ref || this.mapper;
|
|
3647
|
-
const start_idx = edit._match_start_index || 0;
|
|
4028
|
+
const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
|
|
3648
4029
|
const length = edit.target_text ? edit.target_text.length : 0;
|
|
3649
4030
|
const del_id = ["DELETION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
|
|
3650
4031
|
const ins_id = ["INSERTION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
|
|
@@ -3693,6 +4074,76 @@ var RedlineEngine = class {
|
|
|
3693
4074
|
rebuild_map
|
|
3694
4075
|
);
|
|
3695
4076
|
if (!anchor_run && !anchor_para) return false;
|
|
4077
|
+
const _bug233_new = edit.new_text || "";
|
|
4078
|
+
const _bug233_trailing_break = /\n\s*$/.test(_bug233_new);
|
|
4079
|
+
let _bug233_target_para = null;
|
|
4080
|
+
{
|
|
4081
|
+
const startingSpans = active_mapper.spans.filter(
|
|
4082
|
+
(s) => s.paragraph !== null && s.start === start_idx
|
|
4083
|
+
);
|
|
4084
|
+
if (startingSpans.length > 0 && startingSpans[0].paragraph) {
|
|
4085
|
+
_bug233_target_para = startingSpans[0].paragraph._element;
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
if (_bug233_trailing_break && _bug233_target_para && _bug233_target_para.parentNode) {
|
|
4089
|
+
const body = _bug233_target_para.parentNode;
|
|
4090
|
+
const xmlDoc = this.doc.part._element.ownerDocument;
|
|
4091
|
+
const lines = _bug233_new.split(/[\r\n]+/).filter((l) => l !== "");
|
|
4092
|
+
let firstNew = null;
|
|
4093
|
+
let lastNew = null;
|
|
4094
|
+
let lastIns = null;
|
|
4095
|
+
for (const raw_line of lines) {
|
|
4096
|
+
const [clean_text, style_name] = this._parse_markdown_style(raw_line);
|
|
4097
|
+
const new_p = xmlDoc.createElement("w:p");
|
|
4098
|
+
if (style_name) {
|
|
4099
|
+
this._set_paragraph_style(new_p, style_name);
|
|
4100
|
+
} else {
|
|
4101
|
+
const existing_pPr = findChild(_bug233_target_para, "w:pPr");
|
|
4102
|
+
if (existing_pPr) new_p.appendChild(existing_pPr.cloneNode(true));
|
|
4103
|
+
}
|
|
4104
|
+
let pPr = findChild(new_p, "w:pPr");
|
|
4105
|
+
if (!pPr) {
|
|
4106
|
+
pPr = xmlDoc.createElement("w:pPr");
|
|
4107
|
+
new_p.insertBefore(pPr, new_p.firstChild);
|
|
4108
|
+
}
|
|
4109
|
+
let rPr = findChild(pPr, "w:rPr");
|
|
4110
|
+
if (!rPr) {
|
|
4111
|
+
rPr = xmlDoc.createElement("w:rPr");
|
|
4112
|
+
pPr.appendChild(rPr);
|
|
4113
|
+
}
|
|
4114
|
+
rPr.appendChild(this._create_track_change_tag("w:ins", "", ins_id));
|
|
4115
|
+
const content_ins = this._build_tracked_ins_for_line(
|
|
4116
|
+
clean_text,
|
|
4117
|
+
anchor_run,
|
|
4118
|
+
ins_id,
|
|
4119
|
+
xmlDoc
|
|
4120
|
+
);
|
|
4121
|
+
if (content_ins) new_p.appendChild(content_ins);
|
|
4122
|
+
body.insertBefore(new_p, _bug233_target_para);
|
|
4123
|
+
if (!firstNew) firstNew = new_p;
|
|
4124
|
+
lastNew = new_p;
|
|
4125
|
+
lastIns = content_ins;
|
|
4126
|
+
}
|
|
4127
|
+
if (firstNew) {
|
|
4128
|
+
if (edit.comment && lastNew && lastIns) {
|
|
4129
|
+
const ascend = (el, p) => {
|
|
4130
|
+
let cur = el;
|
|
4131
|
+
while (cur.parentNode && cur.parentNode !== p)
|
|
4132
|
+
cur = cur.parentNode;
|
|
4133
|
+
return cur;
|
|
4134
|
+
};
|
|
4135
|
+
const startIns = findAllDescendants(firstNew, "w:ins")[0] || firstNew;
|
|
4136
|
+
this._attach_comment_spanning(
|
|
4137
|
+
firstNew,
|
|
4138
|
+
ascend(startIns, firstNew),
|
|
4139
|
+
lastNew,
|
|
4140
|
+
ascend(lastIns, lastNew),
|
|
4141
|
+
edit.comment
|
|
4142
|
+
);
|
|
4143
|
+
}
|
|
4144
|
+
return true;
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
3696
4147
|
const result = this._track_insert_multiline(
|
|
3697
4148
|
edit.new_text || "",
|
|
3698
4149
|
anchor_run,
|
|
@@ -3750,7 +4201,10 @@ var RedlineEngine = class {
|
|
|
3750
4201
|
if (result.first_node.tagName === "w:p") {
|
|
3751
4202
|
first_anchor_target = findAllDescendants(result.first_node, "w:ins")[0] || result.first_node;
|
|
3752
4203
|
}
|
|
3753
|
-
const anchor = ascend_to_paragraph_child(
|
|
4204
|
+
const anchor = ascend_to_paragraph_child(
|
|
4205
|
+
first_anchor_target,
|
|
4206
|
+
host_p
|
|
4207
|
+
);
|
|
3754
4208
|
this._attach_comment(host_p, anchor, anchor, edit.comment);
|
|
3755
4209
|
}
|
|
3756
4210
|
}
|
|
@@ -3762,7 +4216,10 @@ var RedlineEngine = class {
|
|
|
3762
4216
|
length,
|
|
3763
4217
|
rebuild_map
|
|
3764
4218
|
);
|
|
3765
|
-
const virtual_spans = active_mapper.get_virtual_spans_in_range(
|
|
4219
|
+
const virtual_spans = active_mapper.get_virtual_spans_in_range(
|
|
4220
|
+
start_idx,
|
|
4221
|
+
length
|
|
4222
|
+
);
|
|
3766
4223
|
if (target_runs.length === 0 && virtual_spans.length === 0) return false;
|
|
3767
4224
|
const affected_ps = /* @__PURE__ */ new Set();
|
|
3768
4225
|
for (const run of target_runs) {
|
|
@@ -3845,7 +4302,10 @@ var RedlineEngine = class {
|
|
|
3845
4302
|
let pPr = findChild(p1_element, "w:pPr");
|
|
3846
4303
|
if (!pPr) {
|
|
3847
4304
|
pPr = p1_element.ownerDocument.createElement("w:pPr");
|
|
3848
|
-
p1_element.insertBefore(
|
|
4305
|
+
p1_element.insertBefore(
|
|
4306
|
+
pPr,
|
|
4307
|
+
p1_element.firstChild
|
|
4308
|
+
);
|
|
3849
4309
|
}
|
|
3850
4310
|
let rPr = findChild(pPr, "w:rPr");
|
|
3851
4311
|
if (!rPr) {
|
|
@@ -5402,15 +5862,88 @@ function remove_all_comments(doc) {
|
|
|
5402
5862
|
lines.push(` ${status} "${_truncate(info.text || "", 60)}" (${info.author || "Unknown"})`);
|
|
5403
5863
|
cm.deleteComment(cId);
|
|
5404
5864
|
}
|
|
5405
|
-
for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"
|
|
5865
|
+
for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
|
|
5406
5866
|
for (const el of findAllDescendants(doc.element, tag)) {
|
|
5407
5867
|
el.parentNode?.removeChild(el);
|
|
5408
5868
|
}
|
|
5409
5869
|
}
|
|
5870
|
+
const refs = findAllDescendants(doc.element, "w:commentReference");
|
|
5871
|
+
for (const ref of refs) {
|
|
5872
|
+
const parent = ref.parentNode;
|
|
5873
|
+
if (parent) {
|
|
5874
|
+
if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
|
|
5875
|
+
const nonRprChildren = Array.from(parent.childNodes).filter(
|
|
5876
|
+
(c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
|
|
5877
|
+
);
|
|
5878
|
+
if (nonRprChildren.length <= 1) {
|
|
5879
|
+
parent.parentNode?.removeChild(parent);
|
|
5880
|
+
} else {
|
|
5881
|
+
parent.removeChild(ref);
|
|
5882
|
+
}
|
|
5883
|
+
} else {
|
|
5884
|
+
parent.removeChild(ref);
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
}
|
|
5410
5888
|
const resolvedCount = Object.values(data).filter((c) => c.resolved).length;
|
|
5411
5889
|
const openCount = Object.values(data).filter((c) => !c.resolved).length;
|
|
5412
5890
|
return [`Comments removed: ${keys.length} (${resolvedCount} resolved, ${openCount} open)`].concat(lines);
|
|
5413
5891
|
}
|
|
5892
|
+
function eject_comment_parts(doc) {
|
|
5893
|
+
const pkg = doc.pkg;
|
|
5894
|
+
const comment_partnames = /* @__PURE__ */ new Set();
|
|
5895
|
+
for (const part of pkg.parts) {
|
|
5896
|
+
if (part.partname.toLowerCase().includes("comments")) {
|
|
5897
|
+
comment_partnames.add(part.partname);
|
|
5898
|
+
const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
|
|
5899
|
+
const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
|
|
5900
|
+
comment_partnames.add(withSlash);
|
|
5901
|
+
comment_partnames.add(withoutSlash);
|
|
5902
|
+
}
|
|
5903
|
+
}
|
|
5904
|
+
if (comment_partnames.size === 0) return;
|
|
5905
|
+
for (const part of pkg.parts) {
|
|
5906
|
+
if (part.partname.endsWith(".rels")) {
|
|
5907
|
+
const rels = findAllDescendants(part._element, "Relationship");
|
|
5908
|
+
const toRemove = [];
|
|
5909
|
+
for (const rel of rels) {
|
|
5910
|
+
const target = rel.getAttribute("Target") || "";
|
|
5911
|
+
if (target.toLowerCase().includes("comments")) {
|
|
5912
|
+
toRemove.push(rel);
|
|
5913
|
+
const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
|
|
5914
|
+
const sourcePart = pkg.getPartByPath(sourcePath);
|
|
5915
|
+
if (sourcePart) {
|
|
5916
|
+
const relId = rel.getAttribute("Id");
|
|
5917
|
+
if (relId) sourcePart.rels.delete(relId);
|
|
5918
|
+
}
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
for (const relEl of toRemove) {
|
|
5922
|
+
relEl.parentNode?.removeChild(relEl);
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
}
|
|
5926
|
+
const ctPart = pkg.getPartByPath("[Content_Types].xml");
|
|
5927
|
+
if (ctPart) {
|
|
5928
|
+
const overrides = findAllDescendants(ctPart._element, "Override");
|
|
5929
|
+
const toRemove = [];
|
|
5930
|
+
for (const override of overrides) {
|
|
5931
|
+
const partName = override.getAttribute("PartName") || "";
|
|
5932
|
+
if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
|
|
5933
|
+
toRemove.push(override);
|
|
5934
|
+
}
|
|
5935
|
+
}
|
|
5936
|
+
for (const overrideEl of toRemove) {
|
|
5937
|
+
overrideEl.parentNode?.removeChild(overrideEl);
|
|
5938
|
+
}
|
|
5939
|
+
}
|
|
5940
|
+
pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
|
|
5941
|
+
for (const key of Object.keys(pkg.unzipped)) {
|
|
5942
|
+
if (key.toLowerCase().includes("comments")) {
|
|
5943
|
+
delete pkg.unzipped[key];
|
|
5944
|
+
}
|
|
5945
|
+
}
|
|
5946
|
+
}
|
|
5414
5947
|
function replace_comment_authors(doc, newAuthor) {
|
|
5415
5948
|
const cm = new CommentsManager(doc);
|
|
5416
5949
|
if (!cm.commentsPart) return [];
|
|
@@ -5599,6 +6132,7 @@ async function finalize_document(doc, options) {
|
|
|
5599
6132
|
const commentsSummary = get_comments_summary(doc);
|
|
5600
6133
|
report.comments_removed = commentsSummary.total;
|
|
5601
6134
|
report.add_transform_lines(remove_all_comments(doc));
|
|
6135
|
+
eject_comment_parts(doc);
|
|
5602
6136
|
} else if (options.sanitize_mode === "keep-markup") {
|
|
5603
6137
|
const counts = count_tracked_changes(doc);
|
|
5604
6138
|
report.tracked_changes_found = counts[0] + counts[1] + counts[2];
|