use super::error::LexError;
use super::span::SourcePos;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IndentInfo {
pub spaces: usize,
pub level: usize,
}
pub fn calculate_indent(line: &str, line_num: u32) -> Result<Option<IndentInfo>, LexError> {
let bytes = line.as_bytes();
let mut spaces = 0;
for &b in bytes {
match b {
b' ' => spaces += 1,
b'\t' => {
if bytes[spaces..].iter().all(|&b| b.is_ascii_whitespace()) {
return Ok(None);
}
return Err(LexError::TabInIndentation {
pos: SourcePos::new(line_num as usize, spaces + 1),
});
}
_ => break,
}
}
if spaces == bytes.len() || bytes[spaces..].iter().all(|&b| b.is_ascii_whitespace()) {
return Ok(None);
}
Ok(Some(IndentInfo {
spaces,
level: spaces,
}))
}
pub fn validate_indent(info: IndentInfo, max_depth: usize, line_num: u32) -> Result<(), LexError> {
if info.level > max_depth {
return Err(LexError::IndentTooDeep {
depth: info.level,
max: max_depth,
pos: SourcePos::new(line_num as usize, 1),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_indent_zero() {
let result = calculate_indent("hello", 1).unwrap().unwrap();
assert_eq!(result.spaces, 0);
assert_eq!(result.level, 0);
}
#[test]
fn test_calculate_indent_level_1() {
let result = calculate_indent(" hello", 5).unwrap().unwrap();
assert_eq!(result.spaces, 1);
assert_eq!(result.level, 1);
}
#[test]
fn test_calculate_indent_level_2() {
let result = calculate_indent(" hello", 10).unwrap().unwrap();
assert_eq!(result.spaces, 2);
assert_eq!(result.level, 2);
}
#[test]
fn test_calculate_indent_deep_nesting() {
let result = calculate_indent(" hello", 15).unwrap().unwrap();
assert_eq!(result.spaces, 5);
assert_eq!(result.level, 5);
let result = calculate_indent(" hello", 20).unwrap().unwrap();
assert_eq!(result.spaces, 10);
assert_eq!(result.level, 10);
}
#[test]
fn test_calculate_indent_various_content() {
assert_eq!(
calculate_indent(" key: value", 1).unwrap().unwrap().level,
1
);
assert_eq!(
calculate_indent(" | row, data", 1).unwrap().unwrap().level,
1
);
assert_eq!(
calculate_indent(" @reference", 1).unwrap().unwrap().level,
1
);
assert_eq!(calculate_indent(" # comment", 1).unwrap().unwrap().level, 1);
}
#[test]
fn test_calculate_indent_unicode_content() {
let result = calculate_indent(" 日本語", 3).unwrap().unwrap();
assert_eq!(result.spaces, 1);
assert_eq!(result.level, 1);
let result = calculate_indent(" émoji 😀", 7).unwrap().unwrap();
assert_eq!(result.spaces, 2);
assert_eq!(result.level, 2);
}
#[test]
fn test_blank_line_empty() {
assert!(calculate_indent("", 1).unwrap().is_none());
}
#[test]
fn test_blank_line_spaces_only() {
assert!(calculate_indent(" ", 2).unwrap().is_none());
assert!(calculate_indent(" ", 3).unwrap().is_none());
assert!(calculate_indent(" ", 4).unwrap().is_none());
assert!(calculate_indent(" ", 5).unwrap().is_none());
}
#[test]
fn test_blank_line_mixed_whitespace() {
assert!(calculate_indent(" \t ", 6).unwrap().is_none());
assert!(calculate_indent(" \t", 7).unwrap().is_none());
}
#[test]
fn test_blank_line_with_trailing_whitespace() {
assert!(calculate_indent(" \t ", 8).unwrap().is_none());
}
#[test]
fn test_valid_indent_1_space() {
let result = calculate_indent(" hello", 42).unwrap().unwrap();
assert_eq!(result.spaces, 1);
assert_eq!(result.level, 1);
}
#[test]
fn test_valid_indent_3_spaces() {
let result = calculate_indent(" hello", 15).unwrap().unwrap();
assert_eq!(result.spaces, 3);
assert_eq!(result.level, 3);
}
#[test]
fn test_valid_indent_5_spaces() {
let result = calculate_indent(" hello", 20).unwrap().unwrap();
assert_eq!(result.spaces, 5);
assert_eq!(result.level, 5);
}
#[test]
fn test_valid_indent_various() {
for indent in [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21] {
let line = format!("{}hello", " ".repeat(indent));
let result = calculate_indent(&line, 100).unwrap().unwrap();
assert_eq!(result.spaces, indent);
assert_eq!(result.level, indent);
}
}
#[test]
fn test_tab_at_start() {
let result = calculate_indent("\thello", 10);
assert!(matches!(
result,
Err(LexError::TabInIndentation { pos }) if pos.line() == 10 && pos.column() == 1
));
}
#[test]
fn test_tab_after_spaces() {
let result = calculate_indent(" \thello", 25);
assert!(matches!(
result,
Err(LexError::TabInIndentation { pos }) if pos.line() == 25 && pos.column() == 3
));
}
#[test]
fn test_tab_mixed_with_spaces() {
let result = calculate_indent(" \t hello", 30);
assert!(matches!(
result,
Err(LexError::TabInIndentation { pos }) if pos.line() == 30 && pos.column() == 2
));
}
#[test]
fn test_multiple_tabs() {
let result = calculate_indent("\t\thello", 50);
assert!(matches!(
result,
Err(LexError::TabInIndentation { pos }) if pos.line() == 50
));
}
#[test]
fn test_validate_indent_within_max() {
let info = IndentInfo {
spaces: 2,
level: 2,
};
assert!(validate_indent(info, 10, 5).is_ok());
assert!(validate_indent(info, 2, 10).is_ok());
}
#[test]
fn test_validate_indent_at_max() {
let info = IndentInfo {
spaces: 10,
level: 10,
};
assert!(validate_indent(info, 10, 15).is_ok());
}
#[test]
fn test_validate_indent_exceeds_max() {
let info = IndentInfo {
spaces: 11,
level: 11,
};
let result = validate_indent(info, 10, 42);
assert!(matches!(
result,
Err(LexError::IndentTooDeep { depth: 11, max: 10, pos }) if pos.line() == 42
));
}
#[test]
fn test_validate_indent_zero_max() {
let info = IndentInfo {
spaces: 0,
level: 0,
};
assert!(validate_indent(info, 0, 1).is_ok());
let info = IndentInfo {
spaces: 1,
level: 1,
};
let result = validate_indent(info, 0, 8);
assert!(matches!(
result,
Err(LexError::IndentTooDeep { depth: 1, max: 0, pos }) if pos.line() == 8
));
}
#[test]
fn test_indent_info_equality() {
let a = IndentInfo {
spaces: 2,
level: 2,
};
let b = IndentInfo {
spaces: 2,
level: 2,
};
assert_eq!(a, b);
}
#[test]
fn test_indent_info_inequality() {
let a = IndentInfo {
spaces: 2,
level: 2,
};
let b = IndentInfo {
spaces: 1,
level: 1,
};
assert_ne!(a, b);
}
#[test]
fn test_indent_info_clone() {
let a = IndentInfo {
spaces: 3,
level: 3,
};
let b = a;
assert_eq!(a, b);
}
#[test]
fn test_indent_info_debug() {
let info = IndentInfo {
spaces: 2,
level: 2,
};
let debug_str = format!("{:?}", info);
assert!(debug_str.contains("spaces: 2"));
assert!(debug_str.contains("level: 2"));
}
}