@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/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 ![alt](docx-image:N).
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
- const run_parts: [string, string, Run | null][] = [];
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) run_parts.push(["real", "\n", item]);
488
- if (parts[idx]) {
489
- if (prefix) run_parts.push(["virtual", prefix, null]);
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 working_runs = affected_spans
1112
- .filter((s) => s.run !== null)
1113
- .map((s) => s.run!);
1114
- if (working_runs.length === 0) return [];
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
- const first_real_span = affected_spans.find((s) => s.run !== null);
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
- if (first_real_span) {
1122
- const local_start = start_idx - first_real_span.start;
1123
- if (local_start > 0) {
1124
- const idx_in_working = 0;
1125
- const [, right_run] = this._split_run_at_index(
1126
- working_runs[idx_in_working],
1127
- local_start,
1128
- );
1129
- working_runs[idx_in_working] = right_run;
1130
- dom_modified = true;
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
- const last_real_span = [...affected_spans]
1136
- .reverse()
1137
- .find((s) => s.run !== null);
1138
-
1139
- if (last_real_span) {
1140
- const is_same_run = first_real_span === last_real_span;
1141
- const run_to_split = working_runs[working_runs.length - 1];
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
- if (is_same_run && start_split_adjustment > 0) {
1146
- local_end -= start_split_adjustment;
1147
- }
1198
+ if (is_same_run && start_split_adjustment > 0) {
1199
+ local_end -= start_split_adjustment;
1200
+ }
1148
1201
 
1149
- const run_text = get_run_text(run_to_split);
1150
- if (local_end > 0 && local_end < run_text.length) {
1151
- const [left_run] = this._split_run_at_index(run_to_split, local_end);
1152
- working_runs[working_runs.length - 1] = left_run;
1153
- dom_modified = true;
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];