1use crate::constants::{COLON, HYPHEN, LBRACK, NEWLINE, PERIOD, PLUS, RBRACK, RPAREN, SPACE, STAR};
2use crate::node_pool::NodeID;
3use crate::parse::parse_element;
4use crate::types::{Cursor, Expr, MatchError, ParseOpts, Parseable, Parser, Result};
5use crate::utils::Match;
6
7#[derive(Debug, Clone)]
8pub struct Item<'a> {
9 pub bullet: BulletKind,
10 pub counter_set: Option<&'a str>,
12 pub check_box: Option<CheckBox>,
13 pub tag: Option<&'a str>,
14 pub children: Vec<NodeID>,
15}
16
17impl<'a> Parseable<'a> for Item<'a> {
18 fn parse(
19 parser: &mut Parser<'a>,
20 mut cursor: Cursor<'a>,
21 parent: Option<NodeID>,
22 mut parse_opts: ParseOpts,
23 ) -> Result<NodeID> {
24 let start = cursor.index;
27
28 let bullet_match = BulletKind::parse(cursor)?;
29 let bullet = bullet_match.obj;
30 cursor.move_to(bullet_match.end);
31
32 let counter_set: Option<&'a str> = if let Ok(counter_match) = parse_counter_set(cursor) {
33 cursor.move_to(counter_match.end);
34 Some(counter_match.obj)
35 } else {
36 None
37 };
38
39 let check_box: Option<CheckBox> = if let Ok(check_box_match) = CheckBox::parse(cursor) {
40 cursor.move_to(check_box_match.end);
41 Some(check_box_match.obj)
42 } else {
43 None
44 };
45
46 let tag: Option<&str> = if let BulletKind::Unordered = bullet {
47 if let Ok(tag_match) = parse_tag(cursor) {
48 cursor.move_to(tag_match.end);
49 Some(tag_match.obj)
50 } else {
51 None
52 }
53 } else {
54 None
55 };
56
57 let reserve_id = parser.pool.reserve_id();
58 let mut children: Vec<NodeID> = Vec::new();
59 let mut blank_obj: Option<NodeID> = None;
60
61 cursor.skip_ws();
64 if let Ok(item) = cursor.try_curr() {
65 if item == NEWLINE {
66 cursor.next()
67 } else {
68 parse_opts.list_line = true;
69 }
70 }
71
72 let mut prev_ind = cursor.index;
75
76 while let Ok(element_id) = parse_element(parser, cursor, Some(reserve_id), parse_opts) {
77 let pool_loc = &parser.pool[element_id];
78 match &pool_loc.obj {
79 Expr::BlankLine => {
80 if blank_obj.is_some() {
81 cursor.index = prev_ind;
82 break;
83 } else {
84 blank_obj = Some(element_id);
85 prev_ind = cursor.index;
86 }
87 }
88 Expr::Item(_) => {
89 break;
90 }
91 _ => {
92 if let Some(blank_id) = blank_obj {
93 children.push(blank_id);
94 blank_obj = None;
95 }
96 children.push(element_id);
97 }
98 }
99 parse_opts.list_line = false;
100 cursor.move_to(pool_loc.end);
101 }
102
103 Ok(parser.alloc_with_id(
104 Self {
105 bullet,
106 counter_set,
107 check_box,
108 tag,
109 children,
110 },
111 start,
112 cursor.index,
113 parent,
114 reserve_id,
115 ))
116 }
117}
118
119#[derive(Debug, Clone, Copy)]
120pub enum BulletKind {
121 Unordered,
122 Ordered(CounterKind),
124}
125
126#[derive(Debug, Clone, Copy, PartialEq)]
127pub enum CounterKind {
128 Letter(u8),
129 Number(u8),
130}
131
132impl BulletKind {
133 pub(crate) fn parse(mut cursor: Cursor) -> Result<Match<BulletKind>> {
134 let start = cursor.index;
138 match cursor.curr() {
139 STAR | HYPHEN | PLUS => {
140 if cursor.peek(1)?.is_ascii_whitespace() {
141 Ok(Match {
142 start,
143 end: cursor.index + if cursor.peek(1)? == NEWLINE { 1 } else { 2 },
144 obj: BulletKind::Unordered,
145 })
146 } else {
147 Err(MatchError::InvalidLogic)
148 }
149 }
150 chr if chr.is_ascii_alphanumeric() => {
151 let num_match = cursor.fn_while(|chr| {
152 chr.is_ascii_alphanumeric()
153 })?;
156
157 cursor.index = num_match.end;
158
159 if !((cursor.curr() == PERIOD || cursor.curr() == RPAREN)
160 && cursor.peek(1)?.is_ascii_whitespace())
161 {
162 return Err(MatchError::InvalidLogic);
163 }
164
165 let bullet_kind = if num_match.len() == 1 {
166 let temp = num_match.obj.as_bytes()[0];
167 if temp.is_ascii_alphabetic() {
168 BulletKind::Ordered(CounterKind::Letter(temp))
169 } else if temp.is_ascii_digit() {
170 BulletKind::Ordered(CounterKind::Number(temp - 48))
171 } else {
172 Err(MatchError::InvalidLogic)?
173 }
174 } else {
175 BulletKind::Ordered(CounterKind::Number(
177 num_match.obj.parse().or(Err(MatchError::InvalidLogic))?,
178 ))
179 };
180
181 Ok(Match {
182 start,
183 end: cursor.index + if cursor.peek(1)? == NEWLINE { 1 } else { 2 },
184 obj: bullet_kind,
185 })
186 }
187
188 _ => Err(MatchError::InvalidLogic),
189 }
190 }
191}
192
193fn parse_counter_set(mut cursor: Cursor<'_>) -> Result<Match<&str>> {
195 let start = cursor.index;
196 cursor.skip_ws();
197 cursor.word("[@")?;
198
199 let num_match = cursor.fn_while(|chr| chr.is_ascii_alphanumeric())?;
200
201 cursor.index = num_match.end;
202
203 if cursor.curr() != RBRACK {
205 Err(MatchError::InvalidLogic)?;
206 }
207
208 let counter_kind = if num_match.len() == 1 {
209 let temp = num_match.obj.as_bytes()[0];
210 if temp.is_ascii_alphanumeric() {
211 num_match.obj
212 } else {
213 return Err(MatchError::InvalidLogic);
214 }
215 } else {
216 if num_match
218 .obj
219 .as_bytes()
220 .iter()
221 .all(|byte| byte.is_ascii_digit())
222 {
223 num_match.obj
224 } else {
225 return Err(MatchError::InvalidLogic);
226 }
227 };
228
229 Ok(Match {
230 start,
231 end: cursor.index + 1,
232 obj: counter_kind,
233 })
234}
235
236fn parse_tag<'a>(mut cursor: Cursor<'a>) -> Result<Match<&'a str>> {
237 let start = cursor.index;
239 cursor.curr_valid()?;
240 cursor.skip_ws();
241
242 let end = loop {
243 match cursor.try_curr()? {
244 COLON => {
245 if cursor[cursor.index - 1].is_ascii_whitespace()
246 && COLON == cursor.peek(1)?
247 && cursor.peek(2)?.is_ascii_whitespace()
248 {
249 break cursor.index + 2;
250 } else {
251 cursor.next();
252 }
253 }
254 NEWLINE => Err(MatchError::EofError)?,
255 _ => cursor.next(),
256 }
257 };
258
259 Ok(Match {
260 start,
261 end,
262 obj: cursor.clamp_backwards(start).trim(),
263 })
264}
265
266#[derive(Debug, Clone, PartialEq, Eq)]
267pub enum CheckBox {
268 Intermediate,
270 Off,
272 On,
274}
275
276impl From<&CheckBox> for &str {
277 fn from(value: &CheckBox) -> Self {
278 match value {
279 CheckBox::Intermediate => "-",
280 CheckBox::Off => " ",
281 CheckBox::On => "X",
282 }
283 }
284}
285
286impl CheckBox {
287 fn parse(mut cursor: Cursor) -> Result<Match<CheckBox>> {
288 let start = cursor.index;
289 cursor.skip_ws();
290 if cursor.try_curr()? != LBRACK || cursor.peek(2)? != RBRACK {
295 return Err(MatchError::InvalidLogic);
296 }
297
298 Ok(Match {
299 start,
300 end: cursor.index + 3,
301 obj: match cursor[cursor.index + 1].to_ascii_lowercase() {
302 b'x' => Self::On,
303 SPACE => Self::Off,
304 HYPHEN => Self::Intermediate,
305 _ => Err(MatchError::InvalidLogic)?,
306 },
307 })
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use crate::{expr_in_pool, parse_org};
314
315 use super::*;
316 #[test]
317 fn checkbox() {
318 let input = "- [X]";
319 let ret = parse_org(input);
320 let item = expr_in_pool!(ret, Item).unwrap();
321 assert_eq!(item.check_box, Some(CheckBox::On))
322 }
323
324 #[test]
325 fn counter_set() {
326 let input = "- [@1]";
327 let ret = parse_org(input);
328 let item = expr_in_pool!(ret, Item).unwrap();
329 assert_eq!(item.counter_set, Some("1"));
330
331 let input = "- [@43]";
332 let ret = parse_org(input);
333 let item = expr_in_pool!(ret, Item).unwrap();
334 assert_eq!(item.counter_set, Some("43"))
335 }
336
337 #[test]
338 fn no_newline_hyphen() {
339 let input = "-";
340 let ret = parse_org(input);
341 let item = expr_in_pool!(ret, Plain).unwrap();
342 assert_eq!(item, &"-");
343 }
344 #[test]
345 fn hyphen_space() {
346 let input = "- ";
347 let ret = parse_org(input);
348 let item = expr_in_pool!(ret, Item).unwrap();
349 }
350
351 #[test]
352 fn hyphen_lbrack() {
353 let input = "- [";
354 let ret = parse_org(input);
355 let plain = expr_in_pool!(ret, Plain).unwrap();
356 assert_eq!(plain, &"[");
357 }
358 #[test]
359 fn hyphen_ltag() {
360 let input = "- [@";
361 let ret = parse_org(input);
362 let plain = expr_in_pool!(ret, Plain).unwrap();
363 assert_eq!(plain, &"[@");
364 }
365 #[test]
366 fn item_ordered_start() {
367 let input = "1. ";
368 let ret = parse_org(input);
369 let item = expr_in_pool!(ret, Item).unwrap();
370 assert!(matches!(
371 item.bullet,
372 BulletKind::Ordered(CounterKind::Number(1))
373 ));
374
375 let input = "17. ";
376 let ret = parse_org(input);
377 let item = expr_in_pool!(ret, Item).unwrap();
378 assert!(matches!(
379 item.bullet,
380 BulletKind::Ordered(CounterKind::Number(17))
381 ));
382
383 let input = "a. ";
384 let ret = parse_org(input);
385 let item = expr_in_pool!(ret, Item).unwrap();
386 assert!(matches!(
387 item.bullet,
388 BulletKind::Ordered(CounterKind::Letter(b'a'))
389 ));
390 }
391}