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 Image {
121 name: String,
122 width: i64,
123 height: i64,
124 quality: i64,
125 },
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
137pub struct InlineSegment {
138 pub content: InlineContent,
139 pub fmt_font_family: Option<String>,
140 pub fmt_font_point_size: Option<i64>,
141 pub fmt_font_weight: Option<i64>,
142 pub fmt_font_bold: Option<bool>,
143 pub fmt_font_italic: Option<bool>,
144 pub fmt_font_underline: Option<bool>,
145 pub fmt_font_overline: Option<bool>,
146 pub fmt_font_strikeout: Option<bool>,
147 pub fmt_letter_spacing: Option<i64>,
148 pub fmt_word_spacing: Option<i64>,
149 pub fmt_anchor_href: Option<String>,
150 pub fmt_anchor_names: Vec<String>,
151 pub fmt_is_anchor: Option<bool>,
152 pub fmt_tooltip: Option<String>,
153 pub fmt_underline_style: Option<UnderlineStyle>,
154 pub fmt_vertical_alignment: Option<CharVerticalAlignment>,
155}
156
157#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
162pub struct CharacterFormat {
163 pub font_family: Option<String>,
164 pub font_point_size: Option<i64>,
165 pub font_weight: Option<i64>,
166 pub font_bold: Option<bool>,
167 pub font_italic: Option<bool>,
168 pub font_underline: Option<bool>,
169 pub font_overline: Option<bool>,
170 pub font_strikeout: Option<bool>,
171 pub letter_spacing: Option<i64>,
172 pub word_spacing: Option<i64>,
173 pub anchor_href: Option<String>,
174 pub anchor_names: Vec<String>,
175 pub is_anchor: Option<bool>,
176 pub tooltip: Option<String>,
177 pub underline_style: Option<UnderlineStyle>,
178 pub vertical_alignment: Option<CharVerticalAlignment>,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
185pub struct FormatRun {
186 pub byte_start: u32,
187 pub byte_end: u32,
188 pub format: CharacterFormat,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
198pub struct ImageAnchor {
199 pub byte_offset: u32,
200 pub name: String,
201 pub width: i64,
202 pub height: i64,
203 pub quality: i64,
204 pub format: CharacterFormat,
205}
206
207pub fn debug_assert_well_formed(runs: &[FormatRun], block_text_len: usize) {
220 if cfg!(debug_assertions)
222 && let Err(e) = check_well_formed(runs, block_text_len)
223 {
224 debug_assert!(false, "format runs are malformed: {e}");
225 }
226}
227
228pub fn check_well_formed(runs: &[FormatRun], block_text_len: usize) -> Result<(), FormatRunError> {
235 if runs.is_empty() {
236 return Ok(());
237 }
238 for run in runs {
239 if run.byte_start >= run.byte_end {
240 return Err(FormatRunError::EmptyRun {
241 start: run.byte_start,
242 end: run.byte_end,
243 });
244 }
245 }
246 for i in 0..runs.len() - 1 {
247 if runs[i].byte_end > runs[i + 1].byte_start {
248 return Err(FormatRunError::RunsOverlap {
249 index: i,
250 left: Box::new(runs[i].clone()),
251 right: Box::new(runs[i + 1].clone()),
252 });
253 }
254 if runs[i].byte_end == runs[i + 1].byte_start && runs[i].format == runs[i + 1].format {
255 return Err(FormatRunError::RunsNotCoalesced { index: i });
256 }
257 }
258 let last = runs.last().expect("non-empty");
259 if last.byte_end as usize > block_text_len {
260 return Err(FormatRunError::RunPastEndOfBlock {
261 start: last.byte_start,
262 end: last.byte_end,
263 text_len: block_text_len,
264 });
265 }
266 Ok(())
267}
268
269pub fn coalesce_in_place(runs: &mut Vec<FormatRun>) {
271 if runs.len() < 2 {
272 return;
273 }
274 let mut write = 0usize;
275 for read in 1..runs.len() {
276 if runs[write].byte_end == runs[read].byte_start && runs[write].format == runs[read].format
277 {
278 runs[write].byte_end = runs[read].byte_end;
279 } else {
280 write += 1;
281 if write != read {
282 runs[write] = runs[read].clone();
283 }
284 }
285 }
286 runs.truncate(write + 1);
287}
288
289pub fn splice_range(
299 runs: &mut Vec<FormatRun>,
300 range: std::ops::Range<u32>,
301 replacement: Vec<FormatRun>,
302) {
303 if let Err(e) = try_splice_range(runs, range, replacement) {
304 debug_assert!(false, "splice_range contract violated: {e}");
313 }
314}
315
316pub fn try_splice_range(
321 runs: &mut Vec<FormatRun>,
322 range: std::ops::Range<u32>,
323 replacement: Vec<FormatRun>,
324) -> Result<(), FormatRunError> {
325 if range.start > range.end {
326 return Err(FormatRunError::ReversedRange {
327 start: range.start,
328 end: range.end,
329 });
330 }
331 for r in &replacement {
332 if r.byte_start >= r.byte_end {
333 return Err(FormatRunError::EmptyRun {
334 start: r.byte_start,
335 end: r.byte_end,
336 });
337 }
338 if r.byte_start < range.start || r.byte_end > range.end {
339 return Err(FormatRunError::ReplacementOutsideRange {
340 run_start: r.byte_start,
341 run_end: r.byte_end,
342 range_start: range.start,
343 range_end: range.end,
344 });
345 }
346 }
347 for i in 1..replacement.len() {
348 if replacement[i - 1].byte_end > replacement[i].byte_start {
349 return Err(FormatRunError::RunsOverlap {
350 index: i - 1,
351 left: Box::new(replacement[i - 1].clone()),
352 right: Box::new(replacement[i].clone()),
353 });
354 }
355 }
356
357 let mut result: Vec<FormatRun> = Vec::with_capacity(runs.len() + replacement.len());
358
359 for run in runs.iter() {
361 if run.byte_end <= range.start {
362 result.push(run.clone());
363 } else if run.byte_start < range.start {
364 result.push(FormatRun {
366 byte_start: run.byte_start,
367 byte_end: range.start,
368 format: run.format.clone(),
369 });
370 }
371 }
372
373 result.extend(replacement);
375
376 for run in runs.iter() {
378 if run.byte_start >= range.end {
379 result.push(run.clone());
380 } else if run.byte_end > range.end {
381 result.push(FormatRun {
383 byte_start: range.end,
384 byte_end: run.byte_end,
385 format: run.format.clone(),
386 });
387 }
388 }
389
390 coalesce_in_place(&mut result);
391 *runs = result;
392 Ok(())
393}
394
395pub fn capture_runs_in_range(runs: &[FormatRun], start: u32, end: u32) -> Vec<FormatRun> {
406 let mut out = Vec::new();
407 for run in runs {
408 if run.byte_end <= start || run.byte_start >= end {
409 continue;
410 }
411 let clipped_start = std::cmp::max(run.byte_start, start);
412 let clipped_end = std::cmp::min(run.byte_end, end);
413 if clipped_start < clipped_end {
414 out.push(FormatRun {
415 byte_start: clipped_start,
416 byte_end: clipped_end,
417 format: run.format.clone(),
418 });
419 }
420 }
421 out
422}
423
424pub fn capture_image_formats_in_range(
428 images: &[ImageAnchor],
429 start: u32,
430 end: u32,
431) -> Vec<(u32, CharacterFormat)> {
432 let mut out = Vec::new();
433 for img in images {
434 if img.byte_offset >= start && img.byte_offset < end {
435 out.push((img.byte_offset, img.format.clone()));
436 }
437 }
438 out
439}
440
441pub fn shift_after(runs: &mut [FormatRun], threshold: u32, delta: i32) {
449 for run in runs.iter_mut() {
450 if run.byte_start >= threshold {
451 let new_start = (run.byte_start as i64) + (delta as i64);
452 let new_end = (run.byte_end as i64) + (delta as i64);
453 debug_assert!(new_start >= 0 && new_end >= new_start);
454 run.byte_start = new_start as u32;
455 run.byte_end = new_end as u32;
456 }
457 }
458}
459
460pub fn synth_element_id(block_id: u64, byte_start: u32) -> u64 {
476 const SYNTH_TAG: u64 = 0x4000_0000_0000_0000;
477 SYNTH_TAG | ((block_id & 0x3FFF_FFFF) << 32) | (byte_start as u64)
478}
479
480pub fn shift_images_after(images: &mut [ImageAnchor], threshold: u32, delta: i32) {
483 for img in images.iter_mut() {
484 if img.byte_offset >= threshold {
485 let new_off = (img.byte_offset as i64) + (delta as i64);
486 debug_assert!(new_off >= 0);
487 img.byte_offset = new_off as u32;
488 }
489 }
490}
491
492pub fn shift_runs_for_insert(runs: &mut [FormatRun], byte_offset: u32, inserted_bytes: u32) {
503 if inserted_bytes == 0 {
504 return;
505 }
506 for run in runs.iter_mut() {
507 if run.byte_start >= byte_offset {
508 run.byte_start += inserted_bytes;
509 run.byte_end += inserted_bytes;
510 } else if run.byte_end >= byte_offset {
511 run.byte_end += inserted_bytes;
515 }
516 }
517}
518
519pub fn shift_runs_for_delete(runs: &mut Vec<FormatRun>, byte_start: u32, byte_end: u32) {
524 if byte_end <= byte_start {
525 return;
526 }
527 splice_range(runs, byte_start..byte_end, Vec::new());
528 let delta = (byte_end - byte_start) as i32;
529 shift_after(runs, byte_end, -delta);
530 coalesce_in_place(runs);
533}
534
535pub fn shift_runs_for_replace(
553 runs: &mut Vec<FormatRun>,
554 byte_start: u32,
555 byte_end: u32,
556 replacement_bytes: u32,
557 policy: ReplaceFormatPolicy,
558) -> Result<(), FormatRunError> {
559 if byte_end < byte_start {
560 return Err(FormatRunError::ReversedRange {
561 start: byte_start,
562 end: byte_end,
563 });
564 }
565
566 let destroys_formatting = byte_end > byte_start;
578 let override_format: Option<Option<CharacterFormat>> = match policy {
579 ReplaceFormatPolicy::InheritPreceding => None,
580 ReplaceFormatPolicy::PreserveNothing => Some(None),
581 ReplaceFormatPolicy::PreserveIfFullyCovered => {
582 covering_format(runs, byte_start, byte_end).map(Some)
583 }
584 ReplaceFormatPolicy::KeepDominantRun if destroys_formatting => {
585 Some(dominant_format(runs, byte_start, byte_end))
586 }
587 ReplaceFormatPolicy::KeepDominantRun => None,
588 };
589
590 shift_runs_for_delete(runs, byte_start, byte_end);
592 shift_runs_for_insert(runs, byte_start, replacement_bytes);
593
594 if let Some(format) = override_format
596 && replacement_bytes > 0
597 {
598 let span = byte_start..byte_start + replacement_bytes;
599 let replacement = match format {
600 Some(format) => vec![FormatRun {
601 byte_start: span.start,
602 byte_end: span.end,
603 format,
604 }],
605 None => Vec::new(),
606 };
607 try_splice_range(runs, span, replacement)?;
608 }
609 Ok(())
610}
611
612fn covering_format(runs: &[FormatRun], start: u32, end: u32) -> Option<CharacterFormat> {
618 if end <= start {
619 return None;
620 }
621 runs.iter()
622 .find(|r| r.byte_start <= start && r.byte_end >= end)
623 .map(|r| r.format.clone())
624}
625
626fn dominant_format(runs: &[FormatRun], start: u32, end: u32) -> Option<CharacterFormat> {
633 if end <= start {
634 return None;
635 }
636 let span = u64::from(end - start);
637 let mut covered = 0u64;
638 let mut best: Option<(u64, &FormatRun)> = None;
639
640 for r in runs {
641 let lo = r.byte_start.max(start);
642 let hi = r.byte_end.min(end);
643 if hi <= lo {
644 continue;
645 }
646 let overlap = u64::from(hi - lo);
647 covered += overlap;
648 if best.is_none_or(|(best_overlap, _)| overlap > best_overlap) {
651 best = Some((overlap, r));
652 }
653 }
654
655 let plain = span - covered;
656 match best {
657 Some((overlap, run)) if overlap >= plain => Some(run.format.clone()),
658 _ => None,
659 }
660}
661
662pub fn shift_images_for_insert(images: &mut [ImageAnchor], byte_offset: u32, inserted_bytes: u32) {
665 if inserted_bytes == 0 {
666 return;
667 }
668 for img in images.iter_mut() {
669 if img.byte_offset >= byte_offset {
670 img.byte_offset += inserted_bytes;
671 }
672 }
673}
674
675pub fn shift_images_for_delete(
680 images: &mut Vec<ImageAnchor>,
681 byte_start: u32,
682 byte_end: u32,
683) -> usize {
684 if byte_end <= byte_start {
685 return 0;
686 }
687 let before = images.len();
688 images.retain(|i| !(i.byte_offset >= byte_start && i.byte_offset < byte_end));
689 let removed = before - images.len();
690 let delta = (byte_end - byte_start) as i32;
691 shift_images_after(images, byte_end, -delta);
692 removed
693}
694
695pub fn logical_offset_to_byte(plain_text: &str, images: &[ImageAnchor], char_offset: i64) -> u32 {
704 if char_offset <= 0 {
705 return 0;
706 }
707 let mut logical: i64 = 0;
708 let mut images_consumed = 0usize;
709 for (b, _) in plain_text.char_indices() {
710 while images_consumed < images.len() && images[images_consumed].byte_offset <= b as u32 {
711 if logical == char_offset {
712 return b as u32;
713 }
714 logical += 1;
715 images_consumed += 1;
716 }
717 if logical == char_offset {
718 return b as u32;
719 }
720 logical += 1;
721 }
722 let plain_len = plain_text.len() as u32;
723 while images_consumed < images.len() {
724 if logical == char_offset {
725 return plain_len;
726 }
727 logical += 1;
728 images_consumed += 1;
729 }
730 plain_len
731}
732
733pub fn split_runs_at(runs: &[FormatRun], byte_offset: u32) -> (Vec<FormatRun>, Vec<FormatRun>) {
738 let mut left = Vec::new();
739 let mut right = Vec::new();
740 for run in runs {
741 if run.byte_end <= byte_offset {
742 left.push(run.clone());
743 } else if run.byte_start >= byte_offset {
744 right.push(FormatRun {
745 byte_start: run.byte_start - byte_offset,
746 byte_end: run.byte_end - byte_offset,
747 format: run.format.clone(),
748 });
749 } else {
750 left.push(FormatRun {
751 byte_start: run.byte_start,
752 byte_end: byte_offset,
753 format: run.format.clone(),
754 });
755 right.push(FormatRun {
756 byte_start: 0,
757 byte_end: run.byte_end - byte_offset,
758 format: run.format.clone(),
759 });
760 }
761 }
762 (left, right)
763}
764
765pub fn split_images_at(
768 images: &[ImageAnchor],
769 byte_offset: u32,
770) -> (Vec<ImageAnchor>, Vec<ImageAnchor>) {
771 let mut left = Vec::new();
772 let mut right = Vec::new();
773 for img in images {
774 if img.byte_offset < byte_offset {
775 left.push(img.clone());
776 } else {
777 let mut new = img.clone();
778 new.byte_offset -= byte_offset;
779 right.push(new);
780 }
781 }
782 (left, right)
783}
784
785pub fn character_format_from_segment(seg: &InlineSegment) -> CharacterFormat {
791 CharacterFormat {
792 font_family: seg.fmt_font_family.clone(),
793 font_point_size: seg.fmt_font_point_size,
794 font_weight: seg.fmt_font_weight,
795 font_bold: seg.fmt_font_bold,
796 font_italic: seg.fmt_font_italic,
797 font_underline: seg.fmt_font_underline,
798 font_overline: seg.fmt_font_overline,
799 font_strikeout: seg.fmt_font_strikeout,
800 letter_spacing: seg.fmt_letter_spacing,
801 word_spacing: seg.fmt_word_spacing,
802 anchor_href: seg.fmt_anchor_href.clone(),
803 anchor_names: seg.fmt_anchor_names.clone(),
804 is_anchor: seg.fmt_is_anchor,
805 tooltip: seg.fmt_tooltip.clone(),
806 underline_style: seg.fmt_underline_style.clone(),
807 vertical_alignment: seg.fmt_vertical_alignment.clone(),
808 }
809}
810
811pub fn apply_character_format_to_segment(seg: &mut InlineSegment, fmt: &CharacterFormat) {
813 seg.fmt_font_family = fmt.font_family.clone();
814 seg.fmt_font_point_size = fmt.font_point_size;
815 seg.fmt_font_weight = fmt.font_weight;
816 seg.fmt_font_bold = fmt.font_bold;
817 seg.fmt_font_italic = fmt.font_italic;
818 seg.fmt_font_underline = fmt.font_underline;
819 seg.fmt_font_overline = fmt.font_overline;
820 seg.fmt_font_strikeout = fmt.font_strikeout;
821 seg.fmt_letter_spacing = fmt.letter_spacing;
822 seg.fmt_word_spacing = fmt.word_spacing;
823 seg.fmt_anchor_href = fmt.anchor_href.clone();
824 seg.fmt_anchor_names = fmt.anchor_names.clone();
825 seg.fmt_is_anchor = fmt.is_anchor;
826 seg.fmt_tooltip = fmt.tooltip.clone();
827 seg.fmt_underline_style = fmt.underline_style.clone();
828 seg.fmt_vertical_alignment = fmt.vertical_alignment.clone();
829}
830
831pub fn inline_segments_view(
841 plain_text: &str,
842 runs: &[FormatRun],
843 images: &[ImageAnchor],
844) -> Vec<InlineSegment> {
845 let mut out: Vec<InlineSegment> = Vec::new();
846 let bytes = plain_text.as_bytes();
847
848 let mut img_iter = images.iter().peekable();
849 let mut cursor: u32 = 0;
850
851 let emit_text =
852 |out: &mut Vec<InlineSegment>, bytes: &[u8], start: u32, end: u32, fmt: CharacterFormat| {
853 if start >= end {
854 return;
855 }
856 let slice = &bytes[start as usize..end as usize];
857 let s = std::str::from_utf8(slice)
858 .expect("block plain_text must be valid UTF-8")
859 .to_string();
860 let mut seg = InlineSegment {
861 content: InlineContent::Text(s),
862 ..Default::default()
863 };
864 apply_character_format_to_segment(&mut seg, &fmt);
865 out.push(seg);
866 };
867
868 let emit_image = |out: &mut Vec<InlineSegment>, anchor: &ImageAnchor| {
869 let mut seg = InlineSegment {
870 content: InlineContent::Image {
871 name: anchor.name.clone(),
872 width: anchor.width,
873 height: anchor.height,
874 quality: anchor.quality,
875 },
876 ..Default::default()
877 };
878 apply_character_format_to_segment(&mut seg, &anchor.format);
879 out.push(seg);
880 };
881
882 for run in runs {
883 while let Some(img) = img_iter.peek() {
884 if img.byte_offset < run.byte_start {
885 emit_text(
886 &mut out,
887 bytes,
888 cursor,
889 img.byte_offset,
890 CharacterFormat::default(),
891 );
892 emit_image(&mut out, img);
893 cursor = img.byte_offset;
894 img_iter.next();
895 } else {
896 break;
897 }
898 }
899
900 if cursor < run.byte_start {
901 emit_text(
902 &mut out,
903 bytes,
904 cursor,
905 run.byte_start,
906 CharacterFormat::default(),
907 );
908 }
909
910 emit_text(
911 &mut out,
912 bytes,
913 run.byte_start,
914 run.byte_end,
915 run.format.clone(),
916 );
917 cursor = run.byte_end;
918 }
919
920 for img in img_iter {
921 if img.byte_offset > cursor {
922 emit_text(
923 &mut out,
924 bytes,
925 cursor,
926 img.byte_offset,
927 CharacterFormat::default(),
928 );
929 cursor = img.byte_offset;
930 }
931 emit_image(&mut out, img);
932 }
933
934 if (cursor as usize) < bytes.len() {
935 emit_text(
936 &mut out,
937 bytes,
938 cursor,
939 bytes.len() as u32,
940 CharacterFormat::default(),
941 );
942 }
943
944 out
945}
946
947#[cfg(test)]
948mod tests {
949 use super::*;
950
951 fn run(s: u32, e: u32, bold: bool) -> FormatRun {
952 FormatRun {
953 byte_start: s,
954 byte_end: e,
955 format: CharacterFormat {
956 font_bold: Some(bold),
957 ..Default::default()
958 },
959 }
960 }
961
962 #[test]
963 fn empty_runs_are_well_formed() {
964 debug_assert_well_formed(&[], 0);
965 debug_assert_well_formed(&[], 100);
966 }
967
968 #[test]
969 fn coalesce_merges_adjacent_equal_runs() {
970 let mut rs = vec![run(0, 5, true), run(5, 10, true), run(10, 15, false)];
971 coalesce_in_place(&mut rs);
972 assert_eq!(rs.len(), 2);
973 assert_eq!(rs[0].byte_end, 10);
974 }
975
976 #[test]
977 fn coalesce_leaves_disjoint_runs_alone() {
978 let mut rs = vec![run(0, 5, true), run(7, 10, true)];
979 coalesce_in_place(&mut rs);
980 assert_eq!(rs.len(), 2);
981 }
982
983 #[test]
984 fn splice_range_clips_straddling_runs() {
985 let mut rs = vec![run(0, 20, true)];
986 splice_range(&mut rs, 5..15, vec![run(5, 15, false)]);
987 assert_eq!(rs.len(), 3);
988 assert_eq!(rs[0].byte_end, 5);
989 assert_eq!(rs[1].format.font_bold, Some(false));
990 assert_eq!(rs[2].byte_start, 15);
991 }
992
993 #[test]
994 fn splice_range_empty_replacement_removes_inner_runs() {
995 let mut rs = vec![run(0, 5, true), run(5, 10, false), run(10, 15, true)];
996 splice_range(&mut rs, 5..10, vec![]);
997 assert_eq!(rs.len(), 2);
1000 assert_eq!(rs[0].byte_end, 5);
1001 assert_eq!(rs[1].byte_start, 10);
1002 }
1003
1004 #[test]
1005 fn shift_after_moves_downstream() {
1006 let mut rs = vec![run(0, 5, true), run(10, 15, false)];
1007 shift_after(&mut rs, 5, 3);
1008 assert_eq!(rs[0].byte_start, 0); assert_eq!(rs[1].byte_start, 13);
1010 assert_eq!(rs[1].byte_end, 18);
1011 }
1012}
1013
1014#[cfg(test)]
1021mod replace_policy_tests {
1022 use super::*;
1023
1024 fn fmt(tag: &str) -> CharacterFormat {
1025 CharacterFormat {
1026 font_bold: Some(tag == "B"),
1027 font_italic: Some(tag == "I"),
1028 ..Default::default()
1029 }
1030 }
1031 fn r(start: u32, end: u32, tag: &str) -> FormatRun {
1032 FormatRun {
1033 byte_start: start,
1034 byte_end: end,
1035 format: fmt(tag),
1036 }
1037 }
1038 fn show(runs: &[FormatRun]) -> String {
1040 if runs.is_empty() {
1041 return "[]".to_string();
1042 }
1043 runs.iter()
1044 .map(|x| {
1045 let tag = if x.format.font_bold == Some(true) {
1046 "B"
1047 } else if x.format.font_italic == Some(true) {
1048 "I"
1049 } else {
1050 "p"
1051 };
1052 format!("{}..{}={tag}", x.byte_start, x.byte_end)
1053 })
1054 .collect::<Vec<_>>()
1055 .join(" ")
1056 }
1057 fn replace(
1058 runs: &[FormatRun],
1059 start: u32,
1060 end: u32,
1061 n: u32,
1062 policy: ReplaceFormatPolicy,
1063 ) -> Vec<FormatRun> {
1064 let mut runs = runs.to_vec();
1065 shift_runs_for_replace(&mut runs, start, end, n, policy).expect("valid replace");
1066 runs
1067 }
1068
1069 #[test]
1076 fn inherit_preceding_matches_the_historical_delete_then_insert() {
1077 let corpus: Vec<(&str, Vec<FormatRun>, u32, u32, u32)> = vec![
1078 ("run ends exactly at start", vec![r(0, 5, "B")], 5, 10, 3),
1079 ("run begins exactly at start", vec![r(5, 8, "B")], 5, 10, 3),
1080 (
1081 "run begins at start, outlives end",
1082 vec![r(5, 20, "B")],
1083 5,
1084 10,
1085 3,
1086 ),
1087 (
1088 "run straddles the whole range",
1089 vec![r(0, 20, "B")],
1090 5,
1091 10,
1092 3,
1093 ),
1094 ("no run touches the start", vec![r(12, 20, "B")], 5, 10, 3),
1095 ("bold tail inside the range", vec![r(9, 13, "B")], 5, 13, 4),
1096 ("pure delete", vec![r(0, 20, "B")], 5, 10, 0),
1097 ("pure insert", vec![r(0, 20, "B")], 5, 5, 3),
1098 (
1099 "same format either side coalesces",
1100 vec![r(0, 5, "B"), r(10, 15, "B")],
1101 5,
1102 10,
1103 3,
1104 ),
1105 (
1106 "different formats either side",
1107 vec![r(0, 5, "B"), r(10, 15, "I")],
1108 5,
1109 10,
1110 3,
1111 ),
1112 ("empty run list", vec![], 5, 10, 3),
1113 ("the only run is consumed", vec![r(5, 10, "B")], 5, 10, 3),
1114 (
1115 "replacement longer than the range",
1116 vec![r(0, 5, "B")],
1117 5,
1118 10,
1119 20,
1120 ),
1121 (
1122 "three runs straddled",
1123 vec![r(0, 3, "B"), r(3, 6, "I"), r(6, 9, "B")],
1124 2,
1125 7,
1126 4,
1127 ),
1128 (
1129 "gap between two same-format runs is deleted",
1130 vec![r(0, 5, "B"), r(8, 13, "B")],
1131 5,
1132 8,
1133 0,
1134 ),
1135 ];
1136
1137 for (name, runs, start, end, n) in corpus {
1138 let mut expected = runs.clone();
1140 shift_runs_for_delete(&mut expected, start, end);
1141 shift_runs_for_insert(&mut expected, start, n);
1142
1143 let got = replace(&runs, start, end, n, ReplaceFormatPolicy::InheritPreceding);
1144
1145 assert_eq!(
1146 show(&got),
1147 show(&expected),
1148 "InheritPreceding diverged from delete+insert for {name:?} \
1149 (replace {start}..{end}, n={n})\n before: {}\n historical: {}\n got: {}",
1150 show(&runs),
1151 show(&expected),
1152 show(&got),
1153 );
1154 }
1155 }
1156
1157 #[test]
1161 fn the_four_policies_diverge_on_a_partly_bold_name() {
1162 let runs = vec![r(5, 9, "B")];
1164 let (start, end, n) = (0, 9, 9); use ReplaceFormatPolicy::*;
1167 assert_eq!(
1168 show(&replace(&runs, start, end, n, InheritPreceding)),
1169 "[]",
1170 "the historical default destroys the bold — pinned, not endorsed"
1171 );
1172 assert_eq!(
1173 show(&replace(&runs, start, end, n, PreserveNothing)),
1174 "[]",
1175 "explicitly unformatted"
1176 );
1177 assert_eq!(
1178 show(&replace(&runs, start, end, n, PreserveIfFullyCovered)),
1179 "[]",
1180 "no SINGLE run covers 0..9 — it must fall back to inheritance, not guess"
1181 );
1182 assert_eq!(
1184 show(&replace(&runs, start, end, n, KeepDominantRun)),
1185 "[]",
1186 "plain covers more of the name than the bold does"
1187 );
1188
1189 let mostly_bold = vec![r(1, 9, "B")];
1191 assert_eq!(
1192 show(&replace(&mostly_bold, 0, 9, 9, KeepDominantRun)),
1193 "0..9=B",
1194 "bold covers 8 of 9 bytes — the rename must keep it"
1195 );
1196 }
1197
1198 #[test]
1202 fn fully_covered_means_a_single_run_not_a_gapless_union() {
1203 let two = vec![r(0, 3, "I"), r(3, 10, "B")];
1204 assert_eq!(
1205 show(&replace(
1206 &two,
1207 0,
1208 10,
1209 4,
1210 ReplaceFormatPolicy::PreserveIfFullyCovered
1211 )),
1212 "[]",
1213 "two different-format runs jointly spanning the range are not 'covered'; \
1214 with no run preceding the start, the fallback is unformatted"
1215 );
1216
1217 let one = vec![r(5, 20, "B")];
1220 assert_eq!(
1221 show(&replace(
1222 &one,
1223 5,
1224 10,
1225 3,
1226 ReplaceFormatPolicy::PreserveIfFullyCovered
1227 )),
1228 "5..18=B",
1229 "a single covering run keeps its format across the rename"
1230 );
1231 assert_eq!(
1232 show(&replace(
1233 &one,
1234 5,
1235 10,
1236 3,
1237 ReplaceFormatPolicy::InheritPreceding
1238 )),
1239 "8..18=B",
1240 "…which the default would have lost: the replacement lands unformatted"
1241 );
1242 }
1243
1244 #[test]
1247 fn a_partially_overlapping_run_does_not_count_as_covering() {
1248 let runs = vec![r(0, 8, "B")]; assert_eq!(
1250 show(&replace(
1251 &runs,
1252 5,
1253 12,
1254 4,
1255 ReplaceFormatPolicy::PreserveIfFullyCovered
1256 )),
1257 "0..9=B",
1258 "not covered → falls back to inheritance, which extends the preceding bold; \
1259 it must NOT format the whole replacement as though bold had covered it"
1260 );
1261 }
1262
1263 #[test]
1268 fn a_dominance_tie_between_two_runs_goes_to_the_earlier() {
1269 let runs = vec![r(0, 3, "B"), r(3, 6, "I")]; assert_eq!(
1271 show(&replace(
1272 &runs,
1273 0,
1274 6,
1275 4,
1276 ReplaceFormatPolicy::KeepDominantRun
1277 )),
1278 "0..4=B",
1279 "a true tie must resolve to the earlier run, not to whichever the iterator \
1280 happened to visit last"
1281 );
1282 }
1283
1284 #[test]
1287 fn a_dominance_tie_against_plain_text_keeps_the_formatting() {
1288 let runs = vec![r(4, 8, "B")]; assert_eq!(
1290 show(&replace(
1291 &runs,
1292 0,
1293 8,
1294 5,
1295 ReplaceFormatPolicy::KeepDominantRun
1296 )),
1297 "0..5=B",
1298 "an even split must keep the formatting rather than silently drop it"
1299 );
1300 }
1301
1302 #[test]
1306 fn an_empty_range_is_an_insert_and_no_coverage_policy_overrides_it() {
1307 let runs = vec![r(0, 5, "B"), r(5, 10, "I")];
1308 use ReplaceFormatPolicy::*;
1309 for policy in [InheritPreceding, PreserveIfFullyCovered, KeepDominantRun] {
1310 assert_eq!(
1311 show(&replace(&runs, 5, 5, 2, policy)),
1312 "0..7=B 7..12=I",
1313 "{policy:?}: typing at a boundary must inherit the run to the LEFT (Qt \
1314 convention) — an empty range destroyed no formatting, so there is \
1315 nothing for a coverage policy to override"
1316 );
1317 }
1318 assert_eq!(
1320 show(&replace(&runs, 5, 5, 2, PreserveNothing)),
1321 "0..5=B 7..12=I",
1322 "PreserveNothing asks for unformatted text, and means it even on an insert"
1323 );
1324 }
1325
1326 #[test]
1328 fn a_zero_width_zero_length_replace_is_the_identity() {
1329 let runs = vec![r(0, 5, "B"), r(7, 12, "I")];
1330 for policy in [
1331 ReplaceFormatPolicy::InheritPreceding,
1332 ReplaceFormatPolicy::PreserveIfFullyCovered,
1333 ReplaceFormatPolicy::KeepDominantRun,
1334 ReplaceFormatPolicy::PreserveNothing,
1335 ] {
1336 assert_eq!(
1337 show(&replace(&runs, 6, 6, 0, policy)),
1338 "0..5=B 7..12=I",
1339 "{policy:?} changed a no-op edit"
1340 );
1341 }
1342 }
1343
1344 #[test]
1348 fn preserve_nothing_fabricates_no_default_run() {
1349 let runs = vec![r(0, 5, "B")];
1350 let got = replace(&runs, 7, 9, 2, ReplaceFormatPolicy::PreserveNothing);
1351 assert_eq!(show(&got), "0..5=B", "no run may be invented for the gap");
1352 assert!(
1353 got.iter().all(|x| x.byte_start < 7 || x.byte_end > 9),
1354 "the replaced span must carry no run at all"
1355 );
1356 }
1357
1358 #[test]
1361 fn offsets_are_bytes_not_characters() {
1362 let runs = vec![r(0, 4, "B"), r(5, 9, "I")];
1364 let got = replace(&runs, 0, 4, 2, ReplaceFormatPolicy::KeepDominantRun);
1365 assert_eq!(
1366 show(&got),
1367 "0..2=B 3..7=I",
1368 "the trailing italic must shift back by the BYTE delta (4 -> 2 = -2)"
1369 );
1370 }
1371
1372 #[test]
1376 fn every_policy_leaves_the_runs_well_formed() {
1377 let setups: Vec<(Vec<FormatRun>, u32, u32, u32, usize)> = vec![
1378 (vec![r(0, 3, "B"), r(3, 6, "I"), r(6, 9, "B")], 2, 7, 4, 8),
1379 (vec![r(0, 5, "B"), r(8, 13, "B")], 5, 8, 0, 10),
1380 (vec![r(0, 5, "B"), r(7, 12, "B")], 8, 10, 2, 12),
1381 (vec![r(3, 7, "B")], 3, 7, 0, 6),
1382 (vec![], 2, 6, 3, 9),
1383 ];
1384 for (runs, start, end, n, text_len) in setups {
1385 for policy in [
1386 ReplaceFormatPolicy::InheritPreceding,
1387 ReplaceFormatPolicy::PreserveIfFullyCovered,
1388 ReplaceFormatPolicy::KeepDominantRun,
1389 ReplaceFormatPolicy::PreserveNothing,
1390 ] {
1391 let got = replace(&runs, start, end, n, policy);
1392 check_well_formed(&got, text_len).unwrap_or_else(|e| {
1393 panic!(
1394 "{policy:?} produced malformed runs from {} (replace {start}..{end}, \
1395 n={n}): {} — {e}",
1396 show(&runs),
1397 show(&got)
1398 )
1399 });
1400 }
1401 }
1402 }
1403
1404 #[test]
1408 fn an_untouched_gap_between_equal_runs_survives() {
1409 let runs = vec![r(0, 5, "B"), r(7, 12, "B")];
1410 assert_eq!(
1411 show(&replace(
1412 &runs,
1413 8,
1414 10,
1415 2,
1416 ReplaceFormatPolicy::InheritPreceding
1417 )),
1418 "0..5=B 7..12=B",
1419 "the plain gap at 5..7 must not be swallowed"
1420 );
1421 }
1422
1423 #[test]
1425 fn an_inverted_range_is_an_error_not_a_panic() {
1426 let mut runs = vec![r(0, 5, "B")];
1427 let err =
1428 shift_runs_for_replace(&mut runs, 10, 5, 3, ReplaceFormatPolicy::InheritPreceding)
1429 .expect_err("an inverted range must be rejected");
1430 assert!(matches!(
1431 err,
1432 FormatRunError::ReversedRange { start: 10, end: 5 }
1433 ));
1434 assert_eq!(show(&runs), "0..5=B", "a refused edit must change nothing");
1435 }
1436}
1437
1438#[cfg(test)]
1440mod invariant_check_tests {
1441 use super::*;
1442
1443 fn run(start: u32, end: u32, bold: bool) -> FormatRun {
1444 FormatRun {
1445 byte_start: start,
1446 byte_end: end,
1447 format: CharacterFormat {
1448 font_bold: Some(bold),
1449 ..Default::default()
1450 },
1451 }
1452 }
1453
1454 #[test]
1458 fn a_replacement_outside_the_range_is_rejected_without_mutating() {
1459 let mut runs = vec![run(0, 20, true)];
1460 let before = runs.clone();
1461
1462 let err = try_splice_range(&mut runs, 5..10, vec![run(5, 15, false)])
1463 .expect_err("a replacement run reaching past range.end must be rejected");
1464
1465 assert!(matches!(
1466 err,
1467 FormatRunError::ReplacementOutsideRange {
1468 run_end: 15,
1469 range_end: 10,
1470 ..
1471 }
1472 ));
1473 assert_eq!(
1474 runs, before,
1475 "a rejected splice must not half-apply — validation happens before mutation"
1476 );
1477 }
1478
1479 #[test]
1480 #[allow(clippy::reversed_empty_ranges)]
1483 fn a_reversed_range_is_rejected() {
1484 let mut runs = vec![run(0, 20, true)];
1485 assert!(matches!(
1486 try_splice_range(&mut runs, 10..5, vec![]),
1487 Err(FormatRunError::ReversedRange { start: 10, end: 5 })
1488 ));
1489 }
1490
1491 #[test]
1492 fn a_legal_splice_still_works_through_the_checked_path() {
1493 let mut runs = vec![run(0, 20, true)];
1494 try_splice_range(&mut runs, 5..15, vec![run(5, 15, false)]).expect("legal");
1495 assert_eq!(runs.len(), 3);
1496 assert_eq!(runs[1].format.font_bold, Some(false));
1497 }
1498
1499 #[test]
1500 fn check_well_formed_catches_what_debug_assert_used_to() {
1501 assert!(check_well_formed(&[], 0).is_ok());
1502 assert!(check_well_formed(&[run(0, 5, true)], 5).is_ok());
1503
1504 assert!(matches!(
1505 check_well_formed(&[run(5, 5, true)], 10),
1506 Err(FormatRunError::EmptyRun { .. })
1507 ));
1508 assert!(matches!(
1509 check_well_formed(&[run(0, 8, true), run(5, 10, false)], 10),
1510 Err(FormatRunError::RunsOverlap { .. })
1511 ));
1512 assert!(matches!(
1513 check_well_formed(&[run(0, 5, true), run(5, 10, true)], 10),
1514 Err(FormatRunError::RunsNotCoalesced { .. })
1515 ));
1516 assert!(matches!(
1517 check_well_formed(&[run(0, 20, true)], 10),
1518 Err(FormatRunError::RunPastEndOfBlock { text_len: 10, .. })
1519 ));
1520 }
1521}