Skip to main content

formatparse_core/
indent_block.rs

1//! Strip common leading indentation from a captured block (GitHub issue #69).
2//!
3//! Used for ``{name:blk}`` after the same regex boundaries as ``:ml``. Blank lines and
4//! lines that contain only spaces or tabs do not contribute to the computed margin.
5//! Tabs count as single characters (not expanded). Output lines are joined with ``\\n``.
6
7/// Remove the largest common prefix of spaces/tabs from each line (see module docs).
8pub fn strip_common_indent(s: &str) -> String {
9    let lines: Vec<String> = s
10        .split('\n')
11        .map(|p| p.strip_suffix('\r').unwrap_or(p).to_string())
12        .collect();
13
14    let mut margin: Option<usize> = None;
15    for line in &lines {
16        if line.is_empty() {
17            continue;
18        }
19        if line.chars().all(|c| c == ' ' || c == '\t') {
20            continue;
21        }
22        let indent = line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
23        margin = Some(match margin {
24            None => indent,
25            Some(m) => m.min(indent),
26        });
27    }
28    let m = margin.unwrap_or(0);
29    if m == 0 {
30        return s.to_string();
31    }
32
33    let mut out = String::with_capacity(s.len());
34    for (i, line) in lines.iter().enumerate() {
35        if i > 0 {
36            out.push('\n');
37        }
38        let lead = line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
39        let rest: String = if lead >= m {
40            line.chars().skip(m).collect()
41        } else {
42            line.trim_start_matches([' ', '\t']).to_string()
43        };
44        out.push_str(&rest);
45    }
46    out.trim_start_matches('\n').to_string()
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn empty_string() {
55        assert_eq!(strip_common_indent(""), "");
56    }
57
58    #[test]
59    fn no_leading_indent_unchanged() {
60        assert_eq!(strip_common_indent("a\nb"), "a\nb");
61    }
62
63    #[test]
64    fn strips_common_spaces() {
65        assert_eq!(strip_common_indent("  a\n  b"), "a\nb");
66    }
67
68    #[test]
69    fn blank_lines_do_not_set_margin() {
70        assert_eq!(strip_common_indent("  a\n\n  b"), "a\n\nb");
71    }
72
73    #[test]
74    fn crlf_segments() {
75        assert_eq!(strip_common_indent("  a\r\n  b"), "a\nb");
76    }
77
78    #[test]
79    fn leading_newline_after_dedent_trimmed() {
80        assert_eq!(strip_common_indent("\n  a\n  b"), "a\nb");
81    }
82
83    #[test]
84    fn tabs_count_as_single_indent_chars() {
85        assert_eq!(strip_common_indent("\tx\n\ty"), "x\ny");
86    }
87}