smoldown/parser/block/
ordered_list.rs1use 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 !LIST_BEGIN.is_match(lines[0]) {
13 return None;
14 }
15
16 let mut contents = vec![];
19 let mut prev_newline = false;
20 let mut is_paragraph = false;
21
22 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 {
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 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 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 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}