use alloc::vec::Vec;
use crate::{Event, TextStyleKind};
pub const MAX_STYLE_DEPTH: usize = 32;
#[derive(Debug, Clone)]
struct StyleFrame {
kind: TextStyleKind,
text_emitted: bool,
}
#[derive(Debug, Clone, Default)]
pub struct StyleStack {
frames: Vec<StyleFrame>,
deferred_starts: Vec<Event>,
}
impl StyleStack {
#[inline]
pub fn open(&mut self, kind: TextStyleKind) -> Vec<Event> {
if self.frames.iter().any(|frame| frame.kind == kind)
|| self.frames.len() >= MAX_STYLE_DEPTH
{
return Vec::new();
}
let start = Event::StartTextStyle {
kind: kind.clone(),
id: None,
};
self.frames.push(StyleFrame {
kind,
text_emitted: false,
});
self.deferred_starts.push(start);
Vec::new()
}
#[inline]
pub fn close(&mut self, kind: &TextStyleKind) -> Vec<Event> {
let Some(position) = self.frames.iter().rposition(|frame| frame.kind == *kind) else {
return Vec::new();
};
let Some(after_position) = position.checked_add(1) else {
return Vec::new();
};
if after_position == self.frames.len() {
let Some(frame) = self.frames.pop() else {
return Vec::new();
};
self.rebuild_deferred_starts();
return if frame.text_emitted {
alloc::vec![Event::EndTextStyle]
} else {
Vec::new()
};
}
let mut emitted = Vec::new();
let mut above = self.frames.split_off(after_position);
for frame in above.iter().rev() {
if frame.text_emitted {
emitted.push(Event::EndTextStyle);
}
}
let Some(matched) = self.frames.pop() else {
self.rebuild_deferred_starts();
return emitted;
};
if matched.text_emitted {
emitted.push(Event::EndTextStyle);
}
for frame in above.drain(..) {
self.frames.push(StyleFrame {
kind: frame.kind,
text_emitted: false,
});
}
self.rebuild_deferred_starts();
emitted
}
#[inline]
pub fn note_text(&mut self) -> Vec<Event> {
for frame in &mut self.frames {
frame.text_emitted = true;
}
self.deferred_starts.drain(..).collect()
}
#[inline]
pub fn close_all(&mut self) -> Vec<Event> {
let mut emitted = Vec::new();
for frame in self.frames.iter().rev() {
if frame.text_emitted {
emitted.push(Event::EndTextStyle);
}
}
self.frames.clear();
self.deferred_starts.clear();
emitted
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.frames.is_empty() && self.deferred_starts.is_empty()
}
fn rebuild_deferred_starts(&mut self) {
self.deferred_starts = self
.frames
.iter()
.filter(|frame| !frame.text_emitted)
.map(|frame| Event::StartTextStyle {
kind: frame.kind.clone(),
id: None,
})
.collect();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Color;
use alloc::vec;
fn start(kind: TextStyleKind) -> Event {
Event::StartTextStyle { kind, id: None }
}
fn yellow() -> Color {
Color::Rgb {
r: 255,
g: 255,
b: 0,
}
}
#[test]
fn max_style_depth_is_32() {
assert_eq!(MAX_STYLE_DEPTH, 32);
}
fn blue() -> Color {
Color::Rgb { r: 0, g: 0, b: 255 }
}
fn red() -> Color {
Color::Rgb { r: 255, g: 0, b: 0 }
}
#[test]
fn open_then_close_with_text() {
let mut stack = StyleStack::default();
assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
assert_eq!(stack.note_text(), vec![start(TextStyleKind::Bold)]);
assert_eq!(stack.close(&TextStyleKind::Bold), vec![Event::EndTextStyle]);
assert!(stack.is_empty());
}
#[test]
fn open_then_close_without_text_emits_nothing() {
let mut stack = StyleStack::default();
assert_eq!(stack.open(TextStyleKind::Italic), Vec::new());
assert_eq!(stack.close(&TextStyleKind::Italic), Vec::new());
assert!(stack.is_empty());
}
#[test]
fn same_kind_nesting_idempotent() {
let mut stack = StyleStack::default();
assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
assert_eq!(stack.frames.len(), 1);
assert_eq!(stack.note_text(), vec![start(TextStyleKind::Bold)]);
assert_eq!(stack.close(&TextStyleKind::Bold), vec![Event::EndTextStyle]);
assert_eq!(stack.close(&TextStyleKind::Bold), Vec::new());
assert!(stack.is_empty());
}
#[test]
fn rule_9_mismatched_closers_with_text() {
let mut stack = StyleStack::default();
assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
assert_eq!(stack.note_text(), vec![start(TextStyleKind::Bold)]);
assert_eq!(stack.open(TextStyleKind::Italic), Vec::new());
assert_eq!(stack.note_text(), vec![start(TextStyleKind::Italic)]);
assert_eq!(
stack.close(&TextStyleKind::Bold),
vec![Event::EndTextStyle, Event::EndTextStyle]
);
assert_eq!(stack.note_text(), vec![start(TextStyleKind::Italic)]);
assert_eq!(
stack.close(&TextStyleKind::Italic),
vec![Event::EndTextStyle]
);
assert!(stack.is_empty());
}
#[test]
fn rule_9_mismatched_closers_no_extra_text() {
let mut stack = StyleStack::default();
assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
assert_eq!(stack.note_text(), vec![start(TextStyleKind::Bold)]);
assert_eq!(stack.open(TextStyleKind::Italic), Vec::new());
assert_eq!(stack.note_text(), vec![start(TextStyleKind::Italic)]);
assert_eq!(
stack.close(&TextStyleKind::Bold),
vec![Event::EndTextStyle, Event::EndTextStyle]
);
assert_eq!(stack.close(&TextStyleKind::Italic), Vec::new());
assert!(stack.is_empty());
}
#[test]
fn depth_bound() {
let mut stack = StyleStack::default();
for level in 0..MAX_STYLE_DEPTH {
let level_u8 = u8::try_from(level).unwrap_or(u8::MAX);
assert_eq!(
stack.open(TextStyleKind::Mark(Color::Rgb {
r: level_u8,
g: 0,
b: 0,
})),
Vec::new()
);
}
assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
assert_eq!(stack.frames.len(), MAX_STYLE_DEPTH);
assert_eq!(stack.deferred_starts.len(), MAX_STYLE_DEPTH);
}
#[test]
fn close_unmatched() {
let mut stack = StyleStack::default();
assert_eq!(stack.close(&TextStyleKind::Bold), Vec::new());
assert!(stack.is_empty());
}
#[test]
fn close_all_with_open_frames() {
let mut stack = StyleStack::default();
assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
assert_eq!(stack.open(TextStyleKind::Italic), Vec::new());
assert_eq!(
stack.note_text(),
vec![start(TextStyleKind::Bold), start(TextStyleKind::Italic)]
);
assert_eq!(
stack.close_all(),
vec![Event::EndTextStyle, Event::EndTextStyle]
);
assert!(stack.is_empty());
}
#[test]
fn close_all_with_deferred_only_emits_nothing() {
let mut stack = StyleStack::default();
assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
assert_eq!(stack.open(TextStyleKind::Italic), Vec::new());
assert_eq!(stack.close_all(), Vec::new());
assert!(stack.is_empty());
}
#[test]
fn mark_with_arbitrary_color_round_trips() {
let mut stack = StyleStack::default();
assert_eq!(stack.open(TextStyleKind::Mark(yellow())), Vec::new());
assert_eq!(
stack.note_text(),
vec![start(TextStyleKind::Mark(yellow()))]
);
assert_eq!(
stack.close(&TextStyleKind::Mark(yellow())),
vec![Event::EndTextStyle]
);
assert!(stack.is_empty());
}
#[test]
fn distinct_mark_colors_are_distinct_kinds() {
let mut stack = StyleStack::default();
assert_eq!(stack.open(TextStyleKind::Mark(blue())), Vec::new());
assert_eq!(stack.open(TextStyleKind::Mark(red())), Vec::new());
assert_eq!(stack.frames.len(), 2);
assert_eq!(
stack.note_text(),
vec![
start(TextStyleKind::Mark(blue())),
start(TextStyleKind::Mark(red()))
]
);
assert_eq!(
stack.close(&TextStyleKind::Mark(red())),
vec![Event::EndTextStyle]
);
assert_eq!(
stack.close(&TextStyleKind::Mark(blue())),
vec![Event::EndTextStyle]
);
assert!(stack.is_empty());
}
#[test]
fn text_color_round_trips() {
let mut stack = StyleStack::default();
assert_eq!(stack.open(TextStyleKind::TextColor(red())), Vec::new());
assert_eq!(
stack.note_text(),
vec![start(TextStyleKind::TextColor(red()))]
);
assert_eq!(
stack.close(&TextStyleKind::TextColor(red())),
vec![Event::EndTextStyle]
);
assert!(stack.is_empty());
}
#[test]
fn adversarial_repeated_open_close_is_bounded() {
let mut stack = StyleStack::default();
for _ in 0..10_000 {
assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
}
assert_eq!(stack.frames.len(), 1);
for _ in 0..10_000 {
let _events = stack.close(&TextStyleKind::Bold);
}
assert!(stack.is_empty());
}
}