indent/
lib.rs

1// SPDX-FileCopyrightText: 2021 ilkecan <ilkecan@protonmail.com>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5//! These functions are useful for inserting a multiline string into an already indented context in
6//! another string.
7
8use std::borrow::Cow;
9
10/// Indents every line that is not empty by the given number of spaces, starting from the second
11/// line.
12///
13/// The first line of the string is not indented so that it can be placed after an introduction
14/// sequence that has already begun the line.
15///
16/// # Examples
17/// ```rust
18/// assert_eq!(format!("  items: {}", indent::indent_by(2, "[\n  foo,\n  bar,\n]\n")),
19/// "  items: [
20///     foo,
21///     bar,
22///   ]
23/// ")
24/// ```
25///
26/// For the version that also indents the first line, see [indent_all_by].
27pub fn indent_by<'a, S>(number_of_spaces: usize, input: S) -> String
28where
29    S: Into<Cow<'a, str>>,
30{
31    indent(" ".repeat(number_of_spaces), input, false)
32}
33
34/// Indents every line that is not empty with the given prefix, starting from the second line.
35///
36/// The first line of the string is not indented so that it can be placed after an introduction
37/// sequence that has already begun the line.
38///
39/// # Examples
40/// ```rust
41/// assert_eq!(format!("items:{}", indent::indent_with("- ", "\nfoo\nbar\n")),
42/// "items:
43/// - foo
44/// - bar
45/// ")
46/// ```
47///
48/// For the version that also indents the first line, see [indent_all_with].
49pub fn indent_with<'a, S, T>(prefix: S, input: T) -> String
50where
51    S: Into<Cow<'a, str>>,
52    T: Into<Cow<'a, str>>,
53{
54    indent(prefix, input, false)
55}
56
57/// Indents every line that is not empty by the given number of spaces.
58///
59/// # Examples
60/// ```rust
61/// assert_eq!(format!("items: [\n{}]\n", indent::indent_all_by(2, "foo,\nbar,\n")),
62/// "items: [
63///   foo,
64///   bar,
65/// ]
66/// ")
67/// ```
68///
69/// For the version that doesn't indent the first line, see [indent_by].
70pub fn indent_all_by<'a, S>(number_of_spaces: usize, input: S) -> String
71where
72    S: Into<Cow<'a, str>>,
73{
74    indent(" ".repeat(number_of_spaces), input, true)
75}
76
77/// Indents every line that is not empty with the given prefix.
78///
79/// # Examples
80/// ```rust
81/// assert_eq!(format!("items:\n{}", indent::indent_all_with("- ", "foo\nbar\n")),
82/// "items:
83/// - foo
84/// - bar
85/// ")
86/// ```
87///
88/// For the version that also indents the first line, see [indent_with].
89pub fn indent_all_with<'a, S, T>(prefix: S, input: T) -> String
90where
91    S: Into<Cow<'a, str>>,
92    T: Into<Cow<'a, str>>,
93{
94    indent(prefix, input, true)
95}
96
97fn indent<'a, S, T>(prefix: S, input: T, indent_all: bool) -> String
98where
99    S: Into<Cow<'a, str>>,
100    T: Into<Cow<'a, str>>,
101{
102    let prefix = prefix.into();
103    let input = input.into();
104    let length = input.len();
105    let mut output = String::with_capacity(length + length / 2);
106
107    for (i, line) in input.lines().enumerate() {
108        if i > 0 {
109            output.push('\n');
110
111            if !line.is_empty() {
112                output.push_str(&prefix);
113            }
114        } else if indent_all && !line.is_empty() {
115            output.push_str(&prefix);
116        }
117
118        output.push_str(line);
119    }
120
121    if input.ends_with('\n') {
122        output.push('\n');
123    }
124
125    output
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_single_line_without_newline() {
134        assert_eq!(indent_by(2, "foo"), "foo")
135    }
136
137    #[test]
138    fn test_single_line_with_newline() {
139        assert_eq!(indent_by(2, "foo\n"), "foo\n")
140    }
141
142    #[test]
143    fn test_multiline_without_newline() {
144        assert_eq!(
145            indent_by(
146                2, "
147foo
148bar"
149            ),
150            "
151  foo
152  bar"
153        )
154    }
155
156    #[test]
157    fn test_multiline_with_newline() {
158        assert_eq!(
159            indent_by(
160                2,
161                "
162foo
163bar
164"
165            ),
166            "
167  foo
168  bar
169"
170        )
171    }
172
173    #[test]
174    fn test_empty_line() {
175        assert_eq!(
176            indent_by(
177                2,
178                "
179foo
180
181bar
182"
183            ),
184            "
185  foo
186
187  bar
188"
189        )
190    }
191
192    #[test]
193    fn test_indent_all_by_empty_line() {
194        assert_eq!(
195            indent_all_by(
196                2,
197                "
198foo
199
200bar"
201            ),
202            "
203  foo
204
205  bar"
206        )
207    }
208
209    #[test]
210    fn test_indent_all_by() {
211        assert_eq!(
212            indent_all_by(
213                2, "foo
214
215bar"
216            ),
217            "  foo
218
219  bar"
220        )
221    }
222
223    #[test]
224    fn test_indent_with() {
225        assert_eq!(
226            indent_with(
227                "  ",
228                "
229foo
230
231bar
232"
233            ),
234            "
235  foo
236
237  bar
238"
239        )
240    }
241
242    #[test]
243    fn test_indent_all_with() {
244        assert_eq!(
245            indent_all_with(
246                "  ", "foo
247
248bar"
249            ),
250            "  foo
251
252  bar"
253        )
254    }
255}