@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 +433 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +433 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/engine.bugs.test.ts +1 -1
- package/src/engine.qa_v4_port.test.ts +304 -0
- package/src/engine.sequential.test.ts +140 -0
- package/src/engine.ts +421 -30
- package/src/mapper.ts +133 -20
- package/src/markup.test.ts +1 -1
- package/src/markup.ts +11 -5
- package/src/repro.insertion-anchor.test.ts +91 -0
- package/src/repro_qa_report_v3.test.ts +9 -3
- package/src/utils/text.ts +26 -0
package/dist/index.js
CHANGED
|
@@ -1213,6 +1213,7 @@ function* iter_paragraph_content(paragraph) {
|
|
|
1213
1213
|
}
|
|
1214
1214
|
|
|
1215
1215
|
// src/mapper.ts
|
|
1216
|
+
var STYLE_MARKER_TEXTS = /* @__PURE__ */ new Set(["**", "__", "*", "_"]);
|
|
1216
1217
|
var DocumentMapper = class {
|
|
1217
1218
|
doc;
|
|
1218
1219
|
clean_view;
|
|
@@ -1222,6 +1223,7 @@ var DocumentMapper = class {
|
|
|
1222
1223
|
spans = [];
|
|
1223
1224
|
appendix_start_index = -1;
|
|
1224
1225
|
_text_chunks = [];
|
|
1226
|
+
_plain_projection = null;
|
|
1225
1227
|
constructor(doc, clean_view = false, original_view = false) {
|
|
1226
1228
|
this.doc = doc;
|
|
1227
1229
|
this.clean_view = clean_view;
|
|
@@ -1234,6 +1236,7 @@ var DocumentMapper = class {
|
|
|
1234
1236
|
this.spans = [];
|
|
1235
1237
|
this._text_chunks = [];
|
|
1236
1238
|
this.full_text = "";
|
|
1239
|
+
this._plain_projection = null;
|
|
1237
1240
|
for (const part of iter_document_parts(this.doc)) {
|
|
1238
1241
|
current_offset = this._map_blocks(part, current_offset);
|
|
1239
1242
|
if (this.spans.length > 0 && this.spans[this.spans.length - 1].text !== "\n\n") {
|
|
@@ -1753,6 +1756,64 @@ ${header}`;
|
|
|
1753
1756
|
if (remaining) parts.push(escapeRegExp(remaining));
|
|
1754
1757
|
return parts.join("");
|
|
1755
1758
|
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Returns [plain_text, offset_map] where plain_text is full_text with the
|
|
1761
|
+
* VIRTUAL markdown style delimiters (bold/italic markers emitted around
|
|
1762
|
+
* formatted runs) removed, and offset_map[i] is the full_text index of
|
|
1763
|
+
* plain_text[i].
|
|
1764
|
+
*
|
|
1765
|
+
* Formatting run boundaries can fall mid-word (e.g. a paragraph projected
|
|
1766
|
+
* as "**Al**pha"), where neither exact matching nor the whitespace-anchored
|
|
1767
|
+
* fuzzy regex can find the plain target "Alpha". Matching against this
|
|
1768
|
+
* projection and mapping the span back to full_text closes that gap (QA H2).
|
|
1769
|
+
*
|
|
1770
|
+
* Built lazily and invalidated by _build_map(): most batches never need it.
|
|
1771
|
+
*/
|
|
1772
|
+
_get_plain_projection() {
|
|
1773
|
+
if (this._plain_projection === null) {
|
|
1774
|
+
const chunks = [];
|
|
1775
|
+
const offsets = [];
|
|
1776
|
+
for (const s of this.spans) {
|
|
1777
|
+
if (s.run === null && s.paragraph !== null && STYLE_MARKER_TEXTS.has(s.text)) {
|
|
1778
|
+
continue;
|
|
1779
|
+
}
|
|
1780
|
+
chunks.push(s.text);
|
|
1781
|
+
for (let k = s.start; k < s.end; k++) offsets.push(k);
|
|
1782
|
+
}
|
|
1783
|
+
this._plain_projection = [chunks.join(""), offsets];
|
|
1784
|
+
}
|
|
1785
|
+
return this._plain_projection;
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Matches a markdown-stripped target against the plain projection and maps
|
|
1789
|
+
* each hit back to a [start, length] span in full_text. Interior style
|
|
1790
|
+
* markers end up inside the returned span (so "Alpha" over "**Al**pha"
|
|
1791
|
+
* resolves to the "Al**pha" range); markers just outside the matched
|
|
1792
|
+
* characters are excluded.
|
|
1793
|
+
*/
|
|
1794
|
+
_find_plain_projection_matches(target_text) {
|
|
1795
|
+
const [plain_text, offsets] = this._get_plain_projection();
|
|
1796
|
+
if (plain_text.length === this.full_text.length) {
|
|
1797
|
+
return [];
|
|
1798
|
+
}
|
|
1799
|
+
const norm_target = this._replace_smart_quotes(
|
|
1800
|
+
this._strip_markdown_formatting(target_text)
|
|
1801
|
+
);
|
|
1802
|
+
if (!norm_target) return [];
|
|
1803
|
+
const norm_plain = this._replace_smart_quotes(plain_text);
|
|
1804
|
+
const results = [];
|
|
1805
|
+
let from = 0;
|
|
1806
|
+
while (true) {
|
|
1807
|
+
const p_start = norm_plain.indexOf(norm_target, from);
|
|
1808
|
+
if (p_start === -1) break;
|
|
1809
|
+
const p_end = p_start + norm_target.length;
|
|
1810
|
+
const raw_start = offsets[p_start];
|
|
1811
|
+
const raw_end = offsets[p_end - 1] + 1;
|
|
1812
|
+
results.push([raw_start, raw_end - raw_start]);
|
|
1813
|
+
from = p_end;
|
|
1814
|
+
}
|
|
1815
|
+
return results;
|
|
1816
|
+
}
|
|
1756
1817
|
find_match_index(target_text, is_regex = false) {
|
|
1757
1818
|
if (is_regex) {
|
|
1758
1819
|
try {
|
|
@@ -1774,6 +1835,8 @@ ${header}`;
|
|
|
1774
1835
|
start_idx = this.full_text.indexOf(stripped_target);
|
|
1775
1836
|
return [start_idx, stripped_target.length];
|
|
1776
1837
|
}
|
|
1838
|
+
const plain_first = this._find_plain_projection_matches(target_text);
|
|
1839
|
+
if (plain_first.length > 0) return plain_first[0];
|
|
1777
1840
|
try {
|
|
1778
1841
|
const pattern = new RegExp(this._make_fuzzy_regex(target_text));
|
|
1779
1842
|
const match = pattern.exec(this.full_text);
|
|
@@ -1794,29 +1857,33 @@ ${header}`;
|
|
|
1794
1857
|
}
|
|
1795
1858
|
return [];
|
|
1796
1859
|
}
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1860
|
+
const findAllLiteral = (haystack, needle) => {
|
|
1861
|
+
const out = [];
|
|
1862
|
+
if (!needle) return out;
|
|
1863
|
+
let from = 0;
|
|
1864
|
+
while (true) {
|
|
1865
|
+
const idx = haystack.indexOf(needle, from);
|
|
1866
|
+
if (idx === -1) break;
|
|
1867
|
+
out.push([idx, needle.length]);
|
|
1868
|
+
from = idx + needle.length;
|
|
1869
|
+
}
|
|
1870
|
+
return out;
|
|
1871
|
+
};
|
|
1872
|
+
let matches = findAllLiteral(this.full_text, target_text);
|
|
1873
|
+
if (matches.length > 0) return matches;
|
|
1802
1874
|
const norm_full = this._replace_smart_quotes(this.full_text);
|
|
1803
1875
|
const norm_target = this._replace_smart_quotes(target_text);
|
|
1804
|
-
matches =
|
|
1805
|
-
|
|
1806
|
-
];
|
|
1807
|
-
if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
|
|
1876
|
+
matches = findAllLiteral(norm_full, norm_target);
|
|
1877
|
+
if (matches.length > 0) return matches;
|
|
1808
1878
|
const stripped_target = this._strip_markdown_formatting(target_text);
|
|
1809
|
-
matches =
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
];
|
|
1814
|
-
if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
|
|
1879
|
+
matches = findAllLiteral(this.full_text, stripped_target);
|
|
1880
|
+
if (matches.length > 0) return matches;
|
|
1881
|
+
const plain_matches = this._find_plain_projection_matches(target_text);
|
|
1882
|
+
if (plain_matches.length > 0) return plain_matches;
|
|
1815
1883
|
try {
|
|
1816
1884
|
const pattern = new RegExp(this._make_fuzzy_regex(target_text), "g");
|
|
1817
|
-
|
|
1818
|
-
if (
|
|
1819
|
-
return matches.map((m) => [m.index, m[0].length]);
|
|
1885
|
+
const fuzzy = [...this.full_text.matchAll(pattern)];
|
|
1886
|
+
if (fuzzy.length > 0) return fuzzy.map((m) => [m.index, m[0].length]);
|
|
1820
1887
|
} catch (e) {
|
|
1821
1888
|
}
|
|
1822
1889
|
return [];
|
|
@@ -1890,7 +1957,16 @@ ${header}`;
|
|
|
1890
1957
|
if (preceding[i].run) return [preceding[i].run, preceding[i].paragraph];
|
|
1891
1958
|
}
|
|
1892
1959
|
for (let i = preceding.length - 1; i >= 0; i--) {
|
|
1893
|
-
|
|
1960
|
+
const para = preceding[i].paragraph;
|
|
1961
|
+
if (para) {
|
|
1962
|
+
for (let j = this.spans.length - 1; j >= 0; j--) {
|
|
1963
|
+
const prev = this.spans[j];
|
|
1964
|
+
if (prev.end <= index && prev.run !== null && prev.paragraph === para) {
|
|
1965
|
+
return [prev.run, prev.paragraph];
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
return [null, para];
|
|
1969
|
+
}
|
|
1894
1970
|
}
|
|
1895
1971
|
}
|
|
1896
1972
|
const containing = this.spans.filter(
|
|
@@ -2797,18 +2873,30 @@ function format_ambiguity_error(edit_index, target_text, haystack, match_positio
|
|
|
2797
2873
|
}
|
|
2798
2874
|
lines.push(" To resolve, re-send this edit using ONE of these strategies:");
|
|
2799
2875
|
lines.push(
|
|
2800
|
-
|
|
2876
|
+
' 1. RECOMMENDED: Provide more surrounding context in your target_text to uniquely identify a single location (keep the default "match_mode": "strict").'
|
|
2801
2877
|
);
|
|
2802
2878
|
lines.push(
|
|
2803
|
-
|
|
2879
|
+
` 2. Set "match_mode": "all" to modify ALL ${total} occurrences \u2014 only after verifying from the occurrence list above that EVERY occurrence should change.`
|
|
2804
2880
|
);
|
|
2805
2881
|
lines.push(
|
|
2806
|
-
' 3.
|
|
2882
|
+
' 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.'
|
|
2807
2883
|
);
|
|
2808
2884
|
return lines.join("\n");
|
|
2809
2885
|
}
|
|
2810
2886
|
|
|
2887
|
+
// src/utils/text.ts
|
|
2888
|
+
var REPORT_ECHO_CAP = 500;
|
|
2889
|
+
var PREVIEW_TEXT_CAP = 200;
|
|
2890
|
+
function truncate_middle(text, cap) {
|
|
2891
|
+
if (text === null || text === void 0 || text.length <= cap) return text;
|
|
2892
|
+
const head = Math.max(1, Math.floor(cap * 2 / 3));
|
|
2893
|
+
const tail = Math.max(1, cap - head);
|
|
2894
|
+
const omitted = text.length - head - tail;
|
|
2895
|
+
return `${text.slice(0, head)}\u2026 [${omitted.toLocaleString("en-US")} chars omitted] \u2026${text.slice(-tail)}`;
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2811
2898
|
// src/engine.ts
|
|
2899
|
+
var PREVIEW_CONTEXT_CHARS = 30;
|
|
2812
2900
|
function getNextElement(el) {
|
|
2813
2901
|
let next = el.nextSibling;
|
|
2814
2902
|
while (next) {
|
|
@@ -2883,6 +2971,11 @@ var BatchValidationError = class extends Error {
|
|
|
2883
2971
|
this.errors = errors;
|
|
2884
2972
|
}
|
|
2885
2973
|
};
|
|
2974
|
+
function sequential_context_hint(applied_so_far) {
|
|
2975
|
+
return `
|
|
2976
|
+
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).`;
|
|
2977
|
+
}
|
|
2978
|
+
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.";
|
|
2886
2979
|
function validate_edit_strings(edits, index_offset = 0) {
|
|
2887
2980
|
const errors = [];
|
|
2888
2981
|
for (let i = 0; i < edits.length; i++) {
|
|
@@ -2986,7 +3079,7 @@ function validate_edit_strings(edits, index_offset = 0) {
|
|
|
2986
3079
|
}
|
|
2987
3080
|
return errors;
|
|
2988
3081
|
}
|
|
2989
|
-
var RedlineEngine = class {
|
|
3082
|
+
var RedlineEngine = class _RedlineEngine {
|
|
2990
3083
|
doc;
|
|
2991
3084
|
author;
|
|
2992
3085
|
timestamp;
|
|
@@ -3133,8 +3226,184 @@ var RedlineEngine = class {
|
|
|
3133
3226
|
}
|
|
3134
3227
|
return "";
|
|
3135
3228
|
}
|
|
3229
|
+
// CriticMarkup wrapper pairs used when tidying preview context windows.
|
|
3230
|
+
static _PREVIEW_WRAPPER_PAIRS = [
|
|
3231
|
+
["{--", "--}"],
|
|
3232
|
+
["{++", "++}"],
|
|
3233
|
+
["{==", "==}"],
|
|
3234
|
+
["{>>", "<<}"]
|
|
3235
|
+
];
|
|
3236
|
+
/**
|
|
3237
|
+
* Makes a fixed-width slice of the raw-view projection presentable: drops
|
|
3238
|
+
* complete {>>...<<} meta blocks (annotations of pre-existing changes, not
|
|
3239
|
+
* part of this edit) and any wrapper fragments the window boundary chopped
|
|
3240
|
+
* in half. Without this, previews leak internal scaffolding like
|
|
3241
|
+
* "[Chg:5 delete]" (QA H1). Mirrors the Python engine.
|
|
3242
|
+
*/
|
|
3243
|
+
static _tidy_preview_context(snippet, side) {
|
|
3244
|
+
snippet = snippet.replace(/\{>>[\s\S]*?<<\}/g, "");
|
|
3245
|
+
for (const [open_tok, close_tok] of _RedlineEngine._PREVIEW_WRAPPER_PAIRS) {
|
|
3246
|
+
if (side === "before") {
|
|
3247
|
+
let depth = 0;
|
|
3248
|
+
let cut = 0;
|
|
3249
|
+
let i = 0;
|
|
3250
|
+
while (i < snippet.length) {
|
|
3251
|
+
if (snippet.startsWith(open_tok, i)) {
|
|
3252
|
+
depth += 1;
|
|
3253
|
+
i += open_tok.length;
|
|
3254
|
+
} else if (snippet.startsWith(close_tok, i)) {
|
|
3255
|
+
if (depth === 0) cut = i + close_tok.length;
|
|
3256
|
+
else depth -= 1;
|
|
3257
|
+
i += close_tok.length;
|
|
3258
|
+
} else {
|
|
3259
|
+
i += 1;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
snippet = snippet.substring(cut);
|
|
3263
|
+
} else {
|
|
3264
|
+
const opens = [];
|
|
3265
|
+
let i = 0;
|
|
3266
|
+
while (i < snippet.length) {
|
|
3267
|
+
if (snippet.startsWith(open_tok, i)) {
|
|
3268
|
+
opens.push(i);
|
|
3269
|
+
i += open_tok.length;
|
|
3270
|
+
} else if (snippet.startsWith(close_tok, i)) {
|
|
3271
|
+
if (opens.length > 0) opens.pop();
|
|
3272
|
+
i += close_tok.length;
|
|
3273
|
+
} else {
|
|
3274
|
+
i += 1;
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
if (opens.length > 0) snippet = snippet.substring(0, opens[0]);
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
if (side === "before") {
|
|
3281
|
+
snippet = snippet.replace(/^[-+=<>]{0,2}\}/, "");
|
|
3282
|
+
} else {
|
|
3283
|
+
snippet = snippet.replace(/\{[-+=<>]{0,2}$/, "");
|
|
3284
|
+
}
|
|
3285
|
+
return snippet;
|
|
3286
|
+
}
|
|
3287
|
+
/**
|
|
3288
|
+
* Snapshots the document text around a resolved edit BEFORE anything is
|
|
3289
|
+
* applied. Previews rendered after the batch mutates the DOM cannot slice
|
|
3290
|
+
* full_text at the stored offsets: applied edits shift offsets and inject
|
|
3291
|
+
* tracked-change markup, which produced garbled previews mixing unrelated
|
|
3292
|
+
* edits and internal scaffolding (QA H1).
|
|
3293
|
+
*/
|
|
3294
|
+
_capture_preview_context(edit) {
|
|
3295
|
+
if (edit.type !== "modify") return;
|
|
3296
|
+
const start_idx = edit._resolved_start_idx;
|
|
3297
|
+
if (start_idx === void 0 || start_idx === null) return;
|
|
3298
|
+
const active_mapper = edit._active_mapper_ref || this.mapper;
|
|
3299
|
+
const full_text = active_mapper.full_text;
|
|
3300
|
+
if (!full_text) return;
|
|
3301
|
+
const length = (edit.target_text || "").length;
|
|
3302
|
+
const before = full_text.substring(
|
|
3303
|
+
Math.max(0, start_idx - PREVIEW_CONTEXT_CHARS),
|
|
3304
|
+
start_idx
|
|
3305
|
+
);
|
|
3306
|
+
const after = full_text.substring(
|
|
3307
|
+
start_idx + length,
|
|
3308
|
+
start_idx + length + PREVIEW_CONTEXT_CHARS
|
|
3309
|
+
);
|
|
3310
|
+
edit._preview_context = [
|
|
3311
|
+
_RedlineEngine._tidy_preview_context(before, "before"),
|
|
3312
|
+
_RedlineEngine._tidy_preview_context(after, "after")
|
|
3313
|
+
];
|
|
3314
|
+
}
|
|
3315
|
+
/**
|
|
3316
|
+
* Like _capture_preview_context, but snapshots the context around the
|
|
3317
|
+
* ORIGINAL edit's full matched span (stashed by _pre_resolve_heuristic_edit),
|
|
3318
|
+
* so the report preview can present the complete logical change of a
|
|
3319
|
+
* compound modification instead of its first sub-edit.
|
|
3320
|
+
*/
|
|
3321
|
+
_capture_parent_preview_context(parent) {
|
|
3322
|
+
if (!parent || parent.type !== "modify") return;
|
|
3323
|
+
if (parent._preview_context || !parent._preview_span) return;
|
|
3324
|
+
const [start_idx, match_len] = parent._preview_span;
|
|
3325
|
+
const active_mapper = parent._preview_mapper_ref || this.mapper;
|
|
3326
|
+
const full_text = active_mapper.full_text;
|
|
3327
|
+
if (!full_text) return;
|
|
3328
|
+
const before = full_text.substring(
|
|
3329
|
+
Math.max(0, start_idx - PREVIEW_CONTEXT_CHARS),
|
|
3330
|
+
start_idx
|
|
3331
|
+
);
|
|
3332
|
+
const after = full_text.substring(
|
|
3333
|
+
start_idx + match_len,
|
|
3334
|
+
start_idx + match_len + PREVIEW_CONTEXT_CHARS
|
|
3335
|
+
);
|
|
3336
|
+
parent._preview_context = [
|
|
3337
|
+
_RedlineEngine._tidy_preview_context(before, "before"),
|
|
3338
|
+
_RedlineEngine._tidy_preview_context(after, "after")
|
|
3339
|
+
];
|
|
3340
|
+
}
|
|
3341
|
+
/**
|
|
3342
|
+
* Renders the preview from the edit's full matched span. The common
|
|
3343
|
+
* prefix/suffix between matched and replacement text is moved into the
|
|
3344
|
+
* surrounding context so the {--...--}{++...++} block shows the minimal
|
|
3345
|
+
* complete change.
|
|
3346
|
+
*/
|
|
3347
|
+
_build_full_match_preview(edit) {
|
|
3348
|
+
let [context_before, context_after] = edit._preview_context;
|
|
3349
|
+
let matched = edit._preview_matched_text || "";
|
|
3350
|
+
let new_text = edit._preview_new_text !== void 0 && edit._preview_new_text !== null ? edit._preview_new_text : edit.new_text || "";
|
|
3351
|
+
const [matched_clean, matched_style] = this._parse_markdown_style(matched);
|
|
3352
|
+
const [new_clean, new_style] = this._parse_markdown_style(new_text);
|
|
3353
|
+
if (matched_style && matched_style.startsWith("Heading")) {
|
|
3354
|
+
context_before = context_before + matched.substring(0, matched.length - matched_clean.length);
|
|
3355
|
+
matched = matched_clean;
|
|
3356
|
+
}
|
|
3357
|
+
if (new_style && new_style.startsWith("Heading")) {
|
|
3358
|
+
new_text = new_clean;
|
|
3359
|
+
}
|
|
3360
|
+
const [prefix_len, suffix_len] = trim_common_context(matched, new_text);
|
|
3361
|
+
let display_target = matched.substring(
|
|
3362
|
+
prefix_len,
|
|
3363
|
+
matched.length - suffix_len
|
|
3364
|
+
);
|
|
3365
|
+
let display_new = new_text.substring(
|
|
3366
|
+
prefix_len,
|
|
3367
|
+
new_text.length - suffix_len
|
|
3368
|
+
);
|
|
3369
|
+
context_before = context_before + matched.substring(0, prefix_len);
|
|
3370
|
+
if (suffix_len) {
|
|
3371
|
+
context_after = matched.substring(matched.length - suffix_len) + context_after;
|
|
3372
|
+
}
|
|
3373
|
+
display_target = truncate_middle(display_target, PREVIEW_TEXT_CAP);
|
|
3374
|
+
display_new = truncate_middle(display_new, PREVIEW_TEXT_CAP);
|
|
3375
|
+
let critic_markup;
|
|
3376
|
+
if (!display_target && !display_new) {
|
|
3377
|
+
const anchor = truncate_middle(matched, PREVIEW_TEXT_CAP);
|
|
3378
|
+
const body = anchor ? `{==${anchor}==}` : "";
|
|
3379
|
+
critic_markup = `${context_before.substring(0, context_before.length - matched.length)}${body}${context_after}`;
|
|
3380
|
+
} else {
|
|
3381
|
+
const deletion = display_target ? `{--${display_target}--}` : "";
|
|
3382
|
+
const insertion = display_new ? `{++${display_new}++}` : "";
|
|
3383
|
+
critic_markup = `${context_before}${deletion}${insertion}${context_after}`;
|
|
3384
|
+
}
|
|
3385
|
+
let clean_text = critic_markup;
|
|
3386
|
+
clean_text = clean_text.replace(/\{>>[\s\S]*?<<\}/g, "");
|
|
3387
|
+
clean_text = clean_text.replace(/\{--[\s\S]*?--\}/g, "");
|
|
3388
|
+
clean_text = clean_text.replace(/\{\+\+([\s\S]*?)\+\+\}/g, "$1");
|
|
3389
|
+
return [critic_markup, clean_text];
|
|
3390
|
+
}
|
|
3391
|
+
/**
|
|
3392
|
+
* The "new text" a batch report should show for an edit. InsertTableRow has
|
|
3393
|
+
* no new_text field — surface its cell contents instead of the misleading
|
|
3394
|
+
* empty string the report used to print (QA M4).
|
|
3395
|
+
*/
|
|
3396
|
+
static _report_new_text(edit) {
|
|
3397
|
+
if (edit && edit.type === "insert_row" && Array.isArray(edit.cells)) {
|
|
3398
|
+
return edit.cells.join(" | ");
|
|
3399
|
+
}
|
|
3400
|
+
return edit && edit.new_text || "";
|
|
3401
|
+
}
|
|
3136
3402
|
_build_edit_context_previews(edit) {
|
|
3137
3403
|
if (edit.type !== "modify") return [null, null];
|
|
3404
|
+
if (edit._preview_span && edit._preview_context) {
|
|
3405
|
+
return this._build_full_match_preview(edit);
|
|
3406
|
+
}
|
|
3138
3407
|
if (edit._resolved_proxy_edit) {
|
|
3139
3408
|
edit = edit._resolved_proxy_edit;
|
|
3140
3409
|
}
|
|
@@ -3153,17 +3422,33 @@ var RedlineEngine = class {
|
|
|
3153
3422
|
new_text = clean_new;
|
|
3154
3423
|
}
|
|
3155
3424
|
const length = target_text.length;
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
if (
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3425
|
+
let context_before;
|
|
3426
|
+
let context_after;
|
|
3427
|
+
if (edit._preview_context) {
|
|
3428
|
+
[context_before, context_after] = edit._preview_context;
|
|
3429
|
+
} else {
|
|
3430
|
+
const active_mapper = edit._active_mapper_ref || this.mapper;
|
|
3431
|
+
const full_text = active_mapper.full_text;
|
|
3432
|
+
if (!full_text) return [null, null];
|
|
3433
|
+
context_before = _RedlineEngine._tidy_preview_context(
|
|
3434
|
+
full_text.substring(
|
|
3435
|
+
Math.max(0, start_idx - PREVIEW_CONTEXT_CHARS),
|
|
3436
|
+
start_idx
|
|
3437
|
+
),
|
|
3438
|
+
"before"
|
|
3439
|
+
);
|
|
3440
|
+
context_after = _RedlineEngine._tidy_preview_context(
|
|
3441
|
+
full_text.substring(
|
|
3442
|
+
start_idx + length,
|
|
3443
|
+
start_idx + length + PREVIEW_CONTEXT_CHARS
|
|
3444
|
+
),
|
|
3445
|
+
"after"
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3448
|
+
const display_target = truncate_middle(target_text, PREVIEW_TEXT_CAP);
|
|
3449
|
+
const display_new = truncate_middle(new_text, PREVIEW_TEXT_CAP);
|
|
3450
|
+
const insertion = display_new ? `{++${display_new}++}` : "";
|
|
3451
|
+
const critic_markup = `${context_before}{--${display_target}--}${insertion}${context_after}`;
|
|
3167
3452
|
let clean_text = critic_markup;
|
|
3168
3453
|
clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
|
|
3169
3454
|
clean_text = clean_text.replace(/\{--.*?--\}/gs, "");
|
|
@@ -4104,7 +4389,7 @@ var RedlineEngine = class {
|
|
|
4104
4389
|
const hint = this._nearest_match_hint(edit.target_text, is_regex);
|
|
4105
4390
|
errors.push(
|
|
4106
4391
|
`- Edit ${i + 1 + index_offset} Failed: Target text not found in document:
|
|
4107
|
-
"${edit.target_text}"${hint}`
|
|
4392
|
+
"${truncate_middle(edit.target_text, REPORT_ECHO_CAP)}"${hint}`
|
|
4108
4393
|
);
|
|
4109
4394
|
}
|
|
4110
4395
|
} else if (matches.length > 1 && match_mode === "strict") {
|
|
@@ -4205,9 +4490,48 @@ var RedlineEngine = class {
|
|
|
4205
4490
|
);
|
|
4206
4491
|
}
|
|
4207
4492
|
}
|
|
4493
|
+
if ((edit.type === "insert_row" || edit.type === "delete_row") && matches.length > 0) {
|
|
4494
|
+
const [start, length] = matches[0];
|
|
4495
|
+
const n_cols = _RedlineEngine._column_count_at(
|
|
4496
|
+
target_mapper,
|
|
4497
|
+
start,
|
|
4498
|
+
length
|
|
4499
|
+
);
|
|
4500
|
+
if (n_cols === null) {
|
|
4501
|
+
errors.push(
|
|
4502
|
+
`- 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.`
|
|
4503
|
+
);
|
|
4504
|
+
} else if (edit.type === "insert_row" && Array.isArray(edit.cells) && edit.cells.length > n_cols) {
|
|
4505
|
+
errors.push(
|
|
4506
|
+
`- 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.`
|
|
4507
|
+
);
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4208
4510
|
}
|
|
4209
4511
|
return errors;
|
|
4210
4512
|
}
|
|
4513
|
+
/**
|
|
4514
|
+
* Number of columns (w:tc elements) in the table row containing the text at
|
|
4515
|
+
* [start, start+length) in `mapper`, or null if that text is not inside a
|
|
4516
|
+
* table row.
|
|
4517
|
+
*/
|
|
4518
|
+
static _column_count_at(mapper, start, length) {
|
|
4519
|
+
for (const s of mapper.spans) {
|
|
4520
|
+
if (s.run === null || s.end <= start || s.start >= start + length) {
|
|
4521
|
+
continue;
|
|
4522
|
+
}
|
|
4523
|
+
let curr = s.run._element;
|
|
4524
|
+
while (curr) {
|
|
4525
|
+
if (curr.nodeType === 1 && curr.tagName === "w:tr") {
|
|
4526
|
+
return findAllDescendants(curr, "w:tc").filter(
|
|
4527
|
+
(tc) => tc.parentNode === curr
|
|
4528
|
+
).length;
|
|
4529
|
+
}
|
|
4530
|
+
curr = curr.parentNode;
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
return null;
|
|
4534
|
+
}
|
|
4211
4535
|
validate_review_actions(actions) {
|
|
4212
4536
|
const errors = [];
|
|
4213
4537
|
for (let i = 0; i < actions.length; i++) {
|
|
@@ -4350,19 +4674,25 @@ var RedlineEngine = class {
|
|
|
4350
4674
|
});
|
|
4351
4675
|
if (dry_run_mode) {
|
|
4352
4676
|
const reports_by_input = new Array(edits.length);
|
|
4677
|
+
const validation_failed_idx = /* @__PURE__ */ new Set();
|
|
4353
4678
|
for (const { edit, i } of ordered_edits) {
|
|
4354
|
-
|
|
4679
|
+
let single_errors = this.validate_edits([edit], i);
|
|
4355
4680
|
if (single_errors.length > 0) {
|
|
4681
|
+
if (applied_edits > 0) {
|
|
4682
|
+
const hint = sequential_context_hint(applied_edits);
|
|
4683
|
+
single_errors = single_errors.map((err) => err + hint);
|
|
4684
|
+
}
|
|
4685
|
+
validation_failed_idx.add(i);
|
|
4356
4686
|
skipped_edits++;
|
|
4357
4687
|
const warning = this._check_punctuation_warning(
|
|
4358
4688
|
edit.target_text || ""
|
|
4359
4689
|
);
|
|
4360
4690
|
reports_by_input[i] = {
|
|
4361
4691
|
status: "failed",
|
|
4362
|
-
target_text: edit.target_text || "",
|
|
4363
|
-
new_text: edit
|
|
4692
|
+
target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
|
|
4693
|
+
new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
|
|
4364
4694
|
warning,
|
|
4365
|
-
error: single_errors
|
|
4695
|
+
error: single_errors.join("\n"),
|
|
4366
4696
|
critic_markup: null,
|
|
4367
4697
|
clean_text: null
|
|
4368
4698
|
};
|
|
@@ -4374,8 +4704,8 @@ var RedlineEngine = class {
|
|
|
4374
4704
|
const previews = this._build_edit_context_previews(edit);
|
|
4375
4705
|
reports_by_input[i] = {
|
|
4376
4706
|
status: "applied",
|
|
4377
|
-
target_text: edit.target_text || "",
|
|
4378
|
-
new_text: edit
|
|
4707
|
+
target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
|
|
4708
|
+
new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
|
|
4379
4709
|
warning: null,
|
|
4380
4710
|
error: null,
|
|
4381
4711
|
critic_markup: previews[0],
|
|
@@ -4395,8 +4725,8 @@ var RedlineEngine = class {
|
|
|
4395
4725
|
);
|
|
4396
4726
|
reports_by_input[i] = {
|
|
4397
4727
|
status: "failed",
|
|
4398
|
-
target_text: edit.target_text || "",
|
|
4399
|
-
new_text: edit
|
|
4728
|
+
target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
|
|
4729
|
+
new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
|
|
4400
4730
|
warning,
|
|
4401
4731
|
error: error_msg,
|
|
4402
4732
|
critic_markup: null,
|
|
@@ -4404,18 +4734,45 @@ var RedlineEngine = class {
|
|
|
4404
4734
|
};
|
|
4405
4735
|
}
|
|
4406
4736
|
}
|
|
4737
|
+
if (validation_failed_idx.size > 0) {
|
|
4738
|
+
applied_edits = 0;
|
|
4739
|
+
skipped_edits = edits.length;
|
|
4740
|
+
for (let i = 0; i < reports_by_input.length; i++) {
|
|
4741
|
+
const report = reports_by_input[i];
|
|
4742
|
+
if (!report || validation_failed_idx.has(i)) continue;
|
|
4743
|
+
if (report.status === "applied") {
|
|
4744
|
+
reports_by_input[i] = {
|
|
4745
|
+
status: "failed",
|
|
4746
|
+
target_text: report.target_text,
|
|
4747
|
+
new_text: report.new_text,
|
|
4748
|
+
warning: null,
|
|
4749
|
+
error: TRANSACTIONAL_NOT_APPLIED_ERROR,
|
|
4750
|
+
critic_markup: null,
|
|
4751
|
+
clean_text: null
|
|
4752
|
+
};
|
|
4753
|
+
}
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4407
4756
|
edits_reports.push(...reports_by_input);
|
|
4408
4757
|
} else {
|
|
4409
4758
|
const snapshot = takeSnapshot(this.doc);
|
|
4410
4759
|
const originalCurrentId = this.current_id;
|
|
4411
4760
|
try {
|
|
4412
4761
|
const sequential_errors = [];
|
|
4762
|
+
let applied_so_far = 0;
|
|
4413
4763
|
for (const { edit, i } of ordered_edits) {
|
|
4414
|
-
|
|
4764
|
+
let single_errors = this.validate_edits([edit], i);
|
|
4415
4765
|
if (single_errors.length > 0) {
|
|
4766
|
+
if (applied_so_far > 0) {
|
|
4767
|
+
const hint = sequential_context_hint(applied_so_far);
|
|
4768
|
+
single_errors = single_errors.map((err) => err + hint);
|
|
4769
|
+
}
|
|
4416
4770
|
sequential_errors.push(...single_errors);
|
|
4417
4771
|
} else {
|
|
4418
4772
|
this.apply_edits([edit], page_offsets);
|
|
4773
|
+
if (edit._applied_status) {
|
|
4774
|
+
applied_so_far++;
|
|
4775
|
+
}
|
|
4419
4776
|
this.mapper = new DocumentMapper(this.doc);
|
|
4420
4777
|
this.clean_mapper = null;
|
|
4421
4778
|
}
|
|
@@ -4450,8 +4807,8 @@ var RedlineEngine = class {
|
|
|
4450
4807
|
}
|
|
4451
4808
|
edits_reports.push({
|
|
4452
4809
|
status: success ? "applied" : "failed",
|
|
4453
|
-
target_text: edit.target_text || "",
|
|
4454
|
-
new_text: edit
|
|
4810
|
+
target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
|
|
4811
|
+
new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
|
|
4455
4812
|
warning,
|
|
4456
4813
|
error: error_msg,
|
|
4457
4814
|
critic_markup,
|
|
@@ -4553,6 +4910,12 @@ var RedlineEngine = class {
|
|
|
4553
4910
|
resolved_edits.sort(
|
|
4554
4911
|
(a, b) => (b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0)
|
|
4555
4912
|
);
|
|
4913
|
+
for (const [res_edit] of resolved_edits) {
|
|
4914
|
+
this._capture_preview_context(res_edit);
|
|
4915
|
+
if (res_edit._parent_edit_ref) {
|
|
4916
|
+
this._capture_parent_preview_context(res_edit._parent_edit_ref);
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4556
4919
|
const occupied_ranges = [];
|
|
4557
4920
|
const counted_split_groups = /* @__PURE__ */ new Set();
|
|
4558
4921
|
for (const [edit, orig_new] of resolved_edits) {
|
|
@@ -4753,7 +5116,15 @@ var RedlineEngine = class {
|
|
|
4753
5116
|
const trPr = tr.ownerDocument.createElement("w:trPr");
|
|
4754
5117
|
new_tr.appendChild(trPr);
|
|
4755
5118
|
trPr.appendChild(this._create_track_change_tag("w:ins"));
|
|
4756
|
-
|
|
5119
|
+
const anchor_cols = findAllDescendants(tr, "w:tc").filter(
|
|
5120
|
+
(tc) => tc.parentNode === tr
|
|
5121
|
+
).length;
|
|
5122
|
+
let cell_values = Array.isArray(edit.cells) ? [...edit.cells] : [];
|
|
5123
|
+
if (anchor_cols > 0) {
|
|
5124
|
+
while (cell_values.length < anchor_cols) cell_values.push("");
|
|
5125
|
+
cell_values = cell_values.slice(0, anchor_cols);
|
|
5126
|
+
}
|
|
5127
|
+
for (const cellText of cell_values) {
|
|
4757
5128
|
const tc = tr.ownerDocument.createElement("w:tc");
|
|
4758
5129
|
const p = tr.ownerDocument.createElement("w:p");
|
|
4759
5130
|
const r = tr.ownerDocument.createElement("w:r");
|
|
@@ -4849,6 +5220,12 @@ var RedlineEngine = class {
|
|
|
4849
5220
|
} catch (e) {
|
|
4850
5221
|
}
|
|
4851
5222
|
}
|
|
5223
|
+
if (!edit._preview_span) {
|
|
5224
|
+
edit._preview_span = [start_idx, match_len];
|
|
5225
|
+
edit._preview_matched_text = actual_doc_text;
|
|
5226
|
+
edit._preview_new_text = current_effective_new_text;
|
|
5227
|
+
edit._preview_mapper_ref = active_mapper;
|
|
5228
|
+
}
|
|
4852
5229
|
const [edit_target_clean, edit_target_style] = this._parse_markdown_style(
|
|
4853
5230
|
edit.target_text
|
|
4854
5231
|
);
|
|
@@ -5308,19 +5685,24 @@ var RedlineEngine = class {
|
|
|
5308
5685
|
const is_inline_first = result.first_node.tagName === "w:ins";
|
|
5309
5686
|
if (is_inline_first) {
|
|
5310
5687
|
if (anchor_run) {
|
|
5311
|
-
|
|
5688
|
+
let anchor_el = anchor_run._element;
|
|
5689
|
+
let anchor_parent = anchor_el.parentNode;
|
|
5690
|
+
if (anchor_parent && anchor_parent.tagName === "w:del") {
|
|
5691
|
+
anchor_el = anchor_parent;
|
|
5692
|
+
anchor_parent = anchor_el.parentNode;
|
|
5693
|
+
}
|
|
5312
5694
|
const before_anchor = start_idx === 0;
|
|
5313
5695
|
if (anchor_parent && anchor_parent.tagName === "w:ins") {
|
|
5314
5696
|
this._insert_and_split_ins(
|
|
5315
5697
|
anchor_parent,
|
|
5316
|
-
|
|
5698
|
+
anchor_el,
|
|
5317
5699
|
result.first_node,
|
|
5318
5700
|
before_anchor
|
|
5319
5701
|
);
|
|
5320
5702
|
} else if (before_anchor && anchor_parent) {
|
|
5321
|
-
anchor_parent.insertBefore(result.first_node,
|
|
5703
|
+
anchor_parent.insertBefore(result.first_node, anchor_el);
|
|
5322
5704
|
} else {
|
|
5323
|
-
insertAfter(result.first_node,
|
|
5705
|
+
insertAfter(result.first_node, anchor_el);
|
|
5324
5706
|
}
|
|
5325
5707
|
} else if (anchor_para) {
|
|
5326
5708
|
const para_el = anchor_para._element;
|