#[cfg(doctest)]
doc_comment::doctest!("../README.md");
pub use line_ending::LineEnding;
struct AutoIndent {
line_ending: LineEnding,
}
impl AutoIndent {
fn new(input: &str) -> Self {
Self {
line_ending: LineEnding::from(input),
}
}
fn apply(&self, input: &str) -> String {
if input.trim().is_empty() {
return String::new();
}
let mut lines: Vec<String> = LineEnding::split(input);
let first_line = 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();
}
self.line_ending.join(result)
}
}
pub fn auto_indent(input: &str) -> String {
AutoIndent::new(input).apply(input)
}
#[cfg(test)]
mod tests {
use super::*;
use line_ending::LineEnding;
fn get_readme_contents() -> String {
use std::fs::File;
use std::io::Read;
let readme_file = "README.md";
let mut read_content = String::new();
File::open(readme_file)
.unwrap_or_else(|_| panic!("Failed to open {}", readme_file))
.read_to_string(&mut read_content)
.unwrap_or_else(|_| panic!("Failed to read {}", readme_file));
read_content
}
#[test]
fn test_preserves_formatting() {
let readme_contents = get_readme_contents();
assert_eq!(auto_indent(&readme_contents), readme_contents);
let lines = LineEnding::split(&readme_contents);
assert_eq!(lines.first().unwrap(), "# Multi-line String Auto-Indent");
assert!(
lines.len() > 5,
"Expected README to have more than 5 lines, but got {}",
lines.len()
);
}
#[test]
fn test_basic_implementation() {
let input = r#"Basic Test
1
2
3
"#;
let line_ending = LineEnding::from(input);
assert_eq!(
auto_indent(input),
line_ending.denormalize("Basic Test\n1\n 2\n 3\n")
);
assert_eq!(
input,
line_ending
.denormalize("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::from(input);
assert_eq!(
auto_indent(input),
line_ending.denormalize("\n1\n 2\n 3\n")
);
assert_eq!(
input,
line_ending.denormalize("\n 1\n 2\n 3\n "),
);
}
#[test]
fn test_indented_first_line() {
let input = r#" <- First Line
Second Line
"#;
let line_ending = LineEnding::from(input);
assert_eq!(
auto_indent(input),
line_ending.denormalize(" <- First Line\nSecond Line\n")
);
assert_eq!(
input,
line_ending.denormalize(" <- First Line\n Second Line\n "),
);
}
#[test]
fn test_mixed_indentation() {
let input = r#"First Line
Second Line
Third Line
"#;
let line_ending = LineEnding::from(input);
assert_eq!(
auto_indent(input),
line_ending.denormalize("First Line\n Second Line\nThird Line\n",)
);
assert_eq!(
input,
line_ending.denormalize("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::from(input);
assert_eq!(
auto_indent(input),
line_ending.denormalize("Single line no change")
);
assert_eq!(input, line_ending.denormalize("Single line no change"));
}
#[test]
fn test_multiple_blank_lines() {
let input = r#"First Line
A
B
C
D
E
"#;
let line_ending = LineEnding::from(input);
assert_eq!(
auto_indent(input),
line_ending.denormalize("First Line\n\n A\n\n B\n\n C\n\n D\n\nE\n")
);
assert_eq!(
input,
line_ending.denormalize(
"First Line\n \n A\n\n B\n\n C\n\n D\n\n E\n "
),
);
}
}