@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.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
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1862
|
-
|
|
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
|
-
|
|
1869
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
3208
|
-
|
|
3209
|
-
if (
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
5754
|
+
anchor_parent.insertBefore(result.first_node, anchor_el);
|
|
5373
5755
|
} else {
|
|
5374
|
-
insertAfter(result.first_node,
|
|
5756
|
+
insertAfter(result.first_node, anchor_el);
|
|
5375
5757
|
}
|
|
5376
5758
|
} else if (anchor_para) {
|
|
5377
5759
|
const para_el = anchor_para._element;
|