@adeu/core 1.17.2 → 1.18.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 CHANGED
@@ -3346,6 +3346,93 @@ var RedlineEngine = class {
3346
3346
  }
3347
3347
  }
3348
3348
  }
3349
+ /**
3350
+ * Revert every tracked change, returning the document to the state it had
3351
+ * before any revision was proposed. The exact inverse of
3352
+ * accept_all_revisions:
3353
+ *
3354
+ * - <w:ins> -> removed together with all of its content (the proposed
3355
+ * insertion never existed); an inserted row (<w:ins> in
3356
+ * <w:trPr>) drops the whole row.
3357
+ * - <w:del> -> unwrapped, restoring the original text (<w:delText> becomes
3358
+ * <w:t> again); a row-deletion mark in <w:trPr> is removed so
3359
+ * the row survives.
3360
+ * - paragraph-mark <w:del> in pPr/rPr -> removed, undoing a proposed merge.
3361
+ *
3362
+ * Comments are annotations, not revisions, so standalone comments are left in
3363
+ * place; only anchors stranded inside a rejected insertion are cleaned up.
3364
+ *
3365
+ * Insertions are reverted before deletions are restored so a deletion nested
3366
+ * inside a foreign author's insertion is removed wholesale with the insertion
3367
+ * — the contingent text disappears rather than being promoted to committed
3368
+ * body text.
3369
+ *
3370
+ * Known limitation: tracked paragraph STRUCTURE changes (a split recorded as a
3371
+ * pilcrow <w:ins>, or a merge recorded as a pilcrow <w:del>) are reverted only
3372
+ * to the extent of dropping/keeping the mark; the original paragraph boundary
3373
+ * is not reconstructed, because the merge protocol coalesces paragraphs
3374
+ * destructively at edit time. Reverting run-level insertions/deletions (the
3375
+ * common case) is exact. This limitation is shared with the Python engine.
3376
+ */
3377
+ reject_all_revisions() {
3378
+ const parts_to_process = [this.doc.element];
3379
+ for (const part of this.doc.pkg.parts) {
3380
+ if (part === this.doc.part) continue;
3381
+ if (part.contentType.includes("wordprocessingml") && part.contentType.endsWith("+xml")) {
3382
+ parts_to_process.push(part._element);
3383
+ }
3384
+ }
3385
+ for (const root_element of parts_to_process) {
3386
+ const insNodes = findAllDescendants(root_element, "w:ins");
3387
+ for (const ins of insNodes) {
3388
+ const parent = ins.parentNode;
3389
+ if (!parent) continue;
3390
+ this._clean_wrapping_comments(ins);
3391
+ this._delete_comments_in_element(ins);
3392
+ if (parent.tagName === "w:trPr") {
3393
+ const row = parent.parentNode;
3394
+ if (row && row.parentNode) {
3395
+ row.parentNode.removeChild(row);
3396
+ }
3397
+ } else {
3398
+ parent.removeChild(ins);
3399
+ }
3400
+ }
3401
+ const pNodes = findAllDescendants(root_element, "w:p");
3402
+ for (const p of pNodes) {
3403
+ const pPr = findChild(p, "w:pPr");
3404
+ if (pPr) {
3405
+ const rPr = findChild(pPr, "w:rPr");
3406
+ const delMark = rPr ? findChild(rPr, "w:del") : null;
3407
+ if (rPr && delMark) {
3408
+ rPr.removeChild(delMark);
3409
+ }
3410
+ }
3411
+ }
3412
+ const delNodes = findAllDescendants(root_element, "w:del");
3413
+ for (const d of delNodes) {
3414
+ const parent = d.parentNode;
3415
+ if (!parent) continue;
3416
+ this._clean_wrapping_comments(d);
3417
+ if (parent.tagName === "w:trPr") {
3418
+ parent.removeChild(d);
3419
+ continue;
3420
+ }
3421
+ const delTexts = Array.from(d.getElementsByTagName("w:delText"));
3422
+ for (const dt of delTexts) {
3423
+ const t = d.ownerDocument.createElement("w:t");
3424
+ t.textContent = dt.textContent;
3425
+ if (dt.hasAttribute("xml:space"))
3426
+ t.setAttribute("xml:space", "preserve");
3427
+ dt.parentNode?.replaceChild(t, dt);
3428
+ }
3429
+ while (d.firstChild) {
3430
+ parent.insertBefore(d.firstChild, d);
3431
+ }
3432
+ parent.removeChild(d);
3433
+ }
3434
+ }
3435
+ }
3349
3436
  _getNextId() {
3350
3437
  this.current_id++;
3351
3438
  return this.current_id.toString();
@@ -3993,29 +4080,36 @@ var RedlineEngine = class {
3993
4080
  (edit.new_text || "").length - sfx
3994
4081
  );
3995
4082
  if (final_target.includes("\n\n")) {
3996
- if (final_new.includes("\n\n")) {
3997
- const parts = matched.split("\n\n");
3998
- if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
3999
- errors.push(
4000
- `- Edit ${i + 1 + index_offset} 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.`
4001
- );
4002
- }
4003
- } else {
4004
- const parts = final_target.split("\n\n");
4005
- if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
4006
- errors.push(
4007
- `- Edit ${i + 1 + index_offset} 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.`
4008
- );
4083
+ const balanced = matched.split("\n\n").length === (edit.new_text || "").split("\n\n").length;
4084
+ if (!balanced) {
4085
+ if (final_new.includes("\n\n")) {
4086
+ const parts = matched.split("\n\n");
4087
+ if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
4088
+ errors.push(
4089
+ `- Edit ${i + 1 + index_offset} 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.`
4090
+ );
4091
+ }
4092
+ } else {
4093
+ const parts = final_target.split("\n\n");
4094
+ if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
4095
+ errors.push(
4096
+ `- Edit ${i + 1 + index_offset} 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.`
4097
+ );
4098
+ }
4009
4099
  }
4010
4100
  }
4011
4101
  }
4012
4102
  }
4013
4103
  for (const [start, length] of matches) {
4014
- const spans = this.mapper.spans.filter(
4104
+ const spans = target_mapper.spans.filter(
4015
4105
  (s) => s.end > start && s.start < start + length
4016
4106
  );
4017
- const nestedAuthors = /* @__PURE__ */ new Set();
4107
+ const insAuthors = /* @__PURE__ */ new Set();
4108
+ const commentAuthors = /* @__PURE__ */ new Set();
4109
+ let hasNonForeignRealText = false;
4018
4110
  for (const s of spans) {
4111
+ if (s.run === null) continue;
4112
+ let isForeignIns = false;
4019
4113
  if (s.ins_id) {
4020
4114
  const insNodes = findAllDescendants(
4021
4115
  this.doc.element,
@@ -4024,24 +4118,32 @@ var RedlineEngine = class {
4024
4118
  if (insNodes.length > 0) {
4025
4119
  const auth = insNodes[0].getAttribute("w:author");
4026
4120
  if (auth && auth !== this.author) {
4027
- const is_fully_contained_in_ins = start >= s.start && start + length <= s.end;
4028
- const is_lockout = is_fully_contained_in_ins || match_mode === "all";
4029
- if (is_lockout) {
4030
- nestedAuthors.add(auth);
4031
- }
4121
+ insAuthors.add(auth);
4122
+ isForeignIns = true;
4032
4123
  }
4033
4124
  }
4034
4125
  }
4126
+ if (!isForeignIns) hasNonForeignRealText = true;
4127
+ }
4128
+ for (const s of spans) {
4035
4129
  if (s.comment_ids) {
4036
4130
  for (const cid of s.comment_ids) {
4037
4131
  const c_data = this.mapper.comments_map[cid];
4038
4132
  if (c_data && c_data.author && c_data.author !== this.author) {
4039
- nestedAuthors.add(c_data.author);
4133
+ commentAuthors.add(c_data.author);
4040
4134
  }
4041
4135
  }
4042
4136
  }
4043
4137
  }
4044
- if (nestedAuthors.size > 0) {
4138
+ if (insAuthors.size > 0 || commentAuthors.size > 0) {
4139
+ const fullyWithinForeignIns = insAuthors.size > 0 && !hasNonForeignRealText && commentAuthors.size === 0;
4140
+ if ((match_mode === "strict" || match_mode === "first") && fullyWithinForeignIns) {
4141
+ continue;
4142
+ }
4143
+ const nestedAuthors = /* @__PURE__ */ new Set([
4144
+ ...insAuthors,
4145
+ ...commentAuthors
4146
+ ]);
4045
4147
  errors.push(
4046
4148
  `- Edit ${i + 1 + index_offset} Failed: Modification targets an active insertion from another author (${Array.from(nestedAuthors).join(", ")}). Accept that change first or scope your edit outside of it.`
4047
4149
  );
@@ -4135,59 +4237,6 @@ var RedlineEngine = class {
4135
4237
  const edits = changes.filter(
4136
4238
  (c) => c === null || typeof c !== "object" || !["accept", "reject", "reply"].includes(c.type)
4137
4239
  );
4138
- let mapper_dirty = false;
4139
- for (const edit of edits) {
4140
- if (typeof edit !== "object" || edit === null || !edit.target_text) continue;
4141
- const is_regex = edit.regex || false;
4142
- let matches = this.mapper.find_all_match_indices(edit.target_text, is_regex);
4143
- let target_mapper = this.mapper;
4144
- if (matches.length === 0) {
4145
- if (!this.clean_mapper) {
4146
- this.clean_mapper = new DocumentMapper(this.doc, true);
4147
- }
4148
- matches = this.clean_mapper.find_all_match_indices(edit.target_text, is_regex);
4149
- target_mapper = this.clean_mapper;
4150
- }
4151
- for (const [start, length] of matches) {
4152
- const spans = target_mapper.spans.filter(
4153
- (s) => s.end > start && s.start < start + length
4154
- );
4155
- for (const s of spans) {
4156
- if (s.ins_id) {
4157
- const insNodes = findAllDescendants(this.doc.element, "w:ins").filter(
4158
- (n) => n.getAttribute("w:id") === s.ins_id
4159
- );
4160
- if (insNodes.length > 0) {
4161
- const auth = insNodes[0].getAttribute("w:author");
4162
- if (auth && auth !== this.author) {
4163
- const is_fully_contained_in_ins = start >= s.start && start + length <= s.end;
4164
- const match_mode = edit.match_mode || "strict";
4165
- if (!is_fully_contained_in_ins && match_mode !== "all") {
4166
- const node = insNodes[0];
4167
- this._clean_wrapping_comments(node);
4168
- const parent = node.parentNode;
4169
- if (parent) {
4170
- if (parent.tagName === "w:trPr") {
4171
- parent.removeChild(node);
4172
- } else {
4173
- while (node.firstChild) {
4174
- parent.insertBefore(node.firstChild, node);
4175
- }
4176
- parent.removeChild(node);
4177
- }
4178
- }
4179
- mapper_dirty = true;
4180
- }
4181
- }
4182
- }
4183
- }
4184
- }
4185
- }
4186
- }
4187
- if (mapper_dirty) {
4188
- this.mapper = new DocumentMapper(this.doc);
4189
- this.clean_mapper = null;
4190
- }
4191
4240
  if (!dry_run_mode) {
4192
4241
  const action_errors = actions.length > 0 ? this.validate_review_actions(actions) : [];
4193
4242
  const validate_edits_now = edits.length > 0 && action_errors.length > 0;
@@ -4431,6 +4480,7 @@ var RedlineEngine = class {
4431
4480
  (a, b) => (b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0)
4432
4481
  );
4433
4482
  const occupied_ranges = [];
4483
+ const counted_split_groups = /* @__PURE__ */ new Set();
4434
4484
  for (const [edit, orig_new] of resolved_edits) {
4435
4485
  const start = edit._resolved_start_idx || 0;
4436
4486
  const end = start + (edit.target_text ? edit.target_text.length : 0);
@@ -4459,13 +4509,20 @@ var RedlineEngine = class {
4459
4509
  success = this._apply_table_edit(edit, false);
4460
4510
  }
4461
4511
  if (success) {
4462
- applied++;
4512
+ const group_id = edit._split_group_id;
4513
+ const first_in_group = group_id === void 0 || group_id === null || !counted_split_groups.has(group_id);
4514
+ if (first_in_group && group_id !== void 0 && group_id !== null) {
4515
+ counted_split_groups.add(group_id);
4516
+ }
4517
+ if (first_in_group) applied++;
4463
4518
  occupied_ranges.push([start, end]);
4464
4519
  edit._applied_status = true;
4465
4520
  const parent = edit._parent_edit_ref;
4466
4521
  if (parent) {
4467
4522
  parent._applied_status = true;
4468
- parent._occurrences_modified = (parent._occurrences_modified || 0) + 1;
4523
+ if (first_in_group) {
4524
+ parent._occurrences_modified = (parent._occurrences_modified || 0) + 1;
4525
+ }
4469
4526
  const [path, page] = this._get_heading_path_and_page(
4470
4527
  start,
4471
4528
  this.mapper.full_text,
@@ -4476,7 +4533,9 @@ var RedlineEngine = class {
4476
4533
  parent._pages = pages;
4477
4534
  parent._heading_path = path;
4478
4535
  } else {
4479
- edit._occurrences_modified = (edit._occurrences_modified || 0) + 1;
4536
+ if (first_in_group) {
4537
+ edit._occurrences_modified = (edit._occurrences_modified || 0) + 1;
4538
+ }
4480
4539
  const [path, page] = this._get_heading_path_and_page(
4481
4540
  start,
4482
4541
  this.mapper.full_text,
@@ -4773,6 +4832,51 @@ var RedlineEngine = class {
4773
4832
  final_target = actual_doc_text.substring(prefix_len, t_end);
4774
4833
  final_new = current_effective_new_text.substring(prefix_len, n_end);
4775
4834
  effective_start_idx = start_idx + prefix_len;
4835
+ const target_segs = actual_doc_text.split("\n\n");
4836
+ const new_segs = current_effective_new_text.split("\n\n");
4837
+ if (actual_doc_text.includes("\n\n") && target_segs.length === new_segs.length) {
4838
+ const split_sub_edits = [];
4839
+ let seg_offset = start_idx;
4840
+ let comment_assigned = false;
4841
+ for (let k = 0; k < target_segs.length; k++) {
4842
+ const t_seg = target_segs[k];
4843
+ const n_seg = new_segs[k];
4844
+ if (t_seg !== n_seg) {
4845
+ const [seg_prefix, seg_suffix] = trim_common_context(t_seg, n_seg);
4846
+ const seg_target = t_seg.substring(
4847
+ seg_prefix,
4848
+ t_seg.length - seg_suffix
4849
+ );
4850
+ const seg_new = n_seg.substring(
4851
+ seg_prefix,
4852
+ n_seg.length - seg_suffix
4853
+ );
4854
+ const seg_start = seg_offset + seg_prefix;
4855
+ let seg_op;
4856
+ if (!seg_target && seg_new) seg_op = "INSERTION";
4857
+ else if (seg_target && !seg_new) seg_op = "DELETION";
4858
+ else if (seg_target && seg_new) seg_op = "MODIFICATION";
4859
+ else seg_op = "COMMENT_ONLY";
4860
+ const seg_comment = edit.comment && !comment_assigned ? edit.comment : null;
4861
+ if (seg_comment) comment_assigned = true;
4862
+ split_sub_edits.push({
4863
+ type: "modify",
4864
+ target_text: seg_target,
4865
+ new_text: seg_new,
4866
+ comment: seg_comment,
4867
+ _match_start_index: seg_start,
4868
+ _internal_op: seg_op,
4869
+ _active_mapper_ref: active_mapper,
4870
+ _split_group_id: start_idx
4871
+ });
4872
+ }
4873
+ seg_offset += t_seg.length + 2;
4874
+ }
4875
+ if (split_sub_edits.length > 0) {
4876
+ for (const sub of split_sub_edits) all_sub_edits.push(sub);
4877
+ continue;
4878
+ }
4879
+ }
4776
4880
  if (!final_target && final_new) effective_op = "INSERTION";
4777
4881
  else if (final_target && !final_new) effective_op = "DELETION";
4778
4882
  else if (final_target && final_new) effective_op = "MODIFICATION";
@@ -4792,6 +4896,35 @@ var RedlineEngine = class {
4792
4896
  if (match_mode === "all" || all_sub_edits.length > 1) return all_sub_edits;
4793
4897
  return all_sub_edits[0];
4794
4898
  }
4899
+ /**
4900
+ * Split a <w:ins> so that everything up to and INCLUDING split_after stays in
4901
+ * a left <w:ins>, new_elem is placed between, and the remainder moves to a
4902
+ * right <w:ins> — all at the grandparent level. Used when revising another
4903
+ * author's pending insertion: the <w:del> stays nested in their <w:ins> while
4904
+ * our replacement <w:ins> lands as a sibling, so we never nest <w:ins> in
4905
+ * <w:ins>.
4906
+ */
4907
+ _insert_and_split_ins(parent_ins, split_after, new_elem) {
4908
+ const grandparent = parent_ins.parentNode;
4909
+ if (!grandparent) return;
4910
+ const left = parent_ins.cloneNode(false);
4911
+ const right = parent_ins.cloneNode(false);
4912
+ let toRight = false;
4913
+ for (const kid of Array.from(parent_ins.childNodes)) {
4914
+ parent_ins.removeChild(kid);
4915
+ if (!toRight) {
4916
+ left.appendChild(kid);
4917
+ if (kid === split_after) toRight = true;
4918
+ } else {
4919
+ right.appendChild(kid);
4920
+ }
4921
+ }
4922
+ if (left.childNodes.length > 0) grandparent.insertBefore(left, parent_ins);
4923
+ grandparent.insertBefore(new_elem, parent_ins);
4924
+ if (right.childNodes.length > 0)
4925
+ grandparent.insertBefore(right, parent_ins);
4926
+ grandparent.removeChild(parent_ins);
4927
+ }
4795
4928
  _apply_single_edit_indexed(edit, orig_new, rebuild_map) {
4796
4929
  let op = edit._internal_op;
4797
4930
  const active_mapper = edit._active_mapper_ref || this.mapper;
@@ -4999,7 +5132,16 @@ var RedlineEngine = class {
4999
5132
  const is_inline_first = result.first_node.tagName === "w:ins";
5000
5133
  if (is_inline_first) {
5001
5134
  if (anchor_run) {
5002
- insertAfter(result.first_node, anchor_run._element);
5135
+ const anchor_parent = anchor_run._element.parentNode;
5136
+ if (anchor_parent && anchor_parent.tagName === "w:ins") {
5137
+ this._insert_and_split_ins(
5138
+ anchor_parent,
5139
+ anchor_run._element,
5140
+ result.first_node
5141
+ );
5142
+ } else {
5143
+ insertAfter(result.first_node, anchor_run._element);
5144
+ }
5003
5145
  } else if (anchor_para) {
5004
5146
  anchor_para._element.appendChild(result.first_node);
5005
5147
  }
@@ -5109,7 +5251,12 @@ var RedlineEngine = class {
5109
5251
  if (result.first_node) {
5110
5252
  const is_inline_first = result.first_node.tagName === "w:ins";
5111
5253
  if (is_inline_first) {
5112
- insertAfter(result.first_node, last_del);
5254
+ const del_parent = last_del.parentNode;
5255
+ if (del_parent && del_parent.tagName === "w:ins") {
5256
+ this._insert_and_split_ins(del_parent, last_del, result.first_node);
5257
+ } else {
5258
+ insertAfter(result.first_node, last_del);
5259
+ }
5113
5260
  ins_elem = result.first_node;
5114
5261
  } else {
5115
5262
  ins_elem = result.last_ins;