fec979c2c6
- Combine and simplify string and backtick lexing. - Allow newlines in strings and backticks. - Add triple-delimited indented strings and backticks. Common indented literal non-blank line leading whitespace is stripped. - If a literal newline is escaped, it will be suppressed. - Backticks starting with `#!` are reserved for a future upgrade.
135 lines
2.8 KiB
Rust
135 lines
2.8 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 + 1;
|
|
lines.push(&text[start..end]);
|
|
start = end;
|
|
}
|
|
}
|
|
|
|
let common_indentation = lines
|
|
.iter()
|
|
.filter(|line| !blank(line))
|
|
.cloned()
|
|
.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"), "");
|
|
}
|
|
}
|