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