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),
FootnoteRef {
label: String,
},
Image {
name: String,
#[serde(default)]
alt: 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,
#[serde(default)]
pub alt: String,
pub width: i64,
pub height: i64,
pub quality: i64,
pub format: CharacterFormat,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FootnoteRefAnchor {
pub byte_offset: u32,
pub label: String,
pub format: CharacterFormat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockAnchor<'a> {
Image(&'a ImageAnchor),
FootnoteRef(&'a FootnoteRefAnchor),
}
impl BlockAnchor<'_> {
pub fn byte_offset(&self) -> u32 {
match self {
BlockAnchor::Image(i) => i.byte_offset,
BlockAnchor::FootnoteRef(f) => f.byte_offset,
}
}
}
pub fn block_anchors<'a>(
images: &'a [ImageAnchor],
footnote_refs: &'a [FootnoteRefAnchor],
) -> Vec<BlockAnchor<'a>> {
let mut anchors: Vec<BlockAnchor<'a>> = Vec::with_capacity(images.len() + footnote_refs.len());
anchors.extend(images.iter().map(BlockAnchor::Image));
anchors.extend(footnote_refs.iter().map(BlockAnchor::FootnoteRef));
anchors.sort_by_key(|a| a.byte_offset());
anchors
}
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_footnote_refs_for_insert(
notes: &mut [FootnoteRefAnchor],
byte_offset: u32,
inserted_bytes: u32,
) {
if inserted_bytes == 0 {
return;
}
for note in notes.iter_mut() {
if note.byte_offset >= byte_offset {
note.byte_offset += inserted_bytes;
}
}
}
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;
}
plain_text
.char_indices()
.nth(char_offset as usize)
.map(|(b, _)| b as u32)
.unwrap_or(plain_text.len() as u32)
}
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_footnote_refs_at(
notes: &[FootnoteRefAnchor],
byte_offset: u32,
) -> (Vec<FootnoteRefAnchor>, Vec<FootnoteRefAnchor>) {
let mut left = Vec::new();
let mut right = Vec::new();
for note in notes {
if note.byte_offset < byte_offset {
left.push(note.clone());
} else {
let mut new = note.clone();
new.byte_offset -= byte_offset;
right.push(new);
}
}
(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();
}
#[derive(Debug, Clone, PartialEq)]
pub enum InlinePiece<'a> {
Text {
start: u32,
end: u32,
format: Option<&'a CharacterFormat>,
},
Image(&'a ImageAnchor),
FootnoteRef(&'a FootnoteRefAnchor),
}
pub fn merge_runs_and_anchors<'a>(
plain_text: &str,
runs: &'a [FormatRun],
anchors: &[BlockAnchor<'a>],
) -> Vec<InlinePiece<'a>> {
let text_len = plain_text.len() as u32;
let mut out: Vec<InlinePiece<'a>> = Vec::new();
let mut img_iter = anchors.iter().peekable();
let mut cursor: u32 = 0;
fn sentinel_len(plain_text: &str, byte_offset: u32) -> u32 {
let at = byte_offset as usize;
if plain_text.len() >= at + 3 && plain_text.as_bytes()[at..at + 3] == [0xEF, 0xBF, 0xBC] {
3
} else {
0
}
}
let push_text = |out: &mut Vec<InlinePiece<'a>>,
start: u32,
end: u32,
format: Option<&'a CharacterFormat>| {
if start < end {
out.push(InlinePiece::Text { start, end, format });
}
};
fn piece<'a>(anchor: &BlockAnchor<'a>) -> InlinePiece<'a> {
match *anchor {
BlockAnchor::Image(i) => InlinePiece::Image(i),
BlockAnchor::FootnoteRef(f) => InlinePiece::FootnoteRef(f),
}
}
for run in runs {
while let Some(anchor) = img_iter.peek() {
let at = anchor.byte_offset();
if at >= run.byte_start {
break;
}
push_text(&mut out, cursor, at, None);
out.push(piece(anchor));
cursor = cursor.max(at + sentinel_len(plain_text, at));
img_iter.next();
}
push_text(&mut out, cursor, run.byte_start, None);
cursor = cursor.max(run.byte_start);
while let Some(anchor) = img_iter.peek() {
let at = anchor.byte_offset();
if at > run.byte_end {
break;
}
push_text(&mut out, cursor, at, Some(&run.format));
out.push(piece(anchor));
cursor = cursor.max(at + sentinel_len(plain_text, at));
img_iter.next();
}
push_text(&mut out, cursor, run.byte_end, Some(&run.format));
cursor = cursor.max(run.byte_end);
}
for anchor in img_iter {
let at = anchor.byte_offset();
push_text(&mut out, cursor, at, None);
out.push(piece(anchor));
cursor = cursor.max(at + sentinel_len(plain_text, at));
}
push_text(&mut out, cursor, text_len, None);
out
}
pub fn inline_segments_view(
plain_text: &str,
runs: &[FormatRun],
images: &[ImageAnchor],
footnote_refs: &[FootnoteRefAnchor],
) -> Vec<InlineSegment> {
let bytes = plain_text.as_bytes();
let default_format = CharacterFormat::default();
merge_runs_and_anchors(plain_text, runs, &block_anchors(images, footnote_refs))
.into_iter()
.map(|piece| match piece {
InlinePiece::Text { start, end, format } => {
let slice = &bytes[start as usize..end as usize];
let text = std::str::from_utf8(slice)
.expect("block plain_text must be valid UTF-8")
.to_string();
let mut seg = InlineSegment {
content: InlineContent::Text(text),
..Default::default()
};
apply_character_format_to_segment(&mut seg, format.unwrap_or(&default_format));
seg
}
InlinePiece::Image(anchor) => {
let mut seg = InlineSegment {
content: InlineContent::Image {
name: anchor.name.clone(),
alt: anchor.alt.clone(),
width: anchor.width,
height: anchor.height,
quality: anchor.quality,
},
..Default::default()
};
apply_character_format_to_segment(&mut seg, &anchor.format);
seg
}
InlinePiece::FootnoteRef(anchor) => {
let mut seg = InlineSegment {
content: InlineContent::FootnoteRef {
label: anchor.label.clone(),
},
..Default::default()
};
apply_character_format_to_segment(&mut seg, &anchor.format);
seg
}
})
.collect()
}
#[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);
}
fn anchor(at: u32, name: &str) -> ImageAnchor {
ImageAnchor {
byte_offset: at,
name: name.into(),
alt: String::new(),
width: 10,
height: 10,
quality: 100,
format: CharacterFormat::default(),
}
}
fn fn_anchor(at: u32, label: &str) -> FootnoteRefAnchor {
FootnoteRefAnchor {
byte_offset: at,
label: label.into(),
format: CharacterFormat::default(),
}
}
fn shape(text: &str, runs: &[FormatRun], images: &[ImageAnchor]) -> String {
shape_with(text, runs, images, &[])
}
fn shape_with(
text: &str,
runs: &[FormatRun],
images: &[ImageAnchor],
notes: &[FootnoteRefAnchor],
) -> String {
merge_runs_and_anchors(text, runs, &block_anchors(images, notes))
.into_iter()
.map(|p| match p {
InlinePiece::Text { start, end, format } => {
let s = &text[start as usize..end as usize];
if format.is_some() {
format!("*{s}*")
} else {
s.to_string()
}
}
InlinePiece::Image(a) => format!("[{}]", a.name),
InlinePiece::FootnoteRef(a) => format!("^{}^", a.label),
})
.collect::<Vec<_>>()
.join("|")
}
#[test]
fn a_footnote_reference_inside_a_run_splits_it() {
let text = "abcdef";
let runs = [run(0, 6, true)];
assert_eq!(
shape_with(text, &runs, &[], &[fn_anchor(3, "n1")]),
"*abc*|^n1^|*def*"
);
}
#[test]
fn images_and_references_interleave_by_position() {
let text = "abcdefgh";
assert_eq!(
shape_with(
text,
&[],
&[anchor(6, "img")],
&[fn_anchor(2, "early"), fn_anchor(7, "late")]
),
"ab|^early^|cdef|[img]|g|^late^|h"
);
}
#[test]
fn an_image_inside_a_run_splits_it_instead_of_jumping_to_the_end() {
let text = "abcdef";
let runs = [run(0, 6, true)];
let images = [anchor(3, "img")];
assert_eq!(shape(text, &runs, &images), "*abc*|[img]|*def*");
}
#[test]
fn an_image_on_a_run_start_boundary_stays_in_place() {
let text = "abcdef";
let runs = [run(3, 6, true)];
assert_eq!(shape(text, &runs, &[anchor(3, "i")]), "abc|[i]|*def*");
}
#[test]
fn an_image_on_a_run_end_boundary_stays_in_place() {
let text = "abcdef";
let runs = [run(0, 3, true)];
assert_eq!(shape(text, &runs, &[anchor(3, "i")]), "*abc*|[i]|def");
}
#[test]
fn an_image_between_two_runs_lands_between_them() {
let text = "abcdef";
let runs = [run(0, 3, true), run(3, 6, false)];
let out = shape(text, &runs, &[anchor(3, "i")]);
assert_eq!(out, "*abc*|[i]|*def*");
}
#[test]
fn several_images_inside_one_run_keep_their_order() {
let text = "abcdefgh";
let runs = [run(0, 8, true)];
let images = [anchor(2, "a"), anchor(5, "b")];
assert_eq!(shape(text, &runs, &images), "*ab*|[a]|*cde*|[b]|*fgh*");
}
#[test]
fn two_images_at_the_same_offset_both_survive_in_order() {
let text = "abcd";
let runs = [run(0, 4, true)];
let images = [anchor(2, "a"), anchor(2, "b")];
assert_eq!(shape(text, &runs, &images), "*ab*|[a]|[b]|*cd*");
}
#[test]
fn images_with_no_runs_at_all_are_ordered_with_their_gaps() {
let text = "abcdef";
let images = [anchor(0, "a"), anchor(3, "b"), anchor(6, "c")];
assert_eq!(shape(text, &[], &images), "[a]|abc|[b]|def|[c]");
}
#[test]
fn text_uncovered_by_any_run_stays_unformatted() {
let text = "abcdef";
let runs = [run(2, 4, true)];
assert_eq!(shape(text, &runs, &[]), "ab|*cd*|ef");
}
#[test]
fn a_block_with_neither_runs_nor_images_is_one_plain_piece() {
assert_eq!(shape("abc", &[], &[]), "abc");
assert_eq!(shape("", &[], &[]), "");
}
#[test]
fn every_byte_is_emitted_exactly_once_and_in_order() {
let text = "abcdefghij";
let arrangements: [(&[FormatRun], &[ImageAnchor]); 6] = [
(&[], &[]),
(&[run(0, 10, true)], &[anchor(5, "m")]),
(&[run(2, 5, true), run(5, 8, false)], &[anchor(5, "m")]),
(&[run(2, 5, true)], &[anchor(0, "a"), anchor(10, "z")]),
(&[run(0, 3, true), run(7, 10, true)], &[anchor(3, "m")]),
(
&[run(1, 4, true), run(4, 9, false)],
&[anchor(1, "a"), anchor(4, "b"), anchor(9, "c")],
),
];
for (i, (runs, images)) in arrangements.iter().enumerate() {
let pieces = merge_runs_and_anchors(text, runs, &block_anchors(images, &[]));
let mut cursor = 0u32;
let mut rebuilt = String::new();
for piece in &pieces {
if let InlinePiece::Text { start, end, .. } = piece {
assert_eq!(*start, cursor, "arrangement {i}: gap or overlap");
assert!(start < end, "arrangement {i}: empty piece emitted");
rebuilt.push_str(&text[*start as usize..*end as usize]);
cursor = *end;
}
}
assert_eq!(cursor, text.len() as u32, "arrangement {i}: truncated");
assert_eq!(rebuilt, text, "arrangement {i}");
let img_count = pieces
.iter()
.filter(|p| matches!(p, InlinePiece::Image(_)))
.count();
assert_eq!(img_count, images.len(), "arrangement {i}: lost an image");
}
}
#[test]
fn inline_segments_view_places_a_mid_run_image_correctly() {
let segs = inline_segments_view("abcdef", &[run(0, 6, true)], &[anchor(3, "img")], &[]);
assert_eq!(segs.len(), 3);
assert!(matches!(&segs[0].content, InlineContent::Text(t) if t == "abc"));
assert!(matches!(&segs[1].content, InlineContent::Image { name, .. } if name == "img"));
assert!(matches!(&segs[2].content, InlineContent::Text(t) if t == "def"));
assert_eq!(segs[0].fmt_font_bold, Some(true));
assert_eq!(segs[2].fmt_font_bold, Some(true));
}
#[test]
fn inline_segments_view_carries_alt_text_through() {
let mut a = anchor(1, "img");
a.alt = "a black cat".into();
let segs = inline_segments_view("ab", &[], &[a], &[]);
let alt = segs.iter().find_map(|s| match &s.content {
InlineContent::Image { alt, .. } => Some(alt.clone()),
_ => None,
});
assert_eq!(alt.as_deref(), Some("a black cat"));
}
#[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, .. })
));
}
}