Skip to main content

toon/decode/
scanner.rs

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
36/// Parse a line with indentation and strict-mode validation.
37///
38/// # Errors
39///
40/// Returns an error if strict mode rules are violated (tabs in indentation or
41/// indentation not a multiple of the indent size).
42pub 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    // Check if line is blank before allocating content string
58    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    // Only allocate content string for non-blank lines
70    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 % indent_size != 0 {
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
112/// Parse all lines from the source, skipping blank lines but recording them for validation.
113///
114/// # Errors
115///
116/// Returns an error if any line violates strict indentation rules.
117pub 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            // Store index instead of cloning
172            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 fn at_end_sync(&self) -> bool {
195        self.index >= self.lines.len()
196    }
197}