Skip to main content

panache_parser/parser/utils/
continuation.rs

1//! Continuation/blank-line handling policy.
2//!
3//! This module centralizes the parser's "should this line continue an existing container?"
4//! logic (especially across blank lines). Keeping this logic in one place reduces the
5//! risk of scattered ad-hoc heuristics diverging as blocks move into the dispatcher.
6
7use crate::options::{PandocCompat, ParserOptions};
8
9use crate::parser::block_dispatcher::{BlockContext, BlockParserRegistry};
10use crate::parser::blocks::blockquotes::{count_blockquote_markers, strip_n_blockquote_markers};
11use crate::parser::blocks::{definition_lists, html_blocks, lists, raw_blocks};
12use crate::parser::utils::container_stack::{ContainerStack, leading_indent};
13
14pub(crate) struct ContinuationPolicy<'a, 'cfg> {
15    config: &'cfg ParserOptions,
16    block_registry: &'a BlockParserRegistry,
17}
18
19impl<'a, 'cfg> ContinuationPolicy<'a, 'cfg> {
20    pub(crate) fn new(
21        config: &'cfg ParserOptions,
22        block_registry: &'a BlockParserRegistry,
23    ) -> Self {
24        Self {
25            config,
26            block_registry,
27        }
28    }
29
30    fn definition_min_block_indent(&self, content_col: usize) -> usize {
31        if self.config.effective_pandoc_compat() == PandocCompat::V3_7 {
32            content_col.max(4)
33        } else {
34            content_col
35        }
36    }
37
38    pub(crate) fn compute_levels_to_keep(
39        &self,
40        current_bq_depth: usize,
41        containers: &ContainerStack,
42        lines: &[&str],
43        next_line_pos: usize,
44        next_line: &str,
45    ) -> usize {
46        let (next_bq_depth, next_inner) = count_blockquote_markers(next_line);
47        let (raw_indent_cols, _) = leading_indent(next_inner);
48        let next_marker = lists::try_parse_list_marker(next_inner, self.config);
49        let next_is_definition_marker =
50            definition_lists::try_parse_definition_marker(next_inner).is_some();
51        let next_is_definition_term = !next_inner.trim().is_empty()
52            && definition_lists::next_line_is_definition_marker(lines, next_line_pos).is_some();
53
54        // `current_bq_depth` is used for proper indent calculation when the next line
55        // increases blockquote nesting.
56
57        let mut keep_level = 0;
58        let mut content_indent_so_far = 0usize;
59
60        // First, account for blockquotes
61        for (i, c) in containers.stack.iter().enumerate() {
62            match c {
63                crate::parser::utils::container_stack::Container::BlockQuote { .. } => {
64                    let bq_count = containers.stack[..=i]
65                        .iter()
66                        .filter(|x| {
67                            matches!(
68                                x,
69                                crate::parser::utils::container_stack::Container::BlockQuote { .. }
70                            )
71                        })
72                        .count();
73                    if bq_count <= next_bq_depth {
74                        keep_level = i + 1;
75                    }
76                }
77                crate::parser::utils::container_stack::Container::FootnoteDefinition {
78                    content_col,
79                    ..
80                } => {
81                    content_indent_so_far += *content_col;
82                    let min_indent = (*content_col).max(4);
83                    if raw_indent_cols >= min_indent {
84                        keep_level = i + 1;
85                    }
86                }
87                crate::parser::utils::container_stack::Container::Definition {
88                    content_col,
89                    ..
90                } => {
91                    // A blank line does not necessarily end a definition, but the continuation
92                    // indent must be measured relative to any outer content containers (e.g.
93                    // footnotes). Otherwise a line indented only for the footnote would wrongly
94                    // continue the definition.
95                    let min_indent = self.definition_min_block_indent(*content_col);
96                    let effective_indent = raw_indent_cols.saturating_sub(content_indent_so_far);
97                    if effective_indent >= min_indent {
98                        keep_level = i + 1;
99                    }
100                    content_indent_so_far += *content_col;
101                }
102                crate::parser::utils::container_stack::Container::DefinitionItem { .. }
103                    if next_is_definition_marker =>
104                {
105                    keep_level = i + 1;
106                }
107                crate::parser::utils::container_stack::Container::DefinitionList { .. }
108                    if next_is_definition_marker || next_is_definition_term =>
109                {
110                    keep_level = i + 1;
111                }
112                crate::parser::utils::container_stack::Container::List {
113                    marker,
114                    base_indent_cols,
115                    ..
116                } => {
117                    let definition_ancestor_kept = containers.stack[..i]
118                        .iter()
119                        .enumerate()
120                        .rev()
121                        .find_map(|(idx, container)| {
122                            matches!(
123                                container,
124                                crate::parser::utils::container_stack::Container::Definition { .. }
125                            )
126                            .then_some(keep_level > idx)
127                        })
128                        .unwrap_or(true);
129                    if !definition_ancestor_kept {
130                        continue;
131                    }
132
133                    let effective_indent = raw_indent_cols.saturating_sub(content_indent_so_far);
134                    let continues_list = if let Some(ref marker_match) = next_marker {
135                        lists::markers_match(marker, &marker_match.marker)
136                            && effective_indent <= base_indent_cols + 3
137                    } else {
138                        let item_content_col = containers
139                            .stack
140                            .get(i + 1)
141                            .and_then(|c| match c {
142                                crate::parser::utils::container_stack::Container::ListItem {
143                                    content_col,
144                                    ..
145                                } => Some(*content_col),
146                                _ => None,
147                            })
148                            .unwrap_or(1);
149                        effective_indent >= item_content_col
150                    };
151                    if continues_list {
152                        keep_level = i + 1;
153                    }
154                }
155                crate::parser::utils::container_stack::Container::ListItem {
156                    content_col, ..
157                } => {
158                    let definition_ancestor_kept = containers.stack[..i]
159                        .iter()
160                        .enumerate()
161                        .rev()
162                        .find_map(|(idx, container)| {
163                            matches!(
164                                container,
165                                crate::parser::utils::container_stack::Container::Definition { .. }
166                            )
167                            .then_some(keep_level > idx)
168                        })
169                        .unwrap_or(true);
170                    if !definition_ancestor_kept {
171                        continue;
172                    }
173
174                    let effective_indent = if next_bq_depth > current_bq_depth {
175                        let after_current_bq =
176                            strip_n_blockquote_markers(next_line, current_bq_depth);
177                        let (spaces_before_next_marker, _) = leading_indent(after_current_bq);
178                        spaces_before_next_marker.saturating_sub(content_indent_so_far)
179                    } else {
180                        raw_indent_cols.saturating_sub(content_indent_so_far)
181                    };
182
183                    let is_new_item_at_outer_level = if next_marker.is_some() {
184                        effective_indent < *content_col
185                    } else {
186                        false
187                    };
188
189                    if !is_new_item_at_outer_level && effective_indent >= *content_col {
190                        keep_level = i + 1;
191                    }
192                }
193                _ => {}
194            }
195        }
196
197        keep_level
198    }
199
200    /// Checks whether a line inside a definition should be treated as a plain continuation
201    /// (and buffered into the definition PLAIN), rather than parsed as a new block.
202    pub(crate) fn definition_plain_can_continue(
203        &self,
204        stripped_content: &str,
205        raw_content: &str,
206        content_indent: usize,
207        block_ctx: &BlockContext,
208        lines: &[&str],
209        pos: usize,
210    ) -> bool {
211        let prev_line_blank = if pos > 0 {
212            let prev_line = lines[pos - 1];
213            let (prev_bq_depth, prev_inner) = count_blockquote_markers(prev_line);
214            prev_line.trim().is_empty() || (prev_bq_depth > 0 && prev_inner.trim().is_empty())
215        } else {
216            false
217        };
218
219        // A blank line that isn't indented to the definition content column ends the definition.
220        let (indent_cols, _) = leading_indent(raw_content);
221        if raw_content.trim().is_empty() && indent_cols < content_indent {
222            return false;
223        }
224        let min_block_indent = self.definition_min_block_indent(content_indent);
225        if prev_line_blank && indent_cols < min_block_indent {
226            return false;
227        }
228
229        // If it's a block element marker, don't continue as plain.
230        if definition_lists::try_parse_definition_marker(stripped_content).is_some()
231            && leading_indent(raw_content).0 <= 3
232            && !stripped_content.starts_with(':')
233        {
234            let is_next_definition = self
235                .block_registry
236                .detect_prepared(block_ctx, lines, pos)
237                .map(|match_result| {
238                    match_result.effect
239                        == crate::parser::block_dispatcher::BlockEffect::OpenDefinitionList
240                })
241                .unwrap_or(false);
242            if is_next_definition {
243                return false;
244            }
245        }
246        if lists::try_parse_list_marker(stripped_content, self.config).is_some() {
247            if prev_line_blank {
248                return false;
249            }
250            if block_ctx.in_list {
251                return false;
252            }
253        }
254        if count_blockquote_markers(stripped_content).0 > 0 {
255            return false;
256        }
257        if self.config.extensions.raw_html
258            && html_blocks::try_parse_html_block_start(stripped_content).is_some()
259        {
260            return false;
261        }
262        if self.config.extensions.raw_tex
263            && raw_blocks::extract_environment_name(stripped_content).is_some()
264        {
265            return false;
266        }
267
268        if let Some(match_result) = self.block_registry.detect_prepared(block_ctx, lines, pos) {
269            if match_result.effect == crate::parser::block_dispatcher::BlockEffect::OpenList
270                && !prev_line_blank
271            {
272                return true;
273            }
274            if match_result.effect
275                == crate::parser::block_dispatcher::BlockEffect::OpenDefinitionList
276                && match_result
277                    .payload
278                    .as_ref()
279                    .and_then(|payload| {
280                        payload
281                            .downcast_ref::<crate::parser::block_dispatcher::DefinitionPrepared>()
282                    })
283                    .is_some_and(|prepared| {
284                        matches!(
285                            prepared,
286                            crate::parser::block_dispatcher::DefinitionPrepared::Term { .. }
287                        )
288                    })
289            {
290                return true;
291            }
292            return false;
293        }
294
295        true
296    }
297}