use std::io::{Result, Write};
#[derive(Clone, Copy)]
pub struct IndentInfo {
pub first_line: usize,
pub last_line: usize,
pub leading_whitespace: usize,
}
impl IndentInfo {
pub fn new(input: &str) -> Option<IndentInfo> {
let mut first_char_line = None;
let mut last_char_line = 0;
let mut common: Option<usize> = None;
for (index, line) in input.lines().enumerate() {
let ws = line
.bytes()
.take_while(|&c| c.is_ascii_whitespace())
.count();
if ws == line.len() {
continue;
}
first_char_line.get_or_insert(index);
last_char_line = index;
common = match common {
None => Some(ws),
Some(x) => Some(x.min(ws)),
};
}
first_char_line.map(|x| IndentInfo {
first_line: x,
last_line: last_char_line,
leading_whitespace: common.unwrap_or(0),
})
}
}
pub struct Deindenter<'a> {
indent_info: IndentInfo,
input: &'a str,
}
impl Deindenter<'_> {
pub fn new(input: &str) -> Option<Deindenter> {
let indent_info = IndentInfo::new(input)?;
Some(Deindenter { indent_info, input })
}
pub fn to_writer<T: Write>(&self, mut out: T) -> Result<()> {
let IndentInfo {
first_line,
last_line,
leading_whitespace,
} = self.indent_info;
for line in self
.input
.split_inclusive('\n')
.skip(first_line)
.take(1 + last_line - first_line)
{
let trim = line.bytes().take(leading_whitespace).len();
let skip = if trim < leading_whitespace {
0 } else {
trim
};
out.write_all(&line.as_bytes()[skip..])?;
}
Ok(())
}
pub fn to_string(&self) -> Result<String> {
let mut buf = Vec::new();
self.to_writer(&mut buf)?;
let s = String::from_utf8(buf).expect("deindented string contains non-utf8 text");
Ok(s)
}
pub fn indent_info(&self) -> IndentInfo {
self.indent_info
}
}
impl From<Deindenter<'_>> for IndentInfo {
fn from(value: Deindenter) -> Self {
value.indent_info
}
}
#[cfg(test)]
mod tests {
use crate::Deindenter;
fn test(input: &str, expected: &str) {
let d = Deindenter::new(input).unwrap();
assert_eq!(d.to_string().unwrap(), expected);
}
const EXPECTED: &str = r#"impl From<Deindenter<'_>> for IndentInfo {
fn from(value: Deindenter) -> Self {
value.indent_info
}
}
"#;
#[test]
fn extra_whitespace_lines() {
let input = r#"
impl From<Deindenter<'_>> for IndentInfo {
fn from(value: Deindenter) -> Self {
value.indent_info
}
}
"#;
test(input, EXPECTED);
}
#[test]
fn noop() {
let input = r#"impl From<Deindenter<'_>> for IndentInfo {
fn from(value: Deindenter) -> Self {
value.indent_info
}
}
"#;
test(input, input);
}
#[test]
fn indented() {
let input = r#" impl From<Deindenter<'_>> for IndentInfo {
fn from(value: Deindenter) -> Self {
value.indent_info
}
}
"#;
test(input, EXPECTED);
}
#[test]
fn almost_indented() {
let input = r#"impl From<Deindenter<'_>> for IndentInfo {
fn from(value: Deindenter) -> Self {
value.indent_info
}
}
"#;
test(input, input);
}
#[test]
fn no_trailing_newline() {
let input = r#" impl From<Deindenter<'_>> for IndentInfo {
fn from(value: Deindenter) -> Self {
value.indent_info
}
}"#;
let expected = r#"impl From<Deindenter<'_>> for IndentInfo {
fn from(value: Deindenter) -> Self {
value.indent_info
}
}"#;
test(input, expected);
}
#[test]
fn multiple_paragraphs() {
let mut input = r#"
this is p1
this is p2"#
.to_owned();
let mut expected = r#"this is p1
this is p2"#
.to_owned();
test(&input, &expected);
input.push('\n');
expected.push('\n');
test(&input, &expected);
}
}