@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.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,39 @@ 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(
|
|
2724
|
+
start_idx + length,
|
|
2725
|
+
start_idx + length + 30
|
|
2726
|
+
);
|
|
2727
|
+
const critic_markup = `${context_before}{--${target_text}--}{++${new_text}++}${context_after}`;
|
|
2728
|
+
let clean_text = critic_markup;
|
|
2729
|
+
clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
|
|
2730
|
+
clean_text = clean_text.replace(/\{--.*?--\}/gs, "");
|
|
2731
|
+
clean_text = clean_text.replace(/\{\+\+(.*?)\+\+\}/gs, "$1");
|
|
2732
|
+
return [critic_markup, clean_text];
|
|
2733
|
+
}
|
|
2655
2734
|
_scan_existing_ids() {
|
|
2656
2735
|
let maxId = 0;
|
|
2657
2736
|
for (const tag of ["w:ins", "w:del"]) {
|
|
@@ -2744,28 +2823,86 @@ var RedlineEngine = class {
|
|
|
2744
2823
|
}
|
|
2745
2824
|
}
|
|
2746
2825
|
}
|
|
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);
|
|
2826
|
+
for (const root_element of parts_to_process) {
|
|
2827
|
+
for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
|
|
2828
|
+
for (const el of findAllDescendants(root_element, tag)) {
|
|
2829
|
+
el.parentNode?.removeChild(el);
|
|
2830
|
+
}
|
|
2756
2831
|
}
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2832
|
+
const refs = findAllDescendants(root_element, "w:commentReference");
|
|
2833
|
+
for (const ref of refs) {
|
|
2834
|
+
const parent = ref.parentNode;
|
|
2835
|
+
if (parent) {
|
|
2836
|
+
if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
|
|
2837
|
+
const nonRprChildren = Array.from(parent.childNodes).filter(
|
|
2838
|
+
(c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
|
|
2839
|
+
);
|
|
2840
|
+
if (nonRprChildren.length <= 1) {
|
|
2841
|
+
parent.parentNode?.removeChild(parent);
|
|
2842
|
+
} else {
|
|
2843
|
+
parent.removeChild(ref);
|
|
2844
|
+
}
|
|
2845
|
+
} else {
|
|
2846
|
+
parent.removeChild(ref);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2765
2849
|
}
|
|
2766
2850
|
}
|
|
2767
|
-
|
|
2768
|
-
|
|
2851
|
+
const pkg = this.doc.pkg;
|
|
2852
|
+
const comment_partnames = /* @__PURE__ */ new Set();
|
|
2853
|
+
for (const part of pkg.parts) {
|
|
2854
|
+
if (part.partname.toLowerCase().includes("comments")) {
|
|
2855
|
+
comment_partnames.add(part.partname);
|
|
2856
|
+
const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
|
|
2857
|
+
const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
|
|
2858
|
+
comment_partnames.add(withSlash);
|
|
2859
|
+
comment_partnames.add(withoutSlash);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
if (comment_partnames.size > 0) {
|
|
2863
|
+
for (const part of pkg.parts) {
|
|
2864
|
+
if (part.partname.endsWith(".rels")) {
|
|
2865
|
+
const rels = findAllDescendants(part._element, "Relationship");
|
|
2866
|
+
const toRemove = [];
|
|
2867
|
+
for (const rel of rels) {
|
|
2868
|
+
const target = rel.getAttribute("Target") || "";
|
|
2869
|
+
if (target.toLowerCase().includes("comments")) {
|
|
2870
|
+
toRemove.push(rel);
|
|
2871
|
+
const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
|
|
2872
|
+
const sourcePart = pkg.getPartByPath(sourcePath);
|
|
2873
|
+
if (sourcePart) {
|
|
2874
|
+
const relId = rel.getAttribute("Id");
|
|
2875
|
+
if (relId) sourcePart.rels.delete(relId);
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
for (const relEl of toRemove) {
|
|
2880
|
+
relEl.parentNode?.removeChild(relEl);
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
const ctPart = pkg.getPartByPath("[Content_Types].xml");
|
|
2885
|
+
if (ctPart) {
|
|
2886
|
+
const overrides = findAllDescendants(ctPart._element, "Override");
|
|
2887
|
+
const toRemove = [];
|
|
2888
|
+
for (const override of overrides) {
|
|
2889
|
+
const partName = override.getAttribute("PartName") || "";
|
|
2890
|
+
if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
|
|
2891
|
+
toRemove.push(override);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
for (const overrideEl of toRemove) {
|
|
2895
|
+
overrideEl.parentNode?.removeChild(overrideEl);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
pkg.parts = pkg.parts.filter(
|
|
2899
|
+
(p) => !p.partname.toLowerCase().includes("comments")
|
|
2900
|
+
);
|
|
2901
|
+
for (const key of Object.keys(pkg.unzipped)) {
|
|
2902
|
+
if (key.toLowerCase().includes("comments")) {
|
|
2903
|
+
delete pkg.unzipped[key];
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2769
2906
|
}
|
|
2770
2907
|
}
|
|
2771
2908
|
_getNextId() {
|
|
@@ -2856,40 +2993,40 @@ var RedlineEngine = class {
|
|
|
2856
2993
|
}
|
|
2857
2994
|
}
|
|
2858
2995
|
/**
|
|
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
|
-
|
|
2996
|
+
* Inserts `text` as one or more tracked paragraphs anchored relative to
|
|
2997
|
+
* either an existing run or a paragraph. Returns:
|
|
2998
|
+
* { first_node, last_p, last_ins, used_block_mode }
|
|
2999
|
+
* where:
|
|
3000
|
+
* - first_node: the first <w:ins> (for inline mode) OR the first new <w:p>
|
|
3001
|
+
* (for block mode). The caller uses this for splicing into the DOM and
|
|
3002
|
+
* for anchoring comments.
|
|
3003
|
+
* - last_p: the last new <w:p> created, if any. null when entirely inline.
|
|
3004
|
+
* - last_ins: the last <w:ins> created (inside the last new <w:p>, or the
|
|
3005
|
+
* sole inline ins). Used as the comment's end anchor.
|
|
3006
|
+
* - used_block_mode: true when the first line carried a heading/list style
|
|
3007
|
+
* marker and we created a new paragraph for it (rather than inlining it).
|
|
3008
|
+
*
|
|
3009
|
+
* Multi-paragraph rules (only when text contains '\n'):
|
|
3010
|
+
* - Each additional line becomes a new <w:p>, inserted after the anchor
|
|
3011
|
+
* paragraph in document order.
|
|
3012
|
+
* - Each new <w:p> gets a copy of the anchor paragraph's <w:pPr> (so list
|
|
3013
|
+
* numbering / indentation are preserved) unless the line itself starts
|
|
3014
|
+
* with a markdown heading or list marker, which overrides the style.
|
|
3015
|
+
* - Each new <w:p> carries a tracked paragraph-break marker
|
|
3016
|
+
* (<w:pPr><w:rPr><w:ins/></w:rPr></w:pPr>) so Word natively tracks the
|
|
3017
|
+
* paragraph break.
|
|
3018
|
+
* - Each new <w:p>'s content is wrapped in a <w:ins>, with inline bold/
|
|
3019
|
+
* italic markdown parsed via _parse_inline_markdown.
|
|
3020
|
+
*
|
|
3021
|
+
* The first line:
|
|
3022
|
+
* - If it carries a heading / list marker AND we have a paragraph anchor,
|
|
3023
|
+
* we drop into "block mode": no inline <w:ins>; the first line itself
|
|
3024
|
+
* becomes the first new <w:p>.
|
|
3025
|
+
* - Otherwise we emit a single inline <w:ins> for the first line (current
|
|
3026
|
+
* behaviour) and treat the remaining lines as block extensions.
|
|
3027
|
+
*
|
|
3028
|
+
* Does NOT attach comments; callers handle that.
|
|
3029
|
+
*/
|
|
2893
3030
|
_track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id) {
|
|
2894
3031
|
if (!text) {
|
|
2895
3032
|
return {
|
|
@@ -3029,7 +3166,15 @@ var RedlineEngine = class {
|
|
|
3029
3166
|
const anchor_rPr = findChild(anchor_run._element, "w:rPr");
|
|
3030
3167
|
if (anchor_rPr) {
|
|
3031
3168
|
const clone = anchor_rPr.cloneNode(true);
|
|
3032
|
-
for (const tag of [
|
|
3169
|
+
for (const tag of [
|
|
3170
|
+
"w:vanish",
|
|
3171
|
+
"w:strike",
|
|
3172
|
+
"w:dstrike",
|
|
3173
|
+
"w:i",
|
|
3174
|
+
"w:iCs",
|
|
3175
|
+
"w:b",
|
|
3176
|
+
"w:bCs"
|
|
3177
|
+
]) {
|
|
3033
3178
|
const found = findChild(clone, tag);
|
|
3034
3179
|
if (found) clone.removeChild(found);
|
|
3035
3180
|
}
|
|
@@ -3303,6 +3448,16 @@ var RedlineEngine = class {
|
|
|
3303
3448
|
matches = this.clean_mapper.find_all_match_indices(edit.target_text);
|
|
3304
3449
|
if (matches.length > 0) activeText = this.clean_mapper.full_text;
|
|
3305
3450
|
}
|
|
3451
|
+
if (activeText === this.mapper.full_text && matches.length > 1) {
|
|
3452
|
+
const liveMatches = matches.filter(([start, length]) => {
|
|
3453
|
+
const realSpans = this.mapper.spans.filter(
|
|
3454
|
+
(s) => s.run !== null && s.end > start && s.start < start + length
|
|
3455
|
+
);
|
|
3456
|
+
if (realSpans.length === 0) return true;
|
|
3457
|
+
return realSpans.some((s) => !s.del_id);
|
|
3458
|
+
});
|
|
3459
|
+
if (liveMatches.length > 0) matches = liveMatches;
|
|
3460
|
+
}
|
|
3306
3461
|
if (matches.length === 0) {
|
|
3307
3462
|
errors.push(
|
|
3308
3463
|
`- Edit ${i + 1} Failed: Target text not found in document:
|
|
@@ -3322,6 +3477,34 @@ var RedlineEngine = class {
|
|
|
3322
3477
|
)
|
|
3323
3478
|
);
|
|
3324
3479
|
}
|
|
3480
|
+
if (matches.length === 1) {
|
|
3481
|
+
const [m_start, m_len] = matches[0];
|
|
3482
|
+
const matched = activeText.substring(m_start, m_start + m_len);
|
|
3483
|
+
const [pfx, sfx] = trim_common_context(matched, edit.new_text || "");
|
|
3484
|
+
const t_end = matched.length - sfx;
|
|
3485
|
+
const final_target = matched.substring(pfx, t_end);
|
|
3486
|
+
const final_new = (edit.new_text || "").substring(
|
|
3487
|
+
pfx,
|
|
3488
|
+
(edit.new_text || "").length - sfx
|
|
3489
|
+
);
|
|
3490
|
+
if (final_target.includes("\n\n")) {
|
|
3491
|
+
if (final_new.includes("\n\n")) {
|
|
3492
|
+
const parts = matched.split("\n\n");
|
|
3493
|
+
if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
|
|
3494
|
+
errors.push(
|
|
3495
|
+
`- 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.`
|
|
3496
|
+
);
|
|
3497
|
+
}
|
|
3498
|
+
} else {
|
|
3499
|
+
const parts = final_target.split("\n\n");
|
|
3500
|
+
if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
|
|
3501
|
+
errors.push(
|
|
3502
|
+
`- 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.`
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3325
3508
|
for (const [start, length] of matches) {
|
|
3326
3509
|
const spans = this.mapper.spans.filter(
|
|
3327
3510
|
(s) => s.end > start && s.start < start + length
|
|
@@ -3385,7 +3568,33 @@ var RedlineEngine = class {
|
|
|
3385
3568
|
}
|
|
3386
3569
|
return errors;
|
|
3387
3570
|
}
|
|
3388
|
-
process_batch(changes) {
|
|
3571
|
+
process_batch(changes, dry_run = false) {
|
|
3572
|
+
if (dry_run) {
|
|
3573
|
+
const baselines = /* @__PURE__ */ new Map();
|
|
3574
|
+
for (const part of this.doc.pkg.parts) {
|
|
3575
|
+
if (part._element) {
|
|
3576
|
+
baselines.set(part, part._element.cloneNode(true));
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
try {
|
|
3580
|
+
return this._process_batch_internal(changes, true);
|
|
3581
|
+
} finally {
|
|
3582
|
+
for (const [part, originalEl] of baselines.entries()) {
|
|
3583
|
+
const doc = part._element.ownerDocument;
|
|
3584
|
+
if (doc && doc.documentElement) {
|
|
3585
|
+
doc.replaceChild(originalEl, doc.documentElement);
|
|
3586
|
+
}
|
|
3587
|
+
part._element = originalEl;
|
|
3588
|
+
}
|
|
3589
|
+
this.mapper = new DocumentMapper(this.doc);
|
|
3590
|
+
this.comments_manager = new CommentsManager(this.doc);
|
|
3591
|
+
this.clean_mapper = null;
|
|
3592
|
+
}
|
|
3593
|
+
} else {
|
|
3594
|
+
return this._process_batch_internal(changes, false);
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
_process_batch_internal(changes, dry_run_mode = false) {
|
|
3389
3598
|
this.skipped_details = [];
|
|
3390
3599
|
const actions = changes.filter(
|
|
3391
3600
|
(c) => ["accept", "reject", "reply"].includes(c.type)
|
|
@@ -3393,38 +3602,133 @@ var RedlineEngine = class {
|
|
|
3393
3602
|
const edits = changes.filter(
|
|
3394
3603
|
(c) => !["accept", "reject", "reply"].includes(c.type)
|
|
3395
3604
|
);
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3605
|
+
if (!dry_run_mode) {
|
|
3606
|
+
const all_errors = [];
|
|
3607
|
+
if (actions.length > 0) {
|
|
3608
|
+
all_errors.push(...this.validate_review_actions(actions));
|
|
3609
|
+
}
|
|
3610
|
+
if (edits.length > 0) {
|
|
3611
|
+
all_errors.push(...this.validate_edits(edits));
|
|
3612
|
+
}
|
|
3613
|
+
if (all_errors.length > 0) {
|
|
3614
|
+
throw new BatchValidationError(all_errors);
|
|
3615
|
+
}
|
|
3616
|
+
} else {
|
|
3617
|
+
if (actions.length > 0) {
|
|
3618
|
+
const action_errors = this.validate_review_actions(actions);
|
|
3619
|
+
if (action_errors.length > 0) {
|
|
3620
|
+
throw new BatchValidationError(action_errors);
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3405
3623
|
}
|
|
3406
|
-
let applied_actions = 0
|
|
3624
|
+
let applied_actions = 0;
|
|
3625
|
+
let skipped_actions = 0;
|
|
3407
3626
|
if (actions.length > 0) {
|
|
3408
3627
|
const res = this.apply_review_actions(actions);
|
|
3409
3628
|
applied_actions = res[0];
|
|
3410
3629
|
skipped_actions = res[1];
|
|
3630
|
+
if (skipped_actions > 0) {
|
|
3631
|
+
throw new BatchValidationError(this.skipped_details);
|
|
3632
|
+
}
|
|
3411
3633
|
if (applied_actions > 0) {
|
|
3412
3634
|
this.mapper["_build_map"]();
|
|
3413
3635
|
if (this.clean_mapper) this.clean_mapper["_build_map"]();
|
|
3414
3636
|
}
|
|
3415
3637
|
}
|
|
3416
|
-
|
|
3638
|
+
const edits_reports = [];
|
|
3639
|
+
let applied_edits = 0;
|
|
3640
|
+
let skipped_edits = 0;
|
|
3417
3641
|
if (edits.length > 0) {
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3642
|
+
if (dry_run_mode) {
|
|
3643
|
+
for (const edit of edits) {
|
|
3644
|
+
const single_errors = this.validate_edits([edit]);
|
|
3645
|
+
const warning = this._check_punctuation_warning(
|
|
3646
|
+
edit.target_text || ""
|
|
3647
|
+
);
|
|
3648
|
+
if (single_errors.length > 0) {
|
|
3649
|
+
skipped_edits++;
|
|
3650
|
+
edits_reports.push({
|
|
3651
|
+
status: "failed",
|
|
3652
|
+
target_text: edit.target_text || "",
|
|
3653
|
+
new_text: edit.new_text || "",
|
|
3654
|
+
warning,
|
|
3655
|
+
error: single_errors[0],
|
|
3656
|
+
critic_markup: null,
|
|
3657
|
+
clean_text: null
|
|
3658
|
+
});
|
|
3659
|
+
continue;
|
|
3660
|
+
}
|
|
3661
|
+
const res = this.apply_edits([edit]);
|
|
3662
|
+
const applied = res[0];
|
|
3663
|
+
if (applied > 0) {
|
|
3664
|
+
applied_edits++;
|
|
3665
|
+
const previews = this._build_edit_context_previews(edit);
|
|
3666
|
+
edits_reports.push({
|
|
3667
|
+
status: "applied",
|
|
3668
|
+
target_text: edit.target_text || "",
|
|
3669
|
+
new_text: edit.new_text || "",
|
|
3670
|
+
warning,
|
|
3671
|
+
error: null,
|
|
3672
|
+
critic_markup: previews[0],
|
|
3673
|
+
clean_text: previews[1]
|
|
3674
|
+
});
|
|
3675
|
+
} else {
|
|
3676
|
+
skipped_edits++;
|
|
3677
|
+
const error_msg = this.skipped_details.length > 0 ? this.skipped_details[this.skipped_details.length - 1] : "Failed to apply edit";
|
|
3678
|
+
edits_reports.push({
|
|
3679
|
+
status: "failed",
|
|
3680
|
+
target_text: edit.target_text || "",
|
|
3681
|
+
new_text: edit.new_text || "",
|
|
3682
|
+
warning,
|
|
3683
|
+
error: error_msg,
|
|
3684
|
+
critic_markup: null,
|
|
3685
|
+
clean_text: null
|
|
3686
|
+
});
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
} else {
|
|
3690
|
+
const errors = this.validate_edits(edits);
|
|
3691
|
+
if (errors.length > 0) {
|
|
3692
|
+
throw new BatchValidationError(errors);
|
|
3693
|
+
}
|
|
3694
|
+
const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
|
|
3695
|
+
const res = this.apply_edits(cloned_edits);
|
|
3696
|
+
applied_edits = res[0];
|
|
3697
|
+
skipped_edits = res[1];
|
|
3698
|
+
for (const edit of cloned_edits) {
|
|
3699
|
+
const success = edit._applied_status || false;
|
|
3700
|
+
const error_msg = edit._error_msg || null;
|
|
3701
|
+
const warning = this._check_punctuation_warning(
|
|
3702
|
+
edit.target_text || ""
|
|
3703
|
+
);
|
|
3704
|
+
let critic_markup = null;
|
|
3705
|
+
let clean_text = null;
|
|
3706
|
+
if (success) {
|
|
3707
|
+
const previews = this._build_edit_context_previews(edit);
|
|
3708
|
+
critic_markup = previews[0];
|
|
3709
|
+
clean_text = previews[1];
|
|
3710
|
+
}
|
|
3711
|
+
edits_reports.push({
|
|
3712
|
+
status: success ? "applied" : "failed",
|
|
3713
|
+
target_text: edit.target_text || "",
|
|
3714
|
+
new_text: edit.new_text || "",
|
|
3715
|
+
warning,
|
|
3716
|
+
error: error_msg,
|
|
3717
|
+
critic_markup,
|
|
3718
|
+
clean_text
|
|
3719
|
+
});
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3421
3722
|
}
|
|
3422
3723
|
return {
|
|
3423
3724
|
actions_applied: applied_actions,
|
|
3424
3725
|
actions_skipped: skipped_actions,
|
|
3425
3726
|
edits_applied: applied_edits,
|
|
3426
3727
|
edits_skipped: skipped_edits,
|
|
3427
|
-
skipped_details: this.skipped_details
|
|
3728
|
+
skipped_details: this.skipped_details,
|
|
3729
|
+
edits: edits_reports,
|
|
3730
|
+
engine: "node",
|
|
3731
|
+
version: "1.10.0"
|
|
3428
3732
|
};
|
|
3429
3733
|
}
|
|
3430
3734
|
apply_edits(edits) {
|
|
@@ -3432,50 +3736,90 @@ var RedlineEngine = class {
|
|
|
3432
3736
|
let skipped = 0;
|
|
3433
3737
|
const resolved_edits = [];
|
|
3434
3738
|
for (const edit of edits) {
|
|
3435
|
-
|
|
3739
|
+
edit._applied_status = false;
|
|
3740
|
+
edit._error_msg = null;
|
|
3741
|
+
}
|
|
3742
|
+
for (const edit of edits) {
|
|
3743
|
+
if (edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null) {
|
|
3744
|
+
resolved_edits.push([edit, edit.new_text || null]);
|
|
3745
|
+
} else if (edit._match_start_index !== void 0 && edit._match_start_index !== null) {
|
|
3746
|
+
edit._resolved_start_idx = edit._match_start_index;
|
|
3436
3747
|
resolved_edits.push([edit, edit.new_text || null]);
|
|
3437
3748
|
} else if (edit.type === "insert_row" || edit.type === "delete_row") {
|
|
3438
|
-
|
|
3439
|
-
if (
|
|
3440
|
-
|
|
3749
|
+
let matches = this.mapper.find_all_match_indices(edit.target_text);
|
|
3750
|
+
if (matches.length === 0) {
|
|
3751
|
+
if (!this.clean_mapper) {
|
|
3752
|
+
this.clean_mapper = new DocumentMapper(this.doc, true);
|
|
3753
|
+
}
|
|
3754
|
+
matches = this.clean_mapper.find_all_match_indices(edit.target_text);
|
|
3755
|
+
}
|
|
3756
|
+
if (matches.length > 0) {
|
|
3757
|
+
edit._resolved_start_idx = matches[0][0];
|
|
3441
3758
|
resolved_edits.push([edit, null]);
|
|
3442
3759
|
} else {
|
|
3443
3760
|
skipped++;
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3761
|
+
edit._applied_status = false;
|
|
3762
|
+
const target_snippet = (edit.target_text || "").trim().substring(0, 40);
|
|
3763
|
+
const msg = `- Failed to locate row target: '${target_snippet}...'`;
|
|
3764
|
+
this.skipped_details.push(msg);
|
|
3765
|
+
edit._error_msg = msg;
|
|
3447
3766
|
}
|
|
3448
3767
|
} else {
|
|
3449
3768
|
const resolved = this._pre_resolve_heuristic_edit(edit);
|
|
3450
3769
|
if (resolved) {
|
|
3451
3770
|
if (Array.isArray(resolved)) {
|
|
3452
|
-
for (const r of resolved)
|
|
3771
|
+
for (const r of resolved) {
|
|
3772
|
+
r._resolved_start_idx = r._match_start_index;
|
|
3773
|
+
r._parent_edit_ref = edit;
|
|
3774
|
+
if (edit._resolved_start_idx === void 0 || edit._resolved_start_idx === null) {
|
|
3775
|
+
edit._resolved_start_idx = r._resolved_start_idx;
|
|
3776
|
+
}
|
|
3777
|
+
if (!edit._resolved_proxy_edit) {
|
|
3778
|
+
edit._resolved_proxy_edit = r;
|
|
3779
|
+
}
|
|
3780
|
+
resolved_edits.push([r, r.new_text]);
|
|
3781
|
+
}
|
|
3453
3782
|
} else {
|
|
3783
|
+
resolved._resolved_start_idx = resolved._match_start_index;
|
|
3784
|
+
resolved._parent_edit_ref = edit;
|
|
3785
|
+
edit._resolved_start_idx = resolved._resolved_start_idx;
|
|
3786
|
+
edit._resolved_proxy_edit = resolved;
|
|
3454
3787
|
resolved_edits.push([resolved, resolved.new_text]);
|
|
3455
3788
|
}
|
|
3456
3789
|
} else {
|
|
3457
3790
|
skipped++;
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
);
|
|
3791
|
+
edit._applied_status = false;
|
|
3792
|
+
const display_text = edit.target_text || "insertion";
|
|
3793
|
+
const target_snippet = display_text.trim().substring(0, 40);
|
|
3794
|
+
const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
|
|
3795
|
+
this.skipped_details.push(msg);
|
|
3796
|
+
edit._error_msg = msg;
|
|
3461
3797
|
}
|
|
3462
3798
|
}
|
|
3463
3799
|
}
|
|
3464
3800
|
resolved_edits.sort(
|
|
3465
|
-
(a, b) => (b[0].
|
|
3801
|
+
(a, b) => (b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0)
|
|
3466
3802
|
);
|
|
3467
3803
|
const occupied_ranges = [];
|
|
3468
3804
|
for (const [edit, orig_new] of resolved_edits) {
|
|
3469
|
-
const start = edit.
|
|
3805
|
+
const start = edit._resolved_start_idx || 0;
|
|
3470
3806
|
const end = start + (edit.target_text ? edit.target_text.length : 0);
|
|
3471
3807
|
const overlaps = occupied_ranges.some(
|
|
3472
3808
|
([occ_start, occ_end]) => start < occ_end && end > occ_start
|
|
3473
3809
|
);
|
|
3474
3810
|
if (overlaps) {
|
|
3475
3811
|
skipped++;
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3812
|
+
const display_text = edit.target_text || "insertion";
|
|
3813
|
+
const target_snippet = display_text.trim().substring(0, 40);
|
|
3814
|
+
const msg = `- Skipped overlapping edit targeting: '${target_snippet}...'`;
|
|
3815
|
+
this.skipped_details.push(msg);
|
|
3816
|
+
edit._applied_status = false;
|
|
3817
|
+
edit._error_msg = msg;
|
|
3818
|
+
const parent = edit._parent_edit_ref;
|
|
3819
|
+
if (parent) {
|
|
3820
|
+
parent._applied_status = false;
|
|
3821
|
+
parent._error_msg = msg;
|
|
3822
|
+
}
|
|
3479
3823
|
continue;
|
|
3480
3824
|
}
|
|
3481
3825
|
let success = false;
|
|
@@ -3487,11 +3831,26 @@ var RedlineEngine = class {
|
|
|
3487
3831
|
if (success) {
|
|
3488
3832
|
applied++;
|
|
3489
3833
|
occupied_ranges.push([start, end]);
|
|
3834
|
+
edit._applied_status = true;
|
|
3835
|
+
const parent = edit._parent_edit_ref;
|
|
3836
|
+
if (parent) {
|
|
3837
|
+
parent._applied_status = true;
|
|
3838
|
+
}
|
|
3490
3839
|
} else {
|
|
3491
3840
|
skipped++;
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3841
|
+
const display_text = edit.target_text || "insertion";
|
|
3842
|
+
const target_snippet = display_text.trim().substring(0, 40);
|
|
3843
|
+
const msg = `- Failed to apply edit targeting: '${target_snippet}...'`;
|
|
3844
|
+
this.skipped_details.push(msg);
|
|
3845
|
+
edit._applied_status = false;
|
|
3846
|
+
edit._error_msg = msg;
|
|
3847
|
+
const parent = edit._parent_edit_ref;
|
|
3848
|
+
if (parent) {
|
|
3849
|
+
if (!parent._applied_status) {
|
|
3850
|
+
parent._applied_status = false;
|
|
3851
|
+
parent._error_msg = msg;
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3495
3854
|
}
|
|
3496
3855
|
}
|
|
3497
3856
|
return [applied, skipped];
|
|
@@ -3584,7 +3943,7 @@ var RedlineEngine = class {
|
|
|
3584
3943
|
return [applied, skipped];
|
|
3585
3944
|
}
|
|
3586
3945
|
_apply_table_edit(edit, rebuild_map) {
|
|
3587
|
-
const start_idx = edit._match_start_index || 0;
|
|
3946
|
+
const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
|
|
3588
3947
|
const [anchor_run, anchor_para] = this.mapper.get_insertion_anchor(
|
|
3589
3948
|
start_idx,
|
|
3590
3949
|
rebuild_map
|
|
@@ -3628,9 +3987,31 @@ var RedlineEngine = class {
|
|
|
3628
3987
|
}
|
|
3629
3988
|
return false;
|
|
3630
3989
|
}
|
|
3990
|
+
/**
|
|
3991
|
+
* Returns the first match of `target_text` in the raw mapper that is NOT
|
|
3992
|
+
* entirely contained within a tracked deletion (<w:del>). Tracked-deleted
|
|
3993
|
+
* copies are not live, editable text, so an edit must resolve to a live
|
|
3994
|
+
* occurrence even when a dead copy appears earlier in the document
|
|
3995
|
+
* (BUG-23-5). Falls back to the plain first match when no live copy is
|
|
3996
|
+
* found (e.g. fuzzy/normalized matches the span filter cannot align).
|
|
3997
|
+
*/
|
|
3998
|
+
_first_live_match(target_text) {
|
|
3999
|
+
const all = this.mapper.find_all_match_indices(target_text);
|
|
4000
|
+
if (all.length <= 1) {
|
|
4001
|
+
return this.mapper.find_match_index(target_text);
|
|
4002
|
+
}
|
|
4003
|
+
for (const [start, length] of all) {
|
|
4004
|
+
const realSpans = this.mapper.spans.filter(
|
|
4005
|
+
(s) => s.run !== null && s.end > start && s.start < start + length
|
|
4006
|
+
);
|
|
4007
|
+
if (realSpans.length === 0) return [start, length];
|
|
4008
|
+
if (realSpans.some((s) => !s.del_id)) return [start, length];
|
|
4009
|
+
}
|
|
4010
|
+
return this.mapper.find_match_index(target_text);
|
|
4011
|
+
}
|
|
3631
4012
|
_pre_resolve_heuristic_edit(edit) {
|
|
3632
4013
|
if (!edit.target_text) return null;
|
|
3633
|
-
let [start_idx, match_len] = this.
|
|
4014
|
+
let [start_idx, match_len] = this._first_live_match(edit.target_text);
|
|
3634
4015
|
let use_clean_map = false;
|
|
3635
4016
|
if (start_idx === -1) {
|
|
3636
4017
|
if (!this.clean_mapper)
|
|
@@ -3694,7 +4075,7 @@ var RedlineEngine = class {
|
|
|
3694
4075
|
_apply_single_edit_indexed(edit, orig_new, rebuild_map) {
|
|
3695
4076
|
let op = edit._internal_op;
|
|
3696
4077
|
const active_mapper = edit._active_mapper_ref || this.mapper;
|
|
3697
|
-
const start_idx = edit._match_start_index || 0;
|
|
4078
|
+
const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
|
|
3698
4079
|
const length = edit.target_text ? edit.target_text.length : 0;
|
|
3699
4080
|
const del_id = ["DELETION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
|
|
3700
4081
|
const ins_id = ["INSERTION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
|
|
@@ -3743,6 +4124,76 @@ var RedlineEngine = class {
|
|
|
3743
4124
|
rebuild_map
|
|
3744
4125
|
);
|
|
3745
4126
|
if (!anchor_run && !anchor_para) return false;
|
|
4127
|
+
const _bug233_new = edit.new_text || "";
|
|
4128
|
+
const _bug233_trailing_break = /\n\s*$/.test(_bug233_new);
|
|
4129
|
+
let _bug233_target_para = null;
|
|
4130
|
+
{
|
|
4131
|
+
const startingSpans = active_mapper.spans.filter(
|
|
4132
|
+
(s) => s.paragraph !== null && s.start === start_idx
|
|
4133
|
+
);
|
|
4134
|
+
if (startingSpans.length > 0 && startingSpans[0].paragraph) {
|
|
4135
|
+
_bug233_target_para = startingSpans[0].paragraph._element;
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
if (_bug233_trailing_break && _bug233_target_para && _bug233_target_para.parentNode) {
|
|
4139
|
+
const body = _bug233_target_para.parentNode;
|
|
4140
|
+
const xmlDoc = this.doc.part._element.ownerDocument;
|
|
4141
|
+
const lines = _bug233_new.split(/[\r\n]+/).filter((l) => l !== "");
|
|
4142
|
+
let firstNew = null;
|
|
4143
|
+
let lastNew = null;
|
|
4144
|
+
let lastIns = null;
|
|
4145
|
+
for (const raw_line of lines) {
|
|
4146
|
+
const [clean_text, style_name] = this._parse_markdown_style(raw_line);
|
|
4147
|
+
const new_p = xmlDoc.createElement("w:p");
|
|
4148
|
+
if (style_name) {
|
|
4149
|
+
this._set_paragraph_style(new_p, style_name);
|
|
4150
|
+
} else {
|
|
4151
|
+
const existing_pPr = findChild(_bug233_target_para, "w:pPr");
|
|
4152
|
+
if (existing_pPr) new_p.appendChild(existing_pPr.cloneNode(true));
|
|
4153
|
+
}
|
|
4154
|
+
let pPr = findChild(new_p, "w:pPr");
|
|
4155
|
+
if (!pPr) {
|
|
4156
|
+
pPr = xmlDoc.createElement("w:pPr");
|
|
4157
|
+
new_p.insertBefore(pPr, new_p.firstChild);
|
|
4158
|
+
}
|
|
4159
|
+
let rPr = findChild(pPr, "w:rPr");
|
|
4160
|
+
if (!rPr) {
|
|
4161
|
+
rPr = xmlDoc.createElement("w:rPr");
|
|
4162
|
+
pPr.appendChild(rPr);
|
|
4163
|
+
}
|
|
4164
|
+
rPr.appendChild(this._create_track_change_tag("w:ins", "", ins_id));
|
|
4165
|
+
const content_ins = this._build_tracked_ins_for_line(
|
|
4166
|
+
clean_text,
|
|
4167
|
+
anchor_run,
|
|
4168
|
+
ins_id,
|
|
4169
|
+
xmlDoc
|
|
4170
|
+
);
|
|
4171
|
+
if (content_ins) new_p.appendChild(content_ins);
|
|
4172
|
+
body.insertBefore(new_p, _bug233_target_para);
|
|
4173
|
+
if (!firstNew) firstNew = new_p;
|
|
4174
|
+
lastNew = new_p;
|
|
4175
|
+
lastIns = content_ins;
|
|
4176
|
+
}
|
|
4177
|
+
if (firstNew) {
|
|
4178
|
+
if (edit.comment && lastNew && lastIns) {
|
|
4179
|
+
const ascend = (el, p) => {
|
|
4180
|
+
let cur = el;
|
|
4181
|
+
while (cur.parentNode && cur.parentNode !== p)
|
|
4182
|
+
cur = cur.parentNode;
|
|
4183
|
+
return cur;
|
|
4184
|
+
};
|
|
4185
|
+
const startIns = findAllDescendants(firstNew, "w:ins")[0] || firstNew;
|
|
4186
|
+
this._attach_comment_spanning(
|
|
4187
|
+
firstNew,
|
|
4188
|
+
ascend(startIns, firstNew),
|
|
4189
|
+
lastNew,
|
|
4190
|
+
ascend(lastIns, lastNew),
|
|
4191
|
+
edit.comment
|
|
4192
|
+
);
|
|
4193
|
+
}
|
|
4194
|
+
return true;
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
3746
4197
|
const result = this._track_insert_multiline(
|
|
3747
4198
|
edit.new_text || "",
|
|
3748
4199
|
anchor_run,
|
|
@@ -3800,7 +4251,10 @@ var RedlineEngine = class {
|
|
|
3800
4251
|
if (result.first_node.tagName === "w:p") {
|
|
3801
4252
|
first_anchor_target = findAllDescendants(result.first_node, "w:ins")[0] || result.first_node;
|
|
3802
4253
|
}
|
|
3803
|
-
const anchor = ascend_to_paragraph_child(
|
|
4254
|
+
const anchor = ascend_to_paragraph_child(
|
|
4255
|
+
first_anchor_target,
|
|
4256
|
+
host_p
|
|
4257
|
+
);
|
|
3804
4258
|
this._attach_comment(host_p, anchor, anchor, edit.comment);
|
|
3805
4259
|
}
|
|
3806
4260
|
}
|
|
@@ -3812,7 +4266,10 @@ var RedlineEngine = class {
|
|
|
3812
4266
|
length,
|
|
3813
4267
|
rebuild_map
|
|
3814
4268
|
);
|
|
3815
|
-
const virtual_spans = active_mapper.get_virtual_spans_in_range(
|
|
4269
|
+
const virtual_spans = active_mapper.get_virtual_spans_in_range(
|
|
4270
|
+
start_idx,
|
|
4271
|
+
length
|
|
4272
|
+
);
|
|
3816
4273
|
if (target_runs.length === 0 && virtual_spans.length === 0) return false;
|
|
3817
4274
|
const affected_ps = /* @__PURE__ */ new Set();
|
|
3818
4275
|
for (const run of target_runs) {
|
|
@@ -3895,7 +4352,10 @@ var RedlineEngine = class {
|
|
|
3895
4352
|
let pPr = findChild(p1_element, "w:pPr");
|
|
3896
4353
|
if (!pPr) {
|
|
3897
4354
|
pPr = p1_element.ownerDocument.createElement("w:pPr");
|
|
3898
|
-
p1_element.insertBefore(
|
|
4355
|
+
p1_element.insertBefore(
|
|
4356
|
+
pPr,
|
|
4357
|
+
p1_element.firstChild
|
|
4358
|
+
);
|
|
3899
4359
|
}
|
|
3900
4360
|
let rPr = findChild(pPr, "w:rPr");
|
|
3901
4361
|
if (!rPr) {
|
|
@@ -5452,15 +5912,88 @@ function remove_all_comments(doc) {
|
|
|
5452
5912
|
lines.push(` ${status} "${_truncate(info.text || "", 60)}" (${info.author || "Unknown"})`);
|
|
5453
5913
|
cm.deleteComment(cId);
|
|
5454
5914
|
}
|
|
5455
|
-
for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"
|
|
5915
|
+
for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
|
|
5456
5916
|
for (const el of findAllDescendants(doc.element, tag)) {
|
|
5457
5917
|
el.parentNode?.removeChild(el);
|
|
5458
5918
|
}
|
|
5459
5919
|
}
|
|
5920
|
+
const refs = findAllDescendants(doc.element, "w:commentReference");
|
|
5921
|
+
for (const ref of refs) {
|
|
5922
|
+
const parent = ref.parentNode;
|
|
5923
|
+
if (parent) {
|
|
5924
|
+
if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
|
|
5925
|
+
const nonRprChildren = Array.from(parent.childNodes).filter(
|
|
5926
|
+
(c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
|
|
5927
|
+
);
|
|
5928
|
+
if (nonRprChildren.length <= 1) {
|
|
5929
|
+
parent.parentNode?.removeChild(parent);
|
|
5930
|
+
} else {
|
|
5931
|
+
parent.removeChild(ref);
|
|
5932
|
+
}
|
|
5933
|
+
} else {
|
|
5934
|
+
parent.removeChild(ref);
|
|
5935
|
+
}
|
|
5936
|
+
}
|
|
5937
|
+
}
|
|
5460
5938
|
const resolvedCount = Object.values(data).filter((c) => c.resolved).length;
|
|
5461
5939
|
const openCount = Object.values(data).filter((c) => !c.resolved).length;
|
|
5462
5940
|
return [`Comments removed: ${keys.length} (${resolvedCount} resolved, ${openCount} open)`].concat(lines);
|
|
5463
5941
|
}
|
|
5942
|
+
function eject_comment_parts(doc) {
|
|
5943
|
+
const pkg = doc.pkg;
|
|
5944
|
+
const comment_partnames = /* @__PURE__ */ new Set();
|
|
5945
|
+
for (const part of pkg.parts) {
|
|
5946
|
+
if (part.partname.toLowerCase().includes("comments")) {
|
|
5947
|
+
comment_partnames.add(part.partname);
|
|
5948
|
+
const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
|
|
5949
|
+
const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
|
|
5950
|
+
comment_partnames.add(withSlash);
|
|
5951
|
+
comment_partnames.add(withoutSlash);
|
|
5952
|
+
}
|
|
5953
|
+
}
|
|
5954
|
+
if (comment_partnames.size === 0) return;
|
|
5955
|
+
for (const part of pkg.parts) {
|
|
5956
|
+
if (part.partname.endsWith(".rels")) {
|
|
5957
|
+
const rels = findAllDescendants(part._element, "Relationship");
|
|
5958
|
+
const toRemove = [];
|
|
5959
|
+
for (const rel of rels) {
|
|
5960
|
+
const target = rel.getAttribute("Target") || "";
|
|
5961
|
+
if (target.toLowerCase().includes("comments")) {
|
|
5962
|
+
toRemove.push(rel);
|
|
5963
|
+
const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
|
|
5964
|
+
const sourcePart = pkg.getPartByPath(sourcePath);
|
|
5965
|
+
if (sourcePart) {
|
|
5966
|
+
const relId = rel.getAttribute("Id");
|
|
5967
|
+
if (relId) sourcePart.rels.delete(relId);
|
|
5968
|
+
}
|
|
5969
|
+
}
|
|
5970
|
+
}
|
|
5971
|
+
for (const relEl of toRemove) {
|
|
5972
|
+
relEl.parentNode?.removeChild(relEl);
|
|
5973
|
+
}
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5976
|
+
const ctPart = pkg.getPartByPath("[Content_Types].xml");
|
|
5977
|
+
if (ctPart) {
|
|
5978
|
+
const overrides = findAllDescendants(ctPart._element, "Override");
|
|
5979
|
+
const toRemove = [];
|
|
5980
|
+
for (const override of overrides) {
|
|
5981
|
+
const partName = override.getAttribute("PartName") || "";
|
|
5982
|
+
if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
|
|
5983
|
+
toRemove.push(override);
|
|
5984
|
+
}
|
|
5985
|
+
}
|
|
5986
|
+
for (const overrideEl of toRemove) {
|
|
5987
|
+
overrideEl.parentNode?.removeChild(overrideEl);
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5990
|
+
pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
|
|
5991
|
+
for (const key of Object.keys(pkg.unzipped)) {
|
|
5992
|
+
if (key.toLowerCase().includes("comments")) {
|
|
5993
|
+
delete pkg.unzipped[key];
|
|
5994
|
+
}
|
|
5995
|
+
}
|
|
5996
|
+
}
|
|
5464
5997
|
function replace_comment_authors(doc, newAuthor) {
|
|
5465
5998
|
const cm = new CommentsManager(doc);
|
|
5466
5999
|
if (!cm.commentsPart) return [];
|
|
@@ -5649,6 +6182,7 @@ async function finalize_document(doc, options) {
|
|
|
5649
6182
|
const commentsSummary = get_comments_summary(doc);
|
|
5650
6183
|
report.comments_removed = commentsSummary.total;
|
|
5651
6184
|
report.add_transform_lines(remove_all_comments(doc));
|
|
6185
|
+
eject_comment_parts(doc);
|
|
5652
6186
|
} else if (options.sanitize_mode === "keep-markup") {
|
|
5653
6187
|
const counts = count_tracked_changes(doc);
|
|
5654
6188
|
report.tracked_changes_found = counts[0] + counts[1] + counts[2];
|