pulldown_html_ext/html/
state.rs

1use pulldown_cmark::{Alignment, LinkType};
2
3/// Represents the current state of table parsing
4#[derive(Copy, Clone, Debug, PartialEq, Default)]
5pub enum TableContext {
6    /// Not currently within a table
7    #[default]
8    NotInTable,
9    /// Currently in the header section of a table
10    InHeader,
11    /// Currently in the body section of a table
12    InBody,
13}
14
15/// Represents the type of list currently being processed
16#[derive(Copy, Clone, Debug, PartialEq, Default)]
17pub enum ListContext {
18    /// An ordered list (<ol>) with a starting number
19    Ordered(u32),
20    /// An unordered list (<ul>)
21    #[default]
22    Unordered,
23}
24
25/// Maintains the state of the HTML rendering process
26pub struct HtmlState {
27    /// Stack for tracking list numbers in ordered lists
28    pub numbers: Vec<u32>,
29    /// Current state of table processing
30    pub table_state: TableContext,
31    /// Current index when processing table cells
32    pub table_cell_index: usize,
33    /// Alignments for table columns
34    pub table_alignments: Vec<Alignment>,
35    /// Stack for tracking nested lists
36    pub list_stack: Vec<ListContext>,
37    /// Stack for tracking nested links
38    pub link_stack: Vec<LinkType>,
39    /// Stack for tracking heading IDs
40    pub heading_stack: Vec<String>,
41    /// Whether currently processing a code block
42    pub currently_in_code_block: bool,
43    /// Whether currently processing a footnote definition
44    pub currently_in_footnote: bool,
45}
46
47impl HtmlState {
48    /// Create a new renderer state with default values
49    pub fn new() -> Self {
50        Self {
51            numbers: Vec::new(),
52            table_state: TableContext::default(),
53            table_cell_index: 0,
54            table_alignments: Vec::new(),
55            list_stack: Vec::new(),
56            link_stack: Vec::new(),
57            heading_stack: Vec::new(),
58            currently_in_code_block: false,
59            currently_in_footnote: false,
60        }
61    }
62
63    #[allow(dead_code)]
64    /// Reset all state, typically called between document renders
65    pub fn reset(&mut self) {
66        self.numbers.clear();
67        self.table_state = TableContext::default();
68        self.table_cell_index = 0;
69        self.table_alignments.clear();
70        self.list_stack.clear();
71        self.link_stack.clear();
72        self.heading_stack.clear();
73        self.currently_in_code_block = false;
74    }
75
76    #[allow(dead_code)]
77    /// Check if currently inside a table
78    pub fn in_table(&self) -> bool {
79        self.table_state != TableContext::NotInTable
80    }
81
82    #[allow(dead_code)]
83    /// Check if currently in a table header
84    pub fn in_table_header(&self) -> bool {
85        self.table_state == TableContext::InHeader
86    }
87
88    #[allow(dead_code)]
89    /// Get the current nesting level of lists
90    pub fn list_depth(&self) -> usize {
91        self.list_stack.len()
92    }
93
94    #[allow(dead_code)]
95    /// Get the current list type, if any
96    pub fn current_list_type(&self) -> Option<ListContext> {
97        self.list_stack.last().copied()
98    }
99}
100
101impl Default for HtmlState {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_renderer_state_new() {
113        let state = HtmlState::new();
114        assert_eq!(state.table_state, TableContext::NotInTable);
115        assert_eq!(state.table_cell_index, 0);
116        assert!(state.numbers.is_empty());
117        assert!(state.table_alignments.is_empty());
118        assert!(state.list_stack.is_empty());
119        assert!(state.link_stack.is_empty());
120        assert!(state.heading_stack.is_empty());
121        assert!(!state.currently_in_code_block);
122    }
123
124    #[test]
125    fn test_renderer_state_reset() {
126        let mut state = HtmlState::new();
127
128        // Modify state
129        state.numbers.push(1);
130        state.table_state = TableContext::InHeader;
131        state.table_cell_index = 2;
132        state.list_stack.push(ListContext::Ordered(1));
133        state.currently_in_code_block = true;
134
135        // Reset
136        state.reset();
137
138        // Verify reset
139        assert_eq!(state.table_state, TableContext::NotInTable);
140        assert_eq!(state.table_cell_index, 0);
141        assert!(state.numbers.is_empty());
142        assert!(state.list_stack.is_empty());
143        assert!(!state.currently_in_code_block);
144    }
145
146    #[test]
147    fn test_list_operations() {
148        let mut state = HtmlState::new();
149
150        assert_eq!(state.list_depth(), 0);
151        assert_eq!(state.current_list_type(), None);
152
153        state.list_stack.push(ListContext::Unordered);
154        assert_eq!(state.list_depth(), 1);
155        assert_eq!(state.current_list_type(), Some(ListContext::Unordered));
156
157        state.list_stack.push(ListContext::Ordered(1));
158        assert_eq!(state.list_depth(), 2);
159        assert_eq!(state.current_list_type(), Some(ListContext::Ordered(1)));
160    }
161
162    #[test]
163    fn test_table_state() {
164        let mut state = HtmlState::new();
165
166        assert!(!state.in_table());
167        assert!(!state.in_table_header());
168
169        state.table_state = TableContext::InHeader;
170        assert!(state.in_table());
171        assert!(state.in_table_header());
172
173        state.table_state = TableContext::InBody;
174        assert!(state.in_table());
175        assert!(!state.in_table_header());
176    }
177}