panache_parser/syntax/
lists.rs1use super::ast::{AstChildren, support};
8use super::{AstNode, PanacheLanguage, SyntaxKind, SyntaxNode};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ListKind {
12 Bullet,
13 Ordered,
14 Task,
15}
16
17pub struct List(SyntaxNode);
18
19impl AstNode for List {
20 type Language = PanacheLanguage;
21
22 fn can_cast(kind: SyntaxKind) -> bool {
23 kind == SyntaxKind::LIST
24 }
25
26 fn cast(syntax: SyntaxNode) -> Option<Self> {
27 if Self::can_cast(syntax.kind()) {
28 Some(Self(syntax))
29 } else {
30 None
31 }
32 }
33
34 fn syntax(&self) -> &SyntaxNode {
35 &self.0
36 }
37}
38
39impl List {
40 pub fn is_loose(&self) -> bool {
42 self.0
43 .children()
44 .any(|n| n.kind() == SyntaxKind::BLANK_LINE)
45 }
46
47 pub fn is_compact(&self) -> bool {
51 !self.is_loose()
52 }
53
54 pub fn items(&self) -> AstChildren<ListItem> {
56 support::children(&self.0)
57 }
58
59 pub fn kind(&self) -> Option<ListKind> {
61 let first_item = self.items().next()?;
62 if first_item.is_task() {
63 return Some(ListKind::Task);
64 }
65 let marker = first_item.marker()?;
66 if matches!(marker.as_str(), "-" | "*" | "+") {
67 Some(ListKind::Bullet)
68 } else {
69 Some(ListKind::Ordered)
70 }
71 }
72}
73
74pub struct ListItem(SyntaxNode);
75
76impl AstNode for ListItem {
77 type Language = PanacheLanguage;
78
79 fn can_cast(kind: SyntaxKind) -> bool {
80 kind == SyntaxKind::LIST_ITEM
81 }
82
83 fn cast(syntax: SyntaxNode) -> Option<Self> {
84 if Self::can_cast(syntax.kind()) {
85 Some(Self(syntax))
86 } else {
87 None
88 }
89 }
90
91 fn syntax(&self) -> &SyntaxNode {
92 &self.0
93 }
94}
95
96impl ListItem {
97 pub fn is_loose(&self) -> bool {
99 self.0
100 .children()
101 .any(|child| child.kind() == SyntaxKind::PARAGRAPH)
102 }
103
104 pub fn is_compact(&self) -> bool {
106 self.0
107 .children()
108 .any(|child| child.kind() == SyntaxKind::PLAIN)
109 }
110
111 pub fn marker(&self) -> Option<String> {
112 self.0.children_with_tokens().find_map(|elem| {
113 elem.as_token()
114 .filter(|token| token.kind() == SyntaxKind::LIST_MARKER)
115 .map(|token| token.text().to_string())
116 })
117 }
118
119 pub fn is_task(&self) -> bool {
120 self.0.children_with_tokens().any(|elem| {
121 elem.as_token()
122 .is_some_and(|token| token.kind() == SyntaxKind::TASK_CHECKBOX)
123 })
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::parse;
131
132 #[test]
133 fn list_wrapper_compact() {
134 let input = "- First\n- Second\n- Third\n";
135 let tree = parse(input, None);
136
137 let list_node = tree
138 .descendants()
139 .find(|n| n.kind() == SyntaxKind::LIST)
140 .expect("Should find LIST node");
141
142 let list = List::cast(list_node).expect("Should cast to List");
143
144 assert!(list.is_compact(), "List should be compact");
145 assert!(!list.is_loose(), "List should not be loose");
146 assert_eq!(list.items().count(), 3, "Should have 3 items");
147 }
148
149 #[test]
150 fn list_wrapper_loose() {
151 let input = "- First\n\n- Second\n\n- Third\n";
152 let tree = parse(input, None);
153
154 let list_node = tree
155 .descendants()
156 .find(|n| n.kind() == SyntaxKind::LIST)
157 .expect("Should find LIST node");
158
159 let list = List::cast(list_node).expect("Should cast to List");
160
161 assert!(list.is_loose(), "List should be loose");
162 assert!(!list.is_compact(), "List should not be compact");
163 assert_eq!(list.items().count(), 3, "Should have 3 items");
164 }
165
166 #[test]
167 fn list_item_wrapper() {
168 let input = "- First item\n- Second item\n";
169 let tree = parse(input, None);
170
171 let list = tree
172 .descendants()
173 .find_map(List::cast)
174 .expect("Should find List");
175
176 assert_eq!(list.items().count(), 2, "Should have 2 list items");
177
178 let first_item = list.items().next().expect("Should have list item");
179 assert!(
180 first_item.is_compact(),
181 "First item should be compact (PLAIN)"
182 );
183 assert!(!first_item.is_loose(), "First item should not be loose");
184 }
185
186 #[test]
187 fn list_items_iterator() {
188 let input = "1. First\n2. Second\n3. Third\n4. Fourth\n";
189 let tree = parse(input, None);
190
191 let list_node = tree
192 .descendants()
193 .find(|n| n.kind() == SyntaxKind::LIST)
194 .expect("Should find LIST node");
195
196 let list = List::cast(list_node).expect("Should cast to List");
197
198 assert_eq!(list.items().count(), 4, "Should have 4 items");
199 for item in list.items() {
200 assert_eq!(
201 item.syntax().kind(),
202 SyntaxKind::LIST_ITEM,
203 "Each item should be LIST_ITEM"
204 );
205 }
206 }
207
208 #[test]
209 fn list_kind_detection() {
210 let bullet_tree = parse("- First\n- Second\n", None);
211 let bullet_list = bullet_tree
212 .descendants()
213 .find_map(List::cast)
214 .expect("Should find bullet list");
215 assert_eq!(bullet_list.kind(), Some(ListKind::Bullet));
216
217 let ordered_tree = parse("1. First\n2. Second\n", None);
218 let ordered_list = ordered_tree
219 .descendants()
220 .find_map(List::cast)
221 .expect("Should find ordered list");
222 assert_eq!(ordered_list.kind(), Some(ListKind::Ordered));
223
224 let task_tree = parse("- [ ] First\n- [x] Second\n", None);
225 let task_list = task_tree
226 .descendants()
227 .find_map(List::cast)
228 .expect("Should find task list");
229 assert_eq!(task_list.kind(), Some(ListKind::Task));
230 }
231}