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