@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.cjs
CHANGED
|
@@ -502,6 +502,21 @@ var CommentsManager = class {
|
|
|
502
502
|
return part;
|
|
503
503
|
}
|
|
504
504
|
_ensureNamespaces() {
|
|
505
|
+
const root = this._commentsPart?._element;
|
|
506
|
+
if (!root) return;
|
|
507
|
+
const required = [
|
|
508
|
+
["xmlns:w", NS.w],
|
|
509
|
+
["xmlns:w14", NS.w14],
|
|
510
|
+
["xmlns:w15", NS.w15],
|
|
511
|
+
["xmlns:w16cid", NS.w16cid],
|
|
512
|
+
["xmlns:w16cex", NS.w16cex],
|
|
513
|
+
["xmlns:mc", NS.mc]
|
|
514
|
+
];
|
|
515
|
+
for (const [attr, uri] of required) {
|
|
516
|
+
if (!root.getAttribute(attr)) {
|
|
517
|
+
root.setAttribute(attr, uri);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
505
520
|
}
|
|
506
521
|
_getNextCommentId() {
|
|
507
522
|
const ids = [0];
|
|
@@ -627,9 +642,9 @@ var CommentsManager = class {
|
|
|
627
642
|
return commentId;
|
|
628
643
|
}
|
|
629
644
|
deleteComment(commentId) {
|
|
630
|
-
if (!this.
|
|
645
|
+
if (!this.commentsPart) return;
|
|
631
646
|
let commentEl = null;
|
|
632
|
-
for (const c of findAllDescendants(this.
|
|
647
|
+
for (const c of findAllDescendants(this.commentsPart._element, "w:comment")) {
|
|
633
648
|
if (c.getAttribute("w:id") === commentId) {
|
|
634
649
|
commentEl = c;
|
|
635
650
|
break;
|
|
@@ -653,7 +668,7 @@ var CommentsManager = class {
|
|
|
653
668
|
if (child.getAttribute("w15:paraIdParent") === paraId) {
|
|
654
669
|
const childParaId = child.getAttribute("w15:paraId");
|
|
655
670
|
if (childParaId) {
|
|
656
|
-
for (const c of findAllDescendants(this.
|
|
671
|
+
for (const c of findAllDescendants(this.commentsPart._element, "w:comment")) {
|
|
657
672
|
for (const p of findAllDescendants(c, "w:p")) {
|
|
658
673
|
if (p.getAttribute("w14:paraId") === childParaId) {
|
|
659
674
|
const cid = c.getAttribute("w:id");
|
|
@@ -1887,6 +1902,29 @@ ${header}`;
|
|
|
1887
1902
|
|
|
1888
1903
|
// src/diff.ts
|
|
1889
1904
|
var import_diff_match_patch = __toESM(require("diff-match-patch"), 1);
|
|
1905
|
+
function _count_standalone_underscores(s) {
|
|
1906
|
+
let count = 0;
|
|
1907
|
+
let i = 0;
|
|
1908
|
+
const n = s.length;
|
|
1909
|
+
const isAlnum = (char) => /[a-zA-Z0-9]/.test(char);
|
|
1910
|
+
while (i < n) {
|
|
1911
|
+
if (s[i] === "_") {
|
|
1912
|
+
let is_double = false;
|
|
1913
|
+
if (i > 0 && s[i - 1] === "_" || i < n - 1 && s[i + 1] === "_") {
|
|
1914
|
+
is_double = true;
|
|
1915
|
+
}
|
|
1916
|
+
let is_intra = false;
|
|
1917
|
+
if (i > 0 && isAlnum(s[i - 1]) && i < n - 1 && isAlnum(s[i + 1])) {
|
|
1918
|
+
is_intra = true;
|
|
1919
|
+
}
|
|
1920
|
+
if (!is_double && !is_intra) {
|
|
1921
|
+
count++;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
i++;
|
|
1925
|
+
}
|
|
1926
|
+
return count;
|
|
1927
|
+
}
|
|
1890
1928
|
function trim_common_context(target, new_val) {
|
|
1891
1929
|
if (!target || !new_val) return [0, 0];
|
|
1892
1930
|
const isSpace = (char) => /\s/.test(char);
|
|
@@ -1917,7 +1955,7 @@ function trim_common_context(target, new_val) {
|
|
|
1917
1955
|
const left = target.substring(0, prefix_len);
|
|
1918
1956
|
const b_count = (left.match(/\*\*/g) || []).length;
|
|
1919
1957
|
const u2_count = (left.match(/__/g) || []).length;
|
|
1920
|
-
const u1_count = (left
|
|
1958
|
+
const u1_count = _count_standalone_underscores(left);
|
|
1921
1959
|
if (b_count % 2 !== 0) {
|
|
1922
1960
|
prefix_len = left.lastIndexOf("**");
|
|
1923
1961
|
continue;
|
|
@@ -1928,10 +1966,14 @@ function trim_common_context(target, new_val) {
|
|
|
1928
1966
|
}
|
|
1929
1967
|
if (u1_count % 2 !== 0) {
|
|
1930
1968
|
let idx = left.length - 1;
|
|
1969
|
+
const isAlnum = (char) => /[a-zA-Z0-9]/.test(char);
|
|
1931
1970
|
while (idx >= 0) {
|
|
1932
1971
|
if (left[idx] === "_" && (idx === 0 || left[idx - 1] !== "_") && (idx === left.length - 1 || left[idx + 1] !== "_")) {
|
|
1933
|
-
|
|
1934
|
-
|
|
1972
|
+
const is_intra = idx > 0 && isAlnum(left[idx - 1]) && idx < left.length - 1 && isAlnum(left[idx + 1]);
|
|
1973
|
+
if (!is_intra) {
|
|
1974
|
+
prefix_len = idx;
|
|
1975
|
+
break;
|
|
1976
|
+
}
|
|
1935
1977
|
}
|
|
1936
1978
|
idx--;
|
|
1937
1979
|
}
|
|
@@ -1991,7 +2033,7 @@ function trim_common_context(target, new_val) {
|
|
|
1991
2033
|
const right = target.substring(target.length - suffix_len);
|
|
1992
2034
|
const b_count = (right.match(/\*\*/g) || []).length;
|
|
1993
2035
|
const u2_count = (right.match(/__/g) || []).length;
|
|
1994
|
-
const u1_count = (right
|
|
2036
|
+
const u1_count = _count_standalone_underscores(right);
|
|
1995
2037
|
if (b_count % 2 !== 0) {
|
|
1996
2038
|
suffix_len -= right.indexOf("**") + 2;
|
|
1997
2039
|
continue;
|
|
@@ -2002,10 +2044,14 @@ function trim_common_context(target, new_val) {
|
|
|
2002
2044
|
}
|
|
2003
2045
|
if (u1_count % 2 !== 0) {
|
|
2004
2046
|
let idx_in_right = 0;
|
|
2047
|
+
const isAlnum = (char) => /[a-zA-Z0-9]/.test(char);
|
|
2005
2048
|
while (idx_in_right < right.length) {
|
|
2006
2049
|
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] !== "_")) {
|
|
2007
|
-
|
|
2008
|
-
|
|
2050
|
+
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]);
|
|
2051
|
+
if (!is_intra) {
|
|
2052
|
+
suffix_len -= idx_in_right + 1;
|
|
2053
|
+
break;
|
|
2054
|
+
}
|
|
2009
2055
|
}
|
|
2010
2056
|
idx_in_right++;
|
|
2011
2057
|
}
|
|
@@ -2652,6 +2698,36 @@ var RedlineEngine = class {
|
|
|
2652
2698
|
this.mapper = new DocumentMapper(this.doc);
|
|
2653
2699
|
this.comments_manager = new CommentsManager(this.doc);
|
|
2654
2700
|
}
|
|
2701
|
+
_check_punctuation_warning(target_text) {
|
|
2702
|
+
if (!target_text) return null;
|
|
2703
|
+
if (target_text.includes("_") || target_text.includes("-")) {
|
|
2704
|
+
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.`;
|
|
2705
|
+
}
|
|
2706
|
+
return null;
|
|
2707
|
+
}
|
|
2708
|
+
_build_edit_context_previews(edit) {
|
|
2709
|
+
if (edit.type !== "modify") return [null, null];
|
|
2710
|
+
if (edit._resolved_proxy_edit) {
|
|
2711
|
+
edit = edit._resolved_proxy_edit;
|
|
2712
|
+
}
|
|
2713
|
+
const start_idx = edit._resolved_start_idx;
|
|
2714
|
+
if (start_idx === void 0 || start_idx === null) return [null, null];
|
|
2715
|
+
const target_text = edit.target_text || "";
|
|
2716
|
+
const new_text = edit.new_text || "";
|
|
2717
|
+
const length = target_text.length;
|
|
2718
|
+
const active_mapper = edit._active_mapper_ref || this.mapper;
|
|
2719
|
+
const full_text = active_mapper.full_text;
|
|
2720
|
+
if (!full_text) return [null, null];
|
|
2721
|
+
const before_start = Math.max(0, start_idx - 30);
|
|
2722
|
+
const context_before = full_text.substring(before_start, start_idx);
|
|
2723
|
+
const context_after = full_text.substring(start_idx + length, start_idx + length + 30);
|
|
2724
|
+
const critic_markup = `${context_before}{--${target_text}--}{++${new_text}++}${context_after}`;
|
|
2725
|
+
let clean_text = critic_markup;
|
|
2726
|
+
clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
|
|
2727
|
+
clean_text = clean_text.replace(/\{--.*?--\}/gs, "");
|
|
2728
|
+
clean_text = clean_text.replace(/\{\+\+(.*?)\+\+\}/gs, "$1");
|
|
2729
|
+
return [critic_markup, clean_text];
|
|
2730
|
+
}
|
|
2655
2731
|
_scan_existing_ids() {
|
|
2656
2732
|
let maxId = 0;
|
|
2657
2733
|
for (const tag of ["w:ins", "w:del"]) {
|
|
@@ -2744,28 +2820,84 @@ var RedlineEngine = class {
|
|
|
2744
2820
|
}
|
|
2745
2821
|
}
|
|
2746
2822
|
}
|
|
2747
|
-
const
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
]) {
|
|
2753
|
-
for (const node of findAllDescendants(this.doc.element, tag)) {
|
|
2754
|
-
const cid = node.getAttribute("w:id");
|
|
2755
|
-
if (cid) comment_ids.add(cid);
|
|
2823
|
+
for (const root_element of parts_to_process) {
|
|
2824
|
+
for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
|
|
2825
|
+
for (const el of findAllDescendants(root_element, tag)) {
|
|
2826
|
+
el.parentNode?.removeChild(el);
|
|
2827
|
+
}
|
|
2756
2828
|
}
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2829
|
+
const refs = findAllDescendants(root_element, "w:commentReference");
|
|
2830
|
+
for (const ref of refs) {
|
|
2831
|
+
const parent = ref.parentNode;
|
|
2832
|
+
if (parent) {
|
|
2833
|
+
if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
|
|
2834
|
+
const nonRprChildren = Array.from(parent.childNodes).filter(
|
|
2835
|
+
(c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
|
|
2836
|
+
);
|
|
2837
|
+
if (nonRprChildren.length <= 1) {
|
|
2838
|
+
parent.parentNode?.removeChild(parent);
|
|
2839
|
+
} else {
|
|
2840
|
+
parent.removeChild(ref);
|
|
2841
|
+
}
|
|
2842
|
+
} else {
|
|
2843
|
+
parent.removeChild(ref);
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2765
2846
|
}
|
|
2766
2847
|
}
|
|
2767
|
-
|
|
2768
|
-
|
|
2848
|
+
const pkg = this.doc.pkg;
|
|
2849
|
+
const comment_partnames = /* @__PURE__ */ new Set();
|
|
2850
|
+
for (const part of pkg.parts) {
|
|
2851
|
+
if (part.partname.toLowerCase().includes("comments")) {
|
|
2852
|
+
comment_partnames.add(part.partname);
|
|
2853
|
+
const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
|
|
2854
|
+
const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
|
|
2855
|
+
comment_partnames.add(withSlash);
|
|
2856
|
+
comment_partnames.add(withoutSlash);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
if (comment_partnames.size > 0) {
|
|
2860
|
+
for (const part of pkg.parts) {
|
|
2861
|
+
if (part.partname.endsWith(".rels")) {
|
|
2862
|
+
const rels = findAllDescendants(part._element, "Relationship");
|
|
2863
|
+
const toRemove = [];
|
|
2864
|
+
for (const rel of rels) {
|
|
2865
|
+
const target = rel.getAttribute("Target") || "";
|
|
2866
|
+
if (target.toLowerCase().includes("comments")) {
|
|
2867
|
+
toRemove.push(rel);
|
|
2868
|
+
const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
|
|
2869
|
+
const sourcePart = pkg.getPartByPath(sourcePath);
|
|
2870
|
+
if (sourcePart) {
|
|
2871
|
+
const relId = rel.getAttribute("Id");
|
|
2872
|
+
if (relId) sourcePart.rels.delete(relId);
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
for (const relEl of toRemove) {
|
|
2877
|
+
relEl.parentNode?.removeChild(relEl);
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
const ctPart = pkg.getPartByPath("[Content_Types].xml");
|
|
2882
|
+
if (ctPart) {
|
|
2883
|
+
const overrides = findAllDescendants(ctPart._element, "Override");
|
|
2884
|
+
const toRemove = [];
|
|
2885
|
+
for (const override of overrides) {
|
|
2886
|
+
const partName = override.getAttribute("PartName") || "";
|
|
2887
|
+
if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
|
|
2888
|
+
toRemove.push(override);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
for (const overrideEl of toRemove) {
|
|
2892
|
+
overrideEl.parentNode?.removeChild(overrideEl);
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
|
|
2896
|
+
for (const key of Object.keys(pkg.unzipped)) {
|
|
2897
|
+
if (key.toLowerCase().includes("comments")) {
|
|
2898
|
+
delete pkg.unzipped[key];
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2769
2901
|
}
|
|
2770
2902
|
}
|
|
2771
2903
|
_getNextId() {
|
|
@@ -2856,40 +2988,40 @@ var RedlineEngine = class {
|
|
|
2856
2988
|
}
|
|
2857
2989
|
}
|
|
2858
2990
|
/**
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2991
|
+
* Inserts `text` as one or more tracked paragraphs anchored relative to
|
|
2992
|
+
* either an existing run or a paragraph. Returns:
|
|
2993
|
+
* { first_node, last_p, last_ins, used_block_mode }
|
|
2994
|
+
* where:
|
|
2995
|
+
* - first_node: the first <w:ins> (for inline mode) OR the first new <w:p>
|
|
2996
|
+
* (for block mode). The caller uses this for splicing into the DOM and
|
|
2997
|
+
* for anchoring comments.
|
|
2998
|
+
* - last_p: the last new <w:p> created, if any. null when entirely inline.
|
|
2999
|
+
* - last_ins: the last <w:ins> created (inside the last new <w:p>, or the
|
|
3000
|
+
* sole inline ins). Used as the comment's end anchor.
|
|
3001
|
+
* - used_block_mode: true when the first line carried a heading/list style
|
|
3002
|
+
* marker and we created a new paragraph for it (rather than inlining it).
|
|
3003
|
+
*
|
|
3004
|
+
* Multi-paragraph rules (only when text contains '\n'):
|
|
3005
|
+
* - Each additional line becomes a new <w:p>, inserted after the anchor
|
|
3006
|
+
* paragraph in document order.
|
|
3007
|
+
* - Each new <w:p> gets a copy of the anchor paragraph's <w:pPr> (so list
|
|
3008
|
+
* numbering / indentation are preserved) unless the line itself starts
|
|
3009
|
+
* with a markdown heading or list marker, which overrides the style.
|
|
3010
|
+
* - Each new <w:p> carries a tracked paragraph-break marker
|
|
3011
|
+
* (<w:pPr><w:rPr><w:ins/></w:rPr></w:pPr>) so Word natively tracks the
|
|
3012
|
+
* paragraph break.
|
|
3013
|
+
* - Each new <w:p>'s content is wrapped in a <w:ins>, with inline bold/
|
|
3014
|
+
* italic markdown parsed via _parse_inline_markdown.
|
|
3015
|
+
*
|
|
3016
|
+
* The first line:
|
|
3017
|
+
* - If it carries a heading / list marker AND we have a paragraph anchor,
|
|
3018
|
+
* we drop into "block mode": no inline <w:ins>; the first line itself
|
|
3019
|
+
* becomes the first new <w:p>.
|
|
3020
|
+
* - Otherwise we emit a single inline <w:ins> for the first line (current
|
|
3021
|
+
* behaviour) and treat the remaining lines as block extensions.
|
|
3022
|
+
*
|
|
3023
|
+
* Does NOT attach comments; callers handle that.
|
|
3024
|
+
*/
|
|
2893
3025
|
_track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id) {
|
|
2894
3026
|
if (!text) {
|
|
2895
3027
|
return {
|
|
@@ -3029,7 +3161,15 @@ var RedlineEngine = class {
|
|
|
3029
3161
|
const anchor_rPr = findChild(anchor_run._element, "w:rPr");
|
|
3030
3162
|
if (anchor_rPr) {
|
|
3031
3163
|
const clone = anchor_rPr.cloneNode(true);
|
|
3032
|
-
for (const tag of [
|
|
3164
|
+
for (const tag of [
|
|
3165
|
+
"w:vanish",
|
|
3166
|
+
"w:strike",
|
|
3167
|
+
"w:dstrike",
|
|
3168
|
+
"w:i",
|
|
3169
|
+
"w:iCs",
|
|
3170
|
+
"w:b",
|
|
3171
|
+
"w:bCs"
|
|
3172
|
+
]) {
|
|
3033
3173
|
const found = findChild(clone, tag);
|
|
3034
3174
|
if (found) clone.removeChild(found);
|
|
3035
3175
|
}
|
|
@@ -3303,6 +3443,16 @@ var RedlineEngine = class {
|
|
|
3303
3443
|
matches = this.clean_mapper.find_all_match_indices(edit.target_text);
|
|
3304
3444
|
if (matches.length > 0) activeText = this.clean_mapper.full_text;
|
|
3305
3445
|
}
|
|
3446
|
+
if (activeText === this.mapper.full_text && matches.length > 1) {
|
|
3447
|
+
const liveMatches = matches.filter(([start, length]) => {
|
|
3448
|
+
const realSpans = this.mapper.spans.filter(
|
|
3449
|
+
(s) => s.run !== null && s.end > start && s.start < start + length
|
|
3450
|
+
);
|
|
3451
|
+
if (realSpans.length === 0) return true;
|
|
3452
|
+
return realSpans.some((s) => !s.del_id);
|
|
3453
|
+
});
|
|
3454
|
+
if (liveMatches.length > 0) matches = liveMatches;
|
|
3455
|
+
}
|
|
3306
3456
|
if (matches.length === 0) {
|
|
3307
3457
|
errors.push(
|
|
3308
3458
|
`- Edit ${i + 1} Failed: Target text not found in document:
|
|
@@ -3322,6 +3472,31 @@ var RedlineEngine = class {
|
|
|
3322
3472
|
)
|
|
3323
3473
|
);
|
|
3324
3474
|
}
|
|
3475
|
+
if (matches.length === 1) {
|
|
3476
|
+
const [m_start, m_len] = matches[0];
|
|
3477
|
+
const matched = activeText.substring(m_start, m_start + m_len);
|
|
3478
|
+
const [pfx, sfx] = trim_common_context(matched, edit.new_text || "");
|
|
3479
|
+
const t_end = matched.length - sfx;
|
|
3480
|
+
const final_target = matched.substring(pfx, t_end);
|
|
3481
|
+
const final_new = (edit.new_text || "").substring(pfx, (edit.new_text || "").length - sfx);
|
|
3482
|
+
if (final_target.includes("\n\n")) {
|
|
3483
|
+
if (final_new.includes("\n\n")) {
|
|
3484
|
+
const parts = matched.split("\n\n");
|
|
3485
|
+
if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
|
|
3486
|
+
errors.push(
|
|
3487
|
+
`- 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.`
|
|
3488
|
+
);
|
|
3489
|
+
}
|
|
3490
|
+
} else {
|
|
3491
|
+
const parts = final_target.split("\n\n");
|
|
3492
|
+
if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
|
|
3493
|
+
errors.push(
|
|
3494
|
+
`- 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.`
|
|
3495
|
+
);
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3325
3500
|
for (const [start, length] of matches) {
|
|
3326
3501
|
const spans = this.mapper.spans.filter(
|
|
3327
3502
|
(s) => s.end > start && s.start < start + length
|
|
@@ -3385,7 +3560,33 @@ var RedlineEngine = class {
|
|
|
3385
3560
|
}
|
|
3386
3561
|
return errors;
|
|
3387
3562
|
}
|
|
3388
|
-
process_batch(changes) {
|
|
3563
|
+
process_batch(changes, dry_run = false) {
|
|
3564
|
+
if (dry_run) {
|
|
3565
|
+
const baselines = /* @__PURE__ */ new Map();
|
|
3566
|
+
for (const part of this.doc.pkg.parts) {
|
|
3567
|
+
if (part._element) {
|
|
3568
|
+
baselines.set(part, part._element.cloneNode(true));
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
try {
|
|
3572
|
+
return this._process_batch_internal(changes, true);
|
|
3573
|
+
} finally {
|
|
3574
|
+
for (const [part, originalEl] of baselines.entries()) {
|
|
3575
|
+
const doc = part._element.ownerDocument;
|
|
3576
|
+
if (doc && doc.documentElement) {
|
|
3577
|
+
doc.replaceChild(originalEl, doc.documentElement);
|
|
3578
|
+
}
|
|
3579
|
+
part._element = originalEl;
|
|
3580
|
+
}
|
|
3581
|
+
this.mapper = new DocumentMapper(this.doc);
|
|
3582
|
+
this.comments_manager = new CommentsManager(this.doc);
|
|
3583
|
+
this.clean_mapper = null;
|
|
3584
|
+
}
|
|
3585
|
+
} else {
|
|
3586
|
+
return this._process_batch_internal(changes, false);
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
_process_batch_internal(changes, dry_run_mode = false) {
|
|
3389
3590
|
this.skipped_details = [];
|
|
3390
3591
|
const actions = changes.filter(
|
|
3391
3592
|
(c) => ["accept", "reject", "reply"].includes(c.type)
|
|
@@ -3393,38 +3594,129 @@ var RedlineEngine = class {
|
|
|
3393
3594
|
const edits = changes.filter(
|
|
3394
3595
|
(c) => !["accept", "reject", "reply"].includes(c.type)
|
|
3395
3596
|
);
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3597
|
+
if (!dry_run_mode) {
|
|
3598
|
+
const all_errors = [];
|
|
3599
|
+
if (actions.length > 0) {
|
|
3600
|
+
all_errors.push(...this.validate_review_actions(actions));
|
|
3601
|
+
}
|
|
3602
|
+
if (edits.length > 0) {
|
|
3603
|
+
all_errors.push(...this.validate_edits(edits));
|
|
3604
|
+
}
|
|
3605
|
+
if (all_errors.length > 0) {
|
|
3606
|
+
throw new BatchValidationError(all_errors);
|
|
3607
|
+
}
|
|
3608
|
+
} else {
|
|
3609
|
+
if (actions.length > 0) {
|
|
3610
|
+
const action_errors = this.validate_review_actions(actions);
|
|
3611
|
+
if (action_errors.length > 0) {
|
|
3612
|
+
throw new BatchValidationError(action_errors);
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3405
3615
|
}
|
|
3406
|
-
let applied_actions = 0
|
|
3616
|
+
let applied_actions = 0;
|
|
3617
|
+
let skipped_actions = 0;
|
|
3407
3618
|
if (actions.length > 0) {
|
|
3408
3619
|
const res = this.apply_review_actions(actions);
|
|
3409
3620
|
applied_actions = res[0];
|
|
3410
3621
|
skipped_actions = res[1];
|
|
3622
|
+
if (skipped_actions > 0) {
|
|
3623
|
+
throw new BatchValidationError(this.skipped_details);
|
|
3624
|
+
}
|
|
3411
3625
|
if (applied_actions > 0) {
|
|
3412
3626
|
this.mapper["_build_map"]();
|
|
3413
3627
|
if (this.clean_mapper) this.clean_mapper["_build_map"]();
|
|
3414
3628
|
}
|
|
3415
3629
|
}
|
|
3416
|
-
|
|
3630
|
+
const edits_reports = [];
|
|
3631
|
+
let applied_edits = 0;
|
|
3632
|
+
let skipped_edits = 0;
|
|
3417
3633
|
if (edits.length > 0) {
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3634
|
+
if (dry_run_mode) {
|
|
3635
|
+
for (const edit of edits) {
|
|
3636
|
+
const single_errors = this.validate_edits([edit]);
|
|
3637
|
+
const warning = this._check_punctuation_warning(edit.target_text || "");
|
|
3638
|
+
if (single_errors.length > 0) {
|
|
3639
|
+
skipped_edits++;
|
|
3640
|
+
edits_reports.push({
|
|
3641
|
+
status: "failed",
|
|
3642
|
+
target_text: edit.target_text || "",
|
|
3643
|
+
new_text: edit.new_text || "",
|
|
3644
|
+
warning,
|
|
3645
|
+
error: single_errors[0],
|
|
3646
|
+
critic_markup: null,
|
|
3647
|
+
clean_text: null
|
|
3648
|
+
});
|
|
3649
|
+
continue;
|
|
3650
|
+
}
|
|
3651
|
+
const res = this.apply_edits([edit]);
|
|
3652
|
+
const applied = res[0];
|
|
3653
|
+
if (applied > 0) {
|
|
3654
|
+
applied_edits++;
|
|
3655
|
+
const previews = this._build_edit_context_previews(edit);
|
|
3656
|
+
edits_reports.push({
|
|
3657
|
+
status: "applied",
|
|
3658
|
+
target_text: edit.target_text || "",
|
|
3659
|
+
new_text: edit.new_text || "",
|
|
3660
|
+
warning,
|
|
3661
|
+
error: null,
|
|
3662
|
+
critic_markup: previews[0],
|
|
3663
|
+
clean_text: previews[1]
|
|
3664
|
+
});
|
|
3665
|
+
} else {
|
|
3666
|
+
skipped_edits++;
|
|
3667
|
+
const error_msg = this.skipped_details.length > 0 ? this.skipped_details[this.skipped_details.length - 1] : "Failed to apply edit";
|
|
3668
|
+
edits_reports.push({
|
|
3669
|
+
status: "failed",
|
|
3670
|
+
target_text: edit.target_text || "",
|
|
3671
|
+
new_text: edit.new_text || "",
|
|
3672
|
+
warning,
|
|
3673
|
+
error: error_msg,
|
|
3674
|
+
critic_markup: null,
|
|
3675
|
+
clean_text: null
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
} else {
|
|
3680
|
+
const errors = this.validate_edits(edits);
|
|
3681
|
+
if (errors.length > 0) {
|
|
3682
|
+
throw new BatchValidationError(errors);
|
|
3683
|
+
}
|
|
3684
|
+
const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
|
|
3685
|
+
const res = this.apply_edits(cloned_edits);
|
|
3686
|
+
applied_edits = res[0];
|
|
3687
|
+
skipped_edits = res[1];
|
|
3688
|
+
for (const edit of cloned_edits) {
|
|
3689
|
+
const success = edit._applied_status || false;
|
|
3690
|
+
const error_msg = edit._error_msg || null;
|
|
3691
|
+
const warning = this._check_punctuation_warning(edit.target_text || "");
|
|
3692
|
+
let critic_markup = null;
|
|
3693
|
+
let clean_text = null;
|
|
3694
|
+
if (success) {
|
|
3695
|
+
const previews = this._build_edit_context_previews(edit);
|
|
3696
|
+
critic_markup = previews[0];
|
|
3697
|
+
clean_text = previews[1];
|
|
3698
|
+
}
|
|
3699
|
+
edits_reports.push({
|
|
3700
|
+
status: success ? "applied" : "failed",
|
|
3701
|
+
target_text: edit.target_text || "",
|
|
3702
|
+
new_text: edit.new_text || "",
|
|
3703
|
+
warning,
|
|
3704
|
+
error: error_msg,
|
|
3705
|
+
critic_markup,
|
|
3706
|
+
clean_text
|
|
3707
|
+
});
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3421
3710
|
}
|
|
3422
3711
|
return {
|
|
3423
3712
|
actions_applied: applied_actions,
|
|
3424
3713
|
actions_skipped: skipped_actions,
|
|
3425
3714
|
edits_applied: applied_edits,
|
|
3426
3715
|
edits_skipped: skipped_edits,
|
|
3427
|
-
skipped_details: this.skipped_details
|
|
3716
|
+
skipped_details: this.skipped_details,
|
|
3717
|
+
edits: edits_reports,
|
|
3718
|
+
engine: "node",
|
|
3719
|
+
version: "1.9.0"
|
|
3428
3720
|
};
|
|
3429
3721
|
}
|
|
3430
3722
|
apply_edits(edits) {
|
|
@@ -3432,50 +3724,90 @@ var RedlineEngine = class {
|
|
|
3432
3724
|
let skipped = 0;
|
|
3433
3725
|
const resolved_edits = [];
|
|
3434
3726
|
for (const edit of edits) {
|
|
3435
|
-
|
|
3727
|
+
edit._applied_status = false;
|
|
3728
|
+
edit._error_msg = null;
|
|
3729
|
+
}
|
|
3730
|
+
for (const edit of edits) {
|
|
3731
|
+
if (edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null) {
|
|
3732
|
+
resolved_edits.push([edit, edit.new_text || null]);
|
|
3733
|
+
} else if (edit._match_start_index !== void 0 && edit._match_start_index !== null) {
|
|
3734
|
+
edit._resolved_start_idx = edit._match_start_index;
|
|
3436
3735
|
resolved_edits.push([edit, edit.new_text || null]);
|
|
3437
3736
|
} else if (edit.type === "insert_row" || edit.type === "delete_row") {
|
|
3438
|
-
|
|
3439
|
-
if (
|
|
3440
|
-
|
|
3737
|
+
let matches = this.mapper.find_all_match_indices(edit.target_text);
|
|
3738
|
+
if (matches.length === 0) {
|
|
3739
|
+
if (!this.clean_mapper) {
|
|
3740
|
+
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
3741
|
+
}
|
|
3742
|
+
matches = this.clean_mapper.find_all_match_indices(edit.target_text);
|
|
3743
|
+
}
|
|
3744
|
+
if (matches.length > 0) {
|
|
3745
|
+
edit._resolved_start_idx = matches[0][0];
|
|
3441
3746
|
resolved_edits.push([edit, null]);
|
|
3442
3747
|
} else {
|
|
3443
3748
|
skipped++;
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3749
|
+
edit._applied_status = false;
|
|
3750
|
+
const target_snippet = (edit.target_text || "").trim().substring(0, 40);
|
|
3751
|
+
const msg = `- Failed to locate row target: '${target_snippet}...'`;
|
|
3752
|
+
this.skipped_details.push(msg);
|
|
3753
|
+
edit._error_msg = msg;
|
|
3447
3754
|
}
|
|
3448
3755
|
} else {
|
|
3449
3756
|
const resolved = this._pre_resolve_heuristic_edit(edit);
|
|
3450
3757
|
if (resolved) {
|
|
3451
3758
|
if (Array.isArray(resolved)) {
|
|
3452
|
-
for (const r of resolved)
|
|
3759
|
+
for (const r of resolved) {
|
|
3760
|
+
r._resolved_start_idx = r._match_start_index;
|
|
3761
|
+
r._parent_edit_ref = edit;
|
|
3762
|
+
if (edit._resolved_start_idx === void 0 || edit._resolved_start_idx === null) {
|
|
3763
|
+
edit._resolved_start_idx = r._resolved_start_idx;
|
|
3764
|
+
}
|
|
3765
|
+
if (!edit._resolved_proxy_edit) {
|
|
3766
|
+
edit._resolved_proxy_edit = r;
|
|
3767
|
+
}
|
|
3768
|
+
resolved_edits.push([r, r.new_text]);
|
|
3769
|
+
}
|
|
3453
3770
|
} else {
|
|
3771
|
+
resolved._resolved_start_idx = resolved._match_start_index;
|
|
3772
|
+
resolved._parent_edit_ref = edit;
|
|
3773
|
+
edit._resolved_start_idx = resolved._resolved_start_idx;
|
|
3774
|
+
edit._resolved_proxy_edit = resolved;
|
|
3454
3775
|
resolved_edits.push([resolved, resolved.new_text]);
|
|
3455
3776
|
}
|
|
3456
3777
|
} else {
|
|
3457
3778
|
skipped++;
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
);
|
|
3779
|
+
edit._applied_status = false;
|
|
3780
|
+
const display_text = edit.target_text || "insertion";
|
|
3781
|
+
const target_snippet = display_text.trim().substring(0, 40);
|
|
3782
|
+
const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
|
|
3783
|
+
this.skipped_details.push(msg);
|
|
3784
|
+
edit._error_msg = msg;
|
|
3461
3785
|
}
|
|
3462
3786
|
}
|
|
3463
3787
|
}
|
|
3464
3788
|
resolved_edits.sort(
|
|
3465
|
-
(a, b) => (b[0].
|
|
3789
|
+
(a, b) => (b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0)
|
|
3466
3790
|
);
|
|
3467
3791
|
const occupied_ranges = [];
|
|
3468
3792
|
for (const [edit, orig_new] of resolved_edits) {
|
|
3469
|
-
const start = edit.
|
|
3793
|
+
const start = edit._resolved_start_idx || 0;
|
|
3470
3794
|
const end = start + (edit.target_text ? edit.target_text.length : 0);
|
|
3471
3795
|
const overlaps = occupied_ranges.some(
|
|
3472
3796
|
([occ_start, occ_end]) => start < occ_end && end > occ_start
|
|
3473
3797
|
);
|
|
3474
3798
|
if (overlaps) {
|
|
3475
3799
|
skipped++;
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3800
|
+
const display_text = edit.target_text || "insertion";
|
|
3801
|
+
const target_snippet = display_text.trim().substring(0, 40);
|
|
3802
|
+
const msg = `- Skipped overlapping edit targeting: '${target_snippet}...'`;
|
|
3803
|
+
this.skipped_details.push(msg);
|
|
3804
|
+
edit._applied_status = false;
|
|
3805
|
+
edit._error_msg = msg;
|
|
3806
|
+
const parent = edit._parent_edit_ref;
|
|
3807
|
+
if (parent) {
|
|
3808
|
+
parent._applied_status = false;
|
|
3809
|
+
parent._error_msg = msg;
|
|
3810
|
+
}
|
|
3479
3811
|
continue;
|
|
3480
3812
|
}
|
|
3481
3813
|
let success = false;
|
|
@@ -3487,11 +3819,26 @@ var RedlineEngine = class {
|
|
|
3487
3819
|
if (success) {
|
|
3488
3820
|
applied++;
|
|
3489
3821
|
occupied_ranges.push([start, end]);
|
|
3822
|
+
edit._applied_status = true;
|
|
3823
|
+
const parent = edit._parent_edit_ref;
|
|
3824
|
+
if (parent) {
|
|
3825
|
+
parent._applied_status = true;
|
|
3826
|
+
}
|
|
3490
3827
|
} else {
|
|
3491
3828
|
skipped++;
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3829
|
+
const display_text = edit.target_text || "insertion";
|
|
3830
|
+
const target_snippet = display_text.trim().substring(0, 40);
|
|
3831
|
+
const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
|
|
3832
|
+
this.skipped_details.push(msg);
|
|
3833
|
+
edit._applied_status = false;
|
|
3834
|
+
edit._error_msg = msg;
|
|
3835
|
+
const parent = edit._parent_edit_ref;
|
|
3836
|
+
if (parent) {
|
|
3837
|
+
if (!parent._applied_status) {
|
|
3838
|
+
parent._applied_status = false;
|
|
3839
|
+
parent._error_msg = msg;
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3495
3842
|
}
|
|
3496
3843
|
}
|
|
3497
3844
|
return [applied, skipped];
|
|
@@ -3584,7 +3931,7 @@ var RedlineEngine = class {
|
|
|
3584
3931
|
return [applied, skipped];
|
|
3585
3932
|
}
|
|
3586
3933
|
_apply_table_edit(edit, rebuild_map) {
|
|
3587
|
-
const start_idx = edit._match_start_index || 0;
|
|
3934
|
+
const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
|
|
3588
3935
|
const [anchor_run, anchor_para] = this.mapper.get_insertion_anchor(
|
|
3589
3936
|
start_idx,
|
|
3590
3937
|
rebuild_map
|
|
@@ -3628,9 +3975,31 @@ var RedlineEngine = class {
|
|
|
3628
3975
|
}
|
|
3629
3976
|
return false;
|
|
3630
3977
|
}
|
|
3978
|
+
/**
|
|
3979
|
+
* Returns the first match of `target_text` in the raw mapper that is NOT
|
|
3980
|
+
* entirely contained within a tracked deletion (<w:del>). Tracked-deleted
|
|
3981
|
+
* copies are not live, editable text, so an edit must resolve to a live
|
|
3982
|
+
* occurrence even when a dead copy appears earlier in the document
|
|
3983
|
+
* (BUG-23-5). Falls back to the plain first match when no live copy is
|
|
3984
|
+
* found (e.g. fuzzy/normalized matches the span filter cannot align).
|
|
3985
|
+
*/
|
|
3986
|
+
_first_live_match(target_text) {
|
|
3987
|
+
const all = this.mapper.find_all_match_indices(target_text);
|
|
3988
|
+
if (all.length <= 1) {
|
|
3989
|
+
return this.mapper.find_match_index(target_text);
|
|
3990
|
+
}
|
|
3991
|
+
for (const [start, length] of all) {
|
|
3992
|
+
const realSpans = this.mapper.spans.filter(
|
|
3993
|
+
(s) => s.run !== null && s.end > start && s.start < start + length
|
|
3994
|
+
);
|
|
3995
|
+
if (realSpans.length === 0) return [start, length];
|
|
3996
|
+
if (realSpans.some((s) => !s.del_id)) return [start, length];
|
|
3997
|
+
}
|
|
3998
|
+
return this.mapper.find_match_index(target_text);
|
|
3999
|
+
}
|
|
3631
4000
|
_pre_resolve_heuristic_edit(edit) {
|
|
3632
4001
|
if (!edit.target_text) return null;
|
|
3633
|
-
let [start_idx, match_len] = this.
|
|
4002
|
+
let [start_idx, match_len] = this._first_live_match(edit.target_text);
|
|
3634
4003
|
let use_clean_map = false;
|
|
3635
4004
|
if (start_idx === -1) {
|
|
3636
4005
|
if (!this.clean_mapper)
|
|
@@ -3694,7 +4063,7 @@ var RedlineEngine = class {
|
|
|
3694
4063
|
_apply_single_edit_indexed(edit, orig_new, rebuild_map) {
|
|
3695
4064
|
let op = edit._internal_op;
|
|
3696
4065
|
const active_mapper = edit._active_mapper_ref || this.mapper;
|
|
3697
|
-
const start_idx = edit._match_start_index || 0;
|
|
4066
|
+
const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
|
|
3698
4067
|
const length = edit.target_text ? edit.target_text.length : 0;
|
|
3699
4068
|
const del_id = ["DELETION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
|
|
3700
4069
|
const ins_id = ["INSERTION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
|
|
@@ -3743,6 +4112,76 @@ var RedlineEngine = class {
|
|
|
3743
4112
|
rebuild_map
|
|
3744
4113
|
);
|
|
3745
4114
|
if (!anchor_run && !anchor_para) return false;
|
|
4115
|
+
const _bug233_new = edit.new_text || "";
|
|
4116
|
+
const _bug233_trailing_break = /\n\s*$/.test(_bug233_new);
|
|
4117
|
+
let _bug233_target_para = null;
|
|
4118
|
+
{
|
|
4119
|
+
const startingSpans = active_mapper.spans.filter(
|
|
4120
|
+
(s) => s.paragraph !== null && s.start === start_idx
|
|
4121
|
+
);
|
|
4122
|
+
if (startingSpans.length > 0 && startingSpans[0].paragraph) {
|
|
4123
|
+
_bug233_target_para = startingSpans[0].paragraph._element;
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
if (_bug233_trailing_break && _bug233_target_para && _bug233_target_para.parentNode) {
|
|
4127
|
+
const body = _bug233_target_para.parentNode;
|
|
4128
|
+
const xmlDoc = this.doc.part._element.ownerDocument;
|
|
4129
|
+
const lines = _bug233_new.split(/[\r\n]+/).filter((l) => l !== "");
|
|
4130
|
+
let firstNew = null;
|
|
4131
|
+
let lastNew = null;
|
|
4132
|
+
let lastIns = null;
|
|
4133
|
+
for (const raw_line of lines) {
|
|
4134
|
+
const [clean_text, style_name] = this._parse_markdown_style(raw_line);
|
|
4135
|
+
const new_p = xmlDoc.createElement("w:p");
|
|
4136
|
+
if (style_name) {
|
|
4137
|
+
this._set_paragraph_style(new_p, style_name);
|
|
4138
|
+
} else {
|
|
4139
|
+
const existing_pPr = findChild(_bug233_target_para, "w:pPr");
|
|
4140
|
+
if (existing_pPr) new_p.appendChild(existing_pPr.cloneNode(true));
|
|
4141
|
+
}
|
|
4142
|
+
let pPr = findChild(new_p, "w:pPr");
|
|
4143
|
+
if (!pPr) {
|
|
4144
|
+
pPr = xmlDoc.createElement("w:pPr");
|
|
4145
|
+
new_p.insertBefore(pPr, new_p.firstChild);
|
|
4146
|
+
}
|
|
4147
|
+
let rPr = findChild(pPr, "w:rPr");
|
|
4148
|
+
if (!rPr) {
|
|
4149
|
+
rPr = xmlDoc.createElement("w:rPr");
|
|
4150
|
+
pPr.appendChild(rPr);
|
|
4151
|
+
}
|
|
4152
|
+
rPr.appendChild(this._create_track_change_tag("w:ins", "", ins_id));
|
|
4153
|
+
const content_ins = this._build_tracked_ins_for_line(
|
|
4154
|
+
clean_text,
|
|
4155
|
+
anchor_run,
|
|
4156
|
+
ins_id,
|
|
4157
|
+
xmlDoc
|
|
4158
|
+
);
|
|
4159
|
+
if (content_ins) new_p.appendChild(content_ins);
|
|
4160
|
+
body.insertBefore(new_p, _bug233_target_para);
|
|
4161
|
+
if (!firstNew) firstNew = new_p;
|
|
4162
|
+
lastNew = new_p;
|
|
4163
|
+
lastIns = content_ins;
|
|
4164
|
+
}
|
|
4165
|
+
if (firstNew) {
|
|
4166
|
+
if (edit.comment && lastNew && lastIns) {
|
|
4167
|
+
const ascend = (el, p) => {
|
|
4168
|
+
let cur = el;
|
|
4169
|
+
while (cur.parentNode && cur.parentNode !== p)
|
|
4170
|
+
cur = cur.parentNode;
|
|
4171
|
+
return cur;
|
|
4172
|
+
};
|
|
4173
|
+
const startIns = findAllDescendants(firstNew, "w:ins")[0] || firstNew;
|
|
4174
|
+
this._attach_comment_spanning(
|
|
4175
|
+
firstNew,
|
|
4176
|
+
ascend(startIns, firstNew),
|
|
4177
|
+
lastNew,
|
|
4178
|
+
ascend(lastIns, lastNew),
|
|
4179
|
+
edit.comment
|
|
4180
|
+
);
|
|
4181
|
+
}
|
|
4182
|
+
return true;
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
3746
4185
|
const result = this._track_insert_multiline(
|
|
3747
4186
|
edit.new_text || "",
|
|
3748
4187
|
anchor_run,
|
|
@@ -5452,15 +5891,88 @@ function remove_all_comments(doc) {
|
|
|
5452
5891
|
lines.push(` ${status} "${_truncate(info.text || "", 60)}" (${info.author || "Unknown"})`);
|
|
5453
5892
|
cm.deleteComment(cId);
|
|
5454
5893
|
}
|
|
5455
|
-
for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"
|
|
5894
|
+
for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
|
|
5456
5895
|
for (const el of findAllDescendants(doc.element, tag)) {
|
|
5457
5896
|
el.parentNode?.removeChild(el);
|
|
5458
5897
|
}
|
|
5459
5898
|
}
|
|
5899
|
+
const refs = findAllDescendants(doc.element, "w:commentReference");
|
|
5900
|
+
for (const ref of refs) {
|
|
5901
|
+
const parent = ref.parentNode;
|
|
5902
|
+
if (parent) {
|
|
5903
|
+
if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
|
|
5904
|
+
const nonRprChildren = Array.from(parent.childNodes).filter(
|
|
5905
|
+
(c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
|
|
5906
|
+
);
|
|
5907
|
+
if (nonRprChildren.length <= 1) {
|
|
5908
|
+
parent.parentNode?.removeChild(parent);
|
|
5909
|
+
} else {
|
|
5910
|
+
parent.removeChild(ref);
|
|
5911
|
+
}
|
|
5912
|
+
} else {
|
|
5913
|
+
parent.removeChild(ref);
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5460
5917
|
const resolvedCount = Object.values(data).filter((c) => c.resolved).length;
|
|
5461
5918
|
const openCount = Object.values(data).filter((c) => !c.resolved).length;
|
|
5462
5919
|
return [`Comments removed: ${keys.length} (${resolvedCount} resolved, ${openCount} open)`].concat(lines);
|
|
5463
5920
|
}
|
|
5921
|
+
function eject_comment_parts(doc) {
|
|
5922
|
+
const pkg = doc.pkg;
|
|
5923
|
+
const comment_partnames = /* @__PURE__ */ new Set();
|
|
5924
|
+
for (const part of pkg.parts) {
|
|
5925
|
+
if (part.partname.toLowerCase().includes("comments")) {
|
|
5926
|
+
comment_partnames.add(part.partname);
|
|
5927
|
+
const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
|
|
5928
|
+
const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
|
|
5929
|
+
comment_partnames.add(withSlash);
|
|
5930
|
+
comment_partnames.add(withoutSlash);
|
|
5931
|
+
}
|
|
5932
|
+
}
|
|
5933
|
+
if (comment_partnames.size === 0) return;
|
|
5934
|
+
for (const part of pkg.parts) {
|
|
5935
|
+
if (part.partname.endsWith(".rels")) {
|
|
5936
|
+
const rels = findAllDescendants(part._element, "Relationship");
|
|
5937
|
+
const toRemove = [];
|
|
5938
|
+
for (const rel of rels) {
|
|
5939
|
+
const target = rel.getAttribute("Target") || "";
|
|
5940
|
+
if (target.toLowerCase().includes("comments")) {
|
|
5941
|
+
toRemove.push(rel);
|
|
5942
|
+
const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
|
|
5943
|
+
const sourcePart = pkg.getPartByPath(sourcePath);
|
|
5944
|
+
if (sourcePart) {
|
|
5945
|
+
const relId = rel.getAttribute("Id");
|
|
5946
|
+
if (relId) sourcePart.rels.delete(relId);
|
|
5947
|
+
}
|
|
5948
|
+
}
|
|
5949
|
+
}
|
|
5950
|
+
for (const relEl of toRemove) {
|
|
5951
|
+
relEl.parentNode?.removeChild(relEl);
|
|
5952
|
+
}
|
|
5953
|
+
}
|
|
5954
|
+
}
|
|
5955
|
+
const ctPart = pkg.getPartByPath("[Content_Types].xml");
|
|
5956
|
+
if (ctPart) {
|
|
5957
|
+
const overrides = findAllDescendants(ctPart._element, "Override");
|
|
5958
|
+
const toRemove = [];
|
|
5959
|
+
for (const override of overrides) {
|
|
5960
|
+
const partName = override.getAttribute("PartName") || "";
|
|
5961
|
+
if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
|
|
5962
|
+
toRemove.push(override);
|
|
5963
|
+
}
|
|
5964
|
+
}
|
|
5965
|
+
for (const overrideEl of toRemove) {
|
|
5966
|
+
overrideEl.parentNode?.removeChild(overrideEl);
|
|
5967
|
+
}
|
|
5968
|
+
}
|
|
5969
|
+
pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
|
|
5970
|
+
for (const key of Object.keys(pkg.unzipped)) {
|
|
5971
|
+
if (key.toLowerCase().includes("comments")) {
|
|
5972
|
+
delete pkg.unzipped[key];
|
|
5973
|
+
}
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5464
5976
|
function replace_comment_authors(doc, newAuthor) {
|
|
5465
5977
|
const cm = new CommentsManager(doc);
|
|
5466
5978
|
if (!cm.commentsPart) return [];
|
|
@@ -5649,6 +6161,7 @@ async function finalize_document(doc, options) {
|
|
|
5649
6161
|
const commentsSummary = get_comments_summary(doc);
|
|
5650
6162
|
report.comments_removed = commentsSummary.total;
|
|
5651
6163
|
report.add_transform_lines(remove_all_comments(doc));
|
|
6164
|
+
eject_comment_parts(doc);
|
|
5652
6165
|
} else if (options.sanitize_mode === "keep-markup") {
|
|
5653
6166
|
const counts = count_tracked_changes(doc);
|
|
5654
6167
|
report.tracked_changes_found = counts[0] + counts[1] + counts[2];
|