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