askama/filters/
indent.rs

1use core::convert::Infallible;
2use core::fmt::{self, Write};
3use core::ops::Deref;
4use core::pin::Pin;
5use core::str;
6
7use crate::FastWritable;
8
9/// Indent lines with spaces or a prefix.
10///
11/// The first line and blank lines are not indented by default.
12/// The filter has two optional [`bool`] arguments, `first` and `blank`, that can be set to `true`
13/// to indent the first and blank lines, resp.
14///
15/// ### Example of `indent` with spaces
16///
17/// ```
18/// # #[cfg(feature = "code-in-doc")] {
19/// # use askama::Template;
20/// /// ```jinja
21/// /// <div>{{ example|indent(4) }}</div>
22/// /// ```
23/// #[derive(Template)]
24/// #[template(ext = "html", in_doc = true)]
25/// struct Example<'a> {
26///     example: &'a str,
27/// }
28///
29/// assert_eq!(
30///     Example { example: "hello\nfoo\nbar" }.to_string(),
31///     "<div>hello\n    foo\n    bar</div>"
32/// );
33/// # }
34/// ```
35///
36/// ### Example of `indent` with prefix a custom prefix
37///
38/// ```
39/// # #[cfg(feature = "code-in-doc")] {
40/// # use askama::Template;
41/// /// ```jinja
42/// /// <div>{{ example|indent("$$$ ") }}</div>
43/// /// ```
44/// #[derive(Template)]
45/// #[template(ext = "html", in_doc = true)]
46/// struct Example<'a> {
47///     example: &'a str,
48/// }
49///
50/// assert_eq!(
51///     Example { example: "hello\nfoo\nbar" }.to_string(),
52///     "<div>hello\n$$$ foo\n$$$ bar</div>"
53/// );
54/// # }
55/// ```
56#[inline]
57pub fn indent<S, I: AsIndent>(
58    source: S,
59    indent: I,
60    first: bool,
61    blank: bool,
62) -> Result<Indent<S, I>, Infallible> {
63    Ok(Indent {
64        source,
65        indent,
66        first,
67        blank,
68    })
69}
70
71pub struct Indent<S, I> {
72    source: S,
73    indent: I,
74    first: bool,
75    blank: bool,
76}
77
78impl<S: fmt::Display, I: AsIndent> fmt::Display for Indent<S, I> {
79    fn fmt(&self, dest: &mut fmt::Formatter<'_>) -> fmt::Result {
80        let indent = self.indent.as_indent();
81        write!(
82            IndentWriter::new(dest, indent, self.first, self.blank),
83            "{}",
84            self.source
85        )?;
86        Ok(())
87    }
88}
89
90impl<S: FastWritable, I: AsIndent> FastWritable for Indent<S, I> {
91    fn write_into(
92        &self,
93        dest: &mut dyn fmt::Write,
94        values: &dyn crate::Values,
95    ) -> crate::Result<()> {
96        let indent = self.indent.as_indent();
97        self.source.write_into(
98            &mut IndentWriter::new(dest, indent, self.first, self.blank),
99            values,
100        )?;
101        Ok(())
102    }
103}
104
105struct IndentWriter<'a, W> {
106    dest: W,
107    indent: &'a str,
108    first: bool,
109    blank: bool,
110    is_new_line: bool,
111    is_first_line: bool,
112}
113
114impl<'a, W: fmt::Write> IndentWriter<'a, W> {
115    fn new(dest: W, indent: &'a str, first: bool, blank: bool) -> Self {
116        IndentWriter {
117            dest,
118            indent,
119            first,
120            blank,
121            is_new_line: true,
122            is_first_line: true,
123        }
124    }
125}
126
127impl<W: fmt::Write> fmt::Write for IndentWriter<'_, W> {
128    fn write_str(&mut self, s: &str) -> fmt::Result {
129        if self.indent.is_empty() {
130            return self.dest.write_str(s);
131        }
132
133        for line in s.split_inclusive('\n') {
134            if self.is_new_line {
135                if self.is_first_line {
136                    if self.first && (self.blank || !matches!(line, "\n" | "\r\n")) {
137                        self.dest.write_str(self.indent)?;
138                    }
139                    self.is_first_line = false;
140                } else if self.blank || !matches!(line, "\n" | "\r\n") {
141                    self.dest.write_str(self.indent)?;
142                }
143            }
144            self.dest.write_str(line)?;
145            self.is_new_line = line.ends_with('\n');
146        }
147        Ok(())
148    }
149}
150
151/// A prefix usable for indenting
152#[cfg_attr(
153    feature = "serde_json",
154    doc = "[prettified JSON data](super::json_pretty) and"
155)]
156/// [`|indent`](indent).
157///
158/// ```
159/// # use askama::filters::AsIndent;
160/// assert_eq!(4.as_indent(), "    ");
161/// assert_eq!(" -> ".as_indent(), " -> ");
162/// ```
163pub trait AsIndent {
164    /// Borrow `self` as prefix to use.
165    fn as_indent(&self) -> &str;
166}
167
168impl AsIndent for str {
169    #[inline]
170    fn as_indent(&self) -> &str {
171        self
172    }
173}
174
175#[cfg(feature = "alloc")]
176impl AsIndent for alloc::string::String {
177    #[inline]
178    fn as_indent(&self) -> &str {
179        self
180    }
181}
182
183impl AsIndent for usize {
184    #[inline]
185    fn as_indent(&self) -> &str {
186        spaces(*self)
187    }
188}
189
190impl AsIndent for core::num::Wrapping<usize> {
191    #[inline]
192    fn as_indent(&self) -> &str {
193        spaces(self.0)
194    }
195}
196
197impl AsIndent for core::num::NonZeroUsize {
198    #[inline]
199    fn as_indent(&self) -> &str {
200        spaces(self.get())
201    }
202}
203
204fn spaces(width: usize) -> &'static str {
205    const MAX_SPACES: usize = 16;
206    const SPACES: &str = match str::from_utf8(&[b' '; MAX_SPACES]) {
207        Ok(spaces) => spaces,
208        Err(_) => panic!(),
209    };
210
211    &SPACES[..width.min(SPACES.len())]
212}
213
214#[cfg(feature = "alloc")]
215impl<T: AsIndent + alloc::borrow::ToOwned + ?Sized> AsIndent for alloc::borrow::Cow<'_, T> {
216    #[inline]
217    fn as_indent(&self) -> &str {
218        T::as_indent(self)
219    }
220}
221
222crate::impl_for_ref! {
223    impl AsIndent for T {
224        #[inline]
225        fn as_indent(&self) -> &str {
226            <T>::as_indent(self)
227        }
228    }
229}
230
231impl<T> AsIndent for Pin<T>
232where
233    T: Deref,
234    <T as Deref>::Target: AsIndent,
235{
236    #[inline]
237    fn as_indent(&self) -> &str {
238        self.as_ref().get_ref().as_indent()
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use alloc::string::ToString;
245
246    use super::*;
247
248    #[test]
249    fn test_indent() {
250        assert_eq!(
251            indent("hello", 2, false, false).unwrap().to_string(),
252            "hello"
253        );
254        assert_eq!(
255            indent("hello\n", 2, false, false).unwrap().to_string(),
256            "hello\n"
257        );
258        assert_eq!(
259            indent("hello\nfoo", 2, false, false).unwrap().to_string(),
260            "hello\n  foo"
261        );
262        assert_eq!(
263            indent("hello\nfoo\n bar", 4, false, false)
264                .unwrap()
265                .to_string(),
266            "hello\n    foo\n     bar"
267        );
268        assert_eq!(
269            indent("hello", 267_332_238_858, false, false)
270                .unwrap()
271                .to_string(),
272            "hello"
273        );
274
275        assert_eq!(
276            indent("hello\n\n bar", 4, false, false)
277                .unwrap()
278                .to_string(),
279            "hello\n\n     bar"
280        );
281        assert_eq!(
282            indent("hello\n\n bar", 4, false, true).unwrap().to_string(),
283            "hello\n    \n     bar"
284        );
285        assert_eq!(
286            indent("hello\n\n bar", 4, true, false).unwrap().to_string(),
287            "    hello\n\n     bar"
288        );
289        assert_eq!(
290            indent("hello\n\n bar", 4, true, true).unwrap().to_string(),
291            "    hello\n    \n     bar"
292        );
293    }
294
295    #[test]
296    fn test_indent_str() {
297        assert_eq!(
298            indent("hello\n\n bar", "❗❓", false, false)
299                .unwrap()
300                .to_string(),
301            "hello\n\n❗❓ bar"
302        );
303        assert_eq!(
304            indent("hello\n\n bar", "❗❓", false, true)
305                .unwrap()
306                .to_string(),
307            "hello\n❗❓\n❗❓ bar"
308        );
309        assert_eq!(
310            indent("hello\n\n bar", "❗❓", true, false)
311                .unwrap()
312                .to_string(),
313            "❗❓hello\n\n❗❓ bar"
314        );
315        assert_eq!(
316            indent("hello\n\n bar", "❗❓", true, true)
317                .unwrap()
318                .to_string(),
319            "❗❓hello\n❗❓\n❗❓ bar"
320        );
321    }
322
323    #[test]
324    fn test_indent_chunked() {
325        #[derive(Clone, Copy)]
326        struct Chunked<'a>(&'a str);
327
328        impl<'a> fmt::Display for Chunked<'a> {
329            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330                for chunk in self.0.chars() {
331                    write!(f, "{chunk}")?;
332                }
333                Ok(())
334            }
335        }
336
337        assert_eq!(
338            indent(Chunked("hello"), 2, false, false)
339                .unwrap()
340                .to_string(),
341            "hello"
342        );
343        assert_eq!(
344            indent(Chunked("hello\n"), 2, false, false)
345                .unwrap()
346                .to_string(),
347            "hello\n"
348        );
349        assert_eq!(
350            indent(Chunked("hello\nfoo"), 2, false, false)
351                .unwrap()
352                .to_string(),
353            "hello\n  foo"
354        );
355        assert_eq!(
356            indent(Chunked("hello\nfoo\n bar"), 4, false, false)
357                .unwrap()
358                .to_string(),
359            "hello\n    foo\n     bar"
360        );
361        assert_eq!(
362            indent(Chunked("hello"), 267_332_238_858, false, false)
363                .unwrap()
364                .to_string(),
365            "hello"
366        );
367
368        assert_eq!(
369            indent(Chunked("hello\n\n bar"), 4, false, false)
370                .unwrap()
371                .to_string(),
372            "hello\n\n     bar"
373        );
374        assert_eq!(
375            indent(Chunked("hello\n\n bar"), 4, false, true)
376                .unwrap()
377                .to_string(),
378            "hello\n    \n     bar"
379        );
380        assert_eq!(
381            indent(Chunked("hello\n\n bar"), 4, true, false)
382                .unwrap()
383                .to_string(),
384            "    hello\n\n     bar"
385        );
386        assert_eq!(
387            indent(Chunked("hello\n\n bar"), 4, true, true)
388                .unwrap()
389                .to_string(),
390            "    hello\n    \n     bar"
391        );
392    }
393
394    #[test]
395    #[allow(clippy::arc_with_non_send_sync)] // it's only a test, it does not have to make sense
396    #[allow(clippy::type_complexity)] // it's only a test, it does not have to be pretty
397    fn test_indent_complicated() {
398        use std::borrow::ToOwned;
399        use std::boxed::Box;
400        use std::cell::{RefCell, RefMut};
401        use std::pin::Pin;
402        use std::rc::Rc;
403        use std::string::String;
404        use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockWriteGuard};
405
406        let prefix = Mutex::new(Box::pin("❗❓".to_owned()));
407        let prefix = RefCell::new(Arc::new(prefix.try_lock().unwrap()));
408        let prefix = RwLock::new(Rc::new(prefix.borrow_mut()));
409        let prefix: RwLockWriteGuard<'_, Rc<RefMut<'_, Arc<MutexGuard<'_, Pin<Box<String>>>>>>> =
410            prefix.try_write().unwrap();
411
412        assert_eq!(
413            indent("hello\n\n bar", &prefix, false, false)
414                .unwrap()
415                .to_string(),
416            "hello\n\n❗❓ bar"
417        );
418        assert_eq!(
419            indent("hello\n\n bar", &prefix, false, true)
420                .unwrap()
421                .to_string(),
422            "hello\n❗❓\n❗❓ bar"
423        );
424        assert_eq!(
425            indent("hello\n\n bar", &prefix, true, false)
426                .unwrap()
427                .to_string(),
428            "❗❓hello\n\n❗❓ bar"
429        );
430        assert_eq!(
431            indent("hello\n\n bar", &prefix, true, true)
432                .unwrap()
433                .to_string(),
434            "❗❓hello\n❗❓\n❗❓ bar"
435        );
436    }
437}