Skip to main content

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}