markdown_it_deflist/
lib.rs1use markdown_it::{
14 parser::{
15 block::{BlockRule, BlockState},
16 inline::InlineRoot,
17 },
18 plugins::cmark::block::paragraph::{Paragraph, ParagraphScanner},
19 MarkdownIt, Node, NodeValue, Renderer,
20};
21
22pub fn add(md: &mut MarkdownIt) {
24 md.block
26 .add_rule::<DefinitionListScanner>()
27 .before::<ParagraphScanner>();
28}
29
30#[derive(Debug)]
31pub struct DefinitionList;
32impl NodeValue for DefinitionList {
33 fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
34 fmt.cr();
35 fmt.open("dl", &node.attrs);
36 fmt.cr();
37 fmt.contents(&node.children);
38 fmt.cr();
39 fmt.close("dl");
40 fmt.cr();
41 }
42}
43
44#[derive(Debug)]
45pub struct DefinitionTerm;
46impl NodeValue for DefinitionTerm {
47 fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
48 fmt.cr();
49 fmt.open("dt", &node.attrs);
50 fmt.contents(&node.children);
51 fmt.close("dt");
52 fmt.cr();
53 }
54}
55
56#[derive(Debug)]
57pub struct DefinitionDescription;
58impl NodeValue for DefinitionDescription {
59 fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
60 fmt.cr();
61 fmt.open("dd", &node.attrs);
62 fmt.contents(&node.children);
63 fmt.close("dd");
64 fmt.cr();
65 }
66}
67
68pub struct DefinitionListScanner;
70
71impl BlockRule for DefinitionListScanner {
72 fn check(state: &mut BlockState) -> Option<()> {
73 if state.line_indent(state.line) >= state.md.max_indent {
74 return None;
75 }
76
77 if !state.node.is::<DefinitionDescription>() {
79 return None;
80 }
81 check_for_description(state, state.line)?;
82
83 Some(())
84 }
85
86 fn run(state: &mut BlockState) -> Option<(Node, usize)> {
87 if state.line_indent(state.line) >= state.md.max_indent {
88 return None;
89 }
90
91 let start_line = state.line;
92 let mut next_line = state.line + 1;
93 if next_line >= state.line_max {
94 return None;
95 }
96
97 if state.is_empty(next_line) {
98 next_line += 1;
99 if next_line >= state.line_max {
100 return None;
101 }
102 }
103
104 if state.line_offsets[next_line].indent_nonspace < state.blk_indent as i32 {
105 return None;
106 }
107
108 let mut dd_first_nonspace = check_for_description(state, next_line)?;
109
110 let mut node_dl = Node::new(DefinitionList);
112 let mut dl_tight = true;
113
114 let mut dt_line = state.line;
120 let mut dd_line = next_line;
121
122 'terms: loop {
123 let mut prev_empty_end = false;
124
125 let mut node_dt = Node::new(DefinitionTerm);
127 node_dt.srcmap = state.get_map(dt_line, dt_line);
128 let (content, mapping) = state.get_lines(dt_line, dt_line + 1, state.blk_indent, false);
129 node_dt
130 .children
131 .push(Node::new(InlineRoot::new(content, mapping)));
132 node_dl.children.push(node_dt);
133
134 'descriptions: loop {
135 let mut dd_indent_nonspace = dd_first_nonspace as i32
137 - state.line_offsets[dd_line].first_nonspace as i32
138 + state.line_offsets[dd_line].indent_nonspace;
139 while dd_first_nonspace < state.line_offsets[dd_line].line_end {
140 let c = state.src[dd_first_nonspace..].chars().next()?;
141 if c == ' ' {
142 dd_indent_nonspace += 1;
143 } else if c == '\t' {
144 dd_indent_nonspace += 4 - dd_indent_nonspace % 4;
145 } else {
146 break;
147 }
148 dd_first_nonspace += 1;
149 }
150
151 let cached_state = CachedState {
153 tight: state.tight,
154 blk_indent: state.blk_indent,
155 dd_indent_nonspace: state.line_offsets[dd_line].indent_nonspace,
156 dd_first_nonspace: state.line_offsets[dd_line].first_nonspace,
157 };
158 state.tight = true;
159 state.blk_indent = state.line_offsets[dd_line].indent_nonspace as usize + 2;
160 state.line_offsets[dd_line].indent_nonspace = dd_indent_nonspace;
161 state.line_offsets[dd_line].first_nonspace = dd_first_nonspace;
162 state.line = dd_line;
163
164 let cached_node =
166 std::mem::replace(&mut state.node, Node::new(DefinitionDescription));
167 state.md.block.tokenize(state);
168 let mut node_dd = std::mem::replace(&mut state.node, cached_node);
169 node_dd.srcmap = state.get_map(next_line, state.line - 1);
170 node_dl.children.push(node_dd);
171
172 if !state.tight || prev_empty_end {
174 dl_tight = false;
175 }
176 prev_empty_end = (state.line - dd_line) > 1 && state.is_empty(state.line - 1);
178
179 state.tight = cached_state.tight;
181 state.blk_indent = cached_state.blk_indent;
182 state.line_offsets[dd_line].indent_nonspace = cached_state.dd_indent_nonspace;
183 state.line_offsets[dd_line].first_nonspace = cached_state.dd_first_nonspace;
184
185 next_line = state.line;
186
187 if next_line >= state.line_max
188 || state.line_offsets[next_line].indent_nonspace < state.blk_indent as i32
189 {
190 break 'terms;
191 }
192 match check_for_description(state, next_line) {
193 Some(pos) => dd_first_nonspace = pos,
194 None => break 'descriptions,
195 }
196
197 dd_line = next_line;
198 }
199
200 dt_line = next_line;
201 dd_line = dt_line + 1;
202
203 if next_line >= state.line_max
204 || state.is_empty(dt_line)
205 || state.line_offsets[dt_line].indent_nonspace < state.blk_indent as i32
206 || dd_line >= state.line_max
207 {
208 break 'terms;
209 }
210 if state.is_empty(dd_line) {
211 dd_line += 1;
212 }
213 if dd_line >= state.line_max
214 || state.line_offsets[dd_line].indent_nonspace < state.blk_indent as i32
215 {
216 break 'terms;
217 }
218 match check_for_description(state, dd_line) {
219 Some(pos) => dd_first_nonspace = pos,
220 None => break 'terms,
221 }
222 }
223
224 if dl_tight {
226 for child in node_dl.children.iter_mut() {
227 mark_tight_paragraphs(&mut child.children);
228 }
229 }
230
231 state.line = start_line;
232 Some((node_dl, next_line - start_line))
233 }
234}
235
236struct CachedState {
237 tight: bool,
238 blk_indent: usize,
239 dd_indent_nonspace: i32,
240 dd_first_nonspace: usize,
241}
242
243fn check_for_description(state: &mut BlockState, line: usize) -> Option<usize> {
246 let mut chars = state.get_line(line).chars();
247
248 let first_char = chars.next()?;
250 if first_char != ':' && first_char != '~' {
251 return None;
252 }
253
254 let second_char = chars.next()?;
256 if second_char != ' ' && second_char != '\t' {
257 return None;
258 }
259
260 loop {
263 match chars.next() {
264 Some(' ' | '\t') => {}
265 Some(_) => break,
266 None => return None,
267 }
268 }
269
270 Some(state.line_offsets[line].first_nonspace + 1)
271}
272
273fn mark_tight_paragraphs(nodes: &mut Vec<Node>) {
274 let mut idx = 0;
275 while idx < nodes.len() {
276 if nodes[idx].is::<Paragraph>() {
277 let children = std::mem::take(&mut nodes[idx].children);
278 let len = children.len();
279 nodes.splice(idx..idx + 1, children);
280 idx += len;
281 } else {
282 idx += 1;
283 }
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use markdown_it::plugins::cmark;
290
291 use super::*;
292
293 #[test]
294 fn test_definition_list() {
295 let mut md = MarkdownIt::new();
296 cmark::add(&mut md);
297 add(&mut md);
298
299 println!("test\n : foo\n : bar\n");
300 let ast = md.parse("test\n : foo\n : bar\n");
301 println!("{}", ast.render());
303 }
305}