Skip to main content

panache_parser/parser/utils/
container_stack.rs

1use super::list_item_buffer::ListItemBuffer;
2use super::text_buffer::{ParagraphBuffer, TextBuffer};
3use crate::parser::blocks::lists::ListMarker;
4use rowan::Checkpoint;
5
6/// Which multi-line display-math delimiter is currently open in a paragraph.
7///
8/// One field (rather than parallel per-delimiter flags) so that delimiters of
9/// one kind occurring inside an open region of another kind cannot latch a
10/// second "open" state: while a region is open, only its own closer matters.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub(crate) enum OpenDisplayMath {
13    /// `$$`-delimited; holds the opening run length (a closer needs
14    /// `run_len >= open_len`).
15    Dollars(usize),
16    /// `\[ ... \]` (extension `tex_math_single_backslash`).
17    SingleBrackets,
18    /// `\\[ ... \\]` (extension `tex_math_double_backslash`).
19    DoubleBrackets,
20}
21
22#[derive(Debug, Clone)]
23pub(crate) enum Container {
24    BlockQuote {
25        // No special tracking needed
26    },
27    Alert {
28        blockquote_depth: usize,
29    },
30    FencedDiv {
31        // No special tracking needed - closed by fence marker
32    },
33    /// MyST directive container. Closed by a fence line matching the opener's
34    /// `fence_char` with at least `fence_count` repeats. The fence info is
35    /// tracked here (unlike `FencedDiv`, whose closer is always bare `:::`)
36    /// because backtick directives must distinguish their closer from nested
37    /// code fences of a shorter run.
38    MystDirective {
39        fence_char: u8,
40        fence_count: usize,
41    },
42    /// python-markdown admonition / pymdownx details container. Content is
43    /// indented by `content_col` (4) columns; closes on dedent like a
44    /// footnote definition.
45    Admonition {
46        content_col: usize,
47    },
48    List {
49        marker: ListMarker,
50        base_indent_cols: usize,
51        has_blank_between_items: bool, // Track if list is loose (blank lines between items)
52    },
53    ListItem {
54        content_col: usize,
55        buffer: ListItemBuffer, // Buffer for list item content
56        /// True iff this list item has so far only seen its marker line, with
57        /// no real content (text, nested list, etc.) — a marker-only item.
58        /// Used by CommonMark to close empty list items at the first blank
59        /// line, per spec §5.2 ("a list item can begin with at most one
60        /// blank line"). Pandoc keeps the item open across the blank.
61        marker_only: bool,
62        /// True when the marker's required-1-col space was virtually absorbed
63        /// from a tab in the post-marker text rather than consumed as a
64        /// literal byte. In that case the buffered content's first byte is at
65        /// source column `content_col - 1`, not `content_col`. Used by
66        /// indented-code-from-marker-line detection to walk col-aware leading
67        /// whitespace correctly.
68        virtual_marker_space: bool,
69    },
70    DefinitionList {
71        // Definition lists don't need special tracking
72    },
73    DefinitionItem {
74        // No special tracking needed
75    },
76    Definition {
77        content_col: usize,
78        plain_open: bool,
79        plain_buffer: TextBuffer, // Buffer for accumulating PLAIN content
80    },
81    Paragraph {
82        buffer: ParagraphBuffer, // Interleaved buffer for paragraph content with markers
83        open_inline_math_envs: Vec<String>,
84        open_display_math: Option<OpenDisplayMath>,
85        // Checkpoint at the position the paragraph started; used to retroactively
86        // wrap buffered content as PARAGRAPH (or HEADING for multi-line setext)
87        // when the paragraph is closed.
88        start_checkpoint: Checkpoint,
89    },
90    FootnoteDefinition {
91        content_col: usize,
92    },
93}
94
95pub(crate) struct ContainerStack {
96    pub(crate) stack: Vec<Container>,
97}
98
99const TAB_STOP: usize = 4;
100
101impl ContainerStack {
102    pub(crate) fn new() -> Self {
103        Self { stack: Vec::new() }
104    }
105
106    pub(crate) fn depth(&self) -> usize {
107        self.stack.len()
108    }
109
110    pub(crate) fn last(&self) -> Option<&Container> {
111        self.stack.last()
112    }
113
114    pub(crate) fn push(&mut self, c: Container) {
115        self.stack.push(c);
116    }
117}
118
119/// Expand tabs to columns (tab stop = 4) and return (cols, byte_offset).
120pub(crate) fn leading_indent(line: &str) -> (usize, usize) {
121    leading_indent_from(line, 0)
122}
123
124/// Like [`leading_indent`] but seeds the column counter at `start_col` so tab
125/// expansion honors source-column tab-stops. Use when the leading whitespace
126/// being measured doesn't begin at source column 0 (e.g. the bytes after a
127/// list marker, where the marker itself occupies columns
128/// `[indent_cols, indent_cols + marker_len)`).
129pub(crate) fn leading_indent_from(line: &str, start_col: usize) -> (usize, usize) {
130    let mut cols = 0usize;
131    let mut bytes = 0usize;
132    for b in line.bytes() {
133        match b {
134            b' ' => {
135                cols += 1;
136                bytes += 1;
137            }
138            b'\t' => {
139                let absolute = start_col + cols;
140                cols += TAB_STOP - (absolute % TAB_STOP);
141                bytes += 1;
142            }
143            _ => break,
144        }
145    }
146    (cols, bytes)
147}
148
149/// Return byte index at a given column (tabs = 4).
150pub(crate) fn byte_index_at_column(line: &str, target_col: usize) -> usize {
151    let mut col = 0usize;
152    let mut idx = 0usize;
153    for (i, b) in line.bytes().enumerate() {
154        if col >= target_col {
155            return idx;
156        }
157        match b {
158            b' ' => {
159                col += 1;
160                idx = i + 1;
161            }
162            b'\t' => {
163                col += TAB_STOP - (col % TAB_STOP);
164                idx = i + 1;
165            }
166            _ => break,
167        }
168    }
169    idx
170}