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}