markdown_it/plugins/cmark/block/
list.rs

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}