rust_yaml/scanner/
indentation.rs

1//! Indentation management for YAML scanner
2
3use super::{Token, TokenType};
4use crate::{Error, Position, Result};
5
6/// Indentation style detection
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8#[allow(missing_docs)]
9pub enum IndentationStyle {
10    Spaces(usize),
11    Tabs,
12    Mixed,
13    Unknown,
14}
15
16/// Indentation manager for tracking YAML indentation levels
17#[derive(Debug)]
18pub struct IndentationManager {
19    /// Stack of indentation levels
20    pub indent_stack: Vec<usize>,
21    /// Detected indentation style
22    pub indentation_style: IndentationStyle,
23    /// Current indentation level
24    pub current_indent: usize,
25    /// Whether we've analyzed the indentation pattern
26    pattern_analyzed: bool,
27}
28
29impl IndentationManager {
30    /// Create a new indentation manager
31    pub fn new() -> Self {
32        Self {
33            indent_stack: vec![0],
34            indentation_style: IndentationStyle::Unknown,
35            current_indent: 0,
36            pattern_analyzed: false,
37        }
38    }
39
40    /// Reset the indentation manager
41    pub fn reset(&mut self) {
42        self.indent_stack.clear();
43        self.indent_stack.push(0);
44        self.indentation_style = IndentationStyle::Unknown;
45        self.current_indent = 0;
46        self.pattern_analyzed = false;
47    }
48
49    /// Push a new indentation level
50    pub fn push_indent(&mut self, level: usize) {
51        self.indent_stack.push(level);
52        self.current_indent = level;
53    }
54
55    /// Pop an indentation level
56    pub fn pop_indent(&mut self) -> Option<usize> {
57        if self.indent_stack.len() > 1 {
58            let level = self.indent_stack.pop();
59            self.current_indent = *self.indent_stack.last().unwrap_or(&0);
60            level
61        } else {
62            None
63        }
64    }
65
66    /// Get the current indentation level
67    pub fn current_level(&self) -> usize {
68        *self.indent_stack.last().unwrap_or(&0)
69    }
70
71    /// Check if we're at a dedent position
72    pub fn is_dedent(&self, column: usize) -> bool {
73        column < self.current_level()
74    }
75
76    /// Check if we're at an indent position
77    pub fn is_indent(&self, column: usize) -> bool {
78        column > self.current_level()
79    }
80
81    /// Count dedent levels needed
82    pub fn count_dedents(&self, column: usize) -> usize {
83        self.indent_stack
84            .iter()
85            .rev()
86            .take_while(|&&level| level > column)
87            .count()
88    }
89
90    /// Analyze indentation pattern from a line
91    pub fn analyze_pattern(&mut self, line: &str) -> IndentationStyle {
92        if self.pattern_analyzed {
93            return self.indentation_style;
94        }
95
96        let mut spaces = 0;
97        let mut tabs = 0;
98
99        for ch in line.chars() {
100            match ch {
101                ' ' => spaces += 1,
102                '\t' => tabs += 1,
103                _ => break,
104            }
105        }
106
107        self.indentation_style = if tabs > 0 && spaces > 0 {
108            IndentationStyle::Mixed
109        } else if tabs > 0 {
110            IndentationStyle::Tabs
111        } else if spaces > 0 {
112            // Try to detect common space widths (2, 4, 8)
113            for &width in &[2, 4, 8] {
114                if spaces % width == 0 {
115                    self.indentation_style = IndentationStyle::Spaces(width);
116                    break;
117                }
118            }
119            if self.indentation_style == IndentationStyle::Unknown {
120                self.indentation_style = IndentationStyle::Spaces(spaces);
121            }
122            self.indentation_style
123        } else {
124            IndentationStyle::Unknown
125        };
126
127        self.pattern_analyzed = true;
128        self.indentation_style
129    }
130
131    /// Validate indentation consistency
132    pub fn validate_indentation(&self, column: usize, position: Position) -> Result<()> {
133        match self.indentation_style {
134            IndentationStyle::Spaces(width) if width > 0 => {
135                if column % width != 0 {
136                    return Err(Error::scan(
137                        position,
138                        format!(
139                            "Inconsistent indentation: expected multiple of {} spaces, got {}",
140                            width, column
141                        ),
142                    ));
143                }
144            }
145            IndentationStyle::Mixed => {
146                return Err(Error::scan(
147                    position,
148                    "Mixed indentation (tabs and spaces) is not allowed",
149                ));
150            }
151            _ => {}
152        }
153        Ok(())
154    }
155
156    /// Generate BlockEnd tokens for dedentation
157    pub fn generate_block_ends(&mut self, column: usize, position: Position) -> Vec<Token> {
158        let mut tokens = Vec::new();
159        let dedent_count = self.count_dedents(column);
160
161        for _ in 0..dedent_count {
162            self.pop_indent();
163            tokens.push(Token::simple(TokenType::BlockEnd, position));
164        }
165
166        tokens
167    }
168}
169
170impl Default for IndentationManager {
171    fn default() -> Self {
172        Self::new()
173    }
174}