1use crate::error::{Result, ToonError};
2use crate::shared::constants::{SPACE, TAB};
3
4pub type Depth = usize;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct ParsedLine {
8 pub raw: String,
9 pub indent: usize,
10 pub content: String,
11 pub depth: Depth,
12 pub line_number: usize,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct BlankLineInfo {
17 pub line_number: usize,
18 pub indent: usize,
19 pub depth: Depth,
20}
21
22#[derive(Debug, Clone)]
23pub struct StreamingScanState {
24 pub line_number: usize,
25 pub blank_lines: Vec<BlankLineInfo>,
26}
27
28#[must_use]
29pub const fn create_scan_state() -> StreamingScanState {
30 StreamingScanState {
31 line_number: 0,
32 blank_lines: Vec::new(),
33 }
34}
35
36pub fn parse_line_incremental(
43 raw: &str,
44 state: &mut StreamingScanState,
45 indent_size: usize,
46 strict: bool,
47) -> Result<Option<ParsedLine>> {
48 state.line_number += 1;
49 let line_number = state.line_number;
50
51 let mut indent = 0usize;
52 let raw_bytes = raw.as_bytes();
53 while indent < raw_bytes.len() && raw_bytes[indent] == SPACE as u8 {
54 indent += 1;
55 }
56
57 let content_slice = &raw[indent..];
59 if content_slice.trim().is_empty() {
60 let depth = compute_depth_from_indent(indent, indent_size);
61 state.blank_lines.push(BlankLineInfo {
62 line_number,
63 indent,
64 depth,
65 });
66 return Ok(None);
67 }
68
69 let content = content_slice.to_string();
71 let depth = compute_depth_from_indent(indent, indent_size);
72
73 if strict {
74 let mut whitespace_end = 0usize;
75 while whitespace_end < raw_bytes.len()
76 && (raw_bytes[whitespace_end] == SPACE as u8 || raw_bytes[whitespace_end] == TAB as u8)
77 {
78 whitespace_end += 1;
79 }
80
81 if raw[..whitespace_end].contains(TAB) {
82 return Err(ToonError::tabs_not_allowed(line_number));
83 }
84
85 if indent_size == 0 {
86 if indent > 0 {
87 return Err(ToonError::validation(
88 line_number,
89 format!(
90 "Indentation not allowed when indent size is 0, but found {indent} spaces"
91 ),
92 ));
93 }
94 } else if indent > 0 && !indent.is_multiple_of(indent_size) {
95 return Err(ToonError::invalid_indentation(
96 line_number,
97 indent_size,
98 indent,
99 ));
100 }
101 }
102
103 Ok(Some(ParsedLine {
104 raw: raw.to_string(),
105 indent,
106 content,
107 depth,
108 line_number,
109 }))
110}
111
112pub fn parse_lines_sync(
118 source: impl IntoIterator<Item = String>,
119 indent_size: usize,
120 strict: bool,
121 state: &mut StreamingScanState,
122) -> Result<Vec<ParsedLine>> {
123 let mut lines = Vec::new();
124 for raw in source {
125 if let Some(parsed) = parse_line_incremental(&raw, state, indent_size, strict)? {
126 lines.push(parsed);
127 }
128 }
129 Ok(lines)
130}
131
132#[must_use]
133pub const fn compute_depth_from_indent(indent_spaces: usize, indent_size: usize) -> Depth {
134 if indent_size == 0 {
135 return 0;
136 }
137 indent_spaces / indent_size
138}
139
140#[derive(Debug, Clone)]
141pub struct StreamingLineCursor {
142 lines: Vec<ParsedLine>,
143 index: usize,
144 last_line: Option<ParsedLine>,
145 blank_lines: Vec<BlankLineInfo>,
146}
147
148impl StreamingLineCursor {
149 #[must_use]
150 pub const fn new(lines: Vec<ParsedLine>, blank_lines: Vec<BlankLineInfo>) -> Self {
151 Self {
152 lines,
153 index: 0,
154 last_line: None,
155 blank_lines,
156 }
157 }
158
159 #[must_use]
160 pub fn get_blank_lines(&self) -> &[BlankLineInfo] {
161 &self.blank_lines
162 }
163
164 #[must_use]
165 pub fn peek_sync(&self) -> Option<&ParsedLine> {
166 self.lines.get(self.index)
167 }
168
169 pub fn advance_sync(&mut self) {
170 if self.index < self.lines.len() {
171 self.last_line = Some(self.lines[self.index].clone());
173 self.index += 1;
174 }
175 }
176
177 pub fn next_sync(&mut self) -> Option<ParsedLine> {
178 if self.index < self.lines.len() {
179 let line = self.lines[self.index].clone();
180 self.last_line = Some(line.clone());
181 self.index += 1;
182 Some(line)
183 } else {
184 None
185 }
186 }
187
188 #[must_use]
189 pub const fn current(&self) -> Option<&ParsedLine> {
190 self.last_line.as_ref()
191 }
192
193 #[must_use]
194 pub const fn at_end_sync(&self) -> bool {
195 self.index >= self.lines.len()
196 }
197}