markdown_that/parser/block/state.rs
1// Parser state class
2//
3use crate::common::sourcemap::SourcePos;
4use crate::common::utils::calc_right_whitespace_with_tabstops;
5use crate::parser::extset::RootExtSet;
6use crate::{MarkdownThat, Node};
7
8#[derive(Debug)]
9#[readonly::make]
10/// Sandbox object containing data required to parse block structures.
11pub struct BlockState<'a, 'b>
12where
13 'b: 'a,
14{
15 /// Markdown source.
16 #[readonly]
17 pub src: &'b str,
18
19 /// Link to parser instance.
20 #[readonly]
21 pub md: &'a MarkdownThat,
22
23 pub root_ext: &'b mut RootExtSet,
24
25 /// Current node, your rule is supposed to add children to it.
26 pub node: Node,
27
28 pub line_offsets: Vec<LineOffset>,
29
30 /// Current block content indent (for example, if we are
31 /// inside a list, it would be positioned after the list marker).
32 pub blk_indent: usize,
33
34 /// Current line in src.
35 pub line: usize,
36
37 /// Maximum allowed line in src.
38 pub line_max: usize,
39
40 /// True if there are no empty lines between paragraphs, used to
41 /// toggle loose/tight mode for lists.
42 pub tight: bool,
43
44 /// indent of the current list block.
45 pub list_indent: Option<u32>,
46
47 pub level: u32,
48}
49
50/// Holds start/end/etc. positions for a specific source text line.
51#[derive(Debug, Clone)]
52pub struct LineOffset {
53 /// `line_start` is the actual start of the line.
54 ///
55 /// # const IGNORE : &str = stringify! {
56 /// " > blockquote\r\n"
57 /// ^-- it will always point here (must not be modified by rules)
58 /// # };
59 pub line_start: usize,
60
61 /// `line_end` is first newline character after the line,
62 /// or position after string length if there aren't any newlines left.
63 ///
64 /// # const IGNORE : &str = stringify! {
65 /// " > blockquote\r\n"
66 /// ^-- it will point here
67 /// # };
68 pub line_end: usize,
69
70 /// `first_nonspace` is the byte offset of the first non-space character in
71 /// the current line.
72 ///
73 /// # const IGNORE : &str = stringify! {
74 /// " > blockquote\r\n"
75 /// ^-- it will point here when paragraph is parsed
76 /// ^----- it is initially pointed here
77 /// # };
78 ///
79 /// It will be modified by rules (list and blockquote), chars before it
80 /// must be treated as whitespaces.
81 ///
82 pub first_nonspace: usize,
83
84 /// `indent_nonspace` is the indent (amount of virtual spaces from start)
85 /// of first non-space character in the current line, taking into account
86 /// tab expansion.
87 ///
88 /// For example, in case of ` \t foo`, indent is 5 (tab ends at multiple of 4,
89 /// then one space after it). Only tabs and spaces are counted for it,
90 /// so no funny unicode business (if cmark supported unicode spaces, they'd
91 /// be counted as 1 each regardless of utf8 width).
92 ///
93 /// You should compare `indent_nonspace` with `state.blkindent` when determining
94 /// real indent after taking into account lists.
95 ///
96 /// Most block rules in commonmark are indented 0..=3, and >=4 is code block.
97 /// Special value of ident_nonspace=-1 is used by this library as a sign
98 /// that this rule can only be a paragraph continuation (used in blockquotes),
99 /// so you must take into account that any math can end up negative.
100 ///
101 pub indent_nonspace: i32,
102}
103
104impl<'a, 'b> BlockState<'a, 'b> {
105 pub fn new(
106 src: &'b str,
107 md: &'a MarkdownThat,
108 root_ext: &'b mut RootExtSet,
109 node: Node,
110 ) -> Self {
111 let mut result = Self {
112 src,
113 md,
114 root_ext,
115 node,
116 line_offsets: Vec::new(),
117 blk_indent: 0,
118 line: 0,
119 line_max: 0,
120 tight: false,
121 list_indent: None,
122 level: 0,
123 };
124
125 result.generate_caches();
126 result
127 }
128
129 fn generate_caches(&mut self) {
130 // Create caches
131 // Generate markers.
132 let mut chars = self.src.chars().peekable();
133 let mut indent_found = false;
134 let mut indent = 0;
135 let mut offset = 0;
136 let mut start = 0;
137 let mut pos = 0;
138
139 loop {
140 match chars.next() {
141 Some(ch @ (' ' | '\t')) if !indent_found => {
142 indent += 1;
143 offset += if ch == '\t' { 4 - offset % 4 } else { 1 };
144 pos += 1;
145 }
146 ch @ (Some('\n' | '\r') | None) => {
147 self.line_offsets.push(LineOffset {
148 line_start: start,
149 line_end: pos,
150 first_nonspace: start + indent,
151 indent_nonspace: offset,
152 });
153
154 if ch == Some('\r') && chars.peek() == Some(&'\n') {
155 // treat CR+LF as one linebreak
156 chars.next();
157 pos += 1;
158 }
159
160 indent_found = false;
161 indent = 0;
162 offset = 0;
163 start = pos + 1;
164 pos += 1;
165
166 if ch.is_none() || chars.peek().is_none() {
167 break;
168 }
169 }
170 Some(ch) => {
171 indent_found = true;
172 pos += ch.len_utf8();
173 }
174 }
175 }
176
177 self.line_max = self.line_offsets.len();
178 }
179
180 #[must_use]
181 pub fn test_rules_at_line(&mut self) -> bool {
182 for rule in self.md.block.ruler.iter() {
183 if rule.0(self).is_some() {
184 return true;
185 }
186 }
187 false
188 }
189
190 #[must_use]
191 #[inline]
192 pub fn is_empty(&self, line: usize) -> bool {
193 if let Some(offsets) = self.line_offsets.get(line) {
194 offsets.first_nonspace >= offsets.line_end
195 } else {
196 false
197 }
198 }
199
200 pub fn skip_empty_lines(&self, from: usize) -> usize {
201 let mut line = from;
202 while line != self.line_max && self.is_empty(line) {
203 line += 1;
204 }
205 line
206 }
207
208 /// return line indent of specific line, taking into account blockquotes and lists;
209 /// it may be negative if a text has less indentation than current list item
210 #[must_use]
211 #[inline]
212 pub fn line_indent(&self, line: usize) -> i32 {
213 if line < self.line_max {
214 self.line_offsets[line].indent_nonspace - self.blk_indent as i32
215 } else {
216 0
217 }
218 }
219
220 /// return a single line, trimming initial spaces
221 #[must_use]
222 #[inline]
223 pub fn get_line(&self, line: usize) -> &str {
224 if line < self.line_max {
225 let pos = self.line_offsets[line].first_nonspace;
226 let max = self.line_offsets[line].line_end;
227 &self.src[pos..max]
228 } else {
229 ""
230 }
231 }
232
233 /// Cut a range of lines begin..end (not including end) from the source without preceding indent.
234 /// Returns a string (lines) plus a mapping (start of each line in result -> start of each line in source).
235 pub fn get_lines(
236 &self,
237 begin: usize,
238 end: usize,
239 indent: usize,
240 keep_last_lf: bool,
241 ) -> (String, Vec<(usize, usize)>) {
242 debug_assert!(begin <= end);
243
244 let mut line = begin;
245 let mut result = String::new();
246 let mut mapping = Vec::new();
247
248 while line < end {
249 let offsets = &self.line_offsets[line];
250 let last = offsets.line_end;
251 let add_last_lf = line + 1 < end || keep_last_lf;
252
253 let (num_spaces, first) = calc_right_whitespace_with_tabstops(
254 &self.src[offsets.line_start..offsets.first_nonspace],
255 offsets.indent_nonspace - indent as i32,
256 );
257
258 mapping.push((result.len(), offsets.line_start + first));
259 result += &" ".repeat(num_spaces);
260 result += &self.src[offsets.line_start + first..last];
261 if add_last_lf {
262 result.push('\n');
263 }
264 line += 1;
265 }
266
267 (result, mapping)
268 }
269
270 #[must_use]
271 #[inline]
272 pub fn get_map(&self, start_line: usize, end_line: usize) -> Option<SourcePos> {
273 debug_assert!(start_line <= end_line);
274
275 Some(SourcePos::new(
276 self.line_offsets[start_line].first_nonspace,
277 self.line_offsets[end_line].line_end,
278 ))
279 }
280
281 #[must_use]
282 #[inline]
283 pub fn get_map_from_offsets(&self, start_pos: usize, end_pos: usize) -> Option<SourcePos> {
284 debug_assert!(start_pos <= end_pos);
285
286 Some(SourcePos::new(start_pos, end_pos))
287 }
288}