Skip to main content

text_document_common/parser_tools/
list_grouper.rs

1use crate::entities::ListStyle;
2use crate::types::EntityId;
3
4/// Tracks active list entities across consecutive blocks so that items
5/// belonging to the same logical list share a single list entity.
6///
7/// Uses a vec indexed by indent level. When indent decreases, deeper
8/// entries are truncated so that outer lists resume correctly.
9#[derive(Default)]
10pub struct ListGrouper {
11    /// Index = indent level. Each entry: (entity_id, style).
12    active: Vec<Option<(EntityId, ListStyle)>>,
13}
14
15impl ListGrouper {
16    pub fn new() -> Self {
17        Self::default()
18    }
19
20    /// Returns an existing list entity id if the style and indent match
21    /// a previously registered list at this level. Returns `None` if a
22    /// new list entity must be created (caller should then call `register`).
23    pub fn try_reuse(&mut self, style: &ListStyle, indent: u32) -> Option<EntityId> {
24        let idx = indent as usize;
25        // Truncate deeper levels - we returned to a shallower depth
26        self.active.truncate(idx + 1);
27        if let Some(Some((id, existing_style))) = self.active.get(idx)
28            && existing_style == style
29        {
30            return Some(*id);
31        }
32        None
33    }
34
35    /// Register a newly created list entity at the given indent level.
36    pub fn register(&mut self, id: EntityId, style: ListStyle, indent: u32) {
37        let idx = indent as usize;
38        while self.active.len() <= idx {
39            self.active.push(None);
40        }
41        self.active[idx] = Some((id, style));
42    }
43
44    /// Clear all tracking. Call on non-list blocks, tables, or frame boundaries.
45    pub fn reset(&mut self) {
46        self.active.clear();
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn first_item_returns_none() {
56        let mut g = ListGrouper::new();
57        assert!(g.try_reuse(&ListStyle::Decimal, 0).is_none());
58    }
59
60    #[test]
61    fn consecutive_same_style_reuses() {
62        let mut g = ListGrouper::new();
63        g.register(42, ListStyle::Decimal, 0);
64        assert_eq!(g.try_reuse(&ListStyle::Decimal, 0), Some(42));
65    }
66
67    #[test]
68    fn different_style_creates_new() {
69        let mut g = ListGrouper::new();
70        g.register(42, ListStyle::Decimal, 0);
71        assert!(g.try_reuse(&ListStyle::Disc, 0).is_none());
72    }
73
74    #[test]
75    fn different_indent_creates_new() {
76        let mut g = ListGrouper::new();
77        g.register(42, ListStyle::Decimal, 0);
78        assert!(g.try_reuse(&ListStyle::Decimal, 1).is_none());
79    }
80
81    #[test]
82    fn reset_clears_all() {
83        let mut g = ListGrouper::new();
84        g.register(42, ListStyle::Decimal, 0);
85        g.reset();
86        assert!(g.try_reuse(&ListStyle::Decimal, 0).is_none());
87    }
88
89    #[test]
90    fn nested_indent_resumes_outer() {
91        let mut g = ListGrouper::new();
92        g.register(10, ListStyle::Decimal, 0);
93        g.register(20, ListStyle::LowerAlpha, 1);
94        // Return to indent 0 - should resume outer list
95        assert_eq!(g.try_reuse(&ListStyle::Decimal, 0), Some(10));
96    }
97
98    #[test]
99    fn nested_indent_different_style_creates_new() {
100        let mut g = ListGrouper::new();
101        g.register(10, ListStyle::Decimal, 0);
102        g.register(20, ListStyle::LowerAlpha, 1);
103        // Return to indent 0 with different style
104        assert!(g.try_reuse(&ListStyle::Disc, 0).is_none());
105    }
106}