@adeu/core 1.19.1 → 1.21.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
@@ -1264,6 +1264,7 @@ function* iter_paragraph_content(paragraph) {
1264
1264
  }
1265
1265
 
1266
1266
  // src/mapper.ts
1267
+ var STYLE_MARKER_TEXTS = /* @__PURE__ */ new Set(["**", "__", "*", "_"]);
1267
1268
  var DocumentMapper = class {
1268
1269
  doc;
1269
1270
  clean_view;
@@ -1273,6 +1274,7 @@ var DocumentMapper = class {
1273
1274
  spans = [];
1274
1275
  appendix_start_index = -1;
1275
1276
  _text_chunks = [];
1277
+ _plain_projection = null;
1276
1278
  constructor(doc, clean_view = false, original_view = false) {
1277
1279
  this.doc = doc;
1278
1280
  this.clean_view = clean_view;
@@ -1285,6 +1287,7 @@ var DocumentMapper = class {
1285
1287
  this.spans = [];
1286
1288
  this._text_chunks = [];
1287
1289
  this.full_text = "";
1290
+ this._plain_projection = null;
1288
1291
  for (const part of iter_document_parts(this.doc)) {
1289
1292
  current_offset = this._map_blocks(part, current_offset);
1290
1293
  if (this.spans.length > 0 && this.spans[this.spans.length - 1].text !== "\n\n") {
@@ -1804,6 +1807,64 @@ ${header}`;
1804
1807
  if (remaining) parts.push(escapeRegExp(remaining));
1805
1808
  return parts.join("");
1806
1809
  }
1810
+ /**
1811
+ * Returns [plain_text, offset_map] where plain_text is full_text with the
1812
+ * VIRTUAL markdown style delimiters (bold/italic markers emitted around
1813
+ * formatted runs) removed, and offset_map[i] is the full_text index of
1814
+ * plain_text[i].
1815
+ *
1816
+ * Formatting run boundaries can fall mid-word (e.g. a paragraph projected
1817
+ * as "**Al**pha"), where neither exact matching nor the whitespace-anchored
1818
+ * fuzzy regex can find the plain target "Alpha". Matching against this
1819
+ * projection and mapping the span back to full_text closes that gap (QA H2).
1820
+ *
1821
+ * Built lazily and invalidated by _build_map(): most batches never need it.
1822
+ */
1823
+ _get_plain_projection() {
1824
+ if (this._plain_projection === null) {
1825
+ const chunks = [];
1826
+ const offsets = [];
1827
+ for (const s of this.spans) {
1828
+ if (s.run === null && s.paragraph !== null && STYLE_MARKER_TEXTS.has(s.text)) {
1829
+ continue;
1830
+ }
1831
+ chunks.push(s.text);
1832
+ for (let k = s.start; k < s.end; k++) offsets.push(k);
1833
+ }
1834
+ this._plain_projection = [chunks.join(""), offsets];
1835
+ }
1836
+ return this._plain_projection;
1837
+ }
1838
+ /**
1839
+ * Matches a markdown-stripped target against the plain projection and maps
1840
+ * each hit back to a [start, length] span in full_text. Interior style
1841
+ * markers end up inside the returned span (so "Alpha" over "**Al**pha"
1842
+ * resolves to the "Al**pha" range); markers just outside the matched
1843
+ * characters are excluded.
1844
+ */
1845
+ _find_plain_projection_matches(target_text) {
1846
+ const [plain_text, offsets] = this._get_plain_projection();
1847
+ if (plain_text.length === this.full_text.length) {
1848
+ return [];
1849
+ }
1850
+ const norm_target = this._replace_smart_quotes(
1851
+ this._strip_markdown_formatting(target_text)
1852
+ );
1853
+ if (!norm_target) return [];
1854
+ const norm_plain = this._replace_smart_quotes(plain_text);
1855
+ const results = [];
1856
+ let from = 0;
1857
+ while (true) {
1858
+ const p_start = norm_plain.indexOf(norm_target, from);
1859
+ if (p_start === -1) break;
1860
+ const p_end = p_start + norm_target.length;
1861
+ const raw_start = offsets[p_start];
1862
+ const raw_end = offsets[p_end - 1] + 1;
1863
+ results.push([raw_start, raw_end - raw_start]);
1864
+ from = p_end;
1865
+ }
1866
+ return results;
1867
+ }
1807
1868
  find_match_index(target_text, is_regex = false) {
1808
1869
  if (is_regex) {
1809
1870
  try {
@@ -1825,6 +1886,8 @@ ${header}`;
1825
1886
  start_idx = this.full_text.indexOf(stripped_target);
1826
1887
  return [start_idx, stripped_target.length];
1827
1888
  }
1889
+ const plain_first = this._find_plain_projection_matches(target_text);
1890
+ if (plain_first.length > 0) return plain_first[0];
1828
1891
  try {
1829
1892
  const pattern = new RegExp(this._make_fuzzy_regex(target_text));
1830
1893
  const match = pattern.exec(this.full_text);
@@ -1845,29 +1908,33 @@ ${header}`;
1845
1908
  }
1846
1909
  return [];
1847
1910
  }
1848
- const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1849
- let matches = [
1850
- ...this.full_text.matchAll(new RegExp(escapeRegExp(target_text), "g"))
1851
- ];
1852
- if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1911
+ const findAllLiteral = (haystack, needle) => {
1912
+ const out = [];
1913
+ if (!needle) return out;
1914
+ let from = 0;
1915
+ while (true) {
1916
+ const idx = haystack.indexOf(needle, from);
1917
+ if (idx === -1) break;
1918
+ out.push([idx, needle.length]);
1919
+ from = idx + needle.length;
1920
+ }
1921
+ return out;
1922
+ };
1923
+ let matches = findAllLiteral(this.full_text, target_text);
1924
+ if (matches.length > 0) return matches;
1853
1925
  const norm_full = this._replace_smart_quotes(this.full_text);
1854
1926
  const norm_target = this._replace_smart_quotes(target_text);
1855
- matches = [
1856
- ...norm_full.matchAll(new RegExp(escapeRegExp(norm_target), "g"))
1857
- ];
1858
- if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1927
+ matches = findAllLiteral(norm_full, norm_target);
1928
+ if (matches.length > 0) return matches;
1859
1929
  const stripped_target = this._strip_markdown_formatting(target_text);
1860
- matches = [
1861
- ...this.full_text.matchAll(
1862
- new RegExp(escapeRegExp(stripped_target), "g")
1863
- )
1864
- ];
1865
- if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1930
+ matches = findAllLiteral(this.full_text, stripped_target);
1931
+ if (matches.length > 0) return matches;
1932
+ const plain_matches = this._find_plain_projection_matches(target_text);
1933
+ if (plain_matches.length > 0) return plain_matches;
1866
1934
  try {
1867
1935
  const pattern = new RegExp(this._make_fuzzy_regex(target_text), "g");
1868
- matches = [...this.full_text.matchAll(pattern)];
1869
- if (matches.length > 0)
1870
- return matches.map((m) => [m.index, m[0].length]);
1936
+ const fuzzy = [...this.full_text.matchAll(pattern)];
1937
+ if (fuzzy.length > 0) return fuzzy.map((m) => [m.index, m[0].length]);
1871
1938
  } catch (e) {
1872
1939
  }
1873
1940
  return [];
@@ -1941,7 +2008,16 @@ ${header}`;
1941
2008
  if (preceding[i].run) return [preceding[i].run, preceding[i].paragraph];
1942
2009
  }
1943
2010
  for (let i = preceding.length - 1; i >= 0; i--) {
1944
- if (preceding[i].paragraph) return [null, preceding[i].paragraph];
2011
+ const para = preceding[i].paragraph;
2012
+ if (para) {
2013
+ for (let j = this.spans.length - 1; j >= 0; j--) {
2014
+ const prev = this.spans[j];
2015
+ if (prev.end <= index && prev.run !== null && prev.paragraph === para) {
2016
+ return [prev.run, prev.paragraph];
2017
+ }
2018
+ }
2019
+ return [null, para];
2020
+ }
1945
2021
  }
1946
2022
  }
1947
2023
  const containing = this.spans.filter(
@@ -2848,18 +2924,30 @@ function format_ambiguity_error(edit_index, target_text, haystack, match_positio
2848
2924
  }
2849
2925
  lines.push(" To resolve, re-send this edit using ONE of these strategies:");
2850
2926
  lines.push(
2851
- ` 1. Set "match_mode": "all" to modify ALL ${total} occurrences (same target_text).`
2927
+ ' 1. RECOMMENDED: Provide more surrounding context in your target_text to uniquely identify a single location (keep the default "match_mode": "strict").'
2852
2928
  );
2853
2929
  lines.push(
2854
- ' 2. Set "match_mode": "first" to modify only the FIRST occurrence (same target_text).'
2930
+ ` 2. Set "match_mode": "all" to modify ALL ${total} occurrences \u2014 only after verifying from the occurrence list above that EVERY occurrence should change.`
2855
2931
  );
2856
2932
  lines.push(
2857
- ' 3. Please provide more surrounding context in your target_text to uniquely identify a single location (keep the default "match_mode": "strict").'
2933
+ ' 3. Set "match_mode": "first" to modify only the FIRST occurrence \u2014 only after verifying the first occurrence above is the one you intend to change.'
2858
2934
  );
2859
2935
  return lines.join("\n");
2860
2936
  }
2861
2937
 
2938
+ // src/utils/text.ts
2939
+ var REPORT_ECHO_CAP = 500;
2940
+ var PREVIEW_TEXT_CAP = 200;
2941
+ function truncate_middle(text, cap) {
2942
+ if (text === null || text === void 0 || text.length <= cap) return text;
2943
+ const head = Math.max(1, Math.floor(cap * 2 / 3));
2944
+ const tail = Math.max(1, cap - head);
2945
+ const omitted = text.length - head - tail;
2946
+ return `${text.slice(0, head)}\u2026 [${omitted.toLocaleString("en-US")} chars omitted] \u2026${text.slice(-tail)}`;
2947
+ }
2948
+
2862
2949
  // src/engine.ts
2950
+ var PREVIEW_CONTEXT_CHARS = 30;
2863
2951
  function getNextElement(el) {
2864
2952
  let next = el.nextSibling;
2865
2953
  while (next) {
@@ -2934,6 +3022,11 @@ var BatchValidationError = class extends Error {
2934
3022
  this.errors = errors;
2935
3023
  }
2936
3024
  };
3025
+ function sequential_context_hint(applied_so_far) {
3026
+ return `
3027
+ Note: ${applied_so_far} earlier edit(s) in this batch were already applied. Batches apply sequentially \u2014 each edit must target the document text as it reads AFTER the preceding edits (e.g. target the replacement text an earlier edit introduced, not the original wording).`;
3028
+ }
3029
+ var TRANSACTIONAL_NOT_APPLIED_ERROR = "Not applied: the batch is transactional and other edits failed validation (see their errors). Fix or remove those edits and re-run.";
2937
3030
  function validate_edit_strings(edits, index_offset = 0) {
2938
3031
  const errors = [];
2939
3032
  for (let i = 0; i < edits.length; i++) {
@@ -3037,7 +3130,7 @@ function validate_edit_strings(edits, index_offset = 0) {
3037
3130
  }
3038
3131
  return errors;
3039
3132
  }
3040
- var RedlineEngine = class {
3133
+ var RedlineEngine = class _RedlineEngine {
3041
3134
  doc;
3042
3135
  author;
3043
3136
  timestamp;
@@ -3184,8 +3277,184 @@ var RedlineEngine = class {
3184
3277
  }
3185
3278
  return "";
3186
3279
  }
3280
+ // CriticMarkup wrapper pairs used when tidying preview context windows.
3281
+ static _PREVIEW_WRAPPER_PAIRS = [
3282
+ ["{--", "--}"],
3283
+ ["{++", "++}"],
3284
+ ["{==", "==}"],
3285
+ ["{>>", "<<}"]
3286
+ ];
3287
+ /**
3288
+ * Makes a fixed-width slice of the raw-view projection presentable: drops
3289
+ * complete {>>...<<} meta blocks (annotations of pre-existing changes, not
3290
+ * part of this edit) and any wrapper fragments the window boundary chopped
3291
+ * in half. Without this, previews leak internal scaffolding like
3292
+ * "[Chg:5 delete]" (QA H1). Mirrors the Python engine.
3293
+ */
3294
+ static _tidy_preview_context(snippet, side) {
3295
+ snippet = snippet.replace(/\{>>[\s\S]*?<<\}/g, "");
3296
+ for (const [open_tok, close_tok] of _RedlineEngine._PREVIEW_WRAPPER_PAIRS) {
3297
+ if (side === "before") {
3298
+ let depth = 0;
3299
+ let cut = 0;
3300
+ let i = 0;
3301
+ while (i < snippet.length) {
3302
+ if (snippet.startsWith(open_tok, i)) {
3303
+ depth += 1;
3304
+ i += open_tok.length;
3305
+ } else if (snippet.startsWith(close_tok, i)) {
3306
+ if (depth === 0) cut = i + close_tok.length;
3307
+ else depth -= 1;
3308
+ i += close_tok.length;
3309
+ } else {
3310
+ i += 1;
3311
+ }
3312
+ }
3313
+ snippet = snippet.substring(cut);
3314
+ } else {
3315
+ const opens = [];
3316
+ let i = 0;
3317
+ while (i < snippet.length) {
3318
+ if (snippet.startsWith(open_tok, i)) {
3319
+ opens.push(i);
3320
+ i += open_tok.length;
3321
+ } else if (snippet.startsWith(close_tok, i)) {
3322
+ if (opens.length > 0) opens.pop();
3323
+ i += close_tok.length;
3324
+ } else {
3325
+ i += 1;
3326
+ }
3327
+ }
3328
+ if (opens.length > 0) snippet = snippet.substring(0, opens[0]);
3329
+ }
3330
+ }
3331
+ if (side === "before") {
3332
+ snippet = snippet.replace(/^[-+=<>]{0,2}\}/, "");
3333
+ } else {
3334
+ snippet = snippet.replace(/\{[-+=<>]{0,2}$/, "");
3335
+ }
3336
+ return snippet;
3337
+ }
3338
+ /**
3339
+ * Snapshots the document text around a resolved edit BEFORE anything is
3340
+ * applied. Previews rendered after the batch mutates the DOM cannot slice
3341
+ * full_text at the stored offsets: applied edits shift offsets and inject
3342
+ * tracked-change markup, which produced garbled previews mixing unrelated
3343
+ * edits and internal scaffolding (QA H1).
3344
+ */
3345
+ _capture_preview_context(edit) {
3346
+ if (edit.type !== "modify") return;
3347
+ const start_idx = edit._resolved_start_idx;
3348
+ if (start_idx === void 0 || start_idx === null) return;
3349
+ const active_mapper = edit._active_mapper_ref || this.mapper;
3350
+ const full_text = active_mapper.full_text;
3351
+ if (!full_text) return;
3352
+ const length = (edit.target_text || "").length;
3353
+ const before = full_text.substring(
3354
+ Math.max(0, start_idx - PREVIEW_CONTEXT_CHARS),
3355
+ start_idx
3356
+ );
3357
+ const after = full_text.substring(
3358
+ start_idx + length,
3359
+ start_idx + length + PREVIEW_CONTEXT_CHARS
3360
+ );
3361
+ edit._preview_context = [
3362
+ _RedlineEngine._tidy_preview_context(before, "before"),
3363
+ _RedlineEngine._tidy_preview_context(after, "after")
3364
+ ];
3365
+ }
3366
+ /**
3367
+ * Like _capture_preview_context, but snapshots the context around the
3368
+ * ORIGINAL edit's full matched span (stashed by _pre_resolve_heuristic_edit),
3369
+ * so the report preview can present the complete logical change of a
3370
+ * compound modification instead of its first sub-edit.
3371
+ */
3372
+ _capture_parent_preview_context(parent) {
3373
+ if (!parent || parent.type !== "modify") return;
3374
+ if (parent._preview_context || !parent._preview_span) return;
3375
+ const [start_idx, match_len] = parent._preview_span;
3376
+ const active_mapper = parent._preview_mapper_ref || this.mapper;
3377
+ const full_text = active_mapper.full_text;
3378
+ if (!full_text) return;
3379
+ const before = full_text.substring(
3380
+ Math.max(0, start_idx - PREVIEW_CONTEXT_CHARS),
3381
+ start_idx
3382
+ );
3383
+ const after = full_text.substring(
3384
+ start_idx + match_len,
3385
+ start_idx + match_len + PREVIEW_CONTEXT_CHARS
3386
+ );
3387
+ parent._preview_context = [
3388
+ _RedlineEngine._tidy_preview_context(before, "before"),
3389
+ _RedlineEngine._tidy_preview_context(after, "after")
3390
+ ];
3391
+ }
3392
+ /**
3393
+ * Renders the preview from the edit's full matched span. The common
3394
+ * prefix/suffix between matched and replacement text is moved into the
3395
+ * surrounding context so the {--...--}{++...++} block shows the minimal
3396
+ * complete change.
3397
+ */
3398
+ _build_full_match_preview(edit) {
3399
+ let [context_before, context_after] = edit._preview_context;
3400
+ let matched = edit._preview_matched_text || "";
3401
+ let new_text = edit._preview_new_text !== void 0 && edit._preview_new_text !== null ? edit._preview_new_text : edit.new_text || "";
3402
+ const [matched_clean, matched_style] = this._parse_markdown_style(matched);
3403
+ const [new_clean, new_style] = this._parse_markdown_style(new_text);
3404
+ if (matched_style && matched_style.startsWith("Heading")) {
3405
+ context_before = context_before + matched.substring(0, matched.length - matched_clean.length);
3406
+ matched = matched_clean;
3407
+ }
3408
+ if (new_style && new_style.startsWith("Heading")) {
3409
+ new_text = new_clean;
3410
+ }
3411
+ const [prefix_len, suffix_len] = trim_common_context(matched, new_text);
3412
+ let display_target = matched.substring(
3413
+ prefix_len,
3414
+ matched.length - suffix_len
3415
+ );
3416
+ let display_new = new_text.substring(
3417
+ prefix_len,
3418
+ new_text.length - suffix_len
3419
+ );
3420
+ context_before = context_before + matched.substring(0, prefix_len);
3421
+ if (suffix_len) {
3422
+ context_after = matched.substring(matched.length - suffix_len) + context_after;
3423
+ }
3424
+ display_target = truncate_middle(display_target, PREVIEW_TEXT_CAP);
3425
+ display_new = truncate_middle(display_new, PREVIEW_TEXT_CAP);
3426
+ let critic_markup;
3427
+ if (!display_target && !display_new) {
3428
+ const anchor = truncate_middle(matched, PREVIEW_TEXT_CAP);
3429
+ const body = anchor ? `{==${anchor}==}` : "";
3430
+ critic_markup = `${context_before.substring(0, context_before.length - matched.length)}${body}${context_after}`;
3431
+ } else {
3432
+ const deletion = display_target ? `{--${display_target}--}` : "";
3433
+ const insertion = display_new ? `{++${display_new}++}` : "";
3434
+ critic_markup = `${context_before}${deletion}${insertion}${context_after}`;
3435
+ }
3436
+ let clean_text = critic_markup;
3437
+ clean_text = clean_text.replace(/\{>>[\s\S]*?<<\}/g, "");
3438
+ clean_text = clean_text.replace(/\{--[\s\S]*?--\}/g, "");
3439
+ clean_text = clean_text.replace(/\{\+\+([\s\S]*?)\+\+\}/g, "$1");
3440
+ return [critic_markup, clean_text];
3441
+ }
3442
+ /**
3443
+ * The "new text" a batch report should show for an edit. InsertTableRow has
3444
+ * no new_text field — surface its cell contents instead of the misleading
3445
+ * empty string the report used to print (QA M4).
3446
+ */
3447
+ static _report_new_text(edit) {
3448
+ if (edit && edit.type === "insert_row" && Array.isArray(edit.cells)) {
3449
+ return edit.cells.join(" | ");
3450
+ }
3451
+ return edit && edit.new_text || "";
3452
+ }
3187
3453
  _build_edit_context_previews(edit) {
3188
3454
  if (edit.type !== "modify") return [null, null];
3455
+ if (edit._preview_span && edit._preview_context) {
3456
+ return this._build_full_match_preview(edit);
3457
+ }
3189
3458
  if (edit._resolved_proxy_edit) {
3190
3459
  edit = edit._resolved_proxy_edit;
3191
3460
  }
@@ -3204,17 +3473,33 @@ var RedlineEngine = class {
3204
3473
  new_text = clean_new;
3205
3474
  }
3206
3475
  const length = target_text.length;
3207
- const active_mapper = edit._active_mapper_ref || this.mapper;
3208
- const full_text = active_mapper.full_text;
3209
- if (!full_text) return [null, null];
3210
- const before_start = Math.max(0, start_idx - 30);
3211
- const context_before = full_text.substring(before_start, start_idx);
3212
- const context_after = full_text.substring(
3213
- start_idx + length,
3214
- start_idx + length + 30
3215
- );
3216
- const insertion = new_text ? `{++${new_text}++}` : "";
3217
- const critic_markup = `${context_before}{--${target_text}--}${insertion}${context_after}`;
3476
+ let context_before;
3477
+ let context_after;
3478
+ if (edit._preview_context) {
3479
+ [context_before, context_after] = edit._preview_context;
3480
+ } else {
3481
+ const active_mapper = edit._active_mapper_ref || this.mapper;
3482
+ const full_text = active_mapper.full_text;
3483
+ if (!full_text) return [null, null];
3484
+ context_before = _RedlineEngine._tidy_preview_context(
3485
+ full_text.substring(
3486
+ Math.max(0, start_idx - PREVIEW_CONTEXT_CHARS),
3487
+ start_idx
3488
+ ),
3489
+ "before"
3490
+ );
3491
+ context_after = _RedlineEngine._tidy_preview_context(
3492
+ full_text.substring(
3493
+ start_idx + length,
3494
+ start_idx + length + PREVIEW_CONTEXT_CHARS
3495
+ ),
3496
+ "after"
3497
+ );
3498
+ }
3499
+ const display_target = truncate_middle(target_text, PREVIEW_TEXT_CAP);
3500
+ const display_new = truncate_middle(new_text, PREVIEW_TEXT_CAP);
3501
+ const insertion = display_new ? `{++${display_new}++}` : "";
3502
+ const critic_markup = `${context_before}{--${display_target}--}${insertion}${context_after}`;
3218
3503
  let clean_text = critic_markup;
3219
3504
  clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
3220
3505
  clean_text = clean_text.replace(/\{--.*?--\}/gs, "");
@@ -4155,7 +4440,7 @@ var RedlineEngine = class {
4155
4440
  const hint = this._nearest_match_hint(edit.target_text, is_regex);
4156
4441
  errors.push(
4157
4442
  `- Edit ${i + 1 + index_offset} Failed: Target text not found in document:
4158
- "${edit.target_text}"${hint}`
4443
+ "${truncate_middle(edit.target_text, REPORT_ECHO_CAP)}"${hint}`
4159
4444
  );
4160
4445
  }
4161
4446
  } else if (matches.length > 1 && match_mode === "strict") {
@@ -4256,9 +4541,48 @@ var RedlineEngine = class {
4256
4541
  );
4257
4542
  }
4258
4543
  }
4544
+ if ((edit.type === "insert_row" || edit.type === "delete_row") && matches.length > 0) {
4545
+ const [start, length] = matches[0];
4546
+ const n_cols = _RedlineEngine._column_count_at(
4547
+ target_mapper,
4548
+ start,
4549
+ length
4550
+ );
4551
+ if (n_cols === null) {
4552
+ errors.push(
4553
+ `- Edit ${i + 1 + index_offset} Failed: ${edit.type} target text was found, but it is not inside a table row. Anchor the operation on text that appears within the table.`
4554
+ );
4555
+ } else if (edit.type === "insert_row" && Array.isArray(edit.cells) && edit.cells.length > n_cols) {
4556
+ errors.push(
4557
+ `- Edit ${i + 1 + index_offset} Failed: insert_row provides ${edit.cells.length} cells but the target table has ${n_cols} column(s). The extra cell(s) would be dropped. Provide at most ${n_cols} cells \u2014 rows given fewer cells are padded with empty ones.`
4558
+ );
4559
+ }
4560
+ }
4259
4561
  }
4260
4562
  return errors;
4261
4563
  }
4564
+ /**
4565
+ * Number of columns (w:tc elements) in the table row containing the text at
4566
+ * [start, start+length) in `mapper`, or null if that text is not inside a
4567
+ * table row.
4568
+ */
4569
+ static _column_count_at(mapper, start, length) {
4570
+ for (const s of mapper.spans) {
4571
+ if (s.run === null || s.end <= start || s.start >= start + length) {
4572
+ continue;
4573
+ }
4574
+ let curr = s.run._element;
4575
+ while (curr) {
4576
+ if (curr.nodeType === 1 && curr.tagName === "w:tr") {
4577
+ return findAllDescendants(curr, "w:tc").filter(
4578
+ (tc) => tc.parentNode === curr
4579
+ ).length;
4580
+ }
4581
+ curr = curr.parentNode;
4582
+ }
4583
+ }
4584
+ return null;
4585
+ }
4262
4586
  validate_review_actions(actions) {
4263
4587
  const errors = [];
4264
4588
  for (let i = 0; i < actions.length; i++) {
@@ -4401,19 +4725,25 @@ var RedlineEngine = class {
4401
4725
  });
4402
4726
  if (dry_run_mode) {
4403
4727
  const reports_by_input = new Array(edits.length);
4728
+ const validation_failed_idx = /* @__PURE__ */ new Set();
4404
4729
  for (const { edit, i } of ordered_edits) {
4405
- const single_errors = this.validate_edits([edit], i);
4730
+ let single_errors = this.validate_edits([edit], i);
4406
4731
  if (single_errors.length > 0) {
4732
+ if (applied_edits > 0) {
4733
+ const hint = sequential_context_hint(applied_edits);
4734
+ single_errors = single_errors.map((err) => err + hint);
4735
+ }
4736
+ validation_failed_idx.add(i);
4407
4737
  skipped_edits++;
4408
4738
  const warning = this._check_punctuation_warning(
4409
4739
  edit.target_text || ""
4410
4740
  );
4411
4741
  reports_by_input[i] = {
4412
4742
  status: "failed",
4413
- target_text: edit.target_text || "",
4414
- new_text: edit.new_text || "",
4743
+ target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
4744
+ new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
4415
4745
  warning,
4416
- error: single_errors[0],
4746
+ error: single_errors.join("\n"),
4417
4747
  critic_markup: null,
4418
4748
  clean_text: null
4419
4749
  };
@@ -4425,8 +4755,8 @@ var RedlineEngine = class {
4425
4755
  const previews = this._build_edit_context_previews(edit);
4426
4756
  reports_by_input[i] = {
4427
4757
  status: "applied",
4428
- target_text: edit.target_text || "",
4429
- new_text: edit.new_text || "",
4758
+ target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
4759
+ new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
4430
4760
  warning: null,
4431
4761
  error: null,
4432
4762
  critic_markup: previews[0],
@@ -4446,8 +4776,8 @@ var RedlineEngine = class {
4446
4776
  );
4447
4777
  reports_by_input[i] = {
4448
4778
  status: "failed",
4449
- target_text: edit.target_text || "",
4450
- new_text: edit.new_text || "",
4779
+ target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
4780
+ new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
4451
4781
  warning,
4452
4782
  error: error_msg,
4453
4783
  critic_markup: null,
@@ -4455,18 +4785,45 @@ var RedlineEngine = class {
4455
4785
  };
4456
4786
  }
4457
4787
  }
4788
+ if (validation_failed_idx.size > 0) {
4789
+ applied_edits = 0;
4790
+ skipped_edits = edits.length;
4791
+ for (let i = 0; i < reports_by_input.length; i++) {
4792
+ const report = reports_by_input[i];
4793
+ if (!report || validation_failed_idx.has(i)) continue;
4794
+ if (report.status === "applied") {
4795
+ reports_by_input[i] = {
4796
+ status: "failed",
4797
+ target_text: report.target_text,
4798
+ new_text: report.new_text,
4799
+ warning: null,
4800
+ error: TRANSACTIONAL_NOT_APPLIED_ERROR,
4801
+ critic_markup: null,
4802
+ clean_text: null
4803
+ };
4804
+ }
4805
+ }
4806
+ }
4458
4807
  edits_reports.push(...reports_by_input);
4459
4808
  } else {
4460
4809
  const snapshot = takeSnapshot(this.doc);
4461
4810
  const originalCurrentId = this.current_id;
4462
4811
  try {
4463
4812
  const sequential_errors = [];
4813
+ let applied_so_far = 0;
4464
4814
  for (const { edit, i } of ordered_edits) {
4465
- const single_errors = this.validate_edits([edit], i);
4815
+ let single_errors = this.validate_edits([edit], i);
4466
4816
  if (single_errors.length > 0) {
4817
+ if (applied_so_far > 0) {
4818
+ const hint = sequential_context_hint(applied_so_far);
4819
+ single_errors = single_errors.map((err) => err + hint);
4820
+ }
4467
4821
  sequential_errors.push(...single_errors);
4468
4822
  } else {
4469
4823
  this.apply_edits([edit], page_offsets);
4824
+ if (edit._applied_status) {
4825
+ applied_so_far++;
4826
+ }
4470
4827
  this.mapper = new DocumentMapper(this.doc);
4471
4828
  this.clean_mapper = null;
4472
4829
  }
@@ -4501,8 +4858,8 @@ var RedlineEngine = class {
4501
4858
  }
4502
4859
  edits_reports.push({
4503
4860
  status: success ? "applied" : "failed",
4504
- target_text: edit.target_text || "",
4505
- new_text: edit.new_text || "",
4861
+ target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
4862
+ new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
4506
4863
  warning,
4507
4864
  error: error_msg,
4508
4865
  critic_markup,
@@ -4604,6 +4961,12 @@ var RedlineEngine = class {
4604
4961
  resolved_edits.sort(
4605
4962
  (a, b) => (b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0)
4606
4963
  );
4964
+ for (const [res_edit] of resolved_edits) {
4965
+ this._capture_preview_context(res_edit);
4966
+ if (res_edit._parent_edit_ref) {
4967
+ this._capture_parent_preview_context(res_edit._parent_edit_ref);
4968
+ }
4969
+ }
4607
4970
  const occupied_ranges = [];
4608
4971
  const counted_split_groups = /* @__PURE__ */ new Set();
4609
4972
  for (const [edit, orig_new] of resolved_edits) {
@@ -4804,7 +5167,15 @@ var RedlineEngine = class {
4804
5167
  const trPr = tr.ownerDocument.createElement("w:trPr");
4805
5168
  new_tr.appendChild(trPr);
4806
5169
  trPr.appendChild(this._create_track_change_tag("w:ins"));
4807
- for (const cellText of edit.cells) {
5170
+ const anchor_cols = findAllDescendants(tr, "w:tc").filter(
5171
+ (tc) => tc.parentNode === tr
5172
+ ).length;
5173
+ let cell_values = Array.isArray(edit.cells) ? [...edit.cells] : [];
5174
+ if (anchor_cols > 0) {
5175
+ while (cell_values.length < anchor_cols) cell_values.push("");
5176
+ cell_values = cell_values.slice(0, anchor_cols);
5177
+ }
5178
+ for (const cellText of cell_values) {
4808
5179
  const tc = tr.ownerDocument.createElement("w:tc");
4809
5180
  const p = tr.ownerDocument.createElement("w:p");
4810
5181
  const r = tr.ownerDocument.createElement("w:r");
@@ -4900,6 +5271,12 @@ var RedlineEngine = class {
4900
5271
  } catch (e) {
4901
5272
  }
4902
5273
  }
5274
+ if (!edit._preview_span) {
5275
+ edit._preview_span = [start_idx, match_len];
5276
+ edit._preview_matched_text = actual_doc_text;
5277
+ edit._preview_new_text = current_effective_new_text;
5278
+ edit._preview_mapper_ref = active_mapper;
5279
+ }
4903
5280
  const [edit_target_clean, edit_target_style] = this._parse_markdown_style(
4904
5281
  edit.target_text
4905
5282
  );
@@ -5359,19 +5736,24 @@ var RedlineEngine = class {
5359
5736
  const is_inline_first = result.first_node.tagName === "w:ins";
5360
5737
  if (is_inline_first) {
5361
5738
  if (anchor_run) {
5362
- const anchor_parent = anchor_run._element.parentNode;
5739
+ let anchor_el = anchor_run._element;
5740
+ let anchor_parent = anchor_el.parentNode;
5741
+ if (anchor_parent && anchor_parent.tagName === "w:del") {
5742
+ anchor_el = anchor_parent;
5743
+ anchor_parent = anchor_el.parentNode;
5744
+ }
5363
5745
  const before_anchor = start_idx === 0;
5364
5746
  if (anchor_parent && anchor_parent.tagName === "w:ins") {
5365
5747
  this._insert_and_split_ins(
5366
5748
  anchor_parent,
5367
- anchor_run._element,
5749
+ anchor_el,
5368
5750
  result.first_node,
5369
5751
  before_anchor
5370
5752
  );
5371
5753
  } else if (before_anchor && anchor_parent) {
5372
- anchor_parent.insertBefore(result.first_node, anchor_run._element);
5754
+ anchor_parent.insertBefore(result.first_node, anchor_el);
5373
5755
  } else {
5374
- insertAfter(result.first_node, anchor_run._element);
5756
+ insertAfter(result.first_node, anchor_el);
5375
5757
  }
5376
5758
  } else if (anchor_para) {
5377
5759
  const para_el = anchor_para._element;