askama/filters/
builtin.rs

1use core::cell::Cell;
2use core::convert::Infallible;
3use core::fmt::{self, Write};
4use core::ops::Deref;
5use core::pin::Pin;
6
7use super::MAX_LEN;
8use super::escape::FastWritable;
9use crate::{Error, Result};
10
11/// Limit string length, appends '...' if truncated
12///
13/// ```
14/// # #[cfg(feature = "code-in-doc")] {
15/// # use askama::Template;
16/// /// ```jinja
17/// /// <div>{{ example|truncate(2) }}</div>
18/// /// ```
19/// #[derive(Template)]
20/// #[template(ext = "html", in_doc = true)]
21/// struct Example<'a> {
22///     example: &'a str,
23/// }
24///
25/// assert_eq!(
26///     Example { example: "hello" }.to_string(),
27///     "<div>he...</div>"
28/// );
29/// # }
30/// ```
31#[inline]
32pub fn truncate<S: fmt::Display>(
33    source: S,
34    remaining: usize,
35) -> Result<TruncateFilter<S>, Infallible> {
36    Ok(TruncateFilter { source, remaining })
37}
38
39pub struct TruncateFilter<S> {
40    source: S,
41    remaining: usize,
42}
43
44impl<S: fmt::Display> fmt::Display for TruncateFilter<S> {
45    #[inline]
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        write!(TruncateWriter::new(f, self.remaining), "{}", self.source)
48    }
49}
50
51impl<S: FastWritable> FastWritable for TruncateFilter<S> {
52    #[inline]
53    fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
54        self.source
55            .write_into(&mut TruncateWriter::new(dest, self.remaining))
56    }
57}
58
59struct TruncateWriter<W> {
60    dest: Option<W>,
61    remaining: usize,
62}
63
64impl<W> TruncateWriter<W> {
65    fn new(dest: W, remaining: usize) -> Self {
66        TruncateWriter {
67            dest: Some(dest),
68            remaining,
69        }
70    }
71}
72
73impl<W: fmt::Write> fmt::Write for TruncateWriter<W> {
74    fn write_str(&mut self, s: &str) -> fmt::Result {
75        let Some(dest) = &mut self.dest else {
76            return Ok(());
77        };
78        let mut rem = self.remaining;
79        if rem >= s.len() {
80            dest.write_str(s)?;
81            self.remaining -= s.len();
82        } else {
83            if rem > 0 {
84                while !s.is_char_boundary(rem) {
85                    rem += 1;
86                }
87                if rem == s.len() {
88                    // Don't write "..." if the char bound extends to the end of string.
89                    self.remaining = 0;
90                    return dest.write_str(s);
91                }
92                dest.write_str(&s[..rem])?;
93            }
94            dest.write_str("...")?;
95            self.dest = None;
96        }
97        Ok(())
98    }
99
100    #[inline]
101    fn write_char(&mut self, c: char) -> fmt::Result {
102        match self.dest.is_some() {
103            true => self.write_str(c.encode_utf8(&mut [0; 4])),
104            false => Ok(()),
105        }
106    }
107
108    #[inline]
109    fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
110        match self.dest.is_some() {
111            true => fmt::write(self, args),
112            false => Ok(()),
113        }
114    }
115}
116
117/// Joins iterable into a string separated by provided argument
118///
119/// ```
120/// # #[cfg(feature = "code-in-doc")] {
121/// # use askama::Template;
122/// /// ```jinja
123/// /// <div>{{ example|join(", ") }}</div>
124/// /// ```
125/// #[derive(Template)]
126/// #[template(ext = "html", in_doc = true)]
127/// struct Example<'a> {
128///     example: &'a [&'a str],
129/// }
130///
131/// assert_eq!(
132///     Example { example: &["foo", "bar", "bazz"] }.to_string(),
133///     "<div>foo, bar, bazz</div>"
134/// );
135/// # }
136/// ```
137#[inline]
138pub fn join<I, S>(input: I, separator: S) -> Result<JoinFilter<I, S>, Infallible>
139where
140    I: IntoIterator,
141    I::Item: fmt::Display,
142    S: fmt::Display,
143{
144    Ok(JoinFilter(Cell::new(Some((input, separator)))))
145}
146
147/// Result of the filter [`join()`].
148///
149/// ## Note
150///
151/// This struct implements [`fmt::Display`], but only produces a string once.
152/// Any subsequent call to `.to_string()` will result in an empty string, because the iterator is
153/// already consumed.
154// The filter contains a [`Cell`], so we can modify iterator inside a method that takes `self` by
155// reference: [`fmt::Display::fmt()`] normally has the contract that it will produce the same result
156// in multiple invocations for the same object. We break this contract, because have to consume the
157// iterator, unless we want to enforce `I: Clone`, nor do we want to "memorize" the result of the
158// joined data.
159pub struct JoinFilter<I, S>(Cell<Option<(I, S)>>);
160
161impl<I, S> fmt::Display for JoinFilter<I, S>
162where
163    I: IntoIterator,
164    I::Item: fmt::Display,
165    S: fmt::Display,
166{
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        let Some((iter, separator)) = self.0.take() else {
169            return Ok(());
170        };
171        for (idx, token) in iter.into_iter().enumerate() {
172            match idx {
173                0 => f.write_fmt(format_args!("{token}"))?,
174                _ => f.write_fmt(format_args!("{separator}{token}"))?,
175            }
176        }
177        Ok(())
178    }
179}
180
181/// Centers the value in a field of a given width
182///
183/// ```
184/// # #[cfg(feature = "code-in-doc")] {
185/// # use askama::Template;
186/// /// ```jinja
187/// /// <div>-{{ example|center(5) }}-</div>
188/// /// ```
189/// #[derive(Template)]
190/// #[template(ext = "html", in_doc = true)]
191/// struct Example<'a> {
192///     example: &'a str,
193/// }
194///
195/// assert_eq!(
196///     Example { example: "a" }.to_string(),
197///     "<div>-  a  -</div>"
198/// );
199/// # }
200/// ```
201#[inline]
202pub fn center<T: fmt::Display>(src: T, width: usize) -> Result<Center<T>, Infallible> {
203    Ok(Center { src, width })
204}
205
206pub struct Center<T> {
207    src: T,
208    width: usize,
209}
210
211impl<T: fmt::Display> fmt::Display for Center<T> {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        if self.width < MAX_LEN {
214            write!(f, "{: ^1$}", self.src, self.width)
215        } else {
216            write!(f, "{}", self.src)
217        }
218    }
219}
220
221/// For a value of `±1` by default an empty string `""` is returned, otherwise `"s"`.
222///
223/// # Examples
224///
225/// ## With default arguments
226///
227/// ```
228/// # #[cfg(feature = "code-in-doc")] {
229/// # use askama::Template;
230/// /// ```jinja
231/// /// I have {{dogs}} dog{{dogs|pluralize}} and {{cats}} cat{{cats|pluralize}}.
232/// /// ```
233/// #[derive(Template)]
234/// #[template(ext = "html", in_doc = true)]
235/// struct Pets {
236///     dogs: i8,
237///     cats: i8,
238/// }
239///
240/// assert_eq!(
241///     Pets { dogs: 0, cats: 0 }.to_string(),
242///     "I have 0 dogs and 0 cats."
243/// );
244/// assert_eq!(
245///     Pets { dogs: 1, cats: 1 }.to_string(),
246///     "I have 1 dog and 1 cat."
247/// );
248/// assert_eq!(
249///     Pets { dogs: -1, cats: 99 }.to_string(),
250///     "I have -1 dog and 99 cats."
251/// );
252/// # }
253/// ```
254///
255/// ## Overriding the singular case
256///
257/// ```
258/// # #[cfg(feature = "code-in-doc")] {
259/// # use askama::Template;
260/// /// ```jinja
261/// /// I have {{dogs}} dog{{ dogs|pluralize("go") }}.
262/// /// ```
263/// #[derive(Template)]
264/// #[template(ext = "html", in_doc = true)]
265/// struct Dog {
266///     dogs: i8,
267/// }
268///
269/// assert_eq!(
270///     Dog { dogs: 0 }.to_string(),
271///     "I have 0 dogs."
272/// );
273/// assert_eq!(
274///     Dog { dogs: 1 }.to_string(),
275///     "I have 1 doggo."
276/// );
277/// # }
278/// ```
279///
280/// ## Overriding singular and plural cases
281///
282/// ```
283/// # #[cfg(feature = "code-in-doc")] {
284/// # use askama::Template;
285/// /// ```jinja
286/// /// I have {{mice}} {{ mice|pluralize("mouse", "mice") }}.
287/// /// ```
288/// #[derive(Template)]
289/// #[template(ext = "html", in_doc = true)]
290/// struct Mice {
291///     mice: i8,
292/// }
293///
294/// assert_eq!(
295///     Mice { mice: 42 }.to_string(),
296///     "I have 42 mice."
297/// );
298/// assert_eq!(
299///     Mice { mice: 1 }.to_string(),
300///     "I have 1 mouse."
301/// );
302/// # }
303/// ```
304///
305/// ## Arguments get escaped
306///
307/// ```
308/// # #[cfg(feature = "code-in-doc")] {
309/// # use askama::Template;
310/// /// ```jinja
311/// /// You are number {{ number|pluralize("<b>ONE</b>", number) }}!
312/// /// ```
313/// #[derive(Template)]
314/// #[template(ext = "html", in_doc = true)]
315/// struct Number {
316///     number: usize
317/// }
318///
319/// assert_eq!(
320///     Number { number: 1 }.to_string(),
321///     "You are number &#60;b&#62;ONE&#60;/b&#62;!",
322/// );
323/// assert_eq!(
324///     Number { number: 9000 }.to_string(),
325///     "You are number 9000!",
326/// );
327/// # }
328/// ```
329#[inline]
330pub fn pluralize<C, S, P>(count: C, singular: S, plural: P) -> Result<Pluralize<S, P>, C::Error>
331where
332    C: PluralizeCount,
333{
334    match count.is_singular()? {
335        true => Ok(Pluralize::Singular(singular)),
336        false => Ok(Pluralize::Plural(plural)),
337    }
338}
339
340/// An integer that can have the value `+1` and maybe `-1`.
341pub trait PluralizeCount {
342    /// A possible error that can occur while checking the value.
343    type Error: Into<Error>;
344
345    /// Returns `true` if and only if the value is `±1`.
346    fn is_singular(&self) -> Result<bool, Self::Error>;
347}
348
349const _: () = {
350    crate::impl_for_ref! {
351        impl PluralizeCount for T {
352            type Error = T::Error;
353
354            #[inline]
355            fn is_singular(&self) -> Result<bool, Self::Error> {
356                <T>::is_singular(self)
357            }
358        }
359    }
360
361    impl<T> PluralizeCount for Pin<T>
362    where
363        T: Deref,
364        <T as Deref>::Target: PluralizeCount,
365    {
366        type Error = <<T as Deref>::Target as PluralizeCount>::Error;
367
368        #[inline]
369        fn is_singular(&self) -> Result<bool, Self::Error> {
370            self.as_ref().get_ref().is_singular()
371        }
372    }
373
374    /// implement `PluralizeCount` for unsigned integer types
375    macro_rules! impl_pluralize_for_unsigned_int {
376        ($($ty:ty)*) => { $(
377            impl PluralizeCount for $ty {
378                type Error = Infallible;
379
380                #[inline]
381                fn is_singular(&self) -> Result<bool, Self::Error> {
382                    Ok(*self == 1)
383                }
384            }
385        )* };
386    }
387
388    impl_pluralize_for_unsigned_int!(u8 u16 u32 u64 u128 usize);
389
390    /// implement `PluralizeCount` for signed integer types
391    macro_rules! impl_pluralize_for_signed_int {
392        ($($ty:ty)*) => { $(
393            impl PluralizeCount for $ty {
394                type Error = Infallible;
395
396                #[inline]
397                fn is_singular(&self) -> Result<bool, Self::Error> {
398                    Ok(*self == 1 || *self == -1)
399                }
400            }
401        )* };
402    }
403
404    impl_pluralize_for_signed_int!(i8 i16 i32 i64 i128 isize);
405
406    /// implement `PluralizeCount` for non-zero integer types
407    macro_rules! impl_pluralize_for_non_zero {
408        ($($ty:ident)*) => { $(
409            impl PluralizeCount for core::num::$ty {
410                type Error = Infallible;
411
412                #[inline]
413                fn is_singular(&self) -> Result<bool, Self::Error> {
414                    self.get().is_singular()
415                }
416            }
417        )* };
418    }
419
420    impl_pluralize_for_non_zero! {
421        NonZeroI8 NonZeroI16 NonZeroI32 NonZeroI64 NonZeroI128 NonZeroIsize
422        NonZeroU8 NonZeroU16 NonZeroU32 NonZeroU64 NonZeroU128 NonZeroUsize
423    }
424};
425
426pub enum Pluralize<S, P> {
427    Singular(S),
428    Plural(P),
429}
430
431impl<S: fmt::Display, P: fmt::Display> fmt::Display for Pluralize<S, P> {
432    #[inline]
433    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
434        match self {
435            Pluralize::Singular(value) => write!(f, "{value}"),
436            Pluralize::Plural(value) => write!(f, "{value}"),
437        }
438    }
439}
440
441impl<S: FastWritable, P: FastWritable> FastWritable for Pluralize<S, P> {
442    #[inline]
443    fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
444        match self {
445            Pluralize::Singular(value) => value.write_into(dest),
446            Pluralize::Plural(value) => value.write_into(dest),
447        }
448    }
449}
450
451#[cfg(all(test, feature = "alloc"))]
452mod tests {
453    use alloc::string::{String, ToString};
454    use alloc::vec::Vec;
455
456    use super::*;
457
458    #[allow(clippy::needless_borrow)]
459    #[test]
460    fn test_join() {
461        assert_eq!(
462            join((&["hello", "world"]).iter(), ", ")
463                .unwrap()
464                .to_string(),
465            "hello, world"
466        );
467        assert_eq!(
468            join((&["hello"]).iter(), ", ").unwrap().to_string(),
469            "hello"
470        );
471
472        let empty: &[&str] = &[];
473        assert_eq!(join(empty.iter(), ", ").unwrap().to_string(), "");
474
475        let input: Vec<String> = alloc::vec!["foo".into(), "bar".into(), "bazz".into()];
476        assert_eq!(join(input.iter(), ":").unwrap().to_string(), "foo:bar:bazz");
477
478        let input: &[String] = &["foo".into(), "bar".into()];
479        assert_eq!(join(input.iter(), ":").unwrap().to_string(), "foo:bar");
480
481        let real: String = "blah".into();
482        let input: Vec<&str> = alloc::vec![&real];
483        assert_eq!(join(input.iter(), ";").unwrap().to_string(), "blah");
484
485        assert_eq!(
486            join((&&&&&["foo", "bar"]).iter(), ", ")
487                .unwrap()
488                .to_string(),
489            "foo, bar"
490        );
491    }
492
493    #[test]
494    fn test_center() {
495        assert_eq!(center("f", 3).unwrap().to_string(), " f ".to_string());
496        assert_eq!(center("f", 4).unwrap().to_string(), " f  ".to_string());
497        assert_eq!(center("foo", 1).unwrap().to_string(), "foo".to_string());
498        assert_eq!(
499            center("foo bar", 8).unwrap().to_string(),
500            "foo bar ".to_string()
501        );
502        assert_eq!(
503            center("foo", 111_669_149_696).unwrap().to_string(),
504            "foo".to_string()
505        );
506    }
507}