askama/filters/
alloc.rs

1use alloc::string::String;
2use core::fmt::{self, Write};
3
4use super::MAX_LEN;
5use super::escape::HtmlSafeOutput;
6use crate::Result;
7
8/// Return an ephemeral `&str` for `$src: impl fmt::Display`
9///
10/// If `$str` is `&str` or `String`, this macro simply passes on its content.
11/// If it is neither, then the formatted data is collection into `&buffer`.
12///
13/// `return`s with an error if the formatting failed.
14macro_rules! try_to_str {
15    ($src:expr => $buffer:ident) => {
16        match format_args!("{}", $src) {
17            args => {
18                if let Some(s) = args.as_str() {
19                    s
20                } else {
21                    $buffer = String::new();
22                    $buffer.write_fmt(args)?;
23                    &$buffer
24                }
25            }
26        }
27    };
28}
29
30/// Formats arguments according to the specified format
31///
32/// The *second* argument to this filter must be a string literal (as in normal
33/// Rust). The two arguments are passed through to the `format!()`
34/// [macro](https://doc.rust-lang.org/stable/std/macro.format.html) by
35/// the Askama code generator, but the order is swapped to support filter
36/// composition.
37///
38/// ```ignore
39/// {{ value|fmt("{:?}") }}
40/// ```
41///
42/// ```
43/// # #[cfg(feature = "code-in-doc")] {
44/// # use askama::Template;
45/// /// ```jinja
46/// /// <div>{{ value|fmt("{:?}") }}</div>
47/// /// ```
48/// #[derive(Template)]
49/// #[template(ext = "html", in_doc = true)]
50/// struct Example {
51///     value: (usize, usize),
52/// }
53///
54/// assert_eq!(
55///     Example { value: (3, 4) }.to_string(),
56///     "<div>(3, 4)</div>"
57/// );
58/// # }
59/// ```
60///
61/// Compare with [format](./fn.format.html).
62pub fn fmt() {}
63
64/// Formats arguments according to the specified format
65///
66/// The first argument to this filter must be a string literal (as in normal
67/// Rust). All arguments are passed through to the `format!()`
68/// [macro](https://doc.rust-lang.org/stable/std/macro.format.html) by
69/// the Askama code generator.
70///
71/// ```ignore
72/// {{ "{:?}{:?}"|format(value, other_value) }}
73/// ```
74///
75/// ```
76/// # #[cfg(feature = "code-in-doc")] {
77/// # use askama::Template;
78/// /// ```jinja
79/// /// <div>{{ "{:?}"|format(value) }}</div>
80/// /// ```
81/// #[derive(Template)]
82/// #[template(ext = "html", in_doc = true)]
83/// struct Example {
84///     value: (usize, usize),
85/// }
86///
87/// assert_eq!(
88///     Example { value: (3, 4) }.to_string(),
89///     "<div>(3, 4)</div>"
90/// );
91/// # }
92/// ```
93///
94/// Compare with [fmt](./fn.fmt.html).
95pub fn format() {}
96
97/// Replaces line breaks in plain text with appropriate HTML
98///
99/// A single newline becomes an HTML line break `<br>` and a new line
100/// followed by a blank line becomes a paragraph break `<p>`.
101///
102/// ```
103/// # #[cfg(feature = "code-in-doc")] {
104/// # use askama::Template;
105/// /// ```jinja
106/// /// <div>{{ example|linebreaks }}</div>
107/// /// ```
108/// #[derive(Template)]
109/// #[template(ext = "html", in_doc = true)]
110/// struct Example<'a> {
111///     example: &'a str,
112/// }
113///
114/// assert_eq!(
115///     Example { example: "Foo\nBar\n\nBaz" }.to_string(),
116///     "<div><p>Foo<br/>Bar</p><p>Baz</p></div>"
117/// );
118/// # }
119/// ```
120#[inline]
121pub fn linebreaks(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::Error> {
122    fn linebreaks(s: &str) -> String {
123        let linebroken = s.replace("\n\n", "</p><p>").replace('\n', "<br/>");
124        alloc::format!("<p>{linebroken}</p>")
125    }
126
127    let mut buffer;
128    Ok(HtmlSafeOutput(linebreaks(try_to_str!(s => buffer))))
129}
130
131/// Converts all newlines in a piece of plain text to HTML line breaks
132///
133/// ```
134/// # #[cfg(feature = "code-in-doc")] {
135/// # use askama::Template;
136/// /// ```jinja
137/// /// <div>{{ lines|linebreaksbr }}</div>
138/// /// ```
139/// #[derive(Template)]
140/// #[template(ext = "html", in_doc = true)]
141/// struct Example<'a> {
142///     lines: &'a str,
143/// }
144///
145/// assert_eq!(
146///     Example { lines: "a\nb\nc" }.to_string(),
147///     "<div>a<br/>b<br/>c</div>"
148/// );
149/// # }
150/// ```
151#[inline]
152pub fn linebreaksbr(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::Error> {
153    fn linebreaksbr(s: &str) -> String {
154        s.replace('\n', "<br/>")
155    }
156
157    let mut buffer;
158    Ok(HtmlSafeOutput(linebreaksbr(try_to_str!(s => buffer))))
159}
160
161/// Replaces only paragraph breaks in plain text with appropriate HTML
162///
163/// A new line followed by a blank line becomes a paragraph break `<p>`.
164/// Paragraph tags only wrap content; empty paragraphs are removed.
165/// No `<br/>` tags are added.
166///
167/// ```
168/// # #[cfg(feature = "code-in-doc")] {
169/// # use askama::Template;
170/// /// ```jinja
171/// /// {{ lines|paragraphbreaks }}
172/// /// ```
173/// #[derive(Template)]
174/// #[template(ext = "html", in_doc = true)]
175/// struct Example<'a> {
176///     lines: &'a str,
177/// }
178///
179/// assert_eq!(
180///     Example { lines: "Foo\nBar\n\nBaz" }.to_string(),
181///     "<p>Foo\nBar</p><p>Baz</p>"
182/// );
183/// # }
184/// ```
185#[inline]
186pub fn paragraphbreaks(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::Error> {
187    fn paragraphbreaks(s: &str) -> String {
188        let linebroken = s.replace("\n\n", "</p><p>").replace("<p></p>", "");
189        alloc::format!("<p>{linebroken}</p>")
190    }
191
192    let mut buffer;
193    Ok(HtmlSafeOutput(paragraphbreaks(try_to_str!(s => buffer))))
194}
195
196/// Converts to lowercase
197///
198/// ```
199/// # #[cfg(feature = "code-in-doc")] {
200/// # use askama::Template;
201/// /// ```jinja
202/// /// <div>{{ word|lower }}</div>
203/// /// ```
204/// #[derive(Template)]
205/// #[template(ext = "html", in_doc = true)]
206/// struct Example<'a> {
207///     word: &'a str,
208/// }
209///
210/// assert_eq!(
211///     Example { word: "FOO" }.to_string(),
212///     "<div>foo</div>"
213/// );
214///
215/// assert_eq!(
216///     Example { word: "FooBar" }.to_string(),
217///     "<div>foobar</div>"
218/// );
219/// # }
220/// ```
221#[inline]
222pub fn lower(s: impl fmt::Display) -> Result<String, fmt::Error> {
223    let mut buffer;
224    Ok(try_to_str!(s => buffer).to_lowercase())
225}
226
227/// Converts to lowercase, alias for the `|lower` filter
228///
229/// ```
230/// # #[cfg(feature = "code-in-doc")] {
231/// # use askama::Template;
232/// /// ```jinja
233/// /// <div>{{ word|lowercase }}</div>
234/// /// ```
235/// #[derive(Template)]
236/// #[template(ext = "html", in_doc = true)]
237/// struct Example<'a> {
238///     word: &'a str,
239/// }
240///
241/// assert_eq!(
242///     Example { word: "FOO" }.to_string(),
243///     "<div>foo</div>"
244/// );
245///
246/// assert_eq!(
247///     Example { word: "FooBar" }.to_string(),
248///     "<div>foobar</div>"
249/// );
250/// # }
251/// ```
252#[inline]
253pub fn lowercase(s: impl fmt::Display) -> Result<String, fmt::Error> {
254    lower(s)
255}
256
257/// Converts to uppercase
258///
259/// ```
260/// # #[cfg(feature = "code-in-doc")] {
261/// # use askama::Template;
262/// /// ```jinja
263/// /// <div>{{ word|upper }}</div>
264/// /// ```
265/// #[derive(Template)]
266/// #[template(ext = "html", in_doc = true)]
267/// struct Example<'a> {
268///     word: &'a str,
269/// }
270///
271/// assert_eq!(
272///     Example { word: "foo" }.to_string(),
273///     "<div>FOO</div>"
274/// );
275///
276/// assert_eq!(
277///     Example { word: "FooBar" }.to_string(),
278///     "<div>FOOBAR</div>"
279/// );
280/// # }
281/// ```
282#[inline]
283pub fn upper(s: impl fmt::Display) -> Result<String, fmt::Error> {
284    let mut buffer;
285    Ok(try_to_str!(s => buffer).to_uppercase())
286}
287
288/// Converts to uppercase, alias for the `|upper` filter
289///
290/// ```
291/// # #[cfg(feature = "code-in-doc")] {
292/// # use askama::Template;
293/// /// ```jinja
294/// /// <div>{{ word|uppercase }}</div>
295/// /// ```
296/// #[derive(Template)]
297/// #[template(ext = "html", in_doc = true)]
298/// struct Example<'a> {
299///     word: &'a str,
300/// }
301///
302/// assert_eq!(
303///     Example { word: "foo" }.to_string(),
304///     "<div>FOO</div>"
305/// );
306///
307/// assert_eq!(
308///     Example { word: "FooBar" }.to_string(),
309///     "<div>FOOBAR</div>"
310/// );
311/// # }
312/// ```
313#[inline]
314pub fn uppercase(s: impl fmt::Display) -> Result<String, fmt::Error> {
315    upper(s)
316}
317
318/// Strip leading and trailing whitespace
319///
320/// ```
321/// # #[cfg(feature = "code-in-doc")] {
322/// # use askama::Template;
323/// /// ```jinja
324/// /// <div>{{ example|trim }}</div>
325/// /// ```
326/// #[derive(Template)]
327/// #[template(ext = "html", in_doc = true)]
328/// struct Example<'a> {
329///     example: &'a str,
330/// }
331///
332/// assert_eq!(
333///     Example { example: " Hello\tworld\t" }.to_string(),
334///     "<div>Hello\tworld</div>"
335/// );
336/// # }
337/// ```
338#[cfg(feature = "alloc")]
339pub fn trim<T: fmt::Display>(s: T) -> Result<String> {
340    struct Collector(String);
341
342    impl fmt::Write for Collector {
343        fn write_str(&mut self, s: &str) -> fmt::Result {
344            match self.0.is_empty() {
345                true => self.0.write_str(s.trim_start()),
346                false => self.0.write_str(s),
347            }
348        }
349    }
350
351    let mut collector = Collector(String::new());
352    write!(collector, "{s}")?;
353    let Collector(mut s) = collector;
354    s.truncate(s.trim_end().len());
355    Ok(s)
356}
357
358/// Indent lines with `width` spaces
359///
360/// ```
361/// # #[cfg(feature = "code-in-doc")] {
362/// # use askama::Template;
363/// /// ```jinja
364/// /// <div>{{ example|indent(4) }}</div>
365/// /// ```
366/// #[derive(Template)]
367/// #[template(ext = "html", in_doc = true)]
368/// struct Example<'a> {
369///     example: &'a str,
370/// }
371///
372/// assert_eq!(
373///     Example { example: "hello\nfoo\nbar" }.to_string(),
374///     "<div>hello\n    foo\n    bar</div>"
375/// );
376/// # }
377/// ```
378#[inline]
379pub fn indent(s: impl fmt::Display, width: usize) -> Result<String, fmt::Error> {
380    fn indent(args: fmt::Arguments<'_>, width: usize) -> Result<String, fmt::Error> {
381        let mut buffer = String::new();
382        let s = if width >= MAX_LEN {
383            buffer.write_fmt(args)?;
384            return Ok(buffer);
385        } else if let Some(s) = args.as_str() {
386            if s.len() >= MAX_LEN {
387                return Ok(s.into());
388            } else {
389                s
390            }
391        } else {
392            buffer.write_fmt(args)?;
393            if buffer.len() >= MAX_LEN {
394                return Ok(buffer);
395            }
396            buffer.as_str()
397        };
398
399        let mut indented = String::new();
400        for (i, c) in s.char_indices() {
401            indented.push(c);
402
403            if c == '\n' && i < s.len() - 1 {
404                for _ in 0..width {
405                    indented.push(' ');
406                }
407            }
408        }
409        Ok(indented)
410    }
411    indent(format_args!("{s}"), width)
412}
413
414/// Capitalize a value. The first character will be uppercase, all others lowercase.
415///
416/// ```
417/// # #[cfg(feature = "code-in-doc")] {
418/// # use askama::Template;
419/// /// ```jinja
420/// /// <div>{{ example|capitalize }}</div>
421/// /// ```
422/// #[derive(Template)]
423/// #[template(ext = "html", in_doc = true)]
424/// struct Example<'a> {
425///     example: &'a str,
426/// }
427///
428/// assert_eq!(
429///     Example { example: "hello" }.to_string(),
430///     "<div>Hello</div>"
431/// );
432///
433/// assert_eq!(
434///     Example { example: "hElLO" }.to_string(),
435///     "<div>Hello</div>"
436/// );
437/// # }
438/// ```
439#[inline]
440pub fn capitalize(s: impl fmt::Display) -> Result<String, fmt::Error> {
441    fn capitalize(s: &str) -> Result<String, fmt::Error> {
442        let mut chars = s.chars();
443        if let Some(c) = chars.next() {
444            let mut replacement = String::with_capacity(s.len());
445            replacement.extend(c.to_uppercase());
446            replacement.push_str(&chars.as_str().to_lowercase());
447            Ok(replacement)
448        } else {
449            Ok(String::new())
450        }
451    }
452
453    let mut buffer;
454    capitalize(try_to_str!(s => buffer))
455}
456
457/// Count the words in that string.
458///
459/// ```
460/// # #[cfg(feature = "code-in-doc")] {
461/// # use askama::Template;
462/// /// ```jinja
463/// /// <div>{{ example|wordcount }}</div>
464/// /// ```
465/// #[derive(Template)]
466/// #[template(ext = "html", in_doc = true)]
467/// struct Example<'a> {
468///     example: &'a str,
469/// }
470///
471/// assert_eq!(
472///     Example { example: "askama is sort of cool" }.to_string(),
473///     "<div>5</div>"
474/// );
475/// # }
476/// ```
477pub fn wordcount(s: impl fmt::Display) -> Result<usize, fmt::Error> {
478    let mut buffer;
479    Ok(try_to_str!(s => buffer).split_whitespace().count())
480}
481
482/// Return a title cased version of the value. Words will start with uppercase letters, all
483/// remaining characters are lowercase.
484///
485/// ```
486/// # #[cfg(feature = "code-in-doc")] {
487/// # use askama::Template;
488/// /// ```jinja
489/// /// <div>{{ example|title }}</div>
490/// /// ```
491/// #[derive(Template)]
492/// #[template(ext = "html", in_doc = true)]
493/// struct Example<'a> {
494///     example: &'a str,
495/// }
496///
497/// assert_eq!(
498///     Example { example: "hello WORLD" }.to_string(),
499///     "<div>Hello World</div>"
500/// );
501/// # }
502/// ```
503pub fn title(s: impl fmt::Display) -> Result<String, fmt::Error> {
504    let mut buffer;
505    let s = try_to_str!(s => buffer);
506    let mut need_capitalization = true;
507
508    // Sadly enough, we can't mutate a string when iterating over its chars, likely because it could
509    // change the size of a char, "breaking" the char indices.
510    let mut output = String::with_capacity(s.len());
511    for c in s.chars() {
512        if c.is_whitespace() {
513            output.push(c);
514            need_capitalization = true;
515        } else if need_capitalization {
516            match c.is_uppercase() {
517                true => output.push(c),
518                false => output.extend(c.to_uppercase()),
519            }
520            need_capitalization = false;
521        } else {
522            match c.is_lowercase() {
523                true => output.push(c),
524                false => output.extend(c.to_lowercase()),
525            }
526        }
527    }
528    Ok(output)
529}
530
531#[cfg(test)]
532mod tests {
533    use alloc::string::ToString;
534
535    use super::*;
536
537    #[test]
538    fn test_linebreaks() {
539        assert_eq!(
540            linebreaks("Foo\nBar Baz").unwrap().to_string(),
541            "<p>Foo<br/>Bar Baz</p>"
542        );
543        assert_eq!(
544            linebreaks("Foo\nBar\n\nBaz").unwrap().to_string(),
545            "<p>Foo<br/>Bar</p><p>Baz</p>"
546        );
547    }
548
549    #[test]
550    fn test_linebreaksbr() {
551        assert_eq!(linebreaksbr("Foo\nBar").unwrap().to_string(), "Foo<br/>Bar");
552        assert_eq!(
553            linebreaksbr("Foo\nBar\n\nBaz").unwrap().to_string(),
554            "Foo<br/>Bar<br/><br/>Baz"
555        );
556    }
557
558    #[test]
559    fn test_paragraphbreaks() {
560        assert_eq!(
561            paragraphbreaks("Foo\nBar Baz").unwrap().to_string(),
562            "<p>Foo\nBar Baz</p>"
563        );
564        assert_eq!(
565            paragraphbreaks("Foo\nBar\n\nBaz").unwrap().to_string(),
566            "<p>Foo\nBar</p><p>Baz</p>"
567        );
568        assert_eq!(
569            paragraphbreaks("Foo\n\n\n\n\nBar\n\nBaz")
570                .unwrap()
571                .to_string(),
572            "<p>Foo</p><p>\nBar</p><p>Baz</p>"
573        );
574    }
575
576    #[test]
577    fn test_lower() {
578        assert_eq!(lower("Foo").unwrap().to_string(), "foo");
579        assert_eq!(lower("FOO").unwrap().to_string(), "foo");
580        assert_eq!(lower("FooBar").unwrap().to_string(), "foobar");
581        assert_eq!(lower("foo").unwrap().to_string(), "foo");
582    }
583
584    #[test]
585    fn test_upper() {
586        assert_eq!(upper("Foo").unwrap().to_string(), "FOO");
587        assert_eq!(upper("FOO").unwrap().to_string(), "FOO");
588        assert_eq!(upper("FooBar").unwrap().to_string(), "FOOBAR");
589        assert_eq!(upper("foo").unwrap().to_string(), "FOO");
590    }
591
592    #[test]
593    fn test_trim() {
594        assert_eq!(trim(" Hello\tworld\t").unwrap().to_string(), "Hello\tworld");
595    }
596
597    #[test]
598    fn test_indent() {
599        assert_eq!(indent("hello", 2).unwrap().to_string(), "hello");
600        assert_eq!(indent("hello\n", 2).unwrap().to_string(), "hello\n");
601        assert_eq!(indent("hello\nfoo", 2).unwrap().to_string(), "hello\n  foo");
602        assert_eq!(
603            indent("hello\nfoo\n bar", 4).unwrap().to_string(),
604            "hello\n    foo\n     bar"
605        );
606        assert_eq!(
607            indent("hello", 267_332_238_858).unwrap().to_string(),
608            "hello"
609        );
610    }
611
612    #[test]
613    fn test_capitalize() {
614        assert_eq!(capitalize("foo").unwrap().to_string(), "Foo".to_string());
615        assert_eq!(capitalize("f").unwrap().to_string(), "F".to_string());
616        assert_eq!(capitalize("fO").unwrap().to_string(), "Fo".to_string());
617        assert_eq!(capitalize("").unwrap().to_string(), String::new());
618        assert_eq!(capitalize("FoO").unwrap().to_string(), "Foo".to_string());
619        assert_eq!(
620            capitalize("foO BAR").unwrap().to_string(),
621            "Foo bar".to_string()
622        );
623        assert_eq!(
624            capitalize("äØÄÅÖ").unwrap().to_string(),
625            "Äøäåö".to_string()
626        );
627        assert_eq!(capitalize("ß").unwrap().to_string(), "SS".to_string());
628        assert_eq!(capitalize("ßß").unwrap().to_string(), "SSß".to_string());
629    }
630
631    #[test]
632    fn test_wordcount() {
633        assert_eq!(wordcount("").unwrap(), 0);
634        assert_eq!(wordcount(" \n\t").unwrap(), 0);
635        assert_eq!(wordcount("foo").unwrap(), 1);
636        assert_eq!(wordcount("foo bar").unwrap(), 2);
637        assert_eq!(wordcount("foo  bar").unwrap(), 2);
638    }
639
640    #[test]
641    fn test_title() {
642        assert_eq!(&title("").unwrap(), "");
643        assert_eq!(&title(" \n\t").unwrap(), " \n\t");
644        assert_eq!(&title("foo").unwrap(), "Foo");
645        assert_eq!(&title(" foo").unwrap(), " Foo");
646        assert_eq!(&title("foo bar").unwrap(), "Foo Bar");
647        assert_eq!(&title("foo  bar ").unwrap(), "Foo  Bar ");
648        assert_eq!(&title("fOO").unwrap(), "Foo");
649        assert_eq!(&title("fOo BaR").unwrap(), "Foo Bar");
650    }
651
652    #[test]
653    fn fuzzed_indent_filter() {
654        let s = "hello\nfoo\nbar".to_string().repeat(1024);
655        assert_eq!(indent(s.clone(), 4).unwrap().to_string(), s);
656    }
657}