1use alloc::vec::Vec;
27
28use crate::{Event, TextStyleKind};
29
30pub const MAX_STYLE_DEPTH: usize = 32;
37
38#[derive(Debug, Clone)]
40struct StyleFrame {
41 kind: TextStyleKind,
42 text_emitted: bool,
43}
44
45#[derive(Debug, Clone, Default)]
62pub struct StyleStack {
63 frames: Vec<StyleFrame>,
64 deferred_starts: Vec<Event>,
65}
66
67impl StyleStack {
68 #[inline]
84 pub fn open(&mut self, kind: TextStyleKind) -> Vec<Event> {
85 if self.frames.iter().any(|frame| frame.kind == kind)
86 || self.frames.len() >= MAX_STYLE_DEPTH
87 {
88 return Vec::new();
89 }
90
91 let start = Event::StartTextStyle {
92 kind: kind.clone(),
93 id: None,
94 };
95 self.frames.push(StyleFrame {
96 kind,
97 text_emitted: false,
98 });
99 self.deferred_starts.push(start);
100 Vec::new()
101 }
102
103 #[inline]
118 pub fn close(&mut self, kind: &TextStyleKind) -> Vec<Event> {
119 let Some(position) = self.frames.iter().rposition(|frame| frame.kind == *kind) else {
120 return Vec::new();
121 };
122 let Some(after_position) = position.checked_add(1) else {
123 return Vec::new();
124 };
125
126 if after_position == self.frames.len() {
127 let Some(frame) = self.frames.pop() else {
128 return Vec::new();
129 };
130 self.rebuild_deferred_starts();
131 return if frame.text_emitted {
132 alloc::vec![Event::EndTextStyle]
133 } else {
134 Vec::new()
135 };
136 }
137
138 let mut emitted = Vec::new();
139 let mut above = self.frames.split_off(after_position);
140 for frame in above.iter().rev() {
141 if frame.text_emitted {
142 emitted.push(Event::EndTextStyle);
143 }
144 }
145
146 let Some(matched) = self.frames.pop() else {
147 self.rebuild_deferred_starts();
148 return emitted;
149 };
150 if matched.text_emitted {
151 emitted.push(Event::EndTextStyle);
152 }
153
154 for frame in above.drain(..) {
155 self.frames.push(StyleFrame {
156 kind: frame.kind,
157 text_emitted: false,
158 });
159 }
160 self.rebuild_deferred_starts();
161
162 emitted
163 }
164
165 #[inline]
174 pub fn note_text(&mut self) -> Vec<Event> {
175 for frame in &mut self.frames {
176 frame.text_emitted = true;
177 }
178 self.deferred_starts.drain(..).collect()
179 }
180
181 #[inline]
188 pub fn close_all(&mut self) -> Vec<Event> {
189 let mut emitted = Vec::new();
190 for frame in self.frames.iter().rev() {
191 if frame.text_emitted {
192 emitted.push(Event::EndTextStyle);
193 }
194 }
195 self.frames.clear();
196 self.deferred_starts.clear();
197 emitted
198 }
199
200 #[inline]
202 #[must_use]
203 pub fn is_empty(&self) -> bool {
204 self.frames.is_empty() && self.deferred_starts.is_empty()
205 }
206
207 fn rebuild_deferred_starts(&mut self) {
208 self.deferred_starts = self
209 .frames
210 .iter()
211 .filter(|frame| !frame.text_emitted)
212 .map(|frame| Event::StartTextStyle {
213 kind: frame.kind.clone(),
214 id: None,
215 })
216 .collect();
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use crate::Color;
224 use alloc::vec;
225
226 fn start(kind: TextStyleKind) -> Event {
227 Event::StartTextStyle { kind, id: None }
228 }
229
230 fn yellow() -> Color {
231 Color::Rgb {
232 r: 255,
233 g: 255,
234 b: 0,
235 }
236 }
237
238 #[test]
242 fn max_style_depth_is_32() {
243 assert_eq!(MAX_STYLE_DEPTH, 32);
244 }
245
246 fn blue() -> Color {
247 Color::Rgb { r: 0, g: 0, b: 255 }
248 }
249
250 fn red() -> Color {
251 Color::Rgb { r: 255, g: 0, b: 0 }
252 }
253
254 #[test]
255 fn open_then_close_with_text() {
256 let mut stack = StyleStack::default();
257
258 assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
259 assert_eq!(stack.note_text(), vec![start(TextStyleKind::Bold)]);
260 assert_eq!(stack.close(&TextStyleKind::Bold), vec![Event::EndTextStyle]);
261 assert!(stack.is_empty());
262 }
263
264 #[test]
265 fn open_then_close_without_text_emits_nothing() {
266 let mut stack = StyleStack::default();
267
268 assert_eq!(stack.open(TextStyleKind::Italic), Vec::new());
269 assert_eq!(stack.close(&TextStyleKind::Italic), Vec::new());
270 assert!(stack.is_empty());
271 }
272
273 #[test]
274 fn same_kind_nesting_idempotent() {
275 let mut stack = StyleStack::default();
276
277 assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
278 assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
279 assert_eq!(stack.frames.len(), 1);
280 assert_eq!(stack.note_text(), vec![start(TextStyleKind::Bold)]);
281 assert_eq!(stack.close(&TextStyleKind::Bold), vec![Event::EndTextStyle]);
282 assert_eq!(stack.close(&TextStyleKind::Bold), Vec::new());
283 assert!(stack.is_empty());
284 }
285
286 #[test]
287 fn rule_9_mismatched_closers_with_text() {
288 let mut stack = StyleStack::default();
289
290 assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
291 assert_eq!(stack.note_text(), vec![start(TextStyleKind::Bold)]);
292 assert_eq!(stack.open(TextStyleKind::Italic), Vec::new());
293 assert_eq!(stack.note_text(), vec![start(TextStyleKind::Italic)]);
294
295 assert_eq!(
296 stack.close(&TextStyleKind::Bold),
297 vec![Event::EndTextStyle, Event::EndTextStyle]
298 );
299 assert_eq!(stack.note_text(), vec![start(TextStyleKind::Italic)]);
300 assert_eq!(
301 stack.close(&TextStyleKind::Italic),
302 vec![Event::EndTextStyle]
303 );
304 assert!(stack.is_empty());
305 }
306
307 #[test]
308 fn rule_9_mismatched_closers_no_extra_text() {
309 let mut stack = StyleStack::default();
310
311 assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
312 assert_eq!(stack.note_text(), vec![start(TextStyleKind::Bold)]);
313 assert_eq!(stack.open(TextStyleKind::Italic), Vec::new());
314 assert_eq!(stack.note_text(), vec![start(TextStyleKind::Italic)]);
315 assert_eq!(
316 stack.close(&TextStyleKind::Bold),
317 vec![Event::EndTextStyle, Event::EndTextStyle]
318 );
319
320 assert_eq!(stack.close(&TextStyleKind::Italic), Vec::new());
321 assert!(stack.is_empty());
322 }
323
324 #[test]
325 fn depth_bound() {
326 let mut stack = StyleStack::default();
327
328 for level in 0..MAX_STYLE_DEPTH {
331 let level_u8 = u8::try_from(level).unwrap_or(u8::MAX);
332 assert_eq!(
333 stack.open(TextStyleKind::Mark(Color::Rgb {
334 r: level_u8,
335 g: 0,
336 b: 0,
337 })),
338 Vec::new()
339 );
340 }
341 assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
343
344 assert_eq!(stack.frames.len(), MAX_STYLE_DEPTH);
345 assert_eq!(stack.deferred_starts.len(), MAX_STYLE_DEPTH);
346 }
347
348 #[test]
349 fn close_unmatched() {
350 let mut stack = StyleStack::default();
351
352 assert_eq!(stack.close(&TextStyleKind::Bold), Vec::new());
353 assert!(stack.is_empty());
354 }
355
356 #[test]
357 fn close_all_with_open_frames() {
358 let mut stack = StyleStack::default();
359
360 assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
361 assert_eq!(stack.open(TextStyleKind::Italic), Vec::new());
362 assert_eq!(
363 stack.note_text(),
364 vec![start(TextStyleKind::Bold), start(TextStyleKind::Italic)]
365 );
366 assert_eq!(
367 stack.close_all(),
368 vec![Event::EndTextStyle, Event::EndTextStyle]
369 );
370 assert!(stack.is_empty());
371 }
372
373 #[test]
374 fn close_all_with_deferred_only_emits_nothing() {
375 let mut stack = StyleStack::default();
376
377 assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
378 assert_eq!(stack.open(TextStyleKind::Italic), Vec::new());
379 assert_eq!(stack.close_all(), Vec::new());
380 assert!(stack.is_empty());
381 }
382
383 #[test]
384 fn mark_with_arbitrary_color_round_trips() {
385 let mut stack = StyleStack::default();
386
387 assert_eq!(stack.open(TextStyleKind::Mark(yellow())), Vec::new());
388 assert_eq!(
389 stack.note_text(),
390 vec![start(TextStyleKind::Mark(yellow()))]
391 );
392 assert_eq!(
393 stack.close(&TextStyleKind::Mark(yellow())),
394 vec![Event::EndTextStyle]
395 );
396 assert!(stack.is_empty());
397 }
398
399 #[test]
400 fn distinct_mark_colors_are_distinct_kinds() {
401 let mut stack = StyleStack::default();
406
407 assert_eq!(stack.open(TextStyleKind::Mark(blue())), Vec::new());
408 assert_eq!(stack.open(TextStyleKind::Mark(red())), Vec::new());
409 assert_eq!(stack.frames.len(), 2);
410 assert_eq!(
411 stack.note_text(),
412 vec![
413 start(TextStyleKind::Mark(blue())),
414 start(TextStyleKind::Mark(red()))
415 ]
416 );
417 assert_eq!(
418 stack.close(&TextStyleKind::Mark(red())),
419 vec![Event::EndTextStyle]
420 );
421 assert_eq!(
422 stack.close(&TextStyleKind::Mark(blue())),
423 vec![Event::EndTextStyle]
424 );
425 assert!(stack.is_empty());
426 }
427
428 #[test]
429 fn text_color_round_trips() {
430 let mut stack = StyleStack::default();
431
432 assert_eq!(stack.open(TextStyleKind::TextColor(red())), Vec::new());
433 assert_eq!(
434 stack.note_text(),
435 vec![start(TextStyleKind::TextColor(red()))]
436 );
437 assert_eq!(
438 stack.close(&TextStyleKind::TextColor(red())),
439 vec![Event::EndTextStyle]
440 );
441 assert!(stack.is_empty());
442 }
443
444 #[test]
445 fn adversarial_repeated_open_close_is_bounded() {
446 let mut stack = StyleStack::default();
447
448 for _ in 0..10_000 {
449 assert_eq!(stack.open(TextStyleKind::Bold), Vec::new());
450 }
451 assert_eq!(stack.frames.len(), 1);
452
453 for _ in 0..10_000 {
454 let _events = stack.close(&TextStyleKind::Bold);
455 }
456 assert!(stack.is_empty());
457 }
458}