1//! Ordered and bullet lists
2//!
3//! This plugin parses both kinds of lists (bullet and ordered) as well as list items.
4//!
5//! looks like `1. this` or `- this`
6//!
7//! - <https://spec.commonmark.org/0.30/#lists>
8//! - <https://spec.commonmark.org/0.30/#list-items>
9use crate::common::utils::find_indent_of;
10use crate::parser::block::{BlockRule, BlockState};
11use crate::plugins::cmark::block::hr::HrScanner;
12use crate::plugins::cmark::block::paragraph::Paragraph;
13use crate::{MarkdownIt, Node, NodeValue, Renderer};
14
15#[derive(Debug)]
16pub struct OrderedList {
17 pub start: u32,
18 pub marker: char,
19}
20
21impl NodeValue for OrderedList {
22 fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
23 let mut attrs = node.attrs.clone();
24 let start;
25 if self.start != 1 {
26 start = self.start.to_string();
27 attrs.push(("start", start));
28 }
29 fmt.cr();
30 fmt.open("ol", &attrs);
31 fmt.cr();
32 fmt.contents(&node.children);
33 fmt.cr();
34 fmt.close("ol");
35 fmt.cr();
36 }
37}
38
39#[derive(Debug)]
40pub struct BulletList {
41 pub marker: char,
42}
43
44impl NodeValue for BulletList {
45 fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
46 fmt.cr();
47 fmt.open("ul", &node.attrs);
48 fmt.cr();
49 fmt.contents(&node.children);
50 fmt.cr();
51 fmt.close("ul");
52 fmt.cr();
53 }
54}
55
56#[derive(Debug)]
57pub struct ListItem;
58
59impl NodeValue for ListItem {
60 fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
61 fmt.open("li", &node.attrs);
62 fmt.contents(&node.children);
63 fmt.close("li");
64 fmt.cr();
65 }
66}
67
68pub fn add(md: &mut MarkdownIt) {
69 md.block.add_rule::<ListScanner>()
70 .after::<HrScanner>();
71}
72
73#[doc(hidden)]
74pub struct ListScanner;
75
76impl ListScanner {
77 // Search `[-+*][\n ]`, returns next pos after marker on success
78 // or -1 on fail.
79 fn skip_bullet_list_marker(src: &str) -> Option<usize> {
80 let mut chars = src.chars();
81
82 let Some('*' | '-' | '+') = chars.next() else { return None; };
83
84 match chars.next() {
85 Some(' ' | '\t') | None => Some(1),
86 Some(_) => None, // " -test " - is not a list item
87 }
88 }
89
90 // Search `\d+[.)][\n ]`, returns next pos after marker on success
91 // or -1 on fail.
92 fn skip_ordered_list_marker(src: &str) -> Option<usize> {
93 let mut chars = src.chars();
94 let Some('0'..='9') = chars.next() else { return None; };
95
96 let mut pos = 1;
97 loop {
98 pos += 1;
99 match chars.next() {
100 Some('0'..='9') => {
101 // List marker should have no more than 9 digits
102 // (prevents integer overflow in browsers)
103 if pos >= 10 { return None; }
104 }
105 Some(')' | '.') => {
106 // found valid marker
107 break;
108 }
109 Some(_) | None => { return None; }
110 }
111 }
112
113 match chars.next() {
114 Some(' ' | '\t') | None => Some(pos),
115 Some(_) => None, // " 1.test " - is not a list item
116 }
117 }
118
119 fn mark_tight_paragraphs(nodes: &mut Vec<Node>) {
120 let mut idx = 0;
121 while idx < nodes.len() {
122 if nodes[idx].is::<Paragraph>() {
123 let children = std::mem::take(&mut nodes[idx].children);
124 let len = children.len();
125 nodes.splice(idx..idx+1, children);
126 idx += len;
127 } else {
128 idx += 1;
129 }
130 }
131 }
132
133 fn find_marker(state: &mut BlockState, silent: bool) -> Option<(usize, Option<u32>, char)> {
134
135 if state.line_indent(state.line) >= state.md.max_indent { return None; }
136
137 // Special case:
138 // - item 1
139 // - item 2
140 // - item 3
141 // - item 4
142 // - this one is a paragraph continuation
143 if let Some(list_indent) = state.list_indent {
144 let indent_nonspace = state.line_offsets[state.line].indent_nonspace;
145 if indent_nonspace - list_indent as i32 >= state.md.max_indent &&
146 indent_nonspace < state.blk_indent as i32 {
147 return None;
148 }
149 }
150
151 let mut is_terminating_paragraph = false;
152
153 // limit conditions when list can interrupt
154 // a paragraph (validation mode only)
155 if silent {
156 // Next list item should still terminate previous list item;
157 //
158 // This code can fail if plugins use blkIndent as well as lists,
159 // but I hope the spec gets fixed long before that happens.
160 //
161 if state.line_indent(state.line) >= 0 {
162 is_terminating_paragraph = true;
163 }
164 }
165
166 let current_line = state.get_line(state.line);
167
168 let marker_value;
169 let pos_after_marker;
170
171 // Detect list type and position after marker
172 if let Some(p) = Self::skip_ordered_list_marker(current_line) {
173 pos_after_marker = p;
174 let int = str::parse(&current_line[..pos_after_marker - 1]).unwrap();
175 marker_value = Some(int);
176
177 // If we're starting a new ordered list right after
178 // a paragraph, it should start with 1.
179 if is_terminating_paragraph && int != 1 { return None; }
180
181 } else if let Some(p) = Self::skip_bullet_list_marker(current_line) {
182 pos_after_marker = p;
183 marker_value = None;
184 } else {
185 return None;
186 }
187
188 // If we're starting a new unordered list right after
189 // a paragraph, first line should not be empty.
190 if is_terminating_paragraph {
191 let mut chars = current_line[pos_after_marker..].chars();
192 loop {
193 match chars.next() {
194 Some(' ' | '\t') => {},
195 Some(_) => break,
196 None => return None,
197 }
198 }
199 }
200
201 // We should terminate list on style change. Remember first one to compare.
202 let marker_char = current_line[..pos_after_marker].chars().next_back().unwrap();
203
204 Some((pos_after_marker, marker_value, marker_char))
205 }
206}
207
208impl BlockRule for ListScanner {
209 fn check(state: &mut BlockState) -> Option<()> {
210 if state.node.is::<BulletList>() || state.node.is::<OrderedList>() { return None; }
211
212 Self::find_marker(state, true).map(|_| ())
213 }
214
215 fn run(state: &mut BlockState) -> Option<(Node, usize)> {
216 let (mut pos_after_marker, marker_value, marker_char) = Self::find_marker(state, false)?;
217
218 let new_node = if let Some(int) = marker_value {
219 Node::new(OrderedList {
220 start: int,
221 marker: marker_char
222 })
223 } else {
224 Node::new(BulletList {
225 marker: marker_char
226 })
227 };
228
229 let old_node = std::mem::replace(&mut state.node, new_node);
230
231 //
232 // Iterate list items
233 //
234
235 let start_line = state.line;
236 let mut next_line = state.line;
237 let mut prev_empty_end = false;
238 let mut tight = true;
239 let mut current_line;
240
241 while next_line < state.line_max {
242 let offsets = &state.line_offsets[next_line];
243 let initial = offsets.indent_nonspace as usize + pos_after_marker;
244
245 let ( mut indent_after_marker, first_nonspace ) = find_indent_of(
246 &state.src[offsets.line_start..offsets.line_end],
247 pos_after_marker + offsets.first_nonspace - offsets.line_start);
248
249 let reached_end_of_line = first_nonspace == offsets.line_end - offsets.line_start;
250 let indent_nonspace = initial + indent_after_marker;
251
252 #[allow(clippy::if_same_then_else)]
253 if reached_end_of_line {
254 // trimming space in "- \n 3" case, indent is 1 here
255 indent_after_marker = 1;
256 } else if indent_after_marker as i32 > state.md.max_indent {
257 // If we have more than the max indent, the indent is 1
258 // (the rest is just indented code block)
259 indent_after_marker = 1;
260 }
261
262 // " - test"
263 // ^^^^^ - calculating total length of this thing
264 let indent = initial + indent_after_marker;
265
266 // Run subparser & write tokens
267 let old_node = std::mem::replace(&mut state.node, Node::new(ListItem));
268
269 // change current state, then restore it after parser subcall
270 let old_tight = state.tight;
271 let old_lineoffset = offsets.clone();
272
273 // - example list
274 // ^ listIndent position will be here
275 // ^ blkIndent position will be here
276 //
277 let old_list_indent = state.list_indent;
278 state.list_indent = Some(state.blk_indent as u32);
279 state.blk_indent = indent;
280
281 state.tight = true;
282 state.line_offsets[next_line].first_nonspace = first_nonspace + state.line_offsets[next_line].line_start;
283 state.line_offsets[next_line].indent_nonspace = indent_nonspace as i32;
284
285 if reached_end_of_line && state.is_empty(next_line + 1) {
286 // workaround for this case
287 // (list item is empty, list terminates before "foo"):
288 // ~~~~~~~~
289 // -
290 //
291 // foo
292 // ~~~~~~~~
293 state.line = if state.line + 2 < state.line_max {
294 state.line + 2
295 } else {
296 state.line_max
297 }
298 } else {
299 state.line = next_line;
300 state.md.block.tokenize(state);
301 }
302
303 // If any of list item is tight, mark list as tight
304 if !state.tight || prev_empty_end {
305 tight = false;
306 }
307
308 // Item become loose if finish with empty line,
309 // but we should filter last element, because it means list finish
310 prev_empty_end = (state.line - next_line) > 1 && state.is_empty(state.line - 1);
311
312 state.blk_indent = state.list_indent.unwrap() as usize;
313 state.list_indent = old_list_indent;
314 state.line_offsets[next_line] = old_lineoffset;
315 state.tight = old_tight;
316
317 let end_line = state.line;
318 let mut node = std::mem::replace(&mut state.node, old_node);
319 node.srcmap = state.get_map(next_line, end_line - 1);
320 state.node.children.push(node);
321 next_line = state.line;
322
323 if next_line >= state.line_max { break; }
324
325 //
326 // Try to check if list is terminated or continued.
327 //
328 if state.line_indent(next_line) < 0 { break; }
329
330 if state.line_indent(next_line) >= state.md.max_indent { break; }
331
332 // fail if terminating block found
333 if state.test_rules_at_line() { break; }
334
335 current_line = state.get_line(state.line).to_owned();
336
337 // fail if list has another type
338 #[allow(clippy::collapsible_else_if)]
339 if marker_value.is_some() {
340 if let Some(p) = Self::skip_ordered_list_marker(&current_line) {
341 pos_after_marker = p;
342 } else {
343 break;
344 }
345 } else {
346 if let Some(p) = Self::skip_bullet_list_marker(&current_line) {
347 pos_after_marker = p;
348 } else {
349 break;
350 }
351 }
352
353 let next_marker_char = current_line[..pos_after_marker].chars().next_back().unwrap();
354 if next_marker_char != marker_char { break; }
355 }
356
357 // mark paragraphs tight if needed
358 if tight {
359 for child in state.node.children.iter_mut() {
360 debug_assert!(child.is::<ListItem>());
361 Self::mark_tight_paragraphs(&mut child.children);
362 }
363 }
364
365 // Finalize list
366 state.line = start_line;
367 let node = std::mem::replace(&mut state.node, old_node);
368 Some((node, next_line - state.line))
369 }
370}