@adeu/core 1.17.2 → 1.18.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 CHANGED
@@ -3063,8 +3063,19 @@ var RedlineEngine = class {
3063
3063
  this.mapper = new DocumentMapper(this.doc);
3064
3064
  this.comments_manager = new CommentsManager(this.doc);
3065
3065
  }
3066
+ /**
3067
+ * Return a hint when a short, single-token anchor contains punctuation that
3068
+ * can split awkwardly, else null.
3069
+ *
3070
+ * Surface this ONLY for edits that actually failed to match/apply. On a
3071
+ * successful edit the batch report already carries the redline preview, so
3072
+ * emitting this would be a false positive: the punctuation (dates,
3073
+ * `[_name_]` placeholders, `____` blanks) is frequently the literal target
3074
+ * and the edit succeeds despite it. Mirrors the Python engine.
3075
+ */
3066
3076
  _check_punctuation_warning(target_text) {
3067
3077
  if (!target_text) return null;
3078
+ if (target_text.length > 20 || target_text.includes(" ")) return null;
3068
3079
  if (target_text.includes("_") || target_text.includes("-")) {
3069
3080
  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.`;
3070
3081
  }
@@ -3346,6 +3357,93 @@ var RedlineEngine = class {
3346
3357
  }
3347
3358
  }
3348
3359
  }
3360
+ /**
3361
+ * Revert every tracked change, returning the document to the state it had
3362
+ * before any revision was proposed. The exact inverse of
3363
+ * accept_all_revisions:
3364
+ *
3365
+ * - <w:ins> -> removed together with all of its content (the proposed
3366
+ * insertion never existed); an inserted row (<w:ins> in
3367
+ * <w:trPr>) drops the whole row.
3368
+ * - <w:del> -> unwrapped, restoring the original text (<w:delText> becomes
3369
+ * <w:t> again); a row-deletion mark in <w:trPr> is removed so
3370
+ * the row survives.
3371
+ * - paragraph-mark <w:del> in pPr/rPr -> removed, undoing a proposed merge.
3372
+ *
3373
+ * Comments are annotations, not revisions, so standalone comments are left in
3374
+ * place; only anchors stranded inside a rejected insertion are cleaned up.
3375
+ *
3376
+ * Insertions are reverted before deletions are restored so a deletion nested
3377
+ * inside a foreign author's insertion is removed wholesale with the insertion
3378
+ * — the contingent text disappears rather than being promoted to committed
3379
+ * body text.
3380
+ *
3381
+ * Known limitation: tracked paragraph STRUCTURE changes (a split recorded as a
3382
+ * pilcrow <w:ins>, or a merge recorded as a pilcrow <w:del>) are reverted only
3383
+ * to the extent of dropping/keeping the mark; the original paragraph boundary
3384
+ * is not reconstructed, because the merge protocol coalesces paragraphs
3385
+ * destructively at edit time. Reverting run-level insertions/deletions (the
3386
+ * common case) is exact. This limitation is shared with the Python engine.
3387
+ */
3388
+ reject_all_revisions() {
3389
+ const parts_to_process = [this.doc.element];
3390
+ for (const part of this.doc.pkg.parts) {
3391
+ if (part === this.doc.part) continue;
3392
+ if (part.contentType.includes("wordprocessingml") && part.contentType.endsWith("+xml")) {
3393
+ parts_to_process.push(part._element);
3394
+ }
3395
+ }
3396
+ for (const root_element of parts_to_process) {
3397
+ const insNodes = findAllDescendants(root_element, "w:ins");
3398
+ for (const ins of insNodes) {
3399
+ const parent = ins.parentNode;
3400
+ if (!parent) continue;
3401
+ this._clean_wrapping_comments(ins);
3402
+ this._delete_comments_in_element(ins);
3403
+ if (parent.tagName === "w:trPr") {
3404
+ const row = parent.parentNode;
3405
+ if (row && row.parentNode) {
3406
+ row.parentNode.removeChild(row);
3407
+ }
3408
+ } else {
3409
+ parent.removeChild(ins);
3410
+ }
3411
+ }
3412
+ const pNodes = findAllDescendants(root_element, "w:p");
3413
+ for (const p of pNodes) {
3414
+ const pPr = findChild(p, "w:pPr");
3415
+ if (pPr) {
3416
+ const rPr = findChild(pPr, "w:rPr");
3417
+ const delMark = rPr ? findChild(rPr, "w:del") : null;
3418
+ if (rPr && delMark) {
3419
+ rPr.removeChild(delMark);
3420
+ }
3421
+ }
3422
+ }
3423
+ const delNodes = findAllDescendants(root_element, "w:del");
3424
+ for (const d of delNodes) {
3425
+ const parent = d.parentNode;
3426
+ if (!parent) continue;
3427
+ this._clean_wrapping_comments(d);
3428
+ if (parent.tagName === "w:trPr") {
3429
+ parent.removeChild(d);
3430
+ continue;
3431
+ }
3432
+ const delTexts = Array.from(d.getElementsByTagName("w:delText"));
3433
+ for (const dt of delTexts) {
3434
+ const t = d.ownerDocument.createElement("w:t");
3435
+ t.textContent = dt.textContent;
3436
+ if (dt.hasAttribute("xml:space"))
3437
+ t.setAttribute("xml:space", "preserve");
3438
+ dt.parentNode?.replaceChild(t, dt);
3439
+ }
3440
+ while (d.firstChild) {
3441
+ parent.insertBefore(d.firstChild, d);
3442
+ }
3443
+ parent.removeChild(d);
3444
+ }
3445
+ }
3446
+ }
3349
3447
  _getNextId() {
3350
3448
  this.current_id++;
3351
3449
  return this.current_id.toString();
@@ -3993,29 +4091,36 @@ var RedlineEngine = class {
3993
4091
  (edit.new_text || "").length - sfx
3994
4092
  );
3995
4093
  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
- );
4094
+ const balanced = matched.split("\n\n").length === (edit.new_text || "").split("\n\n").length;
4095
+ if (!balanced) {
4096
+ if (final_new.includes("\n\n")) {
4097
+ const parts = matched.split("\n\n");
4098
+ if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
4099
+ errors.push(
4100
+ `- 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.`
4101
+ );
4102
+ }
4103
+ } else {
4104
+ const parts = final_target.split("\n\n");
4105
+ if (parts.length >= 2 && parts[0].trim() !== "" && parts[parts.length - 1].trim() !== "") {
4106
+ errors.push(
4107
+ `- 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.`
4108
+ );
4109
+ }
4009
4110
  }
4010
4111
  }
4011
4112
  }
4012
4113
  }
4013
4114
  for (const [start, length] of matches) {
4014
- const spans = this.mapper.spans.filter(
4115
+ const spans = target_mapper.spans.filter(
4015
4116
  (s) => s.end > start && s.start < start + length
4016
4117
  );
4017
- const nestedAuthors = /* @__PURE__ */ new Set();
4118
+ const insAuthors = /* @__PURE__ */ new Set();
4119
+ const commentAuthors = /* @__PURE__ */ new Set();
4120
+ let hasNonForeignRealText = false;
4018
4121
  for (const s of spans) {
4122
+ if (s.run === null) continue;
4123
+ let isForeignIns = false;
4019
4124
  if (s.ins_id) {
4020
4125
  const insNodes = findAllDescendants(
4021
4126
  this.doc.element,
@@ -4024,24 +4129,32 @@ var RedlineEngine = class {
4024
4129
  if (insNodes.length > 0) {
4025
4130
  const auth = insNodes[0].getAttribute("w:author");
4026
4131
  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
- }
4132
+ insAuthors.add(auth);
4133
+ isForeignIns = true;
4032
4134
  }
4033
4135
  }
4034
4136
  }
4137
+ if (!isForeignIns) hasNonForeignRealText = true;
4138
+ }
4139
+ for (const s of spans) {
4035
4140
  if (s.comment_ids) {
4036
4141
  for (const cid of s.comment_ids) {
4037
4142
  const c_data = this.mapper.comments_map[cid];
4038
4143
  if (c_data && c_data.author && c_data.author !== this.author) {
4039
- nestedAuthors.add(c_data.author);
4144
+ commentAuthors.add(c_data.author);
4040
4145
  }
4041
4146
  }
4042
4147
  }
4043
4148
  }
4044
- if (nestedAuthors.size > 0) {
4149
+ if (insAuthors.size > 0 || commentAuthors.size > 0) {
4150
+ const fullyWithinForeignIns = insAuthors.size > 0 && !hasNonForeignRealText && commentAuthors.size === 0;
4151
+ if ((match_mode === "strict" || match_mode === "first") && fullyWithinForeignIns) {
4152
+ continue;
4153
+ }
4154
+ const nestedAuthors = /* @__PURE__ */ new Set([
4155
+ ...insAuthors,
4156
+ ...commentAuthors
4157
+ ]);
4045
4158
  errors.push(
4046
4159
  `- 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
4160
  );
@@ -4135,59 +4248,6 @@ var RedlineEngine = class {
4135
4248
  const edits = changes.filter(
4136
4249
  (c) => c === null || typeof c !== "object" || !["accept", "reject", "reply"].includes(c.type)
4137
4250
  );
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
4251
  if (!dry_run_mode) {
4192
4252
  const action_errors = actions.length > 0 ? this.validate_review_actions(actions) : [];
4193
4253
  const validate_edits_now = edits.length > 0 && action_errors.length > 0;
@@ -4232,11 +4292,11 @@ var RedlineEngine = class {
4232
4292
  for (let i = 0; i < edits.length; i++) {
4233
4293
  const edit = edits[i];
4234
4294
  const single_errors = this.validate_edits([edit], i);
4235
- const warning = this._check_punctuation_warning(
4236
- edit.target_text || ""
4237
- );
4238
4295
  if (single_errors.length > 0) {
4239
4296
  skipped_edits++;
4297
+ const warning = this._check_punctuation_warning(
4298
+ edit.target_text || ""
4299
+ );
4240
4300
  edits_reports.push({
4241
4301
  status: "failed",
4242
4302
  target_text: edit.target_text || "",
@@ -4256,7 +4316,7 @@ var RedlineEngine = class {
4256
4316
  status: "applied",
4257
4317
  target_text: edit.target_text || "",
4258
4318
  new_text: edit.new_text || "",
4259
- warning,
4319
+ warning: null,
4260
4320
  error: null,
4261
4321
  critic_markup: previews[0],
4262
4322
  clean_text: previews[1],
@@ -4270,6 +4330,9 @@ var RedlineEngine = class {
4270
4330
  } else {
4271
4331
  skipped_edits++;
4272
4332
  const error_msg = this.skipped_details.length > 0 ? this.skipped_details[this.skipped_details.length - 1] : "Failed to apply edit";
4333
+ const warning = this._check_punctuation_warning(
4334
+ edit.target_text || ""
4335
+ );
4273
4336
  edits_reports.push({
4274
4337
  status: "failed",
4275
4338
  target_text: edit.target_text || "",
@@ -4315,15 +4378,17 @@ var RedlineEngine = class {
4315
4378
  for (const edit of edits) {
4316
4379
  const success = edit._applied_status || false;
4317
4380
  const error_msg = edit._error_msg || null;
4318
- const warning = this._check_punctuation_warning(
4319
- edit.target_text || ""
4320
- );
4321
4381
  let critic_markup = null;
4322
4382
  let clean_text = null;
4383
+ let warning = null;
4323
4384
  if (success) {
4324
4385
  const previews = this._build_edit_context_previews(edit);
4325
4386
  critic_markup = previews[0];
4326
4387
  clean_text = previews[1];
4388
+ } else {
4389
+ warning = this._check_punctuation_warning(
4390
+ edit.target_text || ""
4391
+ );
4327
4392
  }
4328
4393
  edits_reports.push({
4329
4394
  status: success ? "applied" : "failed",
@@ -4431,6 +4496,7 @@ var RedlineEngine = class {
4431
4496
  (a, b) => (b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0)
4432
4497
  );
4433
4498
  const occupied_ranges = [];
4499
+ const counted_split_groups = /* @__PURE__ */ new Set();
4434
4500
  for (const [edit, orig_new] of resolved_edits) {
4435
4501
  const start = edit._resolved_start_idx || 0;
4436
4502
  const end = start + (edit.target_text ? edit.target_text.length : 0);
@@ -4459,13 +4525,20 @@ var RedlineEngine = class {
4459
4525
  success = this._apply_table_edit(edit, false);
4460
4526
  }
4461
4527
  if (success) {
4462
- applied++;
4528
+ const group_id = edit._split_group_id;
4529
+ const first_in_group = group_id === void 0 || group_id === null || !counted_split_groups.has(group_id);
4530
+ if (first_in_group && group_id !== void 0 && group_id !== null) {
4531
+ counted_split_groups.add(group_id);
4532
+ }
4533
+ if (first_in_group) applied++;
4463
4534
  occupied_ranges.push([start, end]);
4464
4535
  edit._applied_status = true;
4465
4536
  const parent = edit._parent_edit_ref;
4466
4537
  if (parent) {
4467
4538
  parent._applied_status = true;
4468
- parent._occurrences_modified = (parent._occurrences_modified || 0) + 1;
4539
+ if (first_in_group) {
4540
+ parent._occurrences_modified = (parent._occurrences_modified || 0) + 1;
4541
+ }
4469
4542
  const [path, page] = this._get_heading_path_and_page(
4470
4543
  start,
4471
4544
  this.mapper.full_text,
@@ -4476,7 +4549,9 @@ var RedlineEngine = class {
4476
4549
  parent._pages = pages;
4477
4550
  parent._heading_path = path;
4478
4551
  } else {
4479
- edit._occurrences_modified = (edit._occurrences_modified || 0) + 1;
4552
+ if (first_in_group) {
4553
+ edit._occurrences_modified = (edit._occurrences_modified || 0) + 1;
4554
+ }
4480
4555
  const [path, page] = this._get_heading_path_and_page(
4481
4556
  start,
4482
4557
  this.mapper.full_text,
@@ -4773,6 +4848,51 @@ var RedlineEngine = class {
4773
4848
  final_target = actual_doc_text.substring(prefix_len, t_end);
4774
4849
  final_new = current_effective_new_text.substring(prefix_len, n_end);
4775
4850
  effective_start_idx = start_idx + prefix_len;
4851
+ const target_segs = actual_doc_text.split("\n\n");
4852
+ const new_segs = current_effective_new_text.split("\n\n");
4853
+ if (actual_doc_text.includes("\n\n") && target_segs.length === new_segs.length) {
4854
+ const split_sub_edits = [];
4855
+ let seg_offset = start_idx;
4856
+ let comment_assigned = false;
4857
+ for (let k = 0; k < target_segs.length; k++) {
4858
+ const t_seg = target_segs[k];
4859
+ const n_seg = new_segs[k];
4860
+ if (t_seg !== n_seg) {
4861
+ const [seg_prefix, seg_suffix] = trim_common_context(t_seg, n_seg);
4862
+ const seg_target = t_seg.substring(
4863
+ seg_prefix,
4864
+ t_seg.length - seg_suffix
4865
+ );
4866
+ const seg_new = n_seg.substring(
4867
+ seg_prefix,
4868
+ n_seg.length - seg_suffix
4869
+ );
4870
+ const seg_start = seg_offset + seg_prefix;
4871
+ let seg_op;
4872
+ if (!seg_target && seg_new) seg_op = "INSERTION";
4873
+ else if (seg_target && !seg_new) seg_op = "DELETION";
4874
+ else if (seg_target && seg_new) seg_op = "MODIFICATION";
4875
+ else seg_op = "COMMENT_ONLY";
4876
+ const seg_comment = edit.comment && !comment_assigned ? edit.comment : null;
4877
+ if (seg_comment) comment_assigned = true;
4878
+ split_sub_edits.push({
4879
+ type: "modify",
4880
+ target_text: seg_target,
4881
+ new_text: seg_new,
4882
+ comment: seg_comment,
4883
+ _match_start_index: seg_start,
4884
+ _internal_op: seg_op,
4885
+ _active_mapper_ref: active_mapper,
4886
+ _split_group_id: start_idx
4887
+ });
4888
+ }
4889
+ seg_offset += t_seg.length + 2;
4890
+ }
4891
+ if (split_sub_edits.length > 0) {
4892
+ for (const sub of split_sub_edits) all_sub_edits.push(sub);
4893
+ continue;
4894
+ }
4895
+ }
4776
4896
  if (!final_target && final_new) effective_op = "INSERTION";
4777
4897
  else if (final_target && !final_new) effective_op = "DELETION";
4778
4898
  else if (final_target && final_new) effective_op = "MODIFICATION";
@@ -4792,6 +4912,35 @@ var RedlineEngine = class {
4792
4912
  if (match_mode === "all" || all_sub_edits.length > 1) return all_sub_edits;
4793
4913
  return all_sub_edits[0];
4794
4914
  }
4915
+ /**
4916
+ * Split a <w:ins> so that everything up to and INCLUDING split_after stays in
4917
+ * a left <w:ins>, new_elem is placed between, and the remainder moves to a
4918
+ * right <w:ins> — all at the grandparent level. Used when revising another
4919
+ * author's pending insertion: the <w:del> stays nested in their <w:ins> while
4920
+ * our replacement <w:ins> lands as a sibling, so we never nest <w:ins> in
4921
+ * <w:ins>.
4922
+ */
4923
+ _insert_and_split_ins(parent_ins, split_after, new_elem) {
4924
+ const grandparent = parent_ins.parentNode;
4925
+ if (!grandparent) return;
4926
+ const left = parent_ins.cloneNode(false);
4927
+ const right = parent_ins.cloneNode(false);
4928
+ let toRight = false;
4929
+ for (const kid of Array.from(parent_ins.childNodes)) {
4930
+ parent_ins.removeChild(kid);
4931
+ if (!toRight) {
4932
+ left.appendChild(kid);
4933
+ if (kid === split_after) toRight = true;
4934
+ } else {
4935
+ right.appendChild(kid);
4936
+ }
4937
+ }
4938
+ if (left.childNodes.length > 0) grandparent.insertBefore(left, parent_ins);
4939
+ grandparent.insertBefore(new_elem, parent_ins);
4940
+ if (right.childNodes.length > 0)
4941
+ grandparent.insertBefore(right, parent_ins);
4942
+ grandparent.removeChild(parent_ins);
4943
+ }
4795
4944
  _apply_single_edit_indexed(edit, orig_new, rebuild_map) {
4796
4945
  let op = edit._internal_op;
4797
4946
  const active_mapper = edit._active_mapper_ref || this.mapper;
@@ -4999,7 +5148,16 @@ var RedlineEngine = class {
4999
5148
  const is_inline_first = result.first_node.tagName === "w:ins";
5000
5149
  if (is_inline_first) {
5001
5150
  if (anchor_run) {
5002
- insertAfter(result.first_node, anchor_run._element);
5151
+ const anchor_parent = anchor_run._element.parentNode;
5152
+ if (anchor_parent && anchor_parent.tagName === "w:ins") {
5153
+ this._insert_and_split_ins(
5154
+ anchor_parent,
5155
+ anchor_run._element,
5156
+ result.first_node
5157
+ );
5158
+ } else {
5159
+ insertAfter(result.first_node, anchor_run._element);
5160
+ }
5003
5161
  } else if (anchor_para) {
5004
5162
  anchor_para._element.appendChild(result.first_node);
5005
5163
  }
@@ -5109,7 +5267,12 @@ var RedlineEngine = class {
5109
5267
  if (result.first_node) {
5110
5268
  const is_inline_first = result.first_node.tagName === "w:ins";
5111
5269
  if (is_inline_first) {
5112
- insertAfter(result.first_node, last_del);
5270
+ const del_parent = last_del.parentNode;
5271
+ if (del_parent && del_parent.tagName === "w:ins") {
5272
+ this._insert_and_split_ins(del_parent, last_del, result.first_node);
5273
+ } else {
5274
+ insertAfter(result.first_node, last_del);
5275
+ }
5113
5276
  ins_elem = result.first_node;
5114
5277
  } else {
5115
5278
  ins_elem = result.last_ins;