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