smoldown/parser/block/
ordered_list.rs

1use crate::parser::block::parse_blocks;
2use crate::parser::Block;
3use crate::parser::Block::{OrderedList, Paragraph};
4use crate::parser::{ListItem, OrderedListType};
5
6pub fn parse_ordered_list(lines: &[&str]) -> Option<(Block, usize)> {
7    crate::regex!(LIST_BEGIN = r"^(?P<indent> *)(?P<numbering>[0-9.]+|[aAiI]+\.) (?P<content>.*)");
8    crate::regex!(NEW_PARAGRAPH = r"^ +");
9    crate::regex!(INDENTED = r"^ {0,4}(?P<content>.*)");
10
11    // if the beginning doesn't match a list don't even bother
12    if !LIST_BEGIN.is_match(lines[0]) {
13        return None;
14    }
15
16    // a vec holding the contents and indentation
17    // of each list item
18    let mut contents = vec![];
19    let mut prev_newline = false;
20    let mut is_paragraph = false;
21
22    // counts the number of parsed lines to return
23    let mut i = 0;
24
25    let mut line_iter = lines.iter();
26    let mut line = line_iter.next();
27    let mut list_num_opt = None;
28
29    // loop for list items
30    loop {
31        if line.is_none() || !LIST_BEGIN.is_match(line.unwrap()) {
32            break;
33        }
34        if prev_newline {
35            is_paragraph = true;
36            prev_newline = false;
37        }
38
39        let caps = LIST_BEGIN.captures(line.unwrap()).unwrap();
40
41        let mut content = caps.name("content").unwrap().as_str().to_owned();
42        let last_indent = caps.name("indent").unwrap().as_str().len();
43        //We use the first list type found
44        // TODO: utf-8 safe?
45        list_num_opt = list_num_opt
46            .or_else(|| Some(caps.name("numbering").unwrap().as_str()[0..1].to_owned()));
47        i += 1;
48
49        // parse additional lines of the listitem
50        loop {
51            line = line_iter.next();
52
53            if line.is_none() || (prev_newline && !NEW_PARAGRAPH.is_match(line.unwrap())) {
54                break;
55            }
56
57            if LIST_BEGIN.is_match(line.unwrap()) {
58                let caps = LIST_BEGIN.captures(line.unwrap()).unwrap();
59                let indent = caps.name("indent").unwrap().as_str().len();
60                if indent < 2 || indent <= last_indent {
61                    break;
62                }
63            }
64
65            // newline means we start a new paragraph
66            if line.unwrap().is_empty() {
67                prev_newline = true;
68            } else {
69                prev_newline = false;
70            }
71
72            content.push('\n');
73            let caps = INDENTED.captures(line.unwrap()).unwrap();
74            content.push_str(&caps.name("content").unwrap().as_str());
75
76            i += 1;
77        }
78        contents.push(parse_blocks(&content));
79    }
80
81    let mut list_contents = vec![];
82
83    for c in contents {
84        if is_paragraph || c.len() > 1 {
85            list_contents.push(ListItem::Paragraph(c));
86        } else if let Paragraph(content) = c[0].clone() {
87            list_contents.push(ListItem::Simple(content));
88        }
89    }
90
91    if i > 0 {
92        let list_num = list_num_opt.unwrap_or("1".to_string());
93        return Some((
94            OrderedList(list_contents, OrderedListType::from_str(&list_num)),
95            i,
96        ));
97    }
98
99    None
100}
101
102#[cfg(test)]
103mod test {
104    use super::parse_ordered_list;
105    use crate::parser::Block::OrderedList;
106    use crate::parser::ListItem::Paragraph;
107    use crate::parser::OrderedListType;
108
109    #[test]
110    fn finds_list() {
111        match parse_ordered_list(&vec!["1. A list", "2. is good"]) {
112            Some((OrderedList(_, lt), 2)) if lt == OrderedListType::Numeric => (),
113            x => panic!("Found {:?}", x),
114        }
115
116        match parse_ordered_list(&vec!["a. A list", "b. is good", "laksjdnflakdsjnf"]) {
117            Some((OrderedList(_, lt), 3)) if lt == OrderedListType::Lowercase => (),
118            x => panic!("Found {:?}", x),
119        }
120
121        match parse_ordered_list(&vec!["A. A list", "B. is good", "laksjdnflakdsjnf"]) {
122            Some((OrderedList(_, lt), 3)) if lt == OrderedListType::Uppercase => (),
123            x => panic!("Found {:?}", x),
124        }
125    }
126
127    #[test]
128    fn knows_when_to_stop() {
129        match parse_ordered_list(&vec!["i. A list", "ii. is good", "", "laksjdnflakdsjnf"]) {
130            Some((OrderedList(_, lt), 3)) if lt == OrderedListType::LowercaseRoman => (),
131            x => panic!("Found {:?}", x),
132        }
133
134        match parse_ordered_list(&vec!["I. A list", "", "laksjdnflakdsjnf"]) {
135            Some((OrderedList(_, lt), 2)) if lt == OrderedListType::UppercaseRoman => (),
136            x => panic!("Found {:?}", x),
137        }
138    }
139
140    #[test]
141    fn multi_level_list() {
142        match parse_ordered_list(&vec![
143            "1. A list",
144            "     1.1. One point one",
145            "     1.2. One point two",
146        ]) {
147            Some((OrderedList(ref items, lt), 3)) if lt == OrderedListType::Numeric => {
148                match &items[0] {
149                    &Paragraph(ref items) => match &items[1] {
150                        &OrderedList(_, ref lt1) if lt1 == &OrderedListType::Numeric => (),
151                        x => panic!("Found {:?}", x),
152                    },
153                    x => panic!("Found {:?}", x),
154                }
155            }
156            x => panic!("Found {:?}", x),
157        }
158    }
159
160    #[test]
161    fn no_false_positives() {
162        assert_eq!(parse_ordered_list(&vec!["test 1. test"]), None);
163    }
164
165    #[test]
166    fn no_early_matching() {
167        assert_eq!(
168            parse_ordered_list(&vec!["test", "1. not", "2. a list"]),
169            None
170        );
171    }
172}