@adeu/core 1.9.0 → 1.10.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 +615 -102
- 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 +615 -102
- 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 +513 -64
- 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,36 @@ 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(start_idx + length, start_idx + length + 30);
|
|
2674
|
+
const critic_markup = `${context_before}{--${target_text}--}{++${new_text}++}${context_after}`;
|
|
2675
|
+
let clean_text = critic_markup;
|
|
2676
|
+
clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
|
|
2677
|
+
clean_text = clean_text.replace(/\{--.*?--\}/gs, "");
|
|
2678
|
+
clean_text = clean_text.replace(/\{\+\+(.*?)\+\+\}/gs, "$1");
|
|
2679
|
+
return [critic_markup, clean_text];
|
|
2680
|
+
}
|
|
2605
2681
|
_scan_existing_ids() {
|
|
2606
2682
|
let maxId = 0;
|
|
2607
2683
|
for (const tag of ["w:ins", "w:del"]) {
|
|
@@ -2694,28 +2770,84 @@ var RedlineEngine = class {
|
|
|
2694
2770
|
}
|
|
2695
2771
|
}
|
|
2696
2772
|
}
|
|
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);
|
|
2773
|
+
for (const root_element of parts_to_process) {
|
|
2774
|
+
for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
|
|
2775
|
+
for (const el of findAllDescendants(root_element, tag)) {
|
|
2776
|
+
el.parentNode?.removeChild(el);
|
|
2777
|
+
}
|
|
2706
2778
|
}
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2779
|
+
const refs = findAllDescendants(root_element, "w:commentReference");
|
|
2780
|
+
for (const ref of refs) {
|
|
2781
|
+
const parent = ref.parentNode;
|
|
2782
|
+
if (parent) {
|
|
2783
|
+
if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
|
|
2784
|
+
const nonRprChildren = Array.from(parent.childNodes).filter(
|
|
2785
|
+
(c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
|
|
2786
|
+
);
|
|
2787
|
+
if (nonRprChildren.length <= 1) {
|
|
2788
|
+
parent.parentNode?.removeChild(parent);
|
|
2789
|
+
} else {
|
|
2790
|
+
parent.removeChild(ref);
|
|
2791
|
+
}
|
|
2792
|
+
} else {
|
|
2793
|
+
parent.removeChild(ref);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2715
2796
|
}
|
|
2716
2797
|
}
|
|
2717
|
-
|
|
2718
|
-
|
|
2798
|
+
const pkg = this.doc.pkg;
|
|
2799
|
+
const comment_partnames = /* @__PURE__ */ new Set();
|
|
2800
|
+
for (const part of pkg.parts) {
|
|
2801
|
+
if (part.partname.toLowerCase().includes("comments")) {
|
|
2802
|
+
comment_partnames.add(part.partname);
|
|
2803
|
+
const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
|
|
2804
|
+
const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
|
|
2805
|
+
comment_partnames.add(withSlash);
|
|
2806
|
+
comment_partnames.add(withoutSlash);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
if (comment_partnames.size > 0) {
|
|
2810
|
+
for (const part of pkg.parts) {
|
|
2811
|
+
if (part.partname.endsWith(".rels")) {
|
|
2812
|
+
const rels = findAllDescendants(part._element, "Relationship");
|
|
2813
|
+
const toRemove = [];
|
|
2814
|
+
for (const rel of rels) {
|
|
2815
|
+
const target = rel.getAttribute("Target") || "";
|
|
2816
|
+
if (target.toLowerCase().includes("comments")) {
|
|
2817
|
+
toRemove.push(rel);
|
|
2818
|
+
const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
|
|
2819
|
+
const sourcePart = pkg.getPartByPath(sourcePath);
|
|
2820
|
+
if (sourcePart) {
|
|
2821
|
+
const relId = rel.getAttribute("Id");
|
|
2822
|
+
if (relId) sourcePart.rels.delete(relId);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
for (const relEl of toRemove) {
|
|
2827
|
+
relEl.parentNode?.removeChild(relEl);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
const ctPart = pkg.getPartByPath("[Content_Types].xml");
|
|
2832
|
+
if (ctPart) {
|
|
2833
|
+
const overrides = findAllDescendants(ctPart._element, "Override");
|
|
2834
|
+
const toRemove = [];
|
|
2835
|
+
for (const override of overrides) {
|
|
2836
|
+
const partName = override.getAttribute("PartName") || "";
|
|
2837
|
+
if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
|
|
2838
|
+
toRemove.push(override);
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
for (const overrideEl of toRemove) {
|
|
2842
|
+
overrideEl.parentNode?.removeChild(overrideEl);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
|
|
2846
|
+
for (const key of Object.keys(pkg.unzipped)) {
|
|
2847
|
+
if (key.toLowerCase().includes("comments")) {
|
|
2848
|
+
delete pkg.unzipped[key];
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2719
2851
|
}
|
|
2720
2852
|
}
|
|
2721
2853
|
_getNextId() {
|
|
@@ -2806,40 +2938,40 @@ var RedlineEngine = class {
|
|
|
2806
2938
|
}
|
|
2807
2939
|
}
|
|
2808
2940
|
/**
|
|
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
|
-
|
|
2941
|
+
* Inserts `text` as one or more tracked paragraphs anchored relative to
|
|
2942
|
+
* either an existing run or a paragraph. Returns:
|
|
2943
|
+
* { first_node, last_p, last_ins, used_block_mode }
|
|
2944
|
+
* where:
|
|
2945
|
+
* - first_node: the first <w:ins> (for inline mode) OR the first new <w:p>
|
|
2946
|
+
* (for block mode). The caller uses this for splicing into the DOM and
|
|
2947
|
+
* for anchoring comments.
|
|
2948
|
+
* - last_p: the last new <w:p> created, if any. null when entirely inline.
|
|
2949
|
+
* - last_ins: the last <w:ins> created (inside the last new <w:p>, or the
|
|
2950
|
+
* sole inline ins). Used as the comment's end anchor.
|
|
2951
|
+
* - used_block_mode: true when the first line carried a heading/list style
|
|
2952
|
+
* marker and we created a new paragraph for it (rather than inlining it).
|
|
2953
|
+
*
|
|
2954
|
+
* Multi-paragraph rules (only when text contains '\n'):
|
|
2955
|
+
* - Each additional line becomes a new <w:p>, inserted after the anchor
|
|
2956
|
+
* paragraph in document order.
|
|
2957
|
+
* - Each new <w:p> gets a copy of the anchor paragraph's <w:pPr> (so list
|
|
2958
|
+
* numbering / indentation are preserved) unless the line itself starts
|
|
2959
|
+
* with a markdown heading or list marker, which overrides the style.
|
|
2960
|
+
* - Each new <w:p> carries a tracked paragraph-break marker
|
|
2961
|
+
* (<w:pPr><w:rPr><w:ins/></w:rPr></w:pPr>) so Word natively tracks the
|
|
2962
|
+
* paragraph break.
|
|
2963
|
+
* - Each new <w:p>'s content is wrapped in a <w:ins>, with inline bold/
|
|
2964
|
+
* italic markdown parsed via _parse_inline_markdown.
|
|
2965
|
+
*
|
|
2966
|
+
* The first line:
|
|
2967
|
+
* - If it carries a heading / list marker AND we have a paragraph anchor,
|
|
2968
|
+
* we drop into "block mode": no inline <w:ins>; the first line itself
|
|
2969
|
+
* becomes the first new <w:p>.
|
|
2970
|
+
* - Otherwise we emit a single inline <w:ins> for the first line (current
|
|
2971
|
+
* behaviour) and treat the remaining lines as block extensions.
|
|
2972
|
+
*
|
|
2973
|
+
* Does NOT attach comments; callers handle that.
|
|
2974
|
+
*/
|
|
2843
2975
|
_track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id) {
|
|
2844
2976
|
if (!text) {
|
|
2845
2977
|
return {
|
|
@@ -2979,7 +3111,15 @@ var RedlineEngine = class {
|
|
|
2979
3111
|
const anchor_rPr = findChild(anchor_run._element, "w:rPr");
|
|
2980
3112
|
if (anchor_rPr) {
|
|
2981
3113
|
const clone = anchor_rPr.cloneNode(true);
|
|
2982
|
-
for (const tag of [
|
|
3114
|
+
for (const tag of [
|
|
3115
|
+
"w:vanish",
|
|
3116
|
+
"w:strike",
|
|
3117
|
+
"w:dstrike",
|
|
3118
|
+
"w:i",
|
|
3119
|
+
"w:iCs",
|
|
3120
|
+
"w:b",
|
|
3121
|
+
"w:bCs"
|
|
3122
|
+
]) {
|
|
2983
3123
|
const found = findChild(clone, tag);
|
|
2984
3124
|
if (found) clone.removeChild(found);
|
|
2985
3125
|
}
|
|
@@ -3253,6 +3393,16 @@ var RedlineEngine = class {
|
|
|
3253
3393
|
matches = this.clean_mapper.find_all_match_indices(edit.target_text);
|
|
3254
3394
|
if (matches.length > 0) activeText = this.clean_mapper.full_text;
|
|
3255
3395
|
}
|
|
3396
|
+
if (activeText === this.mapper.full_text && matches.length > 1) {
|
|
3397
|
+
const liveMatches = matches.filter(([start, length]) => {
|
|
3398
|
+
const realSpans = this.mapper.spans.filter(
|
|
3399
|
+
(s) => s.run !== null && s.end > start && s.start < start + length
|
|
3400
|
+
);
|
|
3401
|
+
if (realSpans.length === 0) return true;
|
|
3402
|
+
return realSpans.some((s) => !s.del_id);
|
|
3403
|
+
});
|
|
3404
|
+
if (liveMatches.length > 0) matches = liveMatches;
|
|
3405
|
+
}
|
|
3256
3406
|
if (matches.length === 0) {
|
|
3257
3407
|
errors.push(
|
|
3258
3408
|
`- Edit ${i + 1} Failed: Target text not found in document:
|
|
@@ -3272,6 +3422,31 @@ var RedlineEngine = class {
|
|
|
3272
3422
|
)
|
|
3273
3423
|
);
|
|
3274
3424
|
}
|
|
3425
|
+
if (matches.length === 1) {
|
|
3426
|
+
const [m_start, m_len] = matches[0];
|
|
3427
|
+
const matched = activeText.substring(m_start, m_start + m_len);
|
|
3428
|
+
const [pfx, sfx] = trim_common_context(matched, edit.new_text || "");
|
|
3429
|
+
const t_end = matched.length - sfx;
|
|
3430
|
+
const final_target = matched.substring(pfx, t_end);
|
|
3431
|
+
const final_new = (edit.new_text || "").substring(pfx, (edit.new_text || "").length - sfx);
|
|
3432
|
+
if (final_target.includes("\n\n")) {
|
|
3433
|
+
if (final_new.includes("\n\n")) {
|
|
3434
|
+
const parts = matched.split("\n\n");
|
|
3435
|
+
if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
|
|
3436
|
+
errors.push(
|
|
3437
|
+
`- 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.`
|
|
3438
|
+
);
|
|
3439
|
+
}
|
|
3440
|
+
} else {
|
|
3441
|
+
const parts = final_target.split("\n\n");
|
|
3442
|
+
if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
|
|
3443
|
+
errors.push(
|
|
3444
|
+
`- 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.`
|
|
3445
|
+
);
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3275
3450
|
for (const [start, length] of matches) {
|
|
3276
3451
|
const spans = this.mapper.spans.filter(
|
|
3277
3452
|
(s) => s.end > start && s.start < start + length
|
|
@@ -3335,7 +3510,33 @@ var RedlineEngine = class {
|
|
|
3335
3510
|
}
|
|
3336
3511
|
return errors;
|
|
3337
3512
|
}
|
|
3338
|
-
process_batch(changes) {
|
|
3513
|
+
process_batch(changes, dry_run = false) {
|
|
3514
|
+
if (dry_run) {
|
|
3515
|
+
const baselines = /* @__PURE__ */ new Map();
|
|
3516
|
+
for (const part of this.doc.pkg.parts) {
|
|
3517
|
+
if (part._element) {
|
|
3518
|
+
baselines.set(part, part._element.cloneNode(true));
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
try {
|
|
3522
|
+
return this._process_batch_internal(changes, true);
|
|
3523
|
+
} finally {
|
|
3524
|
+
for (const [part, originalEl] of baselines.entries()) {
|
|
3525
|
+
const doc = part._element.ownerDocument;
|
|
3526
|
+
if (doc && doc.documentElement) {
|
|
3527
|
+
doc.replaceChild(originalEl, doc.documentElement);
|
|
3528
|
+
}
|
|
3529
|
+
part._element = originalEl;
|
|
3530
|
+
}
|
|
3531
|
+
this.mapper = new DocumentMapper(this.doc);
|
|
3532
|
+
this.comments_manager = new CommentsManager(this.doc);
|
|
3533
|
+
this.clean_mapper = null;
|
|
3534
|
+
}
|
|
3535
|
+
} else {
|
|
3536
|
+
return this._process_batch_internal(changes, false);
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
_process_batch_internal(changes, dry_run_mode = false) {
|
|
3339
3540
|
this.skipped_details = [];
|
|
3340
3541
|
const actions = changes.filter(
|
|
3341
3542
|
(c) => ["accept", "reject", "reply"].includes(c.type)
|
|
@@ -3343,38 +3544,129 @@ var RedlineEngine = class {
|
|
|
3343
3544
|
const edits = changes.filter(
|
|
3344
3545
|
(c) => !["accept", "reject", "reply"].includes(c.type)
|
|
3345
3546
|
);
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3547
|
+
if (!dry_run_mode) {
|
|
3548
|
+
const all_errors = [];
|
|
3549
|
+
if (actions.length > 0) {
|
|
3550
|
+
all_errors.push(...this.validate_review_actions(actions));
|
|
3551
|
+
}
|
|
3552
|
+
if (edits.length > 0) {
|
|
3553
|
+
all_errors.push(...this.validate_edits(edits));
|
|
3554
|
+
}
|
|
3555
|
+
if (all_errors.length > 0) {
|
|
3556
|
+
throw new BatchValidationError(all_errors);
|
|
3557
|
+
}
|
|
3558
|
+
} else {
|
|
3559
|
+
if (actions.length > 0) {
|
|
3560
|
+
const action_errors = this.validate_review_actions(actions);
|
|
3561
|
+
if (action_errors.length > 0) {
|
|
3562
|
+
throw new BatchValidationError(action_errors);
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3355
3565
|
}
|
|
3356
|
-
let applied_actions = 0
|
|
3566
|
+
let applied_actions = 0;
|
|
3567
|
+
let skipped_actions = 0;
|
|
3357
3568
|
if (actions.length > 0) {
|
|
3358
3569
|
const res = this.apply_review_actions(actions);
|
|
3359
3570
|
applied_actions = res[0];
|
|
3360
3571
|
skipped_actions = res[1];
|
|
3572
|
+
if (skipped_actions > 0) {
|
|
3573
|
+
throw new BatchValidationError(this.skipped_details);
|
|
3574
|
+
}
|
|
3361
3575
|
if (applied_actions > 0) {
|
|
3362
3576
|
this.mapper["_build_map"]();
|
|
3363
3577
|
if (this.clean_mapper) this.clean_mapper["_build_map"]();
|
|
3364
3578
|
}
|
|
3365
3579
|
}
|
|
3366
|
-
|
|
3580
|
+
const edits_reports = [];
|
|
3581
|
+
let applied_edits = 0;
|
|
3582
|
+
let skipped_edits = 0;
|
|
3367
3583
|
if (edits.length > 0) {
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3584
|
+
if (dry_run_mode) {
|
|
3585
|
+
for (const edit of edits) {
|
|
3586
|
+
const single_errors = this.validate_edits([edit]);
|
|
3587
|
+
const warning = this._check_punctuation_warning(edit.target_text || "");
|
|
3588
|
+
if (single_errors.length > 0) {
|
|
3589
|
+
skipped_edits++;
|
|
3590
|
+
edits_reports.push({
|
|
3591
|
+
status: "failed",
|
|
3592
|
+
target_text: edit.target_text || "",
|
|
3593
|
+
new_text: edit.new_text || "",
|
|
3594
|
+
warning,
|
|
3595
|
+
error: single_errors[0],
|
|
3596
|
+
critic_markup: null,
|
|
3597
|
+
clean_text: null
|
|
3598
|
+
});
|
|
3599
|
+
continue;
|
|
3600
|
+
}
|
|
3601
|
+
const res = this.apply_edits([edit]);
|
|
3602
|
+
const applied = res[0];
|
|
3603
|
+
if (applied > 0) {
|
|
3604
|
+
applied_edits++;
|
|
3605
|
+
const previews = this._build_edit_context_previews(edit);
|
|
3606
|
+
edits_reports.push({
|
|
3607
|
+
status: "applied",
|
|
3608
|
+
target_text: edit.target_text || "",
|
|
3609
|
+
new_text: edit.new_text || "",
|
|
3610
|
+
warning,
|
|
3611
|
+
error: null,
|
|
3612
|
+
critic_markup: previews[0],
|
|
3613
|
+
clean_text: previews[1]
|
|
3614
|
+
});
|
|
3615
|
+
} else {
|
|
3616
|
+
skipped_edits++;
|
|
3617
|
+
const error_msg = this.skipped_details.length > 0 ? this.skipped_details[this.skipped_details.length - 1] : "Failed to apply edit";
|
|
3618
|
+
edits_reports.push({
|
|
3619
|
+
status: "failed",
|
|
3620
|
+
target_text: edit.target_text || "",
|
|
3621
|
+
new_text: edit.new_text || "",
|
|
3622
|
+
warning,
|
|
3623
|
+
error: error_msg,
|
|
3624
|
+
critic_markup: null,
|
|
3625
|
+
clean_text: null
|
|
3626
|
+
});
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
} else {
|
|
3630
|
+
const errors = this.validate_edits(edits);
|
|
3631
|
+
if (errors.length > 0) {
|
|
3632
|
+
throw new BatchValidationError(errors);
|
|
3633
|
+
}
|
|
3634
|
+
const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
|
|
3635
|
+
const res = this.apply_edits(cloned_edits);
|
|
3636
|
+
applied_edits = res[0];
|
|
3637
|
+
skipped_edits = res[1];
|
|
3638
|
+
for (const edit of cloned_edits) {
|
|
3639
|
+
const success = edit._applied_status || false;
|
|
3640
|
+
const error_msg = edit._error_msg || null;
|
|
3641
|
+
const warning = this._check_punctuation_warning(edit.target_text || "");
|
|
3642
|
+
let critic_markup = null;
|
|
3643
|
+
let clean_text = null;
|
|
3644
|
+
if (success) {
|
|
3645
|
+
const previews = this._build_edit_context_previews(edit);
|
|
3646
|
+
critic_markup = previews[0];
|
|
3647
|
+
clean_text = previews[1];
|
|
3648
|
+
}
|
|
3649
|
+
edits_reports.push({
|
|
3650
|
+
status: success ? "applied" : "failed",
|
|
3651
|
+
target_text: edit.target_text || "",
|
|
3652
|
+
new_text: edit.new_text || "",
|
|
3653
|
+
warning,
|
|
3654
|
+
error: error_msg,
|
|
3655
|
+
critic_markup,
|
|
3656
|
+
clean_text
|
|
3657
|
+
});
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3371
3660
|
}
|
|
3372
3661
|
return {
|
|
3373
3662
|
actions_applied: applied_actions,
|
|
3374
3663
|
actions_skipped: skipped_actions,
|
|
3375
3664
|
edits_applied: applied_edits,
|
|
3376
3665
|
edits_skipped: skipped_edits,
|
|
3377
|
-
skipped_details: this.skipped_details
|
|
3666
|
+
skipped_details: this.skipped_details,
|
|
3667
|
+
edits: edits_reports,
|
|
3668
|
+
engine: "node",
|
|
3669
|
+
version: "1.9.0"
|
|
3378
3670
|
};
|
|
3379
3671
|
}
|
|
3380
3672
|
apply_edits(edits) {
|
|
@@ -3382,50 +3674,90 @@ var RedlineEngine = class {
|
|
|
3382
3674
|
let skipped = 0;
|
|
3383
3675
|
const resolved_edits = [];
|
|
3384
3676
|
for (const edit of edits) {
|
|
3385
|
-
|
|
3677
|
+
edit._applied_status = false;
|
|
3678
|
+
edit._error_msg = null;
|
|
3679
|
+
}
|
|
3680
|
+
for (const edit of edits) {
|
|
3681
|
+
if (edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null) {
|
|
3682
|
+
resolved_edits.push([edit, edit.new_text || null]);
|
|
3683
|
+
} else if (edit._match_start_index !== void 0 && edit._match_start_index !== null) {
|
|
3684
|
+
edit._resolved_start_idx = edit._match_start_index;
|
|
3386
3685
|
resolved_edits.push([edit, edit.new_text || null]);
|
|
3387
3686
|
} else if (edit.type === "insert_row" || edit.type === "delete_row") {
|
|
3388
|
-
|
|
3389
|
-
if (
|
|
3390
|
-
|
|
3687
|
+
let matches = this.mapper.find_all_match_indices(edit.target_text);
|
|
3688
|
+
if (matches.length === 0) {
|
|
3689
|
+
if (!this.clean_mapper) {
|
|
3690
|
+
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
3691
|
+
}
|
|
3692
|
+
matches = this.clean_mapper.find_all_match_indices(edit.target_text);
|
|
3693
|
+
}
|
|
3694
|
+
if (matches.length > 0) {
|
|
3695
|
+
edit._resolved_start_idx = matches[0][0];
|
|
3391
3696
|
resolved_edits.push([edit, null]);
|
|
3392
3697
|
} else {
|
|
3393
3698
|
skipped++;
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3699
|
+
edit._applied_status = false;
|
|
3700
|
+
const target_snippet = (edit.target_text || "").trim().substring(0, 40);
|
|
3701
|
+
const msg = `- Failed to locate row target: '${target_snippet}...'`;
|
|
3702
|
+
this.skipped_details.push(msg);
|
|
3703
|
+
edit._error_msg = msg;
|
|
3397
3704
|
}
|
|
3398
3705
|
} else {
|
|
3399
3706
|
const resolved = this._pre_resolve_heuristic_edit(edit);
|
|
3400
3707
|
if (resolved) {
|
|
3401
3708
|
if (Array.isArray(resolved)) {
|
|
3402
|
-
for (const r of resolved)
|
|
3709
|
+
for (const r of resolved) {
|
|
3710
|
+
r._resolved_start_idx = r._match_start_index;
|
|
3711
|
+
r._parent_edit_ref = edit;
|
|
3712
|
+
if (edit._resolved_start_idx === void 0 || edit._resolved_start_idx === null) {
|
|
3713
|
+
edit._resolved_start_idx = r._resolved_start_idx;
|
|
3714
|
+
}
|
|
3715
|
+
if (!edit._resolved_proxy_edit) {
|
|
3716
|
+
edit._resolved_proxy_edit = r;
|
|
3717
|
+
}
|
|
3718
|
+
resolved_edits.push([r, r.new_text]);
|
|
3719
|
+
}
|
|
3403
3720
|
} else {
|
|
3721
|
+
resolved._resolved_start_idx = resolved._match_start_index;
|
|
3722
|
+
resolved._parent_edit_ref = edit;
|
|
3723
|
+
edit._resolved_start_idx = resolved._resolved_start_idx;
|
|
3724
|
+
edit._resolved_proxy_edit = resolved;
|
|
3404
3725
|
resolved_edits.push([resolved, resolved.new_text]);
|
|
3405
3726
|
}
|
|
3406
3727
|
} else {
|
|
3407
3728
|
skipped++;
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
);
|
|
3729
|
+
edit._applied_status = false;
|
|
3730
|
+
const display_text = edit.target_text || "insertion";
|
|
3731
|
+
const target_snippet = display_text.trim().substring(0, 40);
|
|
3732
|
+
const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
|
|
3733
|
+
this.skipped_details.push(msg);
|
|
3734
|
+
edit._error_msg = msg;
|
|
3411
3735
|
}
|
|
3412
3736
|
}
|
|
3413
3737
|
}
|
|
3414
3738
|
resolved_edits.sort(
|
|
3415
|
-
(a, b) => (b[0].
|
|
3739
|
+
(a, b) => (b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0)
|
|
3416
3740
|
);
|
|
3417
3741
|
const occupied_ranges = [];
|
|
3418
3742
|
for (const [edit, orig_new] of resolved_edits) {
|
|
3419
|
-
const start = edit.
|
|
3743
|
+
const start = edit._resolved_start_idx || 0;
|
|
3420
3744
|
const end = start + (edit.target_text ? edit.target_text.length : 0);
|
|
3421
3745
|
const overlaps = occupied_ranges.some(
|
|
3422
3746
|
([occ_start, occ_end]) => start < occ_end && end > occ_start
|
|
3423
3747
|
);
|
|
3424
3748
|
if (overlaps) {
|
|
3425
3749
|
skipped++;
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3750
|
+
const display_text = edit.target_text || "insertion";
|
|
3751
|
+
const target_snippet = display_text.trim().substring(0, 40);
|
|
3752
|
+
const msg = `- Skipped overlapping edit targeting: '${target_snippet}...'`;
|
|
3753
|
+
this.skipped_details.push(msg);
|
|
3754
|
+
edit._applied_status = false;
|
|
3755
|
+
edit._error_msg = msg;
|
|
3756
|
+
const parent = edit._parent_edit_ref;
|
|
3757
|
+
if (parent) {
|
|
3758
|
+
parent._applied_status = false;
|
|
3759
|
+
parent._error_msg = msg;
|
|
3760
|
+
}
|
|
3429
3761
|
continue;
|
|
3430
3762
|
}
|
|
3431
3763
|
let success = false;
|
|
@@ -3437,11 +3769,26 @@ var RedlineEngine = class {
|
|
|
3437
3769
|
if (success) {
|
|
3438
3770
|
applied++;
|
|
3439
3771
|
occupied_ranges.push([start, end]);
|
|
3772
|
+
edit._applied_status = true;
|
|
3773
|
+
const parent = edit._parent_edit_ref;
|
|
3774
|
+
if (parent) {
|
|
3775
|
+
parent._applied_status = true;
|
|
3776
|
+
}
|
|
3440
3777
|
} else {
|
|
3441
3778
|
skipped++;
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3779
|
+
const display_text = edit.target_text || "insertion";
|
|
3780
|
+
const target_snippet = display_text.trim().substring(0, 40);
|
|
3781
|
+
const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
|
|
3782
|
+
this.skipped_details.push(msg);
|
|
3783
|
+
edit._applied_status = false;
|
|
3784
|
+
edit._error_msg = msg;
|
|
3785
|
+
const parent = edit._parent_edit_ref;
|
|
3786
|
+
if (parent) {
|
|
3787
|
+
if (!parent._applied_status) {
|
|
3788
|
+
parent._applied_status = false;
|
|
3789
|
+
parent._error_msg = msg;
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3445
3792
|
}
|
|
3446
3793
|
}
|
|
3447
3794
|
return [applied, skipped];
|
|
@@ -3534,7 +3881,7 @@ var RedlineEngine = class {
|
|
|
3534
3881
|
return [applied, skipped];
|
|
3535
3882
|
}
|
|
3536
3883
|
_apply_table_edit(edit, rebuild_map) {
|
|
3537
|
-
const start_idx = edit._match_start_index || 0;
|
|
3884
|
+
const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
|
|
3538
3885
|
const [anchor_run, anchor_para] = this.mapper.get_insertion_anchor(
|
|
3539
3886
|
start_idx,
|
|
3540
3887
|
rebuild_map
|
|
@@ -3578,9 +3925,31 @@ var RedlineEngine = class {
|
|
|
3578
3925
|
}
|
|
3579
3926
|
return false;
|
|
3580
3927
|
}
|
|
3928
|
+
/**
|
|
3929
|
+
* Returns the first match of `target_text` in the raw mapper that is NOT
|
|
3930
|
+
* entirely contained within a tracked deletion (<w:del>). Tracked-deleted
|
|
3931
|
+
* copies are not live, editable text, so an edit must resolve to a live
|
|
3932
|
+
* occurrence even when a dead copy appears earlier in the document
|
|
3933
|
+
* (BUG-23-5). Falls back to the plain first match when no live copy is
|
|
3934
|
+
* found (e.g. fuzzy/normalized matches the span filter cannot align).
|
|
3935
|
+
*/
|
|
3936
|
+
_first_live_match(target_text) {
|
|
3937
|
+
const all = this.mapper.find_all_match_indices(target_text);
|
|
3938
|
+
if (all.length <= 1) {
|
|
3939
|
+
return this.mapper.find_match_index(target_text);
|
|
3940
|
+
}
|
|
3941
|
+
for (const [start, length] of all) {
|
|
3942
|
+
const realSpans = this.mapper.spans.filter(
|
|
3943
|
+
(s) => s.run !== null && s.end > start && s.start < start + length
|
|
3944
|
+
);
|
|
3945
|
+
if (realSpans.length === 0) return [start, length];
|
|
3946
|
+
if (realSpans.some((s) => !s.del_id)) return [start, length];
|
|
3947
|
+
}
|
|
3948
|
+
return this.mapper.find_match_index(target_text);
|
|
3949
|
+
}
|
|
3581
3950
|
_pre_resolve_heuristic_edit(edit) {
|
|
3582
3951
|
if (!edit.target_text) return null;
|
|
3583
|
-
let [start_idx, match_len] = this.
|
|
3952
|
+
let [start_idx, match_len] = this._first_live_match(edit.target_text);
|
|
3584
3953
|
let use_clean_map = false;
|
|
3585
3954
|
if (start_idx === -1) {
|
|
3586
3955
|
if (!this.clean_mapper)
|
|
@@ -3644,7 +4013,7 @@ var RedlineEngine = class {
|
|
|
3644
4013
|
_apply_single_edit_indexed(edit, orig_new, rebuild_map) {
|
|
3645
4014
|
let op = edit._internal_op;
|
|
3646
4015
|
const active_mapper = edit._active_mapper_ref || this.mapper;
|
|
3647
|
-
const start_idx = edit._match_start_index || 0;
|
|
4016
|
+
const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
|
|
3648
4017
|
const length = edit.target_text ? edit.target_text.length : 0;
|
|
3649
4018
|
const del_id = ["DELETION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
|
|
3650
4019
|
const ins_id = ["INSERTION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
|
|
@@ -3693,6 +4062,76 @@ var RedlineEngine = class {
|
|
|
3693
4062
|
rebuild_map
|
|
3694
4063
|
);
|
|
3695
4064
|
if (!anchor_run && !anchor_para) return false;
|
|
4065
|
+
const _bug233_new = edit.new_text || "";
|
|
4066
|
+
const _bug233_trailing_break = /\n\s*$/.test(_bug233_new);
|
|
4067
|
+
let _bug233_target_para = null;
|
|
4068
|
+
{
|
|
4069
|
+
const startingSpans = active_mapper.spans.filter(
|
|
4070
|
+
(s) => s.paragraph !== null && s.start === start_idx
|
|
4071
|
+
);
|
|
4072
|
+
if (startingSpans.length > 0 && startingSpans[0].paragraph) {
|
|
4073
|
+
_bug233_target_para = startingSpans[0].paragraph._element;
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
if (_bug233_trailing_break && _bug233_target_para && _bug233_target_para.parentNode) {
|
|
4077
|
+
const body = _bug233_target_para.parentNode;
|
|
4078
|
+
const xmlDoc = this.doc.part._element.ownerDocument;
|
|
4079
|
+
const lines = _bug233_new.split(/[\r\n]+/).filter((l) => l !== "");
|
|
4080
|
+
let firstNew = null;
|
|
4081
|
+
let lastNew = null;
|
|
4082
|
+
let lastIns = null;
|
|
4083
|
+
for (const raw_line of lines) {
|
|
4084
|
+
const [clean_text, style_name] = this._parse_markdown_style(raw_line);
|
|
4085
|
+
const new_p = xmlDoc.createElement("w:p");
|
|
4086
|
+
if (style_name) {
|
|
4087
|
+
this._set_paragraph_style(new_p, style_name);
|
|
4088
|
+
} else {
|
|
4089
|
+
const existing_pPr = findChild(_bug233_target_para, "w:pPr");
|
|
4090
|
+
if (existing_pPr) new_p.appendChild(existing_pPr.cloneNode(true));
|
|
4091
|
+
}
|
|
4092
|
+
let pPr = findChild(new_p, "w:pPr");
|
|
4093
|
+
if (!pPr) {
|
|
4094
|
+
pPr = xmlDoc.createElement("w:pPr");
|
|
4095
|
+
new_p.insertBefore(pPr, new_p.firstChild);
|
|
4096
|
+
}
|
|
4097
|
+
let rPr = findChild(pPr, "w:rPr");
|
|
4098
|
+
if (!rPr) {
|
|
4099
|
+
rPr = xmlDoc.createElement("w:rPr");
|
|
4100
|
+
pPr.appendChild(rPr);
|
|
4101
|
+
}
|
|
4102
|
+
rPr.appendChild(this._create_track_change_tag("w:ins", "", ins_id));
|
|
4103
|
+
const content_ins = this._build_tracked_ins_for_line(
|
|
4104
|
+
clean_text,
|
|
4105
|
+
anchor_run,
|
|
4106
|
+
ins_id,
|
|
4107
|
+
xmlDoc
|
|
4108
|
+
);
|
|
4109
|
+
if (content_ins) new_p.appendChild(content_ins);
|
|
4110
|
+
body.insertBefore(new_p, _bug233_target_para);
|
|
4111
|
+
if (!firstNew) firstNew = new_p;
|
|
4112
|
+
lastNew = new_p;
|
|
4113
|
+
lastIns = content_ins;
|
|
4114
|
+
}
|
|
4115
|
+
if (firstNew) {
|
|
4116
|
+
if (edit.comment && lastNew && lastIns) {
|
|
4117
|
+
const ascend = (el, p) => {
|
|
4118
|
+
let cur = el;
|
|
4119
|
+
while (cur.parentNode && cur.parentNode !== p)
|
|
4120
|
+
cur = cur.parentNode;
|
|
4121
|
+
return cur;
|
|
4122
|
+
};
|
|
4123
|
+
const startIns = findAllDescendants(firstNew, "w:ins")[0] || firstNew;
|
|
4124
|
+
this._attach_comment_spanning(
|
|
4125
|
+
firstNew,
|
|
4126
|
+
ascend(startIns, firstNew),
|
|
4127
|
+
lastNew,
|
|
4128
|
+
ascend(lastIns, lastNew),
|
|
4129
|
+
edit.comment
|
|
4130
|
+
);
|
|
4131
|
+
}
|
|
4132
|
+
return true;
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
3696
4135
|
const result = this._track_insert_multiline(
|
|
3697
4136
|
edit.new_text || "",
|
|
3698
4137
|
anchor_run,
|
|
@@ -5402,15 +5841,88 @@ function remove_all_comments(doc) {
|
|
|
5402
5841
|
lines.push(` ${status} "${_truncate(info.text || "", 60)}" (${info.author || "Unknown"})`);
|
|
5403
5842
|
cm.deleteComment(cId);
|
|
5404
5843
|
}
|
|
5405
|
-
for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"
|
|
5844
|
+
for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
|
|
5406
5845
|
for (const el of findAllDescendants(doc.element, tag)) {
|
|
5407
5846
|
el.parentNode?.removeChild(el);
|
|
5408
5847
|
}
|
|
5409
5848
|
}
|
|
5849
|
+
const refs = findAllDescendants(doc.element, "w:commentReference");
|
|
5850
|
+
for (const ref of refs) {
|
|
5851
|
+
const parent = ref.parentNode;
|
|
5852
|
+
if (parent) {
|
|
5853
|
+
if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
|
|
5854
|
+
const nonRprChildren = Array.from(parent.childNodes).filter(
|
|
5855
|
+
(c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
|
|
5856
|
+
);
|
|
5857
|
+
if (nonRprChildren.length <= 1) {
|
|
5858
|
+
parent.parentNode?.removeChild(parent);
|
|
5859
|
+
} else {
|
|
5860
|
+
parent.removeChild(ref);
|
|
5861
|
+
}
|
|
5862
|
+
} else {
|
|
5863
|
+
parent.removeChild(ref);
|
|
5864
|
+
}
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5410
5867
|
const resolvedCount = Object.values(data).filter((c) => c.resolved).length;
|
|
5411
5868
|
const openCount = Object.values(data).filter((c) => !c.resolved).length;
|
|
5412
5869
|
return [`Comments removed: ${keys.length} (${resolvedCount} resolved, ${openCount} open)`].concat(lines);
|
|
5413
5870
|
}
|
|
5871
|
+
function eject_comment_parts(doc) {
|
|
5872
|
+
const pkg = doc.pkg;
|
|
5873
|
+
const comment_partnames = /* @__PURE__ */ new Set();
|
|
5874
|
+
for (const part of pkg.parts) {
|
|
5875
|
+
if (part.partname.toLowerCase().includes("comments")) {
|
|
5876
|
+
comment_partnames.add(part.partname);
|
|
5877
|
+
const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
|
|
5878
|
+
const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
|
|
5879
|
+
comment_partnames.add(withSlash);
|
|
5880
|
+
comment_partnames.add(withoutSlash);
|
|
5881
|
+
}
|
|
5882
|
+
}
|
|
5883
|
+
if (comment_partnames.size === 0) return;
|
|
5884
|
+
for (const part of pkg.parts) {
|
|
5885
|
+
if (part.partname.endsWith(".rels")) {
|
|
5886
|
+
const rels = findAllDescendants(part._element, "Relationship");
|
|
5887
|
+
const toRemove = [];
|
|
5888
|
+
for (const rel of rels) {
|
|
5889
|
+
const target = rel.getAttribute("Target") || "";
|
|
5890
|
+
if (target.toLowerCase().includes("comments")) {
|
|
5891
|
+
toRemove.push(rel);
|
|
5892
|
+
const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
|
|
5893
|
+
const sourcePart = pkg.getPartByPath(sourcePath);
|
|
5894
|
+
if (sourcePart) {
|
|
5895
|
+
const relId = rel.getAttribute("Id");
|
|
5896
|
+
if (relId) sourcePart.rels.delete(relId);
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5900
|
+
for (const relEl of toRemove) {
|
|
5901
|
+
relEl.parentNode?.removeChild(relEl);
|
|
5902
|
+
}
|
|
5903
|
+
}
|
|
5904
|
+
}
|
|
5905
|
+
const ctPart = pkg.getPartByPath("[Content_Types].xml");
|
|
5906
|
+
if (ctPart) {
|
|
5907
|
+
const overrides = findAllDescendants(ctPart._element, "Override");
|
|
5908
|
+
const toRemove = [];
|
|
5909
|
+
for (const override of overrides) {
|
|
5910
|
+
const partName = override.getAttribute("PartName") || "";
|
|
5911
|
+
if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
|
|
5912
|
+
toRemove.push(override);
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
for (const overrideEl of toRemove) {
|
|
5916
|
+
overrideEl.parentNode?.removeChild(overrideEl);
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5919
|
+
pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
|
|
5920
|
+
for (const key of Object.keys(pkg.unzipped)) {
|
|
5921
|
+
if (key.toLowerCase().includes("comments")) {
|
|
5922
|
+
delete pkg.unzipped[key];
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
}
|
|
5414
5926
|
function replace_comment_authors(doc, newAuthor) {
|
|
5415
5927
|
const cm = new CommentsManager(doc);
|
|
5416
5928
|
if (!cm.commentsPart) return [];
|
|
@@ -5599,6 +6111,7 @@ async function finalize_document(doc, options) {
|
|
|
5599
6111
|
const commentsSummary = get_comments_summary(doc);
|
|
5600
6112
|
report.comments_removed = commentsSummary.total;
|
|
5601
6113
|
report.add_transform_lines(remove_all_comments(doc));
|
|
6114
|
+
eject_comment_parts(doc);
|
|
5602
6115
|
} else if (options.sanitize_mode === "keep-markup") {
|
|
5603
6116
|
const counts = count_tracked_changes(doc);
|
|
5604
6117
|
report.tracked_changes_found = counts[0] + counts[1] + counts[2];
|