#[cfg(doctest)]
doc_comment::doctest!("../README.md");
pub mod line_ending;
pub use line_ending::LineEnding;
struct AutoIndent {
line_ending: LineEnding,
}
impl AutoIndent {
fn new(input: &str) -> Self {
Self {
line_ending: LineEnding::detect(input),
}
}
fn apply(&self, input: &str) -> String {
if input.trim().is_empty() {
return String::new();
}
let input = LineEnding::normalize(input);
let mut lines: Vec<&str> = input.lines().collect();
let ends_with_newline = input.ends_with('\n');
let first_line = if lines.first().map(|s| s.trim()).unwrap_or("").is_empty() {
lines.remove(0);
None
} else {
Some(lines.remove(0)) };
let min_indent = lines
.iter()
.filter(|line| !line.trim().is_empty()) .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
.min()
.unwrap_or(0);
let mut result: Vec<String> = Vec::new();
if let Some(first) = first_line {
result.push(first.to_string()); }
result.extend(lines.iter().map(|line| {
if line.trim().is_empty() {
String::new() } else {
line.chars().skip(min_indent).collect() }
}));
if result.last().map(|s| s.trim()).unwrap_or("").is_empty() {
*result.last_mut().unwrap() = String::new();
}
let mut output = self.line_ending.restore_from_lines(result);
if ends_with_newline {
output.push_str(self.line_ending.as_str());
}
output
}
}
pub fn auto_indent(input: &str) -> String {
AutoIndent::new(input).apply(input)
}
#[cfg(test)]
mod tests {
use super::*;
use line_ending::LineEnding;
#[test]
fn test_basic_implementation() {
let input = r#"Basic Test
1
2
3
"#;
let line_ending = LineEnding::detect(input);
assert_eq!(
auto_indent(input),
line_ending.restore("Basic Test\n1\n 2\n 3\n")
);
assert_eq!(
input,
line_ending
.restore("Basic Test\n 1\n 2\n 3\n ")
);
}
#[test]
fn test_empty_first_line() {
let input = r#"
1
2
3
"#;
let line_ending = LineEnding::detect(input);
assert_eq!(
auto_indent(input),
line_ending.restore("1\n 2\n 3\n")
);
assert_eq!(
input,
line_ending.restore("\n 1\n 2\n 3\n "),
);
}
#[test]
fn test_indented_first_line() {
let input = r#" <- First Line
Second Line
"#;
let line_ending = LineEnding::detect(input);
assert_eq!(
auto_indent(input),
line_ending.restore(" <- First Line\nSecond Line\n")
);
assert_eq!(
input,
line_ending.restore(" <- First Line\n Second Line\n "),
);
}
#[test]
fn test_mixed_indentation() {
let input = r#"First Line
Second Line
Third Line
"#;
let line_ending = LineEnding::detect(input);
assert_eq!(
auto_indent(input),
line_ending.restore("First Line\n Second Line\nThird Line\n",)
);
assert_eq!(
input,
line_ending.restore("First Line\n Second Line\nThird Line\n "),
);
}
#[test]
fn test_single_line_no_change() {
let input = "Single line no change";
let line_ending = LineEnding::detect(input);
assert_eq!(
auto_indent(input),
line_ending.restore("Single line no change")
);
assert_eq!(input, line_ending.restore("Single line no change"));
}
#[test]
fn test_multiple_blank_lines() {
let input = r#"First Line
A
B
C
D
E
"#;
let line_ending = LineEnding::detect(input);
assert_eq!(
auto_indent(input),
line_ending.restore("First Line\n\n A\n\n B\n\n C\n\n D\n\nE\n")
);
assert_eq!(
input,
line_ending.restore(
"First Line\n \n A\n\n B\n\n C\n\n D\n\n E\n "
),
);
}
}