just/src/unindent.rs
Casey Rodarmor c625d61abd
Fix multibyte codepoint crash (#1243)
Co-authored-by: Evan Richter <evanjrichter@gmail.com>
2022-06-21 00:24:13 +00:00

135 lines
2.9 KiB
Rust

#[must_use]
pub fn unindent(text: &str) -> String {
// find line start and end indices
let mut lines = Vec::new();
let mut start = 0;
for (i, c) in text.char_indices() {
if c == '\n' || i == text.len() - c.len_utf8() {
let end = i + c.len_utf8();
lines.push(&text[start..end]);
start = end;
}
}
let common_indentation = lines
.iter()
.filter(|line| !blank(line))
.copied()
.map(indentation)
.fold(
None,
|common_indentation, line_indentation| match common_indentation {
Some(common_indentation) => Some(common(common_indentation, line_indentation)),
None => Some(line_indentation),
},
)
.unwrap_or("");
let mut replacements = Vec::with_capacity(lines.len());
for (i, line) in lines.iter().enumerate() {
let blank = blank(line);
let first = i == 0;
let last = i == lines.len() - 1;
let replacement = match (blank, first, last) {
(true, false, false) => "\n",
(true, _, _) => "",
(false, _, _) => &line[common_indentation.len()..],
};
replacements.push(replacement);
}
replacements.into_iter().collect()
}
fn indentation(line: &str) -> &str {
let i = line
.char_indices()
.take_while(|(_, c)| matches!(c, ' ' | '\t'))
.map(|(i, _)| i + 1)
.last()
.unwrap_or(0);
&line[..i]
}
fn blank(line: &str) -> bool {
line.chars().all(|c| matches!(c, ' ' | '\t' | '\r' | '\n'))
}
fn common<'s>(a: &'s str, b: &'s str) -> &'s str {
let i = a
.char_indices()
.zip(b.chars())
.take_while(|((_, ac), bc)| ac == bc)
.map(|((i, c), _)| i + c.len_utf8())
.last()
.unwrap_or(0);
&a[0..i]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unindents() {
assert_eq!(unindent("foo"), "foo");
assert_eq!(unindent("foo\nbar\nbaz\n"), "foo\nbar\nbaz\n");
assert_eq!(unindent(""), "");
assert_eq!(unindent(" foo\n bar"), "foo\nbar");
assert_eq!(unindent(" foo\n bar\n\n"), "foo\nbar\n");
assert_eq!(
unindent(
"
hello
bar
"
),
"hello\nbar\n"
);
assert_eq!(unindent("hello\n bar\n foo"), "hello\n bar\n foo");
assert_eq!(
unindent(
"
hello
bar
"
),
"\nhello\nbar\n\n"
);
}
#[test]
fn indentations() {
assert_eq!(indentation(""), "");
assert_eq!(indentation("foo"), "");
assert_eq!(indentation(" foo"), " ");
assert_eq!(indentation("\t\tfoo"), "\t\t");
assert_eq!(indentation("\t \t foo"), "\t \t ");
}
#[test]
fn blanks() {
assert!(blank(" \n"));
assert!(!blank(" foo\n"));
assert!(blank("\t\t\n"));
}
#[test]
fn commons() {
assert_eq!(common("foo", "foobar"), "foo");
assert_eq!(common("foo", "bar"), "");
assert_eq!(common("", ""), "");
assert_eq!(common("", "bar"), "");
}
}