markdown_ppp/parser/blocks/
list.rs

1use crate::ast::{ListBulletKind, ListItem, ListKind, ListOrderedKindOptions, TaskState};
2use crate::parser::util::*;
3use crate::parser::MarkdownParserState;
4use nom::combinator::verify;
5use nom::{
6    branch::alt,
7    character::complete::{char, one_of, space0},
8    combinator::{map, not, opt, peek, recognize, value},
9    multi::{many0, many1, many_m_n},
10    sequence::{delimited, preceded, terminated},
11    IResult, Parser,
12};
13use std::rc::Rc;
14
15fn list_item_task_state(input: &str) -> IResult<&str, TaskState> {
16    delimited(
17        char('['),
18        alt((
19            value(TaskState::Complete, one_of("xX")),
20            value(TaskState::Incomplete, char(' ')),
21        )),
22        char(']'),
23    )
24    .parse(input)
25}
26
27fn list_marker(input: &str) -> IResult<&str, ListKind> {
28    alt((
29        list_marker_ordered,
30        list_marker_star,
31        list_marker_plus,
32        list_marker_dash,
33    ))
34    .parse(input)
35}
36
37fn list_marker_star(input: &str) -> IResult<&str, ListKind> {
38    map(char('*'), |_| ListKind::Bullet(ListBulletKind::Star)).parse(input)
39}
40
41fn list_marker_plus(input: &str) -> IResult<&str, ListKind> {
42    map(char('+'), |_| ListKind::Bullet(ListBulletKind::Plus)).parse(input)
43}
44
45fn list_marker_dash(input: &str) -> IResult<&str, ListKind> {
46    map(char('-'), |_| ListKind::Bullet(ListBulletKind::Dash)).parse(input)
47}
48
49fn list_marker_ordered(input: &str) -> IResult<&str, ListKind> {
50    map(
51        terminated(nom::character::complete::u64, one_of(".)")),
52        |start| ListKind::Ordered(ListOrderedKindOptions { start }),
53    )
54    .parse(input)
55}
56
57fn list_marker_followed_by_spaces(
58    input: &str,
59) -> IResult<&str, (ListKind, usize, Option<TaskState>)> {
60    let (remaining, kind) = delimited(
61        many_m_n(0, 3, char(' ')),
62        list_marker,
63        many_m_n(1, 4, char(' ')),
64    )
65    .parse(input)?;
66
67    let consumed = input.len() - remaining.len();
68
69    let (input, task_state) = opt(terminated(list_item_task_state, char(' '))).parse(remaining)?;
70
71    Ok((input, (kind, consumed, task_state)))
72}
73
74fn list_marker_followed_by_newline(
75    input: &str,
76) -> IResult<&str, (ListKind, usize, Option<TaskState>)> {
77    let (remaining, kind) = preceded(many_m_n(0, 3, char(' ')), list_marker).parse(input)?;
78
79    // Cases:
80    // 1.
81    // 1.____
82    if let Ok((tail, _)) = line_terminated(space0).parse(remaining) {
83        // Calculate prefix length: consumed + 1 space
84        let consumed = input.len() - remaining.len() + 1;
85
86        return Ok((tail, (kind, consumed, None)));
87    }
88
89    let (remaining, _) = many_m_n(0, 3, char(' ')).parse(remaining)?;
90    let consumed = input.len() - remaining.len() + 1;
91
92    let (remaining, task_state) = line_terminated(list_item_task_state).parse(remaining)?;
93
94    Ok((remaining, (kind, consumed, Some(task_state))))
95}
96
97pub(crate) fn list_marker_with_span_size(
98    input: &str,
99) -> IResult<&str, (ListKind, usize, Option<TaskState>, String)> {
100    alt((
101        map(
102            list_marker_followed_by_newline,
103            |(list_kind, prefix_length, task_state)| {
104                (list_kind, prefix_length, task_state, String::new())
105            },
106        ),
107        (map(
108            (
109                list_marker_followed_by_spaces,
110                line_terminated(not_eof_or_eol0),
111            ),
112            |((list_kind, prefix_length, task_state), s)| {
113                (list_kind, prefix_length, task_state, s.to_string())
114            },
115        )),
116    ))
117    .parse(input)
118}
119
120fn list_item_rest_line(
121    state: Rc<MarkdownParserState>,
122    list_kind: ListKind,
123    prefix_length: usize,
124) -> impl FnMut(&str) -> IResult<&str, Vec<&str>> {
125    move |input: &str| {
126        // Stop parsing lines on EOF
127        if input.is_empty() {
128            return Err(nom::Err::Error(nom::error::Error::new(
129                input,
130                nom::error::ErrorKind::Eof,
131            )));
132        }
133
134        let marker_parser = match list_kind {
135            ListKind::Ordered(_) => list_marker_ordered,
136            ListKind::Bullet(ListBulletKind::Star) => list_marker_star,
137            ListKind::Bullet(ListBulletKind::Plus) => list_marker_plus,
138            ListKind::Bullet(ListBulletKind::Dash) => list_marker_dash,
139        };
140
141        line_terminated(preceded(
142            peek(not(alt((
143                value(
144                    (),
145                    crate::parser::blocks::thematic_break::thematic_break(state.clone()),
146                ),
147                value(
148                    (),
149                    (
150                        verify(
151                            recognize(many_m_n(0, prefix_length, char(' '))),
152                            |indent: &str| indent.len() < prefix_length,
153                        ),
154                        marker_parser,
155                    ),
156                ),
157            )))),
158            alt((
159                // If starts with 0 <= prefix_length spaces
160                preceded(
161                    many_m_n(0, prefix_length, char(' ')),
162                    map(not_eof_or_eol1, |v| vec![v]),
163                ),
164                // If this is empty line, followed by prefix_length spaces
165                map(
166                    (
167                        recognize(many1(line_terminated(space0))),
168                        preceded(
169                            many_m_n(prefix_length, prefix_length, char(' ')),
170                            not_eof_or_eol1,
171                        ),
172                    ),
173                    |(newlines, content)| vec![newlines, content],
174                ),
175            )),
176        ))
177        .parse(input)
178    }
179}
180
181fn list_item_lines(
182    state: Rc<MarkdownParserState>,
183    list_kind: ListKind,
184    prefix_length: usize,
185) -> impl FnMut(&str) -> IResult<&str, Vec<Vec<&str>>> {
186    move |input: &str| {
187        many0(list_item_rest_line(
188            state.clone(),
189            list_kind.clone(),
190            prefix_length,
191        ))
192        .parse(input)
193    }
194}
195
196pub(crate) fn list_item(
197    state: Rc<MarkdownParserState>,
198) -> impl FnMut(&str) -> IResult<&str, (ListKind, ListItem)> {
199    move |input: &str| {
200        let (input, (list_kind, item_prefix_length, task_state, first_line)) =
201            list_marker_with_span_size(input)?;
202
203        let (input, rest_lines) =
204            list_item_lines(state.clone(), list_kind.clone(), item_prefix_length).parse(input)?;
205
206        let total_size = first_line.len() + rest_lines.len();
207        let mut item_content = String::with_capacity(total_size);
208        if !first_line.is_empty() {
209            item_content.push_str(&first_line)
210        }
211        for line in rest_lines {
212            item_content.push('\n');
213            for subline in line {
214                item_content.push_str(subline)
215            }
216        }
217
218        let (_, blocks) = many0(crate::parser::blocks::block(state.clone()))
219            .parse(&item_content)
220            .map_err(|err| err.map_input(|_| input))?;
221
222        let blocks = blocks.into_iter().flatten().collect();
223
224        let item = ListItem {
225            task: task_state,
226            blocks,
227        };
228        Ok((input, (list_kind, item)))
229    }
230}
231
232pub(crate) fn list(
233    state: Rc<MarkdownParserState>,
234) -> impl FnMut(&str) -> IResult<&str, crate::ast::List> {
235    move |input: &str| {
236        let (input, items) = many1(list_item(state.clone())).parse(input)?;
237
238        // With many1(), first element always present
239        let first_item = items.first().unwrap();
240
241        let list = crate::ast::List {
242            kind: first_item.0.clone(),
243            items: items.into_iter().map(|(_, item)| item).collect(),
244        };
245
246        Ok((input, list))
247    }
248}