org_rust_parser/element/
item.rs

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    // An instance of the pattern [@COUNTER]
11    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        // Will only ever really get called via Plainlist.
25
26        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        // if the last element was a \n, that means we're starting on a new line
62        // so we are Not on a list line.
63        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        // used to restore index to the previous position in the event of two
73        // blank lines
74        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    // Either the pattern COUNTER. or COUNTER)
123    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        // -\n is valid, so we don't want to skip past the newline
135        // since -    \n is also valid
136        // is valid
137        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                    // effectively these ↓
154                    // || chr == PERIOD || chr == RPAREN
155                })?;
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                    // must be a number
176                    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
193// - [@4]
194fn 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    // cursor.curr() is valid because num_match above ensures we're at a valid point
204    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        // must be a number
217        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    // - [@A] [X] | our tag is here :: remainder
238    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    /// [-]
269    Intermediate,
270    /// [ ]
271    Off,
272    /// \[X\]
273    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        // we're at a LBRACK in theory here
291        // 012
292        // [ ]
293
294        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}