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