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}