@adeu/core 1.23.0 → 1.26.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 +478 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +477 -122
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/diff.ts +152 -12
- package/src/engine.ts +267 -57
- package/src/index.ts +1 -1
- package/src/ingest.ts +11 -2
- package/src/mapper.ts +99 -47
- package/src/repro_qa_report_v6.test.ts +486 -0
- package/src/repro_qa_report_v7.test.ts +380 -0
- package/src/sanitize/core.ts +60 -1
- package/src/sanitize/report.ts +5 -3
- package/src/sanitize/sanitize.test.ts +5 -4
- package/src/sanitize/transforms.ts +123 -16
- package/src/utils/docx.ts +30 -2
package/src/mapper.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
get_run_style_markers,
|
|
10
10
|
get_run_text,
|
|
11
11
|
is_heading_paragraph,
|
|
12
|
+
split_boundary_whitespace,
|
|
12
13
|
is_native_heading,
|
|
13
14
|
iter_block_items,
|
|
14
15
|
iter_document_parts_with_kind,
|
|
@@ -31,6 +32,12 @@ export interface TextSpan {
|
|
|
31
32
|
part_index: number;
|
|
32
33
|
// True for the read-only image marker projection .
|
|
33
34
|
is_image_marker?: boolean;
|
|
35
|
+
// Character offset of this span's text within its run's projected text.
|
|
36
|
+
// One run may back several spans: hoisting boundary whitespace outside
|
|
37
|
+
// style markers (QA 2026-07-19 F-03) projects a bold "The Supplier " run
|
|
38
|
+
// as core + trailing-space spans, and only the first starts at run
|
|
39
|
+
// offset 0. All span->run local-offset arithmetic must add this.
|
|
40
|
+
run_offset?: number;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export function renumber_snapshot_ids(
|
|
@@ -411,10 +418,12 @@ export class DocumentMapper {
|
|
|
411
418
|
let current_wrappers: [string, string] = ["", ""];
|
|
412
419
|
let current_style: [string, string] = ["", ""];
|
|
413
420
|
let active_hyperlink_id: string | null = null;
|
|
421
|
+
// [kind, text, run, run_offset, ins_id, del_id, comment_ids]
|
|
414
422
|
let pending_runs: [
|
|
415
423
|
string,
|
|
416
424
|
string,
|
|
417
425
|
Run | null,
|
|
426
|
+
number,
|
|
418
427
|
string | null,
|
|
419
428
|
string | null,
|
|
420
429
|
string[],
|
|
@@ -427,7 +436,7 @@ export class DocumentMapper {
|
|
|
427
436
|
this._add_virtual_text(s_tok, current, paragraph);
|
|
428
437
|
current += s_tok.length;
|
|
429
438
|
}
|
|
430
|
-
for (const [kind, txt, r_obj, i_id, d_id, c_ids] of pending_runs) {
|
|
439
|
+
for (const [kind, txt, r_obj, r_off, i_id, d_id, c_ids] of pending_runs) {
|
|
431
440
|
if (kind === "virtual") {
|
|
432
441
|
this._add_virtual_text(txt, current, paragraph, active_hyperlink_id);
|
|
433
442
|
} else {
|
|
@@ -442,6 +451,7 @@ export class DocumentMapper {
|
|
|
442
451
|
hyperlink_id: active_hyperlink_id || undefined,
|
|
443
452
|
comment_ids: c_ids.length > 0 ? c_ids : undefined,
|
|
444
453
|
part_index: this._current_part_index,
|
|
454
|
+
run_offset: r_off,
|
|
445
455
|
};
|
|
446
456
|
this.spans.push(s);
|
|
447
457
|
this._text_chunks.push(txt);
|
|
@@ -473,7 +483,8 @@ export class DocumentMapper {
|
|
|
473
483
|
|
|
474
484
|
if (item instanceof Run) {
|
|
475
485
|
const [prefix, suffix] = get_run_style_markers(item, native_heading);
|
|
476
|
-
|
|
486
|
+
// [kind, text, run, run_offset]
|
|
487
|
+
const run_parts: [string, string, Run | null, number][] = [];
|
|
477
488
|
const text = get_run_text(item);
|
|
478
489
|
|
|
479
490
|
if (leading_strip_active) {
|
|
@@ -481,20 +492,50 @@ export class DocumentMapper {
|
|
|
481
492
|
leading_strip_active = false;
|
|
482
493
|
}
|
|
483
494
|
|
|
495
|
+
// run_local tracks each real part's offset within the run's projected
|
|
496
|
+
// text, so spans resolve back to exact run positions even when one
|
|
497
|
+
// run backs several spans.
|
|
498
|
+
let run_local = 0;
|
|
499
|
+
const append_wrapped = (segment: string): void => {
|
|
500
|
+
// Boundary whitespace stays OUTSIDE the style markers —
|
|
501
|
+
// `**The Supplier **` is malformed Markdown (QA 2026-07-19 F-03).
|
|
502
|
+
// Must mirror apply_formatting_to_segments exactly (the Virtual
|
|
503
|
+
// Text contract).
|
|
504
|
+
const [lead, core, trail] = split_boundary_whitespace(segment);
|
|
505
|
+
if (!core) {
|
|
506
|
+
run_parts.push(["real", segment, item, run_local]);
|
|
507
|
+
run_local += segment.length;
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (lead) {
|
|
511
|
+
run_parts.push(["real", lead, item, run_local]);
|
|
512
|
+
run_local += lead.length;
|
|
513
|
+
}
|
|
514
|
+
if (prefix) run_parts.push(["virtual", prefix, null, 0]);
|
|
515
|
+
run_parts.push(["real", core, item, run_local]);
|
|
516
|
+
run_local += core.length;
|
|
517
|
+
if (suffix) run_parts.push(["virtual", suffix, null, 0]);
|
|
518
|
+
if (trail) {
|
|
519
|
+
run_parts.push(["real", trail, item, run_local]);
|
|
520
|
+
run_local += trail.length;
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
484
524
|
if (text.includes("\n") && (prefix || suffix)) {
|
|
485
525
|
const parts = text.split("\n");
|
|
486
526
|
for (let idx = 0; idx < parts.length; idx++) {
|
|
487
|
-
if (idx > 0)
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
run_parts.push(["real", parts[idx], item]);
|
|
491
|
-
if (suffix) run_parts.push(["virtual", suffix, null]);
|
|
527
|
+
if (idx > 0) {
|
|
528
|
+
run_parts.push(["real", "\n", item, run_local]);
|
|
529
|
+
run_local += 1;
|
|
492
530
|
}
|
|
531
|
+
if (parts[idx]) append_wrapped(parts[idx]);
|
|
493
532
|
}
|
|
533
|
+
} else if ((prefix || suffix) && text) {
|
|
534
|
+
append_wrapped(text);
|
|
494
535
|
} else {
|
|
495
|
-
if (prefix) run_parts.push(["virtual", prefix, null]);
|
|
496
|
-
if (text) run_parts.push(["real", text, item]);
|
|
497
|
-
if (suffix) run_parts.push(["virtual", suffix, null]);
|
|
536
|
+
if (prefix) run_parts.push(["virtual", prefix, null, 0]);
|
|
537
|
+
if (text) run_parts.push(["real", text, item, 0]);
|
|
538
|
+
if (suffix) run_parts.push(["virtual", suffix, null, 0]);
|
|
498
539
|
}
|
|
499
540
|
|
|
500
541
|
if (this.clean_view && Object.keys(active_del).length > 0) {
|
|
@@ -542,7 +583,7 @@ export class DocumentMapper {
|
|
|
542
583
|
}
|
|
543
584
|
|
|
544
585
|
const curr_comment_ids = Array.from(active_ids);
|
|
545
|
-
for (const [kind, txt, r_obj] of run_parts) {
|
|
586
|
+
for (const [kind, txt, r_obj, r_off] of run_parts) {
|
|
546
587
|
if (
|
|
547
588
|
skip_leading_prefix &&
|
|
548
589
|
kind === "virtual" &&
|
|
@@ -555,6 +596,7 @@ export class DocumentMapper {
|
|
|
555
596
|
kind,
|
|
556
597
|
txt,
|
|
557
598
|
r_obj,
|
|
599
|
+
r_off,
|
|
558
600
|
curr_ins_id,
|
|
559
601
|
curr_del_id,
|
|
560
602
|
curr_comment_ids,
|
|
@@ -566,11 +608,12 @@ export class DocumentMapper {
|
|
|
566
608
|
current_wrappers = new_wrappers;
|
|
567
609
|
current_style = new_style;
|
|
568
610
|
const curr_comment_ids = Array.from(active_ids);
|
|
569
|
-
for (const [kind, txt, r_obj] of run_parts) {
|
|
611
|
+
for (const [kind, txt, r_obj, r_off] of run_parts) {
|
|
570
612
|
pending_runs.push([
|
|
571
613
|
kind,
|
|
572
614
|
txt,
|
|
573
615
|
r_obj,
|
|
616
|
+
r_off,
|
|
574
617
|
curr_ins_id,
|
|
575
618
|
curr_del_id,
|
|
576
619
|
curr_comment_ids,
|
|
@@ -1108,50 +1151,59 @@ export class DocumentMapper {
|
|
|
1108
1151
|
);
|
|
1109
1152
|
if (affected_spans.length === 0) return [];
|
|
1110
1153
|
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1154
|
+
const real_spans = affected_spans.filter((s) => s.run !== null);
|
|
1155
|
+
if (real_spans.length === 0) return [];
|
|
1156
|
+
|
|
1157
|
+
// One run may back several spans (boundary whitespace hoisted outside
|
|
1158
|
+
// style markers projects a run as lead/core/trail spans, QA 2026-07-19
|
|
1159
|
+
// F-03): deduplicate by identity or the run would be split and wrapped
|
|
1160
|
+
// once per span.
|
|
1161
|
+
const working_runs: Run[] = [];
|
|
1162
|
+
for (const s of real_spans) {
|
|
1163
|
+
if (!working_runs.some((r) => r === s.run)) {
|
|
1164
|
+
working_runs.push(s.run!);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1115
1167
|
|
|
1116
1168
|
let dom_modified = false;
|
|
1117
1169
|
|
|
1118
|
-
|
|
1170
|
+
// 1. Start Split — all local offsets are run-relative: span-relative
|
|
1171
|
+
// position plus the span's own offset within the run.
|
|
1172
|
+
const first_real_span = real_spans[0];
|
|
1119
1173
|
let start_split_adjustment = 0;
|
|
1120
1174
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
start_split_adjustment = local_start;
|
|
1175
|
+
const local_start =
|
|
1176
|
+
start_idx - first_real_span.start + (first_real_span.run_offset || 0);
|
|
1177
|
+
if (local_start > 0) {
|
|
1178
|
+
const split_source = working_runs[0];
|
|
1179
|
+
const [, right_run] = this._split_run_at_index(
|
|
1180
|
+
split_source,
|
|
1181
|
+
local_start,
|
|
1182
|
+
);
|
|
1183
|
+
for (let w = 0; w < working_runs.length; w++) {
|
|
1184
|
+
if (working_runs[w] === split_source) working_runs[w] = right_run;
|
|
1132
1185
|
}
|
|
1186
|
+
dom_modified = true;
|
|
1187
|
+
start_split_adjustment = local_start;
|
|
1133
1188
|
}
|
|
1134
1189
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
let overlap_end = Math.min(last_real_span.end, end_idx);
|
|
1143
|
-
let local_end = overlap_end - last_real_span.start;
|
|
1190
|
+
// 2. End Split
|
|
1191
|
+
const last_real_span = real_spans[real_spans.length - 1];
|
|
1192
|
+
const is_same_run = first_real_span.run === last_real_span.run;
|
|
1193
|
+
const run_to_split = working_runs[working_runs.length - 1];
|
|
1194
|
+
const overlap_end = Math.min(last_real_span.end, end_idx);
|
|
1195
|
+
let local_end =
|
|
1196
|
+
overlap_end - last_real_span.start + (last_real_span.run_offset || 0);
|
|
1144
1197
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1198
|
+
if (is_same_run && start_split_adjustment > 0) {
|
|
1199
|
+
local_end -= start_split_adjustment;
|
|
1200
|
+
}
|
|
1148
1201
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
}
|
|
1202
|
+
const run_text = get_run_text(run_to_split);
|
|
1203
|
+
if (local_end > 0 && local_end < run_text.length) {
|
|
1204
|
+
const [left_run] = this._split_run_at_index(run_to_split, local_end);
|
|
1205
|
+
working_runs[working_runs.length - 1] = left_run;
|
|
1206
|
+
dom_modified = true;
|
|
1155
1207
|
}
|
|
1156
1208
|
|
|
1157
1209
|
if (dom_modified && rebuild_map) {
|
|
@@ -1206,7 +1258,7 @@ export class DocumentMapper {
|
|
|
1206
1258
|
}
|
|
1207
1259
|
return [null, span.paragraph];
|
|
1208
1260
|
} else {
|
|
1209
|
-
const offset = index - span.start;
|
|
1261
|
+
const offset = index - span.start + (span.run_offset || 0);
|
|
1210
1262
|
const [left] = this._split_run_at_index(span.run, offset);
|
|
1211
1263
|
if (rebuild_map) this._build_map();
|
|
1212
1264
|
return [left, span.paragraph];
|