Skip to main content

markdown_that/plugins/cmark/block/
blockquote.rs

1//! Block quotes
2//!
3//! `> looks like this`
4//!
5//! <https://spec.commonmark.org/0.30/#block-quotes>
6use crate::common::utils::find_indent_of;
7use crate::parser::block::{BlockRule, BlockState};
8use crate::{MarkdownThat, Node, NodeValue, Renderer};
9
10#[derive(Debug)]
11pub struct Blockquote;
12
13impl NodeValue for Blockquote {
14    fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
15        fmt.cr();
16        fmt.open("blockquote", &node.attrs);
17        fmt.cr();
18        fmt.contents(&node.children);
19        fmt.cr();
20        fmt.close("blockquote");
21        fmt.cr();
22    }
23}
24
25pub fn add(md: &mut MarkdownThat) {
26    md.block.add_rule::<BlockquoteScanner>();
27}
28
29#[doc(hidden)]
30pub struct BlockquoteScanner;
31impl BlockRule for BlockquoteScanner {
32    fn check(state: &mut BlockState) -> Option<()> {
33        if state.line_indent(state.line) >= state.md.max_indent {
34            return None;
35        }
36
37        // check the block quote marker
38        let Some('>') = state.get_line(state.line).chars().next() else {
39            return None;
40        };
41
42        Some(())
43    }
44
45    fn run(state: &mut BlockState) -> Option<(Node, usize)> {
46        Self::check(state)?;
47
48        let mut old_line_offsets = Vec::new();
49        let start_line = state.line;
50        let mut next_line = state.line;
51        let mut last_line_empty = false;
52
53        // Search the end of the block
54        //
55        // Block ends with either:
56        //  1. an empty line outside:
57        //     ```
58        //     > test
59        //
60        //     ```
61        //  2. an empty line inside:
62        //     ```
63        //     >
64        //     test
65        //     ```
66        //  3. another tag:
67        //     ```
68        //     > test
69        //      - - -
70        //     ```
71        while next_line < state.line_max {
72            // check if it's outdented, i.e. it's inside list item and indented
73            // less than said list item:
74            //
75            // ```
76            // 1. anything
77            //    > current blockquote
78            // 2. checking this line
79            // ```
80            let is_outdented = state.line_indent(next_line) < 0;
81            let line = state.get_line(next_line).to_owned();
82            let mut chars = line.chars();
83
84            match chars.next() {
85                None => {
86                    // Case 1: line is not inside the blockquote, and this line is empty.
87                    break;
88                }
89                Some('>') if !is_outdented => {
90                    // This line is inside the blockquote.
91
92                    // set offset past spaces and ">"
93                    let offsets = &state.line_offsets[next_line];
94                    let pos_after_marker = offsets.first_nonspace + 1;
95
96                    old_line_offsets.push(state.line_offsets[next_line].clone());
97
98                    let (mut indent_after_marker, first_nonspace) = find_indent_of(
99                        &state.src[offsets.line_start..offsets.line_end],
100                        pos_after_marker - offsets.line_start,
101                    );
102
103                    last_line_empty = first_nonspace == offsets.line_end - offsets.line_start;
104
105                    // skip one optional space after '>'
106                    if matches!(chars.next(), Some(' ' | '\t')) {
107                        indent_after_marker -= 1;
108                    }
109
110                    state.line_offsets[next_line].indent_nonspace = indent_after_marker as i32;
111                    state.line_offsets[next_line].first_nonspace =
112                        first_nonspace + state.line_offsets[next_line].line_start;
113                    next_line += 1;
114                    continue;
115                }
116                _ => {}
117            }
118
119            // Case 2: line is not inside the blockquote, and the last line was empty.
120            if last_line_empty {
121                break;
122            }
123
124            // Case 3: another tag found.
125            state.line = next_line;
126
127            if state.test_rules_at_line() {
128                // Quirk to enforce "hard termination mode" for paragraphs;
129                // normally if you call `nodeize(state, startLine, nextLine)`,
130                // paragraphs will look below nextLine for paragraph continuation,
131                // but if blockquote is terminated by another tag, they shouldn't
132                //state.line_max = next_line;
133
134                if state.blk_indent != 0 {
135                    // state.blkIndent was non-zero, we now set it to zero,
136                    // so we need to re-calculate all offsets to appear as
137                    // if indent wasn't changed
138                    old_line_offsets.push(state.line_offsets[next_line].clone());
139                    state.line_offsets[next_line].indent_nonspace -= state.blk_indent as i32;
140                }
141
142                break;
143            }
144
145            old_line_offsets.push(state.line_offsets[next_line].clone());
146
147            // A negative indentation means that this is a paragraph continuation
148            //
149            state.line_offsets[next_line].indent_nonspace = -1;
150            next_line += 1;
151        }
152
153        let old_indent = state.blk_indent;
154        state.blk_indent = 0;
155
156        let old_node = std::mem::replace(&mut state.node, Node::new(Blockquote));
157        let old_line_max = state.line_max;
158        state.line = start_line;
159        state.line_max = next_line;
160        state.md.block.tokenize(state);
161        next_line = state.line;
162        state.line = start_line;
163        state.line_max = old_line_max;
164
165        // Restore original tShift; this might not be necessary since the parser
166        // has already been here, but just to make sure we can do that.
167        for (idx, line_offset) in old_line_offsets.iter_mut().enumerate() {
168            std::mem::swap(&mut state.line_offsets[idx + start_line], line_offset);
169        }
170        state.blk_indent = old_indent;
171
172        let node = std::mem::replace(&mut state.node, old_node);
173        Some((node, next_line - start_line))
174    }
175}