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}