rust_yaml/scanner/
indentation.rs1use super::{Token, TokenType};
4use crate::{Error, Position, Result};
5
6#[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#[derive(Debug)]
18pub struct IndentationManager {
19 pub indent_stack: Vec<usize>,
21 pub indentation_style: IndentationStyle,
23 pub current_indent: usize,
25 pattern_analyzed: bool,
27}
28
29impl IndentationManager {
30 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 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 pub fn push_indent(&mut self, level: usize) {
51 self.indent_stack.push(level);
52 self.current_indent = level;
53 }
54
55 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 pub fn current_level(&self) -> usize {
68 *self.indent_stack.last().unwrap_or(&0)
69 }
70
71 pub fn is_dedent(&self, column: usize) -> bool {
73 column < self.current_level()
74 }
75
76 pub fn is_indent(&self, column: usize) -> bool {
78 column > self.current_level()
79 }
80
81 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 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 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 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 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}