1use crate::entities::{CharVerticalAlignment, UnderlineStyle};
25use serde::{Deserialize, Serialize};
26use thiserror::Error;
27
28#[derive(Debug, Clone, PartialEq, Eq, Error)]
36pub enum FormatRunError {
37 #[error("byte range {start}..{end} is reversed")]
38 ReversedRange { start: u32, end: u32 },
39
40 #[error(
41 "replacement run {run_start}..{run_end} falls outside the spliced range \
42 {range_start}..{range_end}"
43 )]
44 ReplacementOutsideRange {
45 run_start: u32,
46 run_end: u32,
47 range_start: u32,
48 range_end: u32,
49 },
50
51 #[error("run {start}..{end} is empty or reversed")]
52 EmptyRun { start: u32, end: u32 },
53
54 #[error("runs overlap or are out of order at index {index}: {left:?} then {right:?}")]
55 RunsOverlap {
56 index: usize,
57 left: Box<FormatRun>,
58 right: Box<FormatRun>,
59 },
60
61 #[error("adjacent runs with identical formatting were left uncoalesced at index {index}")]
62 RunsNotCoalesced { index: usize },
63
64 #[error("run {start}..{end} runs past the end of the block's {text_len} bytes")]
65 RunPastEndOfBlock {
66 start: u32,
67 end: u32,
68 text_len: usize,
69 },
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
83pub enum ReplaceFormatPolicy {
84 #[default]
95 InheritPreceding,
96
97 PreserveIfFullyCovered,
102
103 KeepDominantRun,
109
110 PreserveNothing,
112}
113
114#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)]
116pub enum InlineContent {
117 #[default]
118 Empty,
119 Text(String),
120 FootnoteRef {
133 label: String,
134 },
135 Image {
136 name: String,
137 #[serde(default)]
148 alt: String,
149 width: i64,
150 height: i64,
151 quality: i64,
152 },
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
164pub struct InlineSegment {
165 pub content: InlineContent,
166 pub fmt_font_family: Option<String>,
167 pub fmt_font_point_size: Option<i64>,
168 pub fmt_font_weight: Option<i64>,
169 pub fmt_font_bold: Option<bool>,
170 pub fmt_font_italic: Option<bool>,
171 pub fmt_font_underline: Option<bool>,
172 pub fmt_font_overline: Option<bool>,
173 pub fmt_font_strikeout: Option<bool>,
174 pub fmt_letter_spacing: Option<i64>,
175 pub fmt_word_spacing: Option<i64>,
176 pub fmt_anchor_href: Option<String>,
177 pub fmt_anchor_names: Vec<String>,
178 pub fmt_is_anchor: Option<bool>,
179 pub fmt_tooltip: Option<String>,
180 pub fmt_underline_style: Option<UnderlineStyle>,
181 pub fmt_vertical_alignment: Option<CharVerticalAlignment>,
182}
183
184#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
189pub struct CharacterFormat {
190 pub font_family: Option<String>,
191 pub font_point_size: Option<i64>,
192 pub font_weight: Option<i64>,
193 pub font_bold: Option<bool>,
194 pub font_italic: Option<bool>,
195 pub font_underline: Option<bool>,
196 pub font_overline: Option<bool>,
197 pub font_strikeout: Option<bool>,
198 pub letter_spacing: Option<i64>,
199 pub word_spacing: Option<i64>,
200 pub anchor_href: Option<String>,
201 pub anchor_names: Vec<String>,
202 pub is_anchor: Option<bool>,
203 pub tooltip: Option<String>,
204 pub underline_style: Option<UnderlineStyle>,
205 pub vertical_alignment: Option<CharVerticalAlignment>,
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
212pub struct FormatRun {
213 pub byte_start: u32,
214 pub byte_end: u32,
215 pub format: CharacterFormat,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
225pub struct ImageAnchor {
226 pub byte_offset: u32,
227 pub name: String,
228 #[serde(default)]
231 pub alt: String,
232 pub width: i64,
233 pub height: i64,
234 pub quality: i64,
235 pub format: CharacterFormat,
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub struct FootnoteRefAnchor {
253 pub byte_offset: u32,
254 pub label: String,
257 pub format: CharacterFormat,
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum BlockAnchor<'a> {
268 Image(&'a ImageAnchor),
269 FootnoteRef(&'a FootnoteRefAnchor),
270}
271
272impl BlockAnchor<'_> {
273 pub fn byte_offset(&self) -> u32 {
275 match self {
276 BlockAnchor::Image(i) => i.byte_offset,
277 BlockAnchor::FootnoteRef(f) => f.byte_offset,
278 }
279 }
280}
281
282pub fn block_anchors<'a>(
288 images: &'a [ImageAnchor],
289 footnote_refs: &'a [FootnoteRefAnchor],
290) -> Vec<BlockAnchor<'a>> {
291 let mut anchors: Vec<BlockAnchor<'a>> = Vec::with_capacity(images.len() + footnote_refs.len());
292 anchors.extend(images.iter().map(BlockAnchor::Image));
293 anchors.extend(footnote_refs.iter().map(BlockAnchor::FootnoteRef));
294 anchors.sort_by_key(|a| a.byte_offset());
295 anchors
296}
297
298pub fn debug_assert_well_formed(runs: &[FormatRun], block_text_len: usize) {
311 if cfg!(debug_assertions)
313 && let Err(e) = check_well_formed(runs, block_text_len)
314 {
315 debug_assert!(false, "format runs are malformed: {e}");
316 }
317}
318
319pub fn check_well_formed(runs: &[FormatRun], block_text_len: usize) -> Result<(), FormatRunError> {
326 if runs.is_empty() {
327 return Ok(());
328 }
329 for run in runs {
330 if run.byte_start >= run.byte_end {
331 return Err(FormatRunError::EmptyRun {
332 start: run.byte_start,
333 end: run.byte_end,
334 });
335 }
336 }
337 for i in 0..runs.len() - 1 {
338 if runs[i].byte_end > runs[i + 1].byte_start {
339 return Err(FormatRunError::RunsOverlap {
340 index: i,
341 left: Box::new(runs[i].clone()),
342 right: Box::new(runs[i + 1].clone()),
343 });
344 }
345 if runs[i].byte_end == runs[i + 1].byte_start && runs[i].format == runs[i + 1].format {
346 return Err(FormatRunError::RunsNotCoalesced { index: i });
347 }
348 }
349 let last = runs.last().expect("non-empty");
350 if last.byte_end as usize > block_text_len {
351 return Err(FormatRunError::RunPastEndOfBlock {
352 start: last.byte_start,
353 end: last.byte_end,
354 text_len: block_text_len,
355 });
356 }
357 Ok(())
358}
359
360pub fn coalesce_in_place(runs: &mut Vec<FormatRun>) {
362 if runs.len() < 2 {
363 return;
364 }
365 let mut write = 0usize;
366 for read in 1..runs.len() {
367 if runs[write].byte_end == runs[read].byte_start && runs[write].format == runs[read].format
368 {
369 runs[write].byte_end = runs[read].byte_end;
370 } else {
371 write += 1;
372 if write != read {
373 runs[write] = runs[read].clone();
374 }
375 }
376 }
377 runs.truncate(write + 1);
378}
379
380pub fn splice_range(
390 runs: &mut Vec<FormatRun>,
391 range: std::ops::Range<u32>,
392 replacement: Vec<FormatRun>,
393) {
394 if let Err(e) = try_splice_range(runs, range, replacement) {
395 debug_assert!(false, "splice_range contract violated: {e}");
404 }
405}
406
407pub fn try_splice_range(
412 runs: &mut Vec<FormatRun>,
413 range: std::ops::Range<u32>,
414 replacement: Vec<FormatRun>,
415) -> Result<(), FormatRunError> {
416 if range.start > range.end {
417 return Err(FormatRunError::ReversedRange {
418 start: range.start,
419 end: range.end,
420 });
421 }
422 for r in &replacement {
423 if r.byte_start >= r.byte_end {
424 return Err(FormatRunError::EmptyRun {
425 start: r.byte_start,
426 end: r.byte_end,
427 });
428 }
429 if r.byte_start < range.start || r.byte_end > range.end {
430 return Err(FormatRunError::ReplacementOutsideRange {
431 run_start: r.byte_start,
432 run_end: r.byte_end,
433 range_start: range.start,
434 range_end: range.end,
435 });
436 }
437 }
438 for i in 1..replacement.len() {
439 if replacement[i - 1].byte_end > replacement[i].byte_start {
440 return Err(FormatRunError::RunsOverlap {
441 index: i - 1,
442 left: Box::new(replacement[i - 1].clone()),
443 right: Box::new(replacement[i].clone()),
444 });
445 }
446 }
447
448 let mut result: Vec<FormatRun> = Vec::with_capacity(runs.len() + replacement.len());
449
450 for run in runs.iter() {
452 if run.byte_end <= range.start {
453 result.push(run.clone());
454 } else if run.byte_start < range.start {
455 result.push(FormatRun {
457 byte_start: run.byte_start,
458 byte_end: range.start,
459 format: run.format.clone(),
460 });
461 }
462 }
463
464 result.extend(replacement);
466
467 for run in runs.iter() {
469 if run.byte_start >= range.end {
470 result.push(run.clone());
471 } else if run.byte_end > range.end {
472 result.push(FormatRun {
474 byte_start: range.end,
475 byte_end: run.byte_end,
476 format: run.format.clone(),
477 });
478 }
479 }
480
481 coalesce_in_place(&mut result);
482 *runs = result;
483 Ok(())
484}
485
486pub fn capture_runs_in_range(runs: &[FormatRun], start: u32, end: u32) -> Vec<FormatRun> {
497 let mut out = Vec::new();
498 for run in runs {
499 if run.byte_end <= start || run.byte_start >= end {
500 continue;
501 }
502 let clipped_start = std::cmp::max(run.byte_start, start);
503 let clipped_end = std::cmp::min(run.byte_end, end);
504 if clipped_start < clipped_end {
505 out.push(FormatRun {
506 byte_start: clipped_start,
507 byte_end: clipped_end,
508 format: run.format.clone(),
509 });
510 }
511 }
512 out
513}
514
515pub fn capture_image_formats_in_range(
519 images: &[ImageAnchor],
520 start: u32,
521 end: u32,
522) -> Vec<(u32, CharacterFormat)> {
523 let mut out = Vec::new();
524 for img in images {
525 if img.byte_offset >= start && img.byte_offset < end {
526 out.push((img.byte_offset, img.format.clone()));
527 }
528 }
529 out
530}
531
532pub fn shift_after(runs: &mut [FormatRun], threshold: u32, delta: i32) {
540 for run in runs.iter_mut() {
541 if run.byte_start >= threshold {
542 let new_start = (run.byte_start as i64) + (delta as i64);
543 let new_end = (run.byte_end as i64) + (delta as i64);
544 debug_assert!(new_start >= 0 && new_end >= new_start);
545 run.byte_start = new_start as u32;
546 run.byte_end = new_end as u32;
547 }
548 }
549}
550
551pub fn synth_element_id(block_id: u64, byte_start: u32) -> u64 {
567 const SYNTH_TAG: u64 = 0x4000_0000_0000_0000;
568 SYNTH_TAG | ((block_id & 0x3FFF_FFFF) << 32) | (byte_start as u64)
569}
570
571pub fn shift_images_after(images: &mut [ImageAnchor], threshold: u32, delta: i32) {
574 for img in images.iter_mut() {
575 if img.byte_offset >= threshold {
576 let new_off = (img.byte_offset as i64) + (delta as i64);
577 debug_assert!(new_off >= 0);
578 img.byte_offset = new_off as u32;
579 }
580 }
581}
582
583pub fn shift_runs_for_insert(runs: &mut [FormatRun], byte_offset: u32, inserted_bytes: u32) {
594 if inserted_bytes == 0 {
595 return;
596 }
597 for run in runs.iter_mut() {
598 if run.byte_start >= byte_offset {
599 run.byte_start += inserted_bytes;
600 run.byte_end += inserted_bytes;
601 } else if run.byte_end >= byte_offset {
602 run.byte_end += inserted_bytes;
606 }
607 }
608}
609
610pub fn shift_runs_for_delete(runs: &mut Vec<FormatRun>, byte_start: u32, byte_end: u32) {
615 if byte_end <= byte_start {
616 return;
617 }
618 splice_range(runs, byte_start..byte_end, Vec::new());
619 let delta = (byte_end - byte_start) as i32;
620 shift_after(runs, byte_end, -delta);
621 coalesce_in_place(runs);
624}
625
626pub fn shift_runs_for_replace(
644 runs: &mut Vec<FormatRun>,
645 byte_start: u32,
646 byte_end: u32,
647 replacement_bytes: u32,
648 policy: ReplaceFormatPolicy,
649) -> Result<(), FormatRunError> {
650 if byte_end < byte_start {
651 return Err(FormatRunError::ReversedRange {
652 start: byte_start,
653 end: byte_end,
654 });
655 }
656
657 let destroys_formatting = byte_end > byte_start;
669 let override_format: Option<Option<CharacterFormat>> = match policy {
670 ReplaceFormatPolicy::InheritPreceding => None,
671 ReplaceFormatPolicy::PreserveNothing => Some(None),
672 ReplaceFormatPolicy::PreserveIfFullyCovered => {
673 covering_format(runs, byte_start, byte_end).map(Some)
674 }
675 ReplaceFormatPolicy::KeepDominantRun if destroys_formatting => {
676 Some(dominant_format(runs, byte_start, byte_end))
677 }
678 ReplaceFormatPolicy::KeepDominantRun => None,
679 };
680
681 shift_runs_for_delete(runs, byte_start, byte_end);
683 shift_runs_for_insert(runs, byte_start, replacement_bytes);
684
685 if let Some(format) = override_format
687 && replacement_bytes > 0
688 {
689 let span = byte_start..byte_start + replacement_bytes;
690 let replacement = match format {
691 Some(format) => vec![FormatRun {
692 byte_start: span.start,
693 byte_end: span.end,
694 format,
695 }],
696 None => Vec::new(),
697 };
698 try_splice_range(runs, span, replacement)?;
699 }
700 Ok(())
701}
702
703fn covering_format(runs: &[FormatRun], start: u32, end: u32) -> Option<CharacterFormat> {
709 if end <= start {
710 return None;
711 }
712 runs.iter()
713 .find(|r| r.byte_start <= start && r.byte_end >= end)
714 .map(|r| r.format.clone())
715}
716
717fn dominant_format(runs: &[FormatRun], start: u32, end: u32) -> Option<CharacterFormat> {
724 if end <= start {
725 return None;
726 }
727 let span = u64::from(end - start);
728 let mut covered = 0u64;
729 let mut best: Option<(u64, &FormatRun)> = None;
730
731 for r in runs {
732 let lo = r.byte_start.max(start);
733 let hi = r.byte_end.min(end);
734 if hi <= lo {
735 continue;
736 }
737 let overlap = u64::from(hi - lo);
738 covered += overlap;
739 if best.is_none_or(|(best_overlap, _)| overlap > best_overlap) {
742 best = Some((overlap, r));
743 }
744 }
745
746 let plain = span - covered;
747 match best {
748 Some((overlap, run)) if overlap >= plain => Some(run.format.clone()),
749 _ => None,
750 }
751}
752
753pub fn shift_footnote_refs_for_insert(
758 notes: &mut [FootnoteRefAnchor],
759 byte_offset: u32,
760 inserted_bytes: u32,
761) {
762 if inserted_bytes == 0 {
763 return;
764 }
765 for note in notes.iter_mut() {
766 if note.byte_offset >= byte_offset {
767 note.byte_offset += inserted_bytes;
768 }
769 }
770}
771
772pub fn shift_images_for_insert(images: &mut [ImageAnchor], byte_offset: u32, inserted_bytes: u32) {
773 if inserted_bytes == 0 {
774 return;
775 }
776 for img in images.iter_mut() {
777 if img.byte_offset >= byte_offset {
778 img.byte_offset += inserted_bytes;
779 }
780 }
781}
782
783pub fn shift_images_for_delete(
788 images: &mut Vec<ImageAnchor>,
789 byte_start: u32,
790 byte_end: u32,
791) -> usize {
792 if byte_end <= byte_start {
793 return 0;
794 }
795 let before = images.len();
796 images.retain(|i| !(i.byte_offset >= byte_start && i.byte_offset < byte_end));
797 let removed = before - images.len();
798 let delta = (byte_end - byte_start) as i32;
799 shift_images_after(images, byte_end, -delta);
800 removed
801}
802
803pub fn logical_offset_to_byte(plain_text: &str, _images: &[ImageAnchor], char_offset: i64) -> u32 {
824 if char_offset <= 0 {
825 return 0;
826 }
827 plain_text
828 .char_indices()
829 .nth(char_offset as usize)
830 .map(|(b, _)| b as u32)
831 .unwrap_or(plain_text.len() as u32)
832}
833
834pub fn split_runs_at(runs: &[FormatRun], byte_offset: u32) -> (Vec<FormatRun>, Vec<FormatRun>) {
839 let mut left = Vec::new();
840 let mut right = Vec::new();
841 for run in runs {
842 if run.byte_end <= byte_offset {
843 left.push(run.clone());
844 } else if run.byte_start >= byte_offset {
845 right.push(FormatRun {
846 byte_start: run.byte_start - byte_offset,
847 byte_end: run.byte_end - byte_offset,
848 format: run.format.clone(),
849 });
850 } else {
851 left.push(FormatRun {
852 byte_start: run.byte_start,
853 byte_end: byte_offset,
854 format: run.format.clone(),
855 });
856 right.push(FormatRun {
857 byte_start: 0,
858 byte_end: run.byte_end - byte_offset,
859 format: run.format.clone(),
860 });
861 }
862 }
863 (left, right)
864}
865
866pub fn split_footnote_refs_at(
872 notes: &[FootnoteRefAnchor],
873 byte_offset: u32,
874) -> (Vec<FootnoteRefAnchor>, Vec<FootnoteRefAnchor>) {
875 let mut left = Vec::new();
876 let mut right = Vec::new();
877 for note in notes {
878 if note.byte_offset < byte_offset {
879 left.push(note.clone());
880 } else {
881 let mut new = note.clone();
882 new.byte_offset -= byte_offset;
883 right.push(new);
884 }
885 }
886 (left, right)
887}
888
889pub fn split_images_at(
890 images: &[ImageAnchor],
891 byte_offset: u32,
892) -> (Vec<ImageAnchor>, Vec<ImageAnchor>) {
893 let mut left = Vec::new();
894 let mut right = Vec::new();
895 for img in images {
896 if img.byte_offset < byte_offset {
897 left.push(img.clone());
898 } else {
899 let mut new = img.clone();
900 new.byte_offset -= byte_offset;
901 right.push(new);
902 }
903 }
904 (left, right)
905}
906
907pub fn character_format_from_segment(seg: &InlineSegment) -> CharacterFormat {
913 CharacterFormat {
914 font_family: seg.fmt_font_family.clone(),
915 font_point_size: seg.fmt_font_point_size,
916 font_weight: seg.fmt_font_weight,
917 font_bold: seg.fmt_font_bold,
918 font_italic: seg.fmt_font_italic,
919 font_underline: seg.fmt_font_underline,
920 font_overline: seg.fmt_font_overline,
921 font_strikeout: seg.fmt_font_strikeout,
922 letter_spacing: seg.fmt_letter_spacing,
923 word_spacing: seg.fmt_word_spacing,
924 anchor_href: seg.fmt_anchor_href.clone(),
925 anchor_names: seg.fmt_anchor_names.clone(),
926 is_anchor: seg.fmt_is_anchor,
927 tooltip: seg.fmt_tooltip.clone(),
928 underline_style: seg.fmt_underline_style.clone(),
929 vertical_alignment: seg.fmt_vertical_alignment.clone(),
930 }
931}
932
933pub fn apply_character_format_to_segment(seg: &mut InlineSegment, fmt: &CharacterFormat) {
935 seg.fmt_font_family = fmt.font_family.clone();
936 seg.fmt_font_point_size = fmt.font_point_size;
937 seg.fmt_font_weight = fmt.font_weight;
938 seg.fmt_font_bold = fmt.font_bold;
939 seg.fmt_font_italic = fmt.font_italic;
940 seg.fmt_font_underline = fmt.font_underline;
941 seg.fmt_font_overline = fmt.font_overline;
942 seg.fmt_font_strikeout = fmt.font_strikeout;
943 seg.fmt_letter_spacing = fmt.letter_spacing;
944 seg.fmt_word_spacing = fmt.word_spacing;
945 seg.fmt_anchor_href = fmt.anchor_href.clone();
946 seg.fmt_anchor_names = fmt.anchor_names.clone();
947 seg.fmt_is_anchor = fmt.is_anchor;
948 seg.fmt_tooltip = fmt.tooltip.clone();
949 seg.fmt_underline_style = fmt.underline_style.clone();
950 seg.fmt_vertical_alignment = fmt.vertical_alignment.clone();
951}
952
953#[derive(Debug, Clone, PartialEq)]
958pub enum InlinePiece<'a> {
959 Text {
962 start: u32,
963 end: u32,
964 format: Option<&'a CharacterFormat>,
965 },
966 Image(&'a ImageAnchor),
969 FootnoteRef(&'a FootnoteRefAnchor),
972}
973
974pub fn merge_runs_and_anchors<'a>(
994 plain_text: &str,
995 runs: &'a [FormatRun],
996 anchors: &[BlockAnchor<'a>],
997) -> Vec<InlinePiece<'a>> {
998 let text_len = plain_text.len() as u32;
999 let mut out: Vec<InlinePiece<'a>> = Vec::new();
1000 let mut img_iter = anchors.iter().peekable();
1001 let mut cursor: u32 = 0;
1003
1004 fn sentinel_len(plain_text: &str, byte_offset: u32) -> u32 {
1019 let at = byte_offset as usize;
1020 if plain_text.len() >= at + 3 && plain_text.as_bytes()[at..at + 3] == [0xEF, 0xBF, 0xBC] {
1021 3
1022 } else {
1023 0
1024 }
1025 }
1026
1027 let push_text = |out: &mut Vec<InlinePiece<'a>>,
1028 start: u32,
1029 end: u32,
1030 format: Option<&'a CharacterFormat>| {
1031 if start < end {
1032 out.push(InlinePiece::Text { start, end, format });
1033 }
1034 };
1035
1036 fn piece<'a>(anchor: &BlockAnchor<'a>) -> InlinePiece<'a> {
1038 match *anchor {
1039 BlockAnchor::Image(i) => InlinePiece::Image(i),
1040 BlockAnchor::FootnoteRef(f) => InlinePiece::FootnoteRef(f),
1041 }
1042 }
1043
1044 for run in runs {
1045 while let Some(anchor) = img_iter.peek() {
1047 let at = anchor.byte_offset();
1048 if at >= run.byte_start {
1049 break;
1050 }
1051 push_text(&mut out, cursor, at, None);
1052 out.push(piece(anchor));
1053 cursor = cursor.max(at + sentinel_len(plain_text, at));
1054 img_iter.next();
1055 }
1056
1057 push_text(&mut out, cursor, run.byte_start, None);
1059 cursor = cursor.max(run.byte_start);
1060
1061 while let Some(anchor) = img_iter.peek() {
1066 let at = anchor.byte_offset();
1067 if at > run.byte_end {
1068 break;
1069 }
1070 push_text(&mut out, cursor, at, Some(&run.format));
1071 out.push(piece(anchor));
1072 cursor = cursor.max(at + sentinel_len(plain_text, at));
1073 img_iter.next();
1074 }
1075
1076 push_text(&mut out, cursor, run.byte_end, Some(&run.format));
1077 cursor = cursor.max(run.byte_end);
1078 }
1079
1080 for anchor in img_iter {
1082 let at = anchor.byte_offset();
1083 push_text(&mut out, cursor, at, None);
1084 out.push(piece(anchor));
1085 cursor = cursor.max(at + sentinel_len(plain_text, at));
1086 }
1087
1088 push_text(&mut out, cursor, text_len, None);
1089
1090 out
1091}
1092
1093pub fn inline_segments_view(
1103 plain_text: &str,
1104 runs: &[FormatRun],
1105 images: &[ImageAnchor],
1106 footnote_refs: &[FootnoteRefAnchor],
1107) -> Vec<InlineSegment> {
1108 let bytes = plain_text.as_bytes();
1109 let default_format = CharacterFormat::default();
1110
1111 merge_runs_and_anchors(plain_text, runs, &block_anchors(images, footnote_refs))
1112 .into_iter()
1113 .map(|piece| match piece {
1114 InlinePiece::Text { start, end, format } => {
1115 let slice = &bytes[start as usize..end as usize];
1116 let text = std::str::from_utf8(slice)
1117 .expect("block plain_text must be valid UTF-8")
1118 .to_string();
1119 let mut seg = InlineSegment {
1120 content: InlineContent::Text(text),
1121 ..Default::default()
1122 };
1123 apply_character_format_to_segment(&mut seg, format.unwrap_or(&default_format));
1124 seg
1125 }
1126 InlinePiece::Image(anchor) => {
1127 let mut seg = InlineSegment {
1128 content: InlineContent::Image {
1129 name: anchor.name.clone(),
1130 alt: anchor.alt.clone(),
1131 width: anchor.width,
1132 height: anchor.height,
1133 quality: anchor.quality,
1134 },
1135 ..Default::default()
1136 };
1137 apply_character_format_to_segment(&mut seg, &anchor.format);
1138 seg
1139 }
1140 InlinePiece::FootnoteRef(anchor) => {
1141 let mut seg = InlineSegment {
1142 content: InlineContent::FootnoteRef {
1143 label: anchor.label.clone(),
1144 },
1145 ..Default::default()
1146 };
1147 apply_character_format_to_segment(&mut seg, &anchor.format);
1148 seg
1149 }
1150 })
1151 .collect()
1152}
1153
1154#[cfg(test)]
1155mod tests {
1156 use super::*;
1157
1158 fn run(s: u32, e: u32, bold: bool) -> FormatRun {
1159 FormatRun {
1160 byte_start: s,
1161 byte_end: e,
1162 format: CharacterFormat {
1163 font_bold: Some(bold),
1164 ..Default::default()
1165 },
1166 }
1167 }
1168
1169 #[test]
1170 fn empty_runs_are_well_formed() {
1171 debug_assert_well_formed(&[], 0);
1172 debug_assert_well_formed(&[], 100);
1173 }
1174
1175 fn anchor(at: u32, name: &str) -> ImageAnchor {
1178 ImageAnchor {
1179 byte_offset: at,
1180 name: name.into(),
1181 alt: String::new(),
1182 width: 10,
1183 height: 10,
1184 quality: 100,
1185 format: CharacterFormat::default(),
1186 }
1187 }
1188
1189 fn fn_anchor(at: u32, label: &str) -> FootnoteRefAnchor {
1190 FootnoteRefAnchor {
1191 byte_offset: at,
1192 label: label.into(),
1193 format: CharacterFormat::default(),
1194 }
1195 }
1196
1197 fn shape(text: &str, runs: &[FormatRun], images: &[ImageAnchor]) -> String {
1201 shape_with(text, runs, images, &[])
1202 }
1203
1204 fn shape_with(
1205 text: &str,
1206 runs: &[FormatRun],
1207 images: &[ImageAnchor],
1208 notes: &[FootnoteRefAnchor],
1209 ) -> String {
1210 merge_runs_and_anchors(text, runs, &block_anchors(images, notes))
1211 .into_iter()
1212 .map(|p| match p {
1213 InlinePiece::Text { start, end, format } => {
1214 let s = &text[start as usize..end as usize];
1215 if format.is_some() {
1216 format!("*{s}*")
1217 } else {
1218 s.to_string()
1219 }
1220 }
1221 InlinePiece::Image(a) => format!("[{}]", a.name),
1222 InlinePiece::FootnoteRef(a) => format!("^{}^", a.label),
1223 })
1224 .collect::<Vec<_>>()
1225 .join("|")
1226 }
1227
1228 #[test]
1231 fn a_footnote_reference_inside_a_run_splits_it() {
1232 let text = "abcdef";
1233 let runs = [run(0, 6, true)];
1234 assert_eq!(
1235 shape_with(text, &runs, &[], &[fn_anchor(3, "n1")]),
1236 "*abc*|^n1^|*def*"
1237 );
1238 }
1239
1240 #[test]
1246 fn images_and_references_interleave_by_position() {
1247 let text = "abcdefgh";
1248 assert_eq!(
1249 shape_with(
1250 text,
1251 &[],
1252 &[anchor(6, "img")],
1253 &[fn_anchor(2, "early"), fn_anchor(7, "late")]
1254 ),
1255 "ab|^early^|cdef|[img]|g|^late^|h"
1256 );
1257 }
1258
1259 #[test]
1265 fn an_image_inside_a_run_splits_it_instead_of_jumping_to_the_end() {
1266 let text = "abcdef";
1267 let runs = [run(0, 6, true)];
1268 let images = [anchor(3, "img")];
1269 assert_eq!(shape(text, &runs, &images), "*abc*|[img]|*def*");
1270 }
1271
1272 #[test]
1273 fn an_image_on_a_run_start_boundary_stays_in_place() {
1274 let text = "abcdef";
1275 let runs = [run(3, 6, true)];
1276 assert_eq!(shape(text, &runs, &[anchor(3, "i")]), "abc|[i]|*def*");
1277 }
1278
1279 #[test]
1282 fn an_image_on_a_run_end_boundary_stays_in_place() {
1283 let text = "abcdef";
1284 let runs = [run(0, 3, true)];
1285 assert_eq!(shape(text, &runs, &[anchor(3, "i")]), "*abc*|[i]|def");
1286 }
1287
1288 #[test]
1289 fn an_image_between_two_runs_lands_between_them() {
1290 let text = "abcdef";
1291 let runs = [run(0, 3, true), run(3, 6, false)];
1292 let out = shape(text, &runs, &[anchor(3, "i")]);
1293 assert_eq!(out, "*abc*|[i]|*def*");
1294 }
1295
1296 #[test]
1297 fn several_images_inside_one_run_keep_their_order() {
1298 let text = "abcdefgh";
1299 let runs = [run(0, 8, true)];
1300 let images = [anchor(2, "a"), anchor(5, "b")];
1301 assert_eq!(shape(text, &runs, &images), "*ab*|[a]|*cde*|[b]|*fgh*");
1302 }
1303
1304 #[test]
1305 fn two_images_at_the_same_offset_both_survive_in_order() {
1306 let text = "abcd";
1307 let runs = [run(0, 4, true)];
1308 let images = [anchor(2, "a"), anchor(2, "b")];
1309 assert_eq!(shape(text, &runs, &images), "*ab*|[a]|[b]|*cd*");
1310 }
1311
1312 #[test]
1313 fn images_with_no_runs_at_all_are_ordered_with_their_gaps() {
1314 let text = "abcdef";
1315 let images = [anchor(0, "a"), anchor(3, "b"), anchor(6, "c")];
1316 assert_eq!(shape(text, &[], &images), "[a]|abc|[b]|def|[c]");
1317 }
1318
1319 #[test]
1320 fn text_uncovered_by_any_run_stays_unformatted() {
1321 let text = "abcdef";
1322 let runs = [run(2, 4, true)];
1323 assert_eq!(shape(text, &runs, &[]), "ab|*cd*|ef");
1324 }
1325
1326 #[test]
1327 fn a_block_with_neither_runs_nor_images_is_one_plain_piece() {
1328 assert_eq!(shape("abc", &[], &[]), "abc");
1329 assert_eq!(shape("", &[], &[]), "");
1330 }
1331
1332 #[test]
1336 fn every_byte_is_emitted_exactly_once_and_in_order() {
1337 let text = "abcdefghij";
1338 let arrangements: [(&[FormatRun], &[ImageAnchor]); 6] = [
1339 (&[], &[]),
1340 (&[run(0, 10, true)], &[anchor(5, "m")]),
1341 (&[run(2, 5, true), run(5, 8, false)], &[anchor(5, "m")]),
1342 (&[run(2, 5, true)], &[anchor(0, "a"), anchor(10, "z")]),
1343 (&[run(0, 3, true), run(7, 10, true)], &[anchor(3, "m")]),
1344 (
1345 &[run(1, 4, true), run(4, 9, false)],
1346 &[anchor(1, "a"), anchor(4, "b"), anchor(9, "c")],
1347 ),
1348 ];
1349 for (i, (runs, images)) in arrangements.iter().enumerate() {
1350 let pieces = merge_runs_and_anchors(text, runs, &block_anchors(images, &[]));
1351 let mut cursor = 0u32;
1352 let mut rebuilt = String::new();
1353 for piece in &pieces {
1354 if let InlinePiece::Text { start, end, .. } = piece {
1355 assert_eq!(*start, cursor, "arrangement {i}: gap or overlap");
1356 assert!(start < end, "arrangement {i}: empty piece emitted");
1357 rebuilt.push_str(&text[*start as usize..*end as usize]);
1358 cursor = *end;
1359 }
1360 }
1361 assert_eq!(cursor, text.len() as u32, "arrangement {i}: truncated");
1362 assert_eq!(rebuilt, text, "arrangement {i}");
1363 let img_count = pieces
1364 .iter()
1365 .filter(|p| matches!(p, InlinePiece::Image(_)))
1366 .count();
1367 assert_eq!(img_count, images.len(), "arrangement {i}: lost an image");
1368 }
1369 }
1370
1371 #[test]
1373 fn inline_segments_view_places_a_mid_run_image_correctly() {
1374 let segs = inline_segments_view("abcdef", &[run(0, 6, true)], &[anchor(3, "img")], &[]);
1375 assert_eq!(segs.len(), 3);
1376 assert!(matches!(&segs[0].content, InlineContent::Text(t) if t == "abc"));
1377 assert!(matches!(&segs[1].content, InlineContent::Image { name, .. } if name == "img"));
1378 assert!(matches!(&segs[2].content, InlineContent::Text(t) if t == "def"));
1379 assert_eq!(segs[0].fmt_font_bold, Some(true));
1381 assert_eq!(segs[2].fmt_font_bold, Some(true));
1382 }
1383
1384 #[test]
1385 fn inline_segments_view_carries_alt_text_through() {
1386 let mut a = anchor(1, "img");
1387 a.alt = "a black cat".into();
1388 let segs = inline_segments_view("ab", &[], &[a], &[]);
1389 let alt = segs.iter().find_map(|s| match &s.content {
1390 InlineContent::Image { alt, .. } => Some(alt.clone()),
1391 _ => None,
1392 });
1393 assert_eq!(alt.as_deref(), Some("a black cat"));
1394 }
1395
1396 #[test]
1397 fn coalesce_merges_adjacent_equal_runs() {
1398 let mut rs = vec![run(0, 5, true), run(5, 10, true), run(10, 15, false)];
1399 coalesce_in_place(&mut rs);
1400 assert_eq!(rs.len(), 2);
1401 assert_eq!(rs[0].byte_end, 10);
1402 }
1403
1404 #[test]
1405 fn coalesce_leaves_disjoint_runs_alone() {
1406 let mut rs = vec![run(0, 5, true), run(7, 10, true)];
1407 coalesce_in_place(&mut rs);
1408 assert_eq!(rs.len(), 2);
1409 }
1410
1411 #[test]
1412 fn splice_range_clips_straddling_runs() {
1413 let mut rs = vec![run(0, 20, true)];
1414 splice_range(&mut rs, 5..15, vec![run(5, 15, false)]);
1415 assert_eq!(rs.len(), 3);
1416 assert_eq!(rs[0].byte_end, 5);
1417 assert_eq!(rs[1].format.font_bold, Some(false));
1418 assert_eq!(rs[2].byte_start, 15);
1419 }
1420
1421 #[test]
1422 fn splice_range_empty_replacement_removes_inner_runs() {
1423 let mut rs = vec![run(0, 5, true), run(5, 10, false), run(10, 15, true)];
1424 splice_range(&mut rs, 5..10, vec![]);
1425 assert_eq!(rs.len(), 2);
1428 assert_eq!(rs[0].byte_end, 5);
1429 assert_eq!(rs[1].byte_start, 10);
1430 }
1431
1432 #[test]
1433 fn shift_after_moves_downstream() {
1434 let mut rs = vec![run(0, 5, true), run(10, 15, false)];
1435 shift_after(&mut rs, 5, 3);
1436 assert_eq!(rs[0].byte_start, 0); assert_eq!(rs[1].byte_start, 13);
1438 assert_eq!(rs[1].byte_end, 18);
1439 }
1440}
1441
1442#[cfg(test)]
1449mod replace_policy_tests {
1450 use super::*;
1451
1452 fn fmt(tag: &str) -> CharacterFormat {
1453 CharacterFormat {
1454 font_bold: Some(tag == "B"),
1455 font_italic: Some(tag == "I"),
1456 ..Default::default()
1457 }
1458 }
1459 fn r(start: u32, end: u32, tag: &str) -> FormatRun {
1460 FormatRun {
1461 byte_start: start,
1462 byte_end: end,
1463 format: fmt(tag),
1464 }
1465 }
1466 fn show(runs: &[FormatRun]) -> String {
1468 if runs.is_empty() {
1469 return "[]".to_string();
1470 }
1471 runs.iter()
1472 .map(|x| {
1473 let tag = if x.format.font_bold == Some(true) {
1474 "B"
1475 } else if x.format.font_italic == Some(true) {
1476 "I"
1477 } else {
1478 "p"
1479 };
1480 format!("{}..{}={tag}", x.byte_start, x.byte_end)
1481 })
1482 .collect::<Vec<_>>()
1483 .join(" ")
1484 }
1485 fn replace(
1486 runs: &[FormatRun],
1487 start: u32,
1488 end: u32,
1489 n: u32,
1490 policy: ReplaceFormatPolicy,
1491 ) -> Vec<FormatRun> {
1492 let mut runs = runs.to_vec();
1493 shift_runs_for_replace(&mut runs, start, end, n, policy).expect("valid replace");
1494 runs
1495 }
1496
1497 #[test]
1504 fn inherit_preceding_matches_the_historical_delete_then_insert() {
1505 let corpus: Vec<(&str, Vec<FormatRun>, u32, u32, u32)> = vec![
1506 ("run ends exactly at start", vec![r(0, 5, "B")], 5, 10, 3),
1507 ("run begins exactly at start", vec![r(5, 8, "B")], 5, 10, 3),
1508 (
1509 "run begins at start, outlives end",
1510 vec![r(5, 20, "B")],
1511 5,
1512 10,
1513 3,
1514 ),
1515 (
1516 "run straddles the whole range",
1517 vec![r(0, 20, "B")],
1518 5,
1519 10,
1520 3,
1521 ),
1522 ("no run touches the start", vec![r(12, 20, "B")], 5, 10, 3),
1523 ("bold tail inside the range", vec![r(9, 13, "B")], 5, 13, 4),
1524 ("pure delete", vec![r(0, 20, "B")], 5, 10, 0),
1525 ("pure insert", vec![r(0, 20, "B")], 5, 5, 3),
1526 (
1527 "same format either side coalesces",
1528 vec![r(0, 5, "B"), r(10, 15, "B")],
1529 5,
1530 10,
1531 3,
1532 ),
1533 (
1534 "different formats either side",
1535 vec![r(0, 5, "B"), r(10, 15, "I")],
1536 5,
1537 10,
1538 3,
1539 ),
1540 ("empty run list", vec![], 5, 10, 3),
1541 ("the only run is consumed", vec![r(5, 10, "B")], 5, 10, 3),
1542 (
1543 "replacement longer than the range",
1544 vec![r(0, 5, "B")],
1545 5,
1546 10,
1547 20,
1548 ),
1549 (
1550 "three runs straddled",
1551 vec![r(0, 3, "B"), r(3, 6, "I"), r(6, 9, "B")],
1552 2,
1553 7,
1554 4,
1555 ),
1556 (
1557 "gap between two same-format runs is deleted",
1558 vec![r(0, 5, "B"), r(8, 13, "B")],
1559 5,
1560 8,
1561 0,
1562 ),
1563 ];
1564
1565 for (name, runs, start, end, n) in corpus {
1566 let mut expected = runs.clone();
1568 shift_runs_for_delete(&mut expected, start, end);
1569 shift_runs_for_insert(&mut expected, start, n);
1570
1571 let got = replace(&runs, start, end, n, ReplaceFormatPolicy::InheritPreceding);
1572
1573 assert_eq!(
1574 show(&got),
1575 show(&expected),
1576 "InheritPreceding diverged from delete+insert for {name:?} \
1577 (replace {start}..{end}, n={n})\n before: {}\n historical: {}\n got: {}",
1578 show(&runs),
1579 show(&expected),
1580 show(&got),
1581 );
1582 }
1583 }
1584
1585 #[test]
1589 fn the_four_policies_diverge_on_a_partly_bold_name() {
1590 let runs = vec![r(5, 9, "B")];
1592 let (start, end, n) = (0, 9, 9); use ReplaceFormatPolicy::*;
1595 assert_eq!(
1596 show(&replace(&runs, start, end, n, InheritPreceding)),
1597 "[]",
1598 "the historical default destroys the bold — pinned, not endorsed"
1599 );
1600 assert_eq!(
1601 show(&replace(&runs, start, end, n, PreserveNothing)),
1602 "[]",
1603 "explicitly unformatted"
1604 );
1605 assert_eq!(
1606 show(&replace(&runs, start, end, n, PreserveIfFullyCovered)),
1607 "[]",
1608 "no SINGLE run covers 0..9 — it must fall back to inheritance, not guess"
1609 );
1610 assert_eq!(
1612 show(&replace(&runs, start, end, n, KeepDominantRun)),
1613 "[]",
1614 "plain covers more of the name than the bold does"
1615 );
1616
1617 let mostly_bold = vec![r(1, 9, "B")];
1619 assert_eq!(
1620 show(&replace(&mostly_bold, 0, 9, 9, KeepDominantRun)),
1621 "0..9=B",
1622 "bold covers 8 of 9 bytes — the rename must keep it"
1623 );
1624 }
1625
1626 #[test]
1630 fn fully_covered_means_a_single_run_not_a_gapless_union() {
1631 let two = vec![r(0, 3, "I"), r(3, 10, "B")];
1632 assert_eq!(
1633 show(&replace(
1634 &two,
1635 0,
1636 10,
1637 4,
1638 ReplaceFormatPolicy::PreserveIfFullyCovered
1639 )),
1640 "[]",
1641 "two different-format runs jointly spanning the range are not 'covered'; \
1642 with no run preceding the start, the fallback is unformatted"
1643 );
1644
1645 let one = vec![r(5, 20, "B")];
1648 assert_eq!(
1649 show(&replace(
1650 &one,
1651 5,
1652 10,
1653 3,
1654 ReplaceFormatPolicy::PreserveIfFullyCovered
1655 )),
1656 "5..18=B",
1657 "a single covering run keeps its format across the rename"
1658 );
1659 assert_eq!(
1660 show(&replace(
1661 &one,
1662 5,
1663 10,
1664 3,
1665 ReplaceFormatPolicy::InheritPreceding
1666 )),
1667 "8..18=B",
1668 "…which the default would have lost: the replacement lands unformatted"
1669 );
1670 }
1671
1672 #[test]
1675 fn a_partially_overlapping_run_does_not_count_as_covering() {
1676 let runs = vec![r(0, 8, "B")]; assert_eq!(
1678 show(&replace(
1679 &runs,
1680 5,
1681 12,
1682 4,
1683 ReplaceFormatPolicy::PreserveIfFullyCovered
1684 )),
1685 "0..9=B",
1686 "not covered → falls back to inheritance, which extends the preceding bold; \
1687 it must NOT format the whole replacement as though bold had covered it"
1688 );
1689 }
1690
1691 #[test]
1696 fn a_dominance_tie_between_two_runs_goes_to_the_earlier() {
1697 let runs = vec![r(0, 3, "B"), r(3, 6, "I")]; assert_eq!(
1699 show(&replace(
1700 &runs,
1701 0,
1702 6,
1703 4,
1704 ReplaceFormatPolicy::KeepDominantRun
1705 )),
1706 "0..4=B",
1707 "a true tie must resolve to the earlier run, not to whichever the iterator \
1708 happened to visit last"
1709 );
1710 }
1711
1712 #[test]
1715 fn a_dominance_tie_against_plain_text_keeps_the_formatting() {
1716 let runs = vec![r(4, 8, "B")]; assert_eq!(
1718 show(&replace(
1719 &runs,
1720 0,
1721 8,
1722 5,
1723 ReplaceFormatPolicy::KeepDominantRun
1724 )),
1725 "0..5=B",
1726 "an even split must keep the formatting rather than silently drop it"
1727 );
1728 }
1729
1730 #[test]
1734 fn an_empty_range_is_an_insert_and_no_coverage_policy_overrides_it() {
1735 let runs = vec![r(0, 5, "B"), r(5, 10, "I")];
1736 use ReplaceFormatPolicy::*;
1737 for policy in [InheritPreceding, PreserveIfFullyCovered, KeepDominantRun] {
1738 assert_eq!(
1739 show(&replace(&runs, 5, 5, 2, policy)),
1740 "0..7=B 7..12=I",
1741 "{policy:?}: typing at a boundary must inherit the run to the LEFT (Qt \
1742 convention) — an empty range destroyed no formatting, so there is \
1743 nothing for a coverage policy to override"
1744 );
1745 }
1746 assert_eq!(
1748 show(&replace(&runs, 5, 5, 2, PreserveNothing)),
1749 "0..5=B 7..12=I",
1750 "PreserveNothing asks for unformatted text, and means it even on an insert"
1751 );
1752 }
1753
1754 #[test]
1756 fn a_zero_width_zero_length_replace_is_the_identity() {
1757 let runs = vec![r(0, 5, "B"), r(7, 12, "I")];
1758 for policy in [
1759 ReplaceFormatPolicy::InheritPreceding,
1760 ReplaceFormatPolicy::PreserveIfFullyCovered,
1761 ReplaceFormatPolicy::KeepDominantRun,
1762 ReplaceFormatPolicy::PreserveNothing,
1763 ] {
1764 assert_eq!(
1765 show(&replace(&runs, 6, 6, 0, policy)),
1766 "0..5=B 7..12=I",
1767 "{policy:?} changed a no-op edit"
1768 );
1769 }
1770 }
1771
1772 #[test]
1776 fn preserve_nothing_fabricates_no_default_run() {
1777 let runs = vec![r(0, 5, "B")];
1778 let got = replace(&runs, 7, 9, 2, ReplaceFormatPolicy::PreserveNothing);
1779 assert_eq!(show(&got), "0..5=B", "no run may be invented for the gap");
1780 assert!(
1781 got.iter().all(|x| x.byte_start < 7 || x.byte_end > 9),
1782 "the replaced span must carry no run at all"
1783 );
1784 }
1785
1786 #[test]
1789 fn offsets_are_bytes_not_characters() {
1790 let runs = vec![r(0, 4, "B"), r(5, 9, "I")];
1792 let got = replace(&runs, 0, 4, 2, ReplaceFormatPolicy::KeepDominantRun);
1793 assert_eq!(
1794 show(&got),
1795 "0..2=B 3..7=I",
1796 "the trailing italic must shift back by the BYTE delta (4 -> 2 = -2)"
1797 );
1798 }
1799
1800 #[test]
1804 fn every_policy_leaves_the_runs_well_formed() {
1805 let setups: Vec<(Vec<FormatRun>, u32, u32, u32, usize)> = vec![
1806 (vec![r(0, 3, "B"), r(3, 6, "I"), r(6, 9, "B")], 2, 7, 4, 8),
1807 (vec![r(0, 5, "B"), r(8, 13, "B")], 5, 8, 0, 10),
1808 (vec![r(0, 5, "B"), r(7, 12, "B")], 8, 10, 2, 12),
1809 (vec![r(3, 7, "B")], 3, 7, 0, 6),
1810 (vec![], 2, 6, 3, 9),
1811 ];
1812 for (runs, start, end, n, text_len) in setups {
1813 for policy in [
1814 ReplaceFormatPolicy::InheritPreceding,
1815 ReplaceFormatPolicy::PreserveIfFullyCovered,
1816 ReplaceFormatPolicy::KeepDominantRun,
1817 ReplaceFormatPolicy::PreserveNothing,
1818 ] {
1819 let got = replace(&runs, start, end, n, policy);
1820 check_well_formed(&got, text_len).unwrap_or_else(|e| {
1821 panic!(
1822 "{policy:?} produced malformed runs from {} (replace {start}..{end}, \
1823 n={n}): {} — {e}",
1824 show(&runs),
1825 show(&got)
1826 )
1827 });
1828 }
1829 }
1830 }
1831
1832 #[test]
1836 fn an_untouched_gap_between_equal_runs_survives() {
1837 let runs = vec![r(0, 5, "B"), r(7, 12, "B")];
1838 assert_eq!(
1839 show(&replace(
1840 &runs,
1841 8,
1842 10,
1843 2,
1844 ReplaceFormatPolicy::InheritPreceding
1845 )),
1846 "0..5=B 7..12=B",
1847 "the plain gap at 5..7 must not be swallowed"
1848 );
1849 }
1850
1851 #[test]
1853 fn an_inverted_range_is_an_error_not_a_panic() {
1854 let mut runs = vec![r(0, 5, "B")];
1855 let err =
1856 shift_runs_for_replace(&mut runs, 10, 5, 3, ReplaceFormatPolicy::InheritPreceding)
1857 .expect_err("an inverted range must be rejected");
1858 assert!(matches!(
1859 err,
1860 FormatRunError::ReversedRange { start: 10, end: 5 }
1861 ));
1862 assert_eq!(show(&runs), "0..5=B", "a refused edit must change nothing");
1863 }
1864}
1865
1866#[cfg(test)]
1868mod invariant_check_tests {
1869 use super::*;
1870
1871 fn run(start: u32, end: u32, bold: bool) -> FormatRun {
1872 FormatRun {
1873 byte_start: start,
1874 byte_end: end,
1875 format: CharacterFormat {
1876 font_bold: Some(bold),
1877 ..Default::default()
1878 },
1879 }
1880 }
1881
1882 #[test]
1886 fn a_replacement_outside_the_range_is_rejected_without_mutating() {
1887 let mut runs = vec![run(0, 20, true)];
1888 let before = runs.clone();
1889
1890 let err = try_splice_range(&mut runs, 5..10, vec![run(5, 15, false)])
1891 .expect_err("a replacement run reaching past range.end must be rejected");
1892
1893 assert!(matches!(
1894 err,
1895 FormatRunError::ReplacementOutsideRange {
1896 run_end: 15,
1897 range_end: 10,
1898 ..
1899 }
1900 ));
1901 assert_eq!(
1902 runs, before,
1903 "a rejected splice must not half-apply — validation happens before mutation"
1904 );
1905 }
1906
1907 #[test]
1908 #[allow(clippy::reversed_empty_ranges)]
1911 fn a_reversed_range_is_rejected() {
1912 let mut runs = vec![run(0, 20, true)];
1913 assert!(matches!(
1914 try_splice_range(&mut runs, 10..5, vec![]),
1915 Err(FormatRunError::ReversedRange { start: 10, end: 5 })
1916 ));
1917 }
1918
1919 #[test]
1920 fn a_legal_splice_still_works_through_the_checked_path() {
1921 let mut runs = vec![run(0, 20, true)];
1922 try_splice_range(&mut runs, 5..15, vec![run(5, 15, false)]).expect("legal");
1923 assert_eq!(runs.len(), 3);
1924 assert_eq!(runs[1].format.font_bold, Some(false));
1925 }
1926
1927 #[test]
1928 fn check_well_formed_catches_what_debug_assert_used_to() {
1929 assert!(check_well_formed(&[], 0).is_ok());
1930 assert!(check_well_formed(&[run(0, 5, true)], 5).is_ok());
1931
1932 assert!(matches!(
1933 check_well_formed(&[run(5, 5, true)], 10),
1934 Err(FormatRunError::EmptyRun { .. })
1935 ));
1936 assert!(matches!(
1937 check_well_formed(&[run(0, 8, true), run(5, 10, false)], 10),
1938 Err(FormatRunError::RunsOverlap { .. })
1939 ));
1940 assert!(matches!(
1941 check_well_formed(&[run(0, 5, true), run(5, 10, true)], 10),
1942 Err(FormatRunError::RunsNotCoalesced { .. })
1943 ));
1944 assert!(matches!(
1945 check_well_formed(&[run(0, 20, true)], 10),
1946 Err(FormatRunError::RunPastEndOfBlock { text_len: 10, .. })
1947 ));
1948 }
1949}