use crate::entities::{CharVerticalAlignment, UnderlineStyle};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum FormatRunError {
#[error("byte range {start}..{end} is reversed")]
ReversedRange { start: u32, end: u32 },
#[error(
"replacement run {run_start}..{run_end} falls outside the spliced range \
{range_start}..{range_end}"
)]
ReplacementOutsideRange {
run_start: u32,
run_end: u32,
range_start: u32,
range_end: u32,
},
#[error("run {start}..{end} is empty or reversed")]
EmptyRun { start: u32, end: u32 },
#[error("runs overlap or are out of order at index {index}: {left:?} then {right:?}")]
RunsOverlap {
index: usize,
left: Box<FormatRun>,
right: Box<FormatRun>,
},
#[error("adjacent runs with identical formatting were left uncoalesced at index {index}")]
RunsNotCoalesced { index: usize },
#[error("run {start}..{end} runs past the end of the block's {text_len} bytes")]
RunPastEndOfBlock {
start: u32,
end: u32,
text_len: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ReplaceFormatPolicy {
#[default]
InheritPreceding,
PreserveIfFullyCovered,
KeepDominantRun,
PreserveNothing,
}
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)]
pub enum InlineContent {
#[default]
Empty,
Text(String),
Image {
name: String,
width: i64,
height: i64,
quality: i64,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct InlineSegment {
pub content: InlineContent,
pub fmt_font_family: Option<String>,
pub fmt_font_point_size: Option<i64>,
pub fmt_font_weight: Option<i64>,
pub fmt_font_bold: Option<bool>,
pub fmt_font_italic: Option<bool>,
pub fmt_font_underline: Option<bool>,
pub fmt_font_overline: Option<bool>,
pub fmt_font_strikeout: Option<bool>,
pub fmt_letter_spacing: Option<i64>,
pub fmt_word_spacing: Option<i64>,
pub fmt_anchor_href: Option<String>,
pub fmt_anchor_names: Vec<String>,
pub fmt_is_anchor: Option<bool>,
pub fmt_tooltip: Option<String>,
pub fmt_underline_style: Option<UnderlineStyle>,
pub fmt_vertical_alignment: Option<CharVerticalAlignment>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CharacterFormat {
pub font_family: Option<String>,
pub font_point_size: Option<i64>,
pub font_weight: Option<i64>,
pub font_bold: Option<bool>,
pub font_italic: Option<bool>,
pub font_underline: Option<bool>,
pub font_overline: Option<bool>,
pub font_strikeout: Option<bool>,
pub letter_spacing: Option<i64>,
pub word_spacing: Option<i64>,
pub anchor_href: Option<String>,
pub anchor_names: Vec<String>,
pub is_anchor: Option<bool>,
pub tooltip: Option<String>,
pub underline_style: Option<UnderlineStyle>,
pub vertical_alignment: Option<CharVerticalAlignment>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FormatRun {
pub byte_start: u32,
pub byte_end: u32,
pub format: CharacterFormat,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ImageAnchor {
pub byte_offset: u32,
pub name: String,
pub width: i64,
pub height: i64,
pub quality: i64,
pub format: CharacterFormat,
}
pub fn debug_assert_well_formed(runs: &[FormatRun], block_text_len: usize) {
if cfg!(debug_assertions)
&& let Err(e) = check_well_formed(runs, block_text_len)
{
debug_assert!(false, "format runs are malformed: {e}");
}
}
pub fn check_well_formed(runs: &[FormatRun], block_text_len: usize) -> Result<(), FormatRunError> {
if runs.is_empty() {
return Ok(());
}
for run in runs {
if run.byte_start >= run.byte_end {
return Err(FormatRunError::EmptyRun {
start: run.byte_start,
end: run.byte_end,
});
}
}
for i in 0..runs.len() - 1 {
if runs[i].byte_end > runs[i + 1].byte_start {
return Err(FormatRunError::RunsOverlap {
index: i,
left: Box::new(runs[i].clone()),
right: Box::new(runs[i + 1].clone()),
});
}
if runs[i].byte_end == runs[i + 1].byte_start && runs[i].format == runs[i + 1].format {
return Err(FormatRunError::RunsNotCoalesced { index: i });
}
}
let last = runs.last().expect("non-empty");
if last.byte_end as usize > block_text_len {
return Err(FormatRunError::RunPastEndOfBlock {
start: last.byte_start,
end: last.byte_end,
text_len: block_text_len,
});
}
Ok(())
}
pub fn coalesce_in_place(runs: &mut Vec<FormatRun>) {
if runs.len() < 2 {
return;
}
let mut write = 0usize;
for read in 1..runs.len() {
if runs[write].byte_end == runs[read].byte_start && runs[write].format == runs[read].format
{
runs[write].byte_end = runs[read].byte_end;
} else {
write += 1;
if write != read {
runs[write] = runs[read].clone();
}
}
}
runs.truncate(write + 1);
}
pub fn splice_range(
runs: &mut Vec<FormatRun>,
range: std::ops::Range<u32>,
replacement: Vec<FormatRun>,
) {
if let Err(e) = try_splice_range(runs, range, replacement) {
debug_assert!(false, "splice_range contract violated: {e}");
}
}
pub fn try_splice_range(
runs: &mut Vec<FormatRun>,
range: std::ops::Range<u32>,
replacement: Vec<FormatRun>,
) -> Result<(), FormatRunError> {
if range.start > range.end {
return Err(FormatRunError::ReversedRange {
start: range.start,
end: range.end,
});
}
for r in &replacement {
if r.byte_start >= r.byte_end {
return Err(FormatRunError::EmptyRun {
start: r.byte_start,
end: r.byte_end,
});
}
if r.byte_start < range.start || r.byte_end > range.end {
return Err(FormatRunError::ReplacementOutsideRange {
run_start: r.byte_start,
run_end: r.byte_end,
range_start: range.start,
range_end: range.end,
});
}
}
for i in 1..replacement.len() {
if replacement[i - 1].byte_end > replacement[i].byte_start {
return Err(FormatRunError::RunsOverlap {
index: i - 1,
left: Box::new(replacement[i - 1].clone()),
right: Box::new(replacement[i].clone()),
});
}
}
let mut result: Vec<FormatRun> = Vec::with_capacity(runs.len() + replacement.len());
for run in runs.iter() {
if run.byte_end <= range.start {
result.push(run.clone());
} else if run.byte_start < range.start {
result.push(FormatRun {
byte_start: run.byte_start,
byte_end: range.start,
format: run.format.clone(),
});
}
}
result.extend(replacement);
for run in runs.iter() {
if run.byte_start >= range.end {
result.push(run.clone());
} else if run.byte_end > range.end {
result.push(FormatRun {
byte_start: range.end,
byte_end: run.byte_end,
format: run.format.clone(),
});
}
}
coalesce_in_place(&mut result);
*runs = result;
Ok(())
}
pub fn capture_runs_in_range(runs: &[FormatRun], start: u32, end: u32) -> Vec<FormatRun> {
let mut out = Vec::new();
for run in runs {
if run.byte_end <= start || run.byte_start >= end {
continue;
}
let clipped_start = std::cmp::max(run.byte_start, start);
let clipped_end = std::cmp::min(run.byte_end, end);
if clipped_start < clipped_end {
out.push(FormatRun {
byte_start: clipped_start,
byte_end: clipped_end,
format: run.format.clone(),
});
}
}
out
}
pub fn capture_image_formats_in_range(
images: &[ImageAnchor],
start: u32,
end: u32,
) -> Vec<(u32, CharacterFormat)> {
let mut out = Vec::new();
for img in images {
if img.byte_offset >= start && img.byte_offset < end {
out.push((img.byte_offset, img.format.clone()));
}
}
out
}
pub fn shift_after(runs: &mut [FormatRun], threshold: u32, delta: i32) {
for run in runs.iter_mut() {
if run.byte_start >= threshold {
let new_start = (run.byte_start as i64) + (delta as i64);
let new_end = (run.byte_end as i64) + (delta as i64);
debug_assert!(new_start >= 0 && new_end >= new_start);
run.byte_start = new_start as u32;
run.byte_end = new_end as u32;
}
}
}
pub fn synth_element_id(block_id: u64, byte_start: u32) -> u64 {
const SYNTH_TAG: u64 = 0x4000_0000_0000_0000;
SYNTH_TAG | ((block_id & 0x3FFF_FFFF) << 32) | (byte_start as u64)
}
pub fn shift_images_after(images: &mut [ImageAnchor], threshold: u32, delta: i32) {
for img in images.iter_mut() {
if img.byte_offset >= threshold {
let new_off = (img.byte_offset as i64) + (delta as i64);
debug_assert!(new_off >= 0);
img.byte_offset = new_off as u32;
}
}
}
pub fn shift_runs_for_insert(runs: &mut [FormatRun], byte_offset: u32, inserted_bytes: u32) {
if inserted_bytes == 0 {
return;
}
for run in runs.iter_mut() {
if run.byte_start >= byte_offset {
run.byte_start += inserted_bytes;
run.byte_end += inserted_bytes;
} else if run.byte_end >= byte_offset {
run.byte_end += inserted_bytes;
}
}
}
pub fn shift_runs_for_delete(runs: &mut Vec<FormatRun>, byte_start: u32, byte_end: u32) {
if byte_end <= byte_start {
return;
}
splice_range(runs, byte_start..byte_end, Vec::new());
let delta = (byte_end - byte_start) as i32;
shift_after(runs, byte_end, -delta);
coalesce_in_place(runs);
}
pub fn shift_runs_for_replace(
runs: &mut Vec<FormatRun>,
byte_start: u32,
byte_end: u32,
replacement_bytes: u32,
policy: ReplaceFormatPolicy,
) -> Result<(), FormatRunError> {
if byte_end < byte_start {
return Err(FormatRunError::ReversedRange {
start: byte_start,
end: byte_end,
});
}
let destroys_formatting = byte_end > byte_start;
let override_format: Option<Option<CharacterFormat>> = match policy {
ReplaceFormatPolicy::InheritPreceding => None,
ReplaceFormatPolicy::PreserveNothing => Some(None),
ReplaceFormatPolicy::PreserveIfFullyCovered => {
covering_format(runs, byte_start, byte_end).map(Some)
}
ReplaceFormatPolicy::KeepDominantRun if destroys_formatting => {
Some(dominant_format(runs, byte_start, byte_end))
}
ReplaceFormatPolicy::KeepDominantRun => None,
};
shift_runs_for_delete(runs, byte_start, byte_end);
shift_runs_for_insert(runs, byte_start, replacement_bytes);
if let Some(format) = override_format
&& replacement_bytes > 0
{
let span = byte_start..byte_start + replacement_bytes;
let replacement = match format {
Some(format) => vec![FormatRun {
byte_start: span.start,
byte_end: span.end,
format,
}],
None => Vec::new(),
};
try_splice_range(runs, span, replacement)?;
}
Ok(())
}
fn covering_format(runs: &[FormatRun], start: u32, end: u32) -> Option<CharacterFormat> {
if end <= start {
return None;
}
runs.iter()
.find(|r| r.byte_start <= start && r.byte_end >= end)
.map(|r| r.format.clone())
}
fn dominant_format(runs: &[FormatRun], start: u32, end: u32) -> Option<CharacterFormat> {
if end <= start {
return None;
}
let span = u64::from(end - start);
let mut covered = 0u64;
let mut best: Option<(u64, &FormatRun)> = None;
for r in runs {
let lo = r.byte_start.max(start);
let hi = r.byte_end.min(end);
if hi <= lo {
continue;
}
let overlap = u64::from(hi - lo);
covered += overlap;
if best.is_none_or(|(best_overlap, _)| overlap > best_overlap) {
best = Some((overlap, r));
}
}
let plain = span - covered;
match best {
Some((overlap, run)) if overlap >= plain => Some(run.format.clone()),
_ => None,
}
}
pub fn shift_images_for_insert(images: &mut [ImageAnchor], byte_offset: u32, inserted_bytes: u32) {
if inserted_bytes == 0 {
return;
}
for img in images.iter_mut() {
if img.byte_offset >= byte_offset {
img.byte_offset += inserted_bytes;
}
}
}
pub fn shift_images_for_delete(
images: &mut Vec<ImageAnchor>,
byte_start: u32,
byte_end: u32,
) -> usize {
if byte_end <= byte_start {
return 0;
}
let before = images.len();
images.retain(|i| !(i.byte_offset >= byte_start && i.byte_offset < byte_end));
let removed = before - images.len();
let delta = (byte_end - byte_start) as i32;
shift_images_after(images, byte_end, -delta);
removed
}
pub fn logical_offset_to_byte(plain_text: &str, images: &[ImageAnchor], char_offset: i64) -> u32 {
if char_offset <= 0 {
return 0;
}
let mut logical: i64 = 0;
let mut images_consumed = 0usize;
for (b, _) in plain_text.char_indices() {
while images_consumed < images.len() && images[images_consumed].byte_offset <= b as u32 {
if logical == char_offset {
return b as u32;
}
logical += 1;
images_consumed += 1;
}
if logical == char_offset {
return b as u32;
}
logical += 1;
}
let plain_len = plain_text.len() as u32;
while images_consumed < images.len() {
if logical == char_offset {
return plain_len;
}
logical += 1;
images_consumed += 1;
}
plain_len
}
pub fn split_runs_at(runs: &[FormatRun], byte_offset: u32) -> (Vec<FormatRun>, Vec<FormatRun>) {
let mut left = Vec::new();
let mut right = Vec::new();
for run in runs {
if run.byte_end <= byte_offset {
left.push(run.clone());
} else if run.byte_start >= byte_offset {
right.push(FormatRun {
byte_start: run.byte_start - byte_offset,
byte_end: run.byte_end - byte_offset,
format: run.format.clone(),
});
} else {
left.push(FormatRun {
byte_start: run.byte_start,
byte_end: byte_offset,
format: run.format.clone(),
});
right.push(FormatRun {
byte_start: 0,
byte_end: run.byte_end - byte_offset,
format: run.format.clone(),
});
}
}
(left, right)
}
pub fn split_images_at(
images: &[ImageAnchor],
byte_offset: u32,
) -> (Vec<ImageAnchor>, Vec<ImageAnchor>) {
let mut left = Vec::new();
let mut right = Vec::new();
for img in images {
if img.byte_offset < byte_offset {
left.push(img.clone());
} else {
let mut new = img.clone();
new.byte_offset -= byte_offset;
right.push(new);
}
}
(left, right)
}
pub fn character_format_from_segment(seg: &InlineSegment) -> CharacterFormat {
CharacterFormat {
font_family: seg.fmt_font_family.clone(),
font_point_size: seg.fmt_font_point_size,
font_weight: seg.fmt_font_weight,
font_bold: seg.fmt_font_bold,
font_italic: seg.fmt_font_italic,
font_underline: seg.fmt_font_underline,
font_overline: seg.fmt_font_overline,
font_strikeout: seg.fmt_font_strikeout,
letter_spacing: seg.fmt_letter_spacing,
word_spacing: seg.fmt_word_spacing,
anchor_href: seg.fmt_anchor_href.clone(),
anchor_names: seg.fmt_anchor_names.clone(),
is_anchor: seg.fmt_is_anchor,
tooltip: seg.fmt_tooltip.clone(),
underline_style: seg.fmt_underline_style.clone(),
vertical_alignment: seg.fmt_vertical_alignment.clone(),
}
}
pub fn apply_character_format_to_segment(seg: &mut InlineSegment, fmt: &CharacterFormat) {
seg.fmt_font_family = fmt.font_family.clone();
seg.fmt_font_point_size = fmt.font_point_size;
seg.fmt_font_weight = fmt.font_weight;
seg.fmt_font_bold = fmt.font_bold;
seg.fmt_font_italic = fmt.font_italic;
seg.fmt_font_underline = fmt.font_underline;
seg.fmt_font_overline = fmt.font_overline;
seg.fmt_font_strikeout = fmt.font_strikeout;
seg.fmt_letter_spacing = fmt.letter_spacing;
seg.fmt_word_spacing = fmt.word_spacing;
seg.fmt_anchor_href = fmt.anchor_href.clone();
seg.fmt_anchor_names = fmt.anchor_names.clone();
seg.fmt_is_anchor = fmt.is_anchor;
seg.fmt_tooltip = fmt.tooltip.clone();
seg.fmt_underline_style = fmt.underline_style.clone();
seg.fmt_vertical_alignment = fmt.vertical_alignment.clone();
}
pub fn inline_segments_view(
plain_text: &str,
runs: &[FormatRun],
images: &[ImageAnchor],
) -> Vec<InlineSegment> {
let mut out: Vec<InlineSegment> = Vec::new();
let bytes = plain_text.as_bytes();
let mut img_iter = images.iter().peekable();
let mut cursor: u32 = 0;
let emit_text =
|out: &mut Vec<InlineSegment>, bytes: &[u8], start: u32, end: u32, fmt: CharacterFormat| {
if start >= end {
return;
}
let slice = &bytes[start as usize..end as usize];
let s = std::str::from_utf8(slice)
.expect("block plain_text must be valid UTF-8")
.to_string();
let mut seg = InlineSegment {
content: InlineContent::Text(s),
..Default::default()
};
apply_character_format_to_segment(&mut seg, &fmt);
out.push(seg);
};
let emit_image = |out: &mut Vec<InlineSegment>, anchor: &ImageAnchor| {
let mut seg = InlineSegment {
content: InlineContent::Image {
name: anchor.name.clone(),
width: anchor.width,
height: anchor.height,
quality: anchor.quality,
},
..Default::default()
};
apply_character_format_to_segment(&mut seg, &anchor.format);
out.push(seg);
};
for run in runs {
while let Some(img) = img_iter.peek() {
if img.byte_offset < run.byte_start {
emit_text(
&mut out,
bytes,
cursor,
img.byte_offset,
CharacterFormat::default(),
);
emit_image(&mut out, img);
cursor = img.byte_offset;
img_iter.next();
} else {
break;
}
}
if cursor < run.byte_start {
emit_text(
&mut out,
bytes,
cursor,
run.byte_start,
CharacterFormat::default(),
);
}
emit_text(
&mut out,
bytes,
run.byte_start,
run.byte_end,
run.format.clone(),
);
cursor = run.byte_end;
}
for img in img_iter {
if img.byte_offset > cursor {
emit_text(
&mut out,
bytes,
cursor,
img.byte_offset,
CharacterFormat::default(),
);
cursor = img.byte_offset;
}
emit_image(&mut out, img);
}
if (cursor as usize) < bytes.len() {
emit_text(
&mut out,
bytes,
cursor,
bytes.len() as u32,
CharacterFormat::default(),
);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn run(s: u32, e: u32, bold: bool) -> FormatRun {
FormatRun {
byte_start: s,
byte_end: e,
format: CharacterFormat {
font_bold: Some(bold),
..Default::default()
},
}
}
#[test]
fn empty_runs_are_well_formed() {
debug_assert_well_formed(&[], 0);
debug_assert_well_formed(&[], 100);
}
#[test]
fn coalesce_merges_adjacent_equal_runs() {
let mut rs = vec![run(0, 5, true), run(5, 10, true), run(10, 15, false)];
coalesce_in_place(&mut rs);
assert_eq!(rs.len(), 2);
assert_eq!(rs[0].byte_end, 10);
}
#[test]
fn coalesce_leaves_disjoint_runs_alone() {
let mut rs = vec![run(0, 5, true), run(7, 10, true)];
coalesce_in_place(&mut rs);
assert_eq!(rs.len(), 2);
}
#[test]
fn splice_range_clips_straddling_runs() {
let mut rs = vec![run(0, 20, true)];
splice_range(&mut rs, 5..15, vec![run(5, 15, false)]);
assert_eq!(rs.len(), 3);
assert_eq!(rs[0].byte_end, 5);
assert_eq!(rs[1].format.font_bold, Some(false));
assert_eq!(rs[2].byte_start, 15);
}
#[test]
fn splice_range_empty_replacement_removes_inner_runs() {
let mut rs = vec![run(0, 5, true), run(5, 10, false), run(10, 15, true)];
splice_range(&mut rs, 5..10, vec![]);
assert_eq!(rs.len(), 2);
assert_eq!(rs[0].byte_end, 5);
assert_eq!(rs[1].byte_start, 10);
}
#[test]
fn shift_after_moves_downstream() {
let mut rs = vec![run(0, 5, true), run(10, 15, false)];
shift_after(&mut rs, 5, 3);
assert_eq!(rs[0].byte_start, 0); assert_eq!(rs[1].byte_start, 13);
assert_eq!(rs[1].byte_end, 18);
}
}
#[cfg(test)]
mod replace_policy_tests {
use super::*;
fn fmt(tag: &str) -> CharacterFormat {
CharacterFormat {
font_bold: Some(tag == "B"),
font_italic: Some(tag == "I"),
..Default::default()
}
}
fn r(start: u32, end: u32, tag: &str) -> FormatRun {
FormatRun {
byte_start: start,
byte_end: end,
format: fmt(tag),
}
}
fn show(runs: &[FormatRun]) -> String {
if runs.is_empty() {
return "[]".to_string();
}
runs.iter()
.map(|x| {
let tag = if x.format.font_bold == Some(true) {
"B"
} else if x.format.font_italic == Some(true) {
"I"
} else {
"p"
};
format!("{}..{}={tag}", x.byte_start, x.byte_end)
})
.collect::<Vec<_>>()
.join(" ")
}
fn replace(
runs: &[FormatRun],
start: u32,
end: u32,
n: u32,
policy: ReplaceFormatPolicy,
) -> Vec<FormatRun> {
let mut runs = runs.to_vec();
shift_runs_for_replace(&mut runs, start, end, n, policy).expect("valid replace");
runs
}
#[test]
fn inherit_preceding_matches_the_historical_delete_then_insert() {
let corpus: Vec<(&str, Vec<FormatRun>, u32, u32, u32)> = vec![
("run ends exactly at start", vec![r(0, 5, "B")], 5, 10, 3),
("run begins exactly at start", vec![r(5, 8, "B")], 5, 10, 3),
(
"run begins at start, outlives end",
vec![r(5, 20, "B")],
5,
10,
3,
),
(
"run straddles the whole range",
vec![r(0, 20, "B")],
5,
10,
3,
),
("no run touches the start", vec![r(12, 20, "B")], 5, 10, 3),
("bold tail inside the range", vec![r(9, 13, "B")], 5, 13, 4),
("pure delete", vec![r(0, 20, "B")], 5, 10, 0),
("pure insert", vec![r(0, 20, "B")], 5, 5, 3),
(
"same format either side coalesces",
vec![r(0, 5, "B"), r(10, 15, "B")],
5,
10,
3,
),
(
"different formats either side",
vec![r(0, 5, "B"), r(10, 15, "I")],
5,
10,
3,
),
("empty run list", vec![], 5, 10, 3),
("the only run is consumed", vec![r(5, 10, "B")], 5, 10, 3),
(
"replacement longer than the range",
vec![r(0, 5, "B")],
5,
10,
20,
),
(
"three runs straddled",
vec![r(0, 3, "B"), r(3, 6, "I"), r(6, 9, "B")],
2,
7,
4,
),
(
"gap between two same-format runs is deleted",
vec![r(0, 5, "B"), r(8, 13, "B")],
5,
8,
0,
),
];
for (name, runs, start, end, n) in corpus {
let mut expected = runs.clone();
shift_runs_for_delete(&mut expected, start, end);
shift_runs_for_insert(&mut expected, start, n);
let got = replace(&runs, start, end, n, ReplaceFormatPolicy::InheritPreceding);
assert_eq!(
show(&got),
show(&expected),
"InheritPreceding diverged from delete+insert for {name:?} \
(replace {start}..{end}, n={n})\n before: {}\n historical: {}\n got: {}",
show(&runs),
show(&expected),
show(&got),
);
}
}
#[test]
fn the_four_policies_diverge_on_a_partly_bold_name() {
let runs = vec![r(5, 9, "B")];
let (start, end, n) = (0, 9, 9);
use ReplaceFormatPolicy::*;
assert_eq!(
show(&replace(&runs, start, end, n, InheritPreceding)),
"[]",
"the historical default destroys the bold — pinned, not endorsed"
);
assert_eq!(
show(&replace(&runs, start, end, n, PreserveNothing)),
"[]",
"explicitly unformatted"
);
assert_eq!(
show(&replace(&runs, start, end, n, PreserveIfFullyCovered)),
"[]",
"no SINGLE run covers 0..9 — it must fall back to inheritance, not guess"
);
assert_eq!(
show(&replace(&runs, start, end, n, KeepDominantRun)),
"[]",
"plain covers more of the name than the bold does"
);
let mostly_bold = vec![r(1, 9, "B")];
assert_eq!(
show(&replace(&mostly_bold, 0, 9, 9, KeepDominantRun)),
"0..9=B",
"bold covers 8 of 9 bytes — the rename must keep it"
);
}
#[test]
fn fully_covered_means_a_single_run_not_a_gapless_union() {
let two = vec![r(0, 3, "I"), r(3, 10, "B")];
assert_eq!(
show(&replace(
&two,
0,
10,
4,
ReplaceFormatPolicy::PreserveIfFullyCovered
)),
"[]",
"two different-format runs jointly spanning the range are not 'covered'; \
with no run preceding the start, the fallback is unformatted"
);
let one = vec![r(5, 20, "B")];
assert_eq!(
show(&replace(
&one,
5,
10,
3,
ReplaceFormatPolicy::PreserveIfFullyCovered
)),
"5..18=B",
"a single covering run keeps its format across the rename"
);
assert_eq!(
show(&replace(
&one,
5,
10,
3,
ReplaceFormatPolicy::InheritPreceding
)),
"8..18=B",
"…which the default would have lost: the replacement lands unformatted"
);
}
#[test]
fn a_partially_overlapping_run_does_not_count_as_covering() {
let runs = vec![r(0, 8, "B")]; assert_eq!(
show(&replace(
&runs,
5,
12,
4,
ReplaceFormatPolicy::PreserveIfFullyCovered
)),
"0..9=B",
"not covered → falls back to inheritance, which extends the preceding bold; \
it must NOT format the whole replacement as though bold had covered it"
);
}
#[test]
fn a_dominance_tie_between_two_runs_goes_to_the_earlier() {
let runs = vec![r(0, 3, "B"), r(3, 6, "I")]; assert_eq!(
show(&replace(
&runs,
0,
6,
4,
ReplaceFormatPolicy::KeepDominantRun
)),
"0..4=B",
"a true tie must resolve to the earlier run, not to whichever the iterator \
happened to visit last"
);
}
#[test]
fn a_dominance_tie_against_plain_text_keeps_the_formatting() {
let runs = vec![r(4, 8, "B")]; assert_eq!(
show(&replace(
&runs,
0,
8,
5,
ReplaceFormatPolicy::KeepDominantRun
)),
"0..5=B",
"an even split must keep the formatting rather than silently drop it"
);
}
#[test]
fn an_empty_range_is_an_insert_and_no_coverage_policy_overrides_it() {
let runs = vec![r(0, 5, "B"), r(5, 10, "I")];
use ReplaceFormatPolicy::*;
for policy in [InheritPreceding, PreserveIfFullyCovered, KeepDominantRun] {
assert_eq!(
show(&replace(&runs, 5, 5, 2, policy)),
"0..7=B 7..12=I",
"{policy:?}: typing at a boundary must inherit the run to the LEFT (Qt \
convention) — an empty range destroyed no formatting, so there is \
nothing for a coverage policy to override"
);
}
assert_eq!(
show(&replace(&runs, 5, 5, 2, PreserveNothing)),
"0..5=B 7..12=I",
"PreserveNothing asks for unformatted text, and means it even on an insert"
);
}
#[test]
fn a_zero_width_zero_length_replace_is_the_identity() {
let runs = vec![r(0, 5, "B"), r(7, 12, "I")];
for policy in [
ReplaceFormatPolicy::InheritPreceding,
ReplaceFormatPolicy::PreserveIfFullyCovered,
ReplaceFormatPolicy::KeepDominantRun,
ReplaceFormatPolicy::PreserveNothing,
] {
assert_eq!(
show(&replace(&runs, 6, 6, 0, policy)),
"0..5=B 7..12=I",
"{policy:?} changed a no-op edit"
);
}
}
#[test]
fn preserve_nothing_fabricates_no_default_run() {
let runs = vec![r(0, 5, "B")];
let got = replace(&runs, 7, 9, 2, ReplaceFormatPolicy::PreserveNothing);
assert_eq!(show(&got), "0..5=B", "no run may be invented for the gap");
assert!(
got.iter().all(|x| x.byte_start < 7 || x.byte_end > 9),
"the replaced span must carry no run at all"
);
}
#[test]
fn offsets_are_bytes_not_characters() {
let runs = vec![r(0, 4, "B"), r(5, 9, "I")];
let got = replace(&runs, 0, 4, 2, ReplaceFormatPolicy::KeepDominantRun);
assert_eq!(
show(&got),
"0..2=B 3..7=I",
"the trailing italic must shift back by the BYTE delta (4 -> 2 = -2)"
);
}
#[test]
fn every_policy_leaves_the_runs_well_formed() {
let setups: Vec<(Vec<FormatRun>, u32, u32, u32, usize)> = vec![
(vec![r(0, 3, "B"), r(3, 6, "I"), r(6, 9, "B")], 2, 7, 4, 8),
(vec![r(0, 5, "B"), r(8, 13, "B")], 5, 8, 0, 10),
(vec![r(0, 5, "B"), r(7, 12, "B")], 8, 10, 2, 12),
(vec![r(3, 7, "B")], 3, 7, 0, 6),
(vec![], 2, 6, 3, 9),
];
for (runs, start, end, n, text_len) in setups {
for policy in [
ReplaceFormatPolicy::InheritPreceding,
ReplaceFormatPolicy::PreserveIfFullyCovered,
ReplaceFormatPolicy::KeepDominantRun,
ReplaceFormatPolicy::PreserveNothing,
] {
let got = replace(&runs, start, end, n, policy);
check_well_formed(&got, text_len).unwrap_or_else(|e| {
panic!(
"{policy:?} produced malformed runs from {} (replace {start}..{end}, \
n={n}): {} — {e}",
show(&runs),
show(&got)
)
});
}
}
}
#[test]
fn an_untouched_gap_between_equal_runs_survives() {
let runs = vec![r(0, 5, "B"), r(7, 12, "B")];
assert_eq!(
show(&replace(
&runs,
8,
10,
2,
ReplaceFormatPolicy::InheritPreceding
)),
"0..5=B 7..12=B",
"the plain gap at 5..7 must not be swallowed"
);
}
#[test]
fn an_inverted_range_is_an_error_not_a_panic() {
let mut runs = vec![r(0, 5, "B")];
let err =
shift_runs_for_replace(&mut runs, 10, 5, 3, ReplaceFormatPolicy::InheritPreceding)
.expect_err("an inverted range must be rejected");
assert!(matches!(
err,
FormatRunError::ReversedRange { start: 10, end: 5 }
));
assert_eq!(show(&runs), "0..5=B", "a refused edit must change nothing");
}
}
#[cfg(test)]
mod invariant_check_tests {
use super::*;
fn run(start: u32, end: u32, bold: bool) -> FormatRun {
FormatRun {
byte_start: start,
byte_end: end,
format: CharacterFormat {
font_bold: Some(bold),
..Default::default()
},
}
}
#[test]
fn a_replacement_outside_the_range_is_rejected_without_mutating() {
let mut runs = vec![run(0, 20, true)];
let before = runs.clone();
let err = try_splice_range(&mut runs, 5..10, vec![run(5, 15, false)])
.expect_err("a replacement run reaching past range.end must be rejected");
assert!(matches!(
err,
FormatRunError::ReplacementOutsideRange {
run_end: 15,
range_end: 10,
..
}
));
assert_eq!(
runs, before,
"a rejected splice must not half-apply — validation happens before mutation"
);
}
#[test]
#[allow(clippy::reversed_empty_ranges)]
fn a_reversed_range_is_rejected() {
let mut runs = vec![run(0, 20, true)];
assert!(matches!(
try_splice_range(&mut runs, 10..5, vec![]),
Err(FormatRunError::ReversedRange { start: 10, end: 5 })
));
}
#[test]
fn a_legal_splice_still_works_through_the_checked_path() {
let mut runs = vec![run(0, 20, true)];
try_splice_range(&mut runs, 5..15, vec![run(5, 15, false)]).expect("legal");
assert_eq!(runs.len(), 3);
assert_eq!(runs[1].format.font_bold, Some(false));
}
#[test]
fn check_well_formed_catches_what_debug_assert_used_to() {
assert!(check_well_formed(&[], 0).is_ok());
assert!(check_well_formed(&[run(0, 5, true)], 5).is_ok());
assert!(matches!(
check_well_formed(&[run(5, 5, true)], 10),
Err(FormatRunError::EmptyRun { .. })
));
assert!(matches!(
check_well_formed(&[run(0, 8, true), run(5, 10, false)], 10),
Err(FormatRunError::RunsOverlap { .. })
));
assert!(matches!(
check_well_formed(&[run(0, 5, true), run(5, 10, true)], 10),
Err(FormatRunError::RunsNotCoalesced { .. })
));
assert!(matches!(
check_well_formed(&[run(0, 20, true)], 10),
Err(FormatRunError::RunPastEndOfBlock { text_len: 10, .. })
));
}
}