markdown_it/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::{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 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, }
88 }
89
90 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 if pos >= 10 { return None; }
104 }
105 Some(')' | '.') => {
106 break;
108 }
109 Some(_) | None => { return None; }
110 }
111 }
112
113 match chars.next() {
114 Some(' ' | '\t') | None => Some(pos),
115 Some(_) => None, }
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 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 if silent {
156 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 if let Some(p) = Self::skip_ordered_list_marker(current_line) {
173 pos_after_marker = p;
174 let int = str::parse(¤t_line[..pos_after_marker - 1]).unwrap();
175 marker_value = Some(int);
176
177 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 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 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 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 indent_after_marker = 1;
256 } else if indent_after_marker as i32 > state.md.max_indent {
257 indent_after_marker = 1;
260 }
261
262 let indent = initial + indent_after_marker;
265
266 let old_node = std::mem::replace(&mut state.node, Node::new(ListItem));
268
269 let old_tight = state.tight;
271 let old_lineoffset = offsets.clone();
272
273 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 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 !state.tight || prev_empty_end {
305 tight = false;
306 }
307
308 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 if state.line_indent(next_line) < 0 { break; }
329
330 if state.line_indent(next_line) >= state.md.max_indent { break; }
331
332 if state.test_rules_at_line() { break; }
334
335 current_line = state.get_line(state.line).to_owned();
336
337 #[allow(clippy::collapsible_else_if)]
339 if marker_value.is_some() {
340 if let Some(p) = Self::skip_ordered_list_marker(¤t_line) {
341 pos_after_marker = p;
342 } else {
343 break;
344 }
345 } else {
346 if let Some(p) = Self::skip_bullet_list_marker(¤t_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 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 state.line = start_line;
367 let node = std::mem::replace(&mut state.node, old_node);
368 Some((node, next_line - state.line))
369 }
370}