use alloc::collections::VecDeque;
use std::collections::HashMap;
use docspec_core::{Event, ListStyleType, TextStyleKind};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) struct ListStackEntry {
pub num_id: u32,
pub ilvl: u32,
pub is_ordered: bool,
}
#[derive(Debug, Default)]
pub(crate) struct EmitState {
queue: VecDeque<Event>,
open_styles: Vec<TextStyleKind>,
list_stack: Vec<ListStackEntry>,
list_counters: HashMap<(u32, u32), u64>,
pending_preformatted_close: bool,
}
impl EmitState {
pub(crate) fn push(&mut self, event: Event) {
self.queue.push_back(event);
}
pub(crate) fn pop(&mut self) -> Option<Event> {
self.queue.pop_front()
}
pub(crate) fn queued(&self) -> usize {
self.queue.len()
}
pub(crate) fn is_empty(&self) -> bool {
self.queue.is_empty()
}
pub(crate) fn drain_since(&mut self, checkpoint: usize, sink: &mut Vec<Event>) {
sink.extend(self.queue.drain(checkpoint..));
}
pub(crate) fn defer_preformatted_close(&mut self) {
self.pending_preformatted_close = true;
}
pub(crate) fn has_pending_preformatted_close(&self) -> bool {
self.pending_preformatted_close
}
pub(crate) fn cancel_preformatted_close(&mut self) {
self.pending_preformatted_close = false;
}
pub(crate) fn extend<I>(&mut self, events: I)
where
I: IntoIterator<Item = Event>,
{
self.queue.extend(events);
}
pub(crate) fn close_all_styles(&mut self) {
while self.open_styles.pop().is_some() {
self.push(Event::EndTextStyle);
}
}
pub(crate) fn flush_pending_preformatted_close(&mut self) {
if self.pending_preformatted_close {
self.push(Event::EndPreformatted);
self.pending_preformatted_close = false;
}
}
pub(crate) fn flush_list_stack(&mut self) {
while let Some(entry) = self.list_stack.pop() {
self.emit_list_item_end(entry.is_ordered);
}
}
pub(crate) fn compute_start(
&mut self,
num_id: u32,
ilvl: u32,
sequential: bool,
) -> Option<u64> {
let counter = self.list_counters.entry((num_id, ilvl)).or_insert(0);
let is_first = *counter == 0;
let emitted_counter = counter.saturating_add(1);
*counter = emitted_counter;
(is_first || !sequential).then_some(emitted_counter)
}
pub(crate) fn reconcile_list_stack(
&mut self,
num_id: u32,
ilvl: u32,
is_ordered: bool,
) -> bool {
let mut found_sequential = false;
while let Some(top) = self.list_stack.last().copied() {
match top.ilvl.cmp(&ilvl) {
core::cmp::Ordering::Greater => {
self.list_stack.pop();
self.emit_list_item_end(top.is_ordered);
}
core::cmp::Ordering::Equal => {
self.list_stack.pop();
self.emit_list_item_end(top.is_ordered);
found_sequential = true;
break;
}
core::cmp::Ordering::Less => break,
}
}
let target_depth = usize::try_from(ilvl).unwrap_or(usize::MAX);
while self.list_stack.len() < target_depth {
let phantom_ilvl = u32::try_from(self.list_stack.len()).unwrap_or(u32::MAX);
let phantom_style = if is_ordered {
ListStyleType::Decimal
} else {
ListStyleType::Disc
};
self.list_stack.push(ListStackEntry {
num_id,
ilvl: phantom_ilvl,
is_ordered,
});
if is_ordered {
self.emit_list_item_start_ordered(num_id, phantom_ilvl, None, phantom_style);
} else {
self.emit_list_item_start_unordered(num_id, phantom_ilvl, phantom_style);
}
}
self.list_stack.push(ListStackEntry {
num_id,
ilvl,
is_ordered,
});
found_sequential
}
pub(crate) fn emit_list_item_start_ordered(
&mut self,
num_id: u32,
ilvl: u32,
start: Option<u64>,
style_type: ListStyleType,
) {
self.push(Event::StartOrderedListItem {
id: Some(num_id.to_string()),
level: ilvl,
start,
style_type,
});
}
pub(crate) fn emit_list_item_start_unordered(
&mut self,
num_id: u32,
ilvl: u32,
style_type: ListStyleType,
) {
self.push(Event::StartUnorderedListItem {
id: Some(num_id.to_string()),
level: ilvl,
style_type,
});
}
pub(crate) fn emit_list_item_end(&mut self, is_ordered: bool) {
self.push(if is_ordered {
Event::EndOrderedListItem
} else {
Event::EndUnorderedListItem
});
}
pub(crate) fn emit_style_if_not_open(&mut self, kind: TextStyleKind) {
if !self.open_styles.contains(&kind) {
self.push(Event::StartTextStyle {
kind: kind.clone(),
id: None,
});
self.open_styles.push(kind);
}
}
pub(crate) fn close_styles(&mut self, kinds: Vec<TextStyleKind>) {
for kind in kinds.into_iter().rev() {
if let Some(index) = self.open_styles.iter().rposition(|open| open == &kind) {
self.open_styles.remove(index);
self.push(Event::EndTextStyle);
}
}
}
}