typstyle_core/
utils.rs

1/// Strip trailing whitespace in each line of the input string.
2pub fn strip_trailing_whitespace(s: &str) -> String {
3    if s.is_empty() {
4        return "\n".to_string();
5    }
6    let mut res = String::with_capacity(s.len());
7    for line in s.lines() {
8        res.push_str(line.trim_end());
9        res.push('\n');
10    }
11    res
12}
13
14pub fn count_spaces_after_last_newline(s: &str, i: usize) -> usize {
15    // Ensure the byte position `i` is a valid UTF-8 boundary
16    debug_assert!(
17        s.is_char_boundary(i),
18        "Position i is not a valid UTF-8 boundary"
19    );
20
21    // Find the last newline (`\n`) before position `i`
22    if let Some(pos) = s[..i].rfind('\n') {
23        // Get the substring after the newline and up to position `i`
24        let after_newline = &s[pos + 1..i];
25        // Count the number of consecutive spaces in the substring
26        after_newline.chars().take_while(|&c| c == ' ').count()
27    } else {
28        // If no newline is found, return 0
29        0
30    }
31}
32
33/// Changes the indentation of a formatted string from one size to another.
34///
35/// This function converts space-only indentation from one size to another while
36/// preserving the relative indentation structure. Assumes input uses only spaces
37/// and indentation is always a multiple of the given indent size.
38///
39/// # Arguments
40/// - `text`: The input text with existing space indentation
41/// - `from_indent`: The current indentation size (e.g., 4 for 4 spaces)
42/// - `to_indent`: The desired indentation size (e.g., 2 for 2 spaces)
43///
44/// # Examples
45/// ```
46/// use typstyle_core::utils::change_indent;
47///
48/// let input = "    line1\n        line2\n    line3";
49/// let output = change_indent(input, 4, 2);
50/// assert_eq!(output, "  line1\n    line2\n  line3");
51/// ```
52pub fn change_indent(text: &str, from_indent: usize, to_indent: usize) -> String {
53    if text.is_empty() || from_indent == 0 || from_indent == to_indent {
54        return text.to_string();
55    }
56
57    let mut result = String::with_capacity(text.len());
58    let mut first = true;
59
60    for line in text.lines() {
61        if !first {
62            result.push('\n');
63        }
64        first = false;
65
66        let trimmed = line.trim_start();
67        if trimmed.is_empty() {
68            // Keep blank lines empty
69            continue;
70        } else {
71            let leading_spaces = line.len() - trimmed.len();
72            let indent_level = leading_spaces / from_indent;
73            let new_indent_size = to_indent * indent_level;
74
75            // Build new line with correct indentation
76            for _ in 0..new_indent_size {
77                result.push(' ');
78            }
79            result.push_str(trimmed);
80        }
81    }
82
83    result
84}
85
86/// Convenience function to change space indentation from 4 to 2 spaces
87pub fn indent_4_to_2(text: &str) -> String {
88    change_indent(text, 4, 2)
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn test_strip_trailing_whitespace() {
97        let s = strip_trailing_whitespace("");
98        assert_eq!(s, "\n");
99        let s = strip_trailing_whitespace(" ");
100        assert_eq!(s, "\n");
101        let s = strip_trailing_whitespace("\n");
102        assert_eq!(s, "\n");
103        let s = strip_trailing_whitespace(" \n - \n");
104        assert_eq!(s, "\n -\n");
105        let s = strip_trailing_whitespace(" \n - \n ");
106        assert_eq!(s, "\n -\n\n");
107    }
108
109    #[test]
110    fn test_change_indent_basic() {
111        let input = "    line1\n        line2\n    line3";
112        let output = change_indent(input, 4, 2);
113        assert_eq!(output, "  line1\n    line2\n  line3");
114    }
115
116    #[test]
117    fn test_change_indent_empty() {
118        assert_eq!(change_indent("", 4, 2), "");
119        assert_eq!(change_indent("   ", 4, 2), "");
120    }
121}