markdown_that/plugins/cmark/block/
list.rs1use 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::{MarkdownThat, 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 MarkdownThat) {
69 md.block.add_rule::<ListScanner>().after::<HrScanner>();
70}
71
72#[doc(hidden)]
73pub struct ListScanner;
74
75impl ListScanner {
76 fn skip_bullet_list_marker(src: &str) -> Option<usize> {
79 let mut chars = src.chars();
80
81 let Some('*' | '-' | '+') = chars.next() else {
82 return None;
83 };
84
85 match chars.next() {
86 Some(' ' | '\t') | None => Some(1),
87 Some(_) => None, }
89 }
90
91 fn skip_ordered_list_marker(src: &str) -> Option<usize> {
94 let mut chars = src.chars();
95 let Some('0'..='9') = chars.next() else {
96 return None;
97 };
98
99 let mut pos = 1;
100 loop {
101 pos += 1;
102 match chars.next() {
103 Some('0'..='9') => {
104 if pos >= 10 {
107 return None;
108 }
109 }
110 Some(')' | '.') => {
111 break;
113 }
114 Some(_) | None => {
115 return None;
116 }
117 }
118 }
119
120 match chars.next() {
121 Some(' ' | '\t') | None => Some(pos),
122 Some(_) => None, }
124 }
125
126 fn mark_tight_paragraphs(nodes: &mut Vec<Node>) {
127 let mut idx = 0;
128 while idx < nodes.len() {
129 if nodes[idx].is::<Paragraph>() {
130 let children = std::mem::take(&mut nodes[idx].children);
131 let len = children.len();
132 nodes.splice(idx..idx + 1, children);
133 idx += len;
134 } else {
135 idx += 1;
136 }
137 }
138 }
139
140 fn find_marker(state: &mut BlockState, silent: bool) -> Option<(usize, Option<u32>, char)> {
141 if state.line_indent(state.line) >= state.md.max_indent {
142 return None;
143 }
144
145 if let Some(list_indent) = state.list_indent {
152 let indent_nonspace = state.line_offsets[state.line].indent_nonspace;
153 if indent_nonspace - list_indent as i32 >= state.md.max_indent
154 && indent_nonspace < state.blk_indent as i32
155 {
156 return None;
157 }
158 }
159
160 let mut is_terminating_paragraph = false;
161
162 if silent {
165 if state.line_indent(state.line) >= 0 {
171 is_terminating_paragraph = true;
172 }
173 }
174
175 let current_line = state.get_line(state.line);
176
177 let marker_value;
178 let pos_after_marker;
179
180 if let Some(p) = Self::skip_ordered_list_marker(current_line) {
182 pos_after_marker = p;
183 let int = str::parse(¤t_line[..pos_after_marker - 1]).unwrap();
184 marker_value = Some(int);
185
186 if is_terminating_paragraph && int != 1 {
189 return None;
190 }
191 } else if let Some(p) = Self::skip_bullet_list_marker(current_line) {
192 pos_after_marker = p;
193 marker_value = None;
194 } else {
195 return None;
196 }
197
198 if is_terminating_paragraph {
201 let mut chars = current_line[pos_after_marker..].chars();
202 loop {
203 match chars.next() {
204 Some(' ' | '\t') => {}
205 Some(_) => break,
206 None => return None,
207 }
208 }
209 }
210
211 let marker_char = current_line[..pos_after_marker]
213 .chars()
214 .next_back()
215 .unwrap();
216
217 Some((pos_after_marker, marker_value, marker_char))
218 }
219}
220
221impl BlockRule for ListScanner {
222 fn check(state: &mut BlockState) -> Option<()> {
223 if state.node.is::<BulletList>() || state.node.is::<OrderedList>() {
224 return None;
225 }
226
227 Self::find_marker(state, true).map(|_| ())
228 }
229
230 fn run(state: &mut BlockState) -> Option<(Node, usize)> {
231 let (mut pos_after_marker, marker_value, marker_char) = Self::find_marker(state, false)?;
232
233 let new_node = if let Some(int) = marker_value {
234 Node::new(OrderedList {
235 start: int,
236 marker: marker_char,
237 })
238 } else {
239 Node::new(BulletList {
240 marker: marker_char,
241 })
242 };
243
244 let old_node = std::mem::replace(&mut state.node, new_node);
245
246 let start_line = state.line;
251 let mut next_line = state.line;
252 let mut prev_empty_end = false;
253 let mut tight = true;
254 let mut current_line;
255
256 while next_line < state.line_max {
257 let offsets = &state.line_offsets[next_line];
258 let initial = offsets.indent_nonspace as usize + pos_after_marker;
259
260 let (mut indent_after_marker, first_nonspace) = find_indent_of(
261 &state.src[offsets.line_start..offsets.line_end],
262 pos_after_marker + offsets.first_nonspace - offsets.line_start,
263 );
264
265 let reached_end_of_line = first_nonspace == offsets.line_end - offsets.line_start;
266 let indent_nonspace = initial + indent_after_marker;
267
268 #[allow(clippy::if_same_then_else)]
269 if reached_end_of_line {
270 indent_after_marker = 1;
272 } else if indent_after_marker as i32 > state.md.max_indent {
273 indent_after_marker = 1;
276 }
277
278 let indent = initial + indent_after_marker;
281
282 let old_node = std::mem::replace(&mut state.node, Node::new(ListItem));
284
285 let old_tight = state.tight;
287 let old_lineoffset = offsets.clone();
288
289 let old_list_indent = state.list_indent;
294 state.list_indent = Some(state.blk_indent as u32);
295 state.blk_indent = indent;
296
297 state.tight = true;
298 state.line_offsets[next_line].first_nonspace =
299 first_nonspace + state.line_offsets[next_line].line_start;
300 state.line_offsets[next_line].indent_nonspace = indent_nonspace as i32;
301
302 if reached_end_of_line && state.is_empty(next_line + 1) {
303 state.line = if state.line + 2 < state.line_max {
311 state.line + 2
312 } else {
313 state.line_max
314 }
315 } else {
316 state.line = next_line;
317 state.md.block.tokenize(state);
318 }
319
320 if !state.tight || prev_empty_end {
322 tight = false;
323 }
324
325 prev_empty_end = (state.line - next_line) > 1 && state.is_empty(state.line - 1);
328
329 state.blk_indent = state.list_indent.unwrap() as usize;
330 state.list_indent = old_list_indent;
331 state.line_offsets[next_line] = old_lineoffset;
332 state.tight = old_tight;
333
334 let end_line = state.line;
335 let mut node = std::mem::replace(&mut state.node, old_node);
336 node.srcmap = state.get_map(next_line, end_line - 1);
337 state.node.children.push(node);
338 next_line = state.line;
339
340 if next_line >= state.line_max {
341 break;
342 }
343
344 if state.line_indent(next_line) < 0 {
348 break;
349 }
350
351 if state.line_indent(next_line) >= state.md.max_indent {
352 break;
353 }
354
355 if state.test_rules_at_line() {
357 break;
358 }
359
360 current_line = state.get_line(state.line).to_owned();
361
362 #[allow(clippy::collapsible_else_if)]
364 if marker_value.is_some() {
365 if let Some(p) = Self::skip_ordered_list_marker(¤t_line) {
366 pos_after_marker = p;
367 } else {
368 break;
369 }
370 } else {
371 if let Some(p) = Self::skip_bullet_list_marker(¤t_line) {
372 pos_after_marker = p;
373 } else {
374 break;
375 }
376 }
377
378 let next_marker_char = current_line[..pos_after_marker]
379 .chars()
380 .next_back()
381 .unwrap();
382 if next_marker_char != marker_char {
383 break;
384 }
385 }
386
387 if tight {
389 for child in state.node.children.iter_mut() {
390 debug_assert!(child.is::<ListItem>());
391 Self::mark_tight_paragraphs(&mut child.children);
392 }
393 }
394
395 state.line = start_line;
397 let node = std::mem::replace(&mut state.node, old_node);
398 Some((node, next_line - state.line))
399 }
400}