askama_filters/
lib.rs

1//! # Askama Filters
2//!
3//! A collection of additional template filters for the
4//! [Askama templating engine](https://github.com/djc/askama)
5//!
6//! ## Using
7//!
8//! This library is intended to be used alongside Askama and is
9//! effectively useless without it.
10//!
11//! Inside any module that is defining an Askama `Template`
12//! just `use askama_filters::filters;` and they will be available
13//! in the HTML template.
14//!
15//! If you wish to use this library in addition to your own filters
16//! create a module named `filters` and add `use askama_filters::filters::*`
17//! to it. Then import that module wherever you are creating `Template`s
18//! and both sets of filters should be available.
19//!
20//! A number of filters in this library return HTML markup.
21//! For security reasons these are not automatically considered safe
22//! and you most likely will want to chain them with the `safe` filter
23//! like so `var|markdown|safe`. Escaping inbetween is also recommeneded
24//! such as `var|e|md|safe`.
25//!
26//! ## Features
27//!
28//! There are a few optional features that can be enabled:
29//!
30//! - `markdown`: Markdown parsing using a filter.
31//! - `date`: Parse and format date/time strings.
32
33mod error;
34pub use crate::error::{Error, Result};
35
36#[cfg(feature = "markdown")]
37mod md;
38
39#[cfg(feature = "date")]
40mod date;
41
42pub mod filters {
43    //! The main collection of filters.
44    //! These filters do not require a feature flag.
45
46    use super::error::Result;
47    use regex as re;
48    use std::fmt;
49
50    #[cfg(feature = "markdown")]
51    pub use crate::md::*;
52
53    #[cfg(feature = "date")]
54    pub use crate::date::*;
55
56    /// Replaces straight quotes with curly quotes,
57    /// and turns `--` and `---` to en- and em-dash
58    /// and ... into an ellipsis.
59    pub fn smarty(s: &dyn fmt::Display) -> Result<String> {
60        Ok(s.to_string()
61            // Smart Quotes
62            .split_whitespace()
63            .map(|w| {
64                let mut w = String::from(w);
65                if w.starts_with('\'') {
66                    w = w.replacen('\'', "&lsquo;", 1);
67                } else if w.starts_with('"') {
68                    w = w.replacen('"', "&ldquo;", 1);
69                }
70                if w.ends_with('\'') {
71                    w.pop();
72                    w.push_str("&rsquo;");
73                } else if w.ends_with('"') {
74                    w.pop();
75                    w.push_str("&rdquo;");
76                }
77                w
78            })
79            .collect::<Vec<String>>()
80            .join(" ")
81            // Em-dash first
82            .replace("---", "&mdash;")
83            // then En-dash
84            .replace("--", "&ndash;")
85            // Ellipsis
86            .replace("...", "&#8230;"))
87    }
88
89    /// Converts text into title case.
90    pub fn title(s: &dyn fmt::Display) -> Result<String> {
91        // Taken from https://gist.github.com/jpastuszek/2704f3c5a3864b05c48ee688d0fd21d7#gistcomment-2125912
92        Ok(s.to_string()
93            .split_whitespace()
94            .map(|w| w.chars())
95            .map(|mut c| {
96                c.next()
97                    .into_iter()
98                    .flat_map(|c| c.to_uppercase())
99                    .chain(c.flat_map(|c| c.to_lowercase()))
100            })
101            .map(|c| c.collect::<String>())
102            .collect::<Vec<String>>()
103            .join(" "))
104    }
105
106    /// Expands links into `a` tags to the link
107    /// with the same text.
108    pub fn link(s: &dyn fmt::Display) -> Result<String> {
109        // Taken from https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
110        let r = re::Regex::new(
111            r"[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)",
112        )?;
113        Ok(s.to_string()
114            .split_whitespace()
115            .map(|w| {
116                if r.is_match(w) {
117                    format!("<a href=\"{}\">{}</a>", w, w)
118                } else {
119                    String::from(w)
120                }
121            })
122            .collect::<Vec<String>>()
123            .join(" "))
124    }
125
126    /// Applies a Regular Expression replacement to the text.
127    /// Will throw an error if the supplied regex string is invalid.
128    /// See the [`regex` crate docs](https://docs.rs/regex) for more info.
129    /// The second argument the regex should be a raw string like `r"\d+"`.
130    pub fn regex<S, R>(s: S, regex: R, replace: R) -> Result<String>
131    where
132        S: fmt::Display,
133        R: AsRef<str>,
134    {
135        let r = re::Regex::new(regex.as_ref())?;
136        Ok(String::from(
137            r.replace_all(&s.to_string(), replace.as_ref()),
138        ))
139    }
140
141    /// Replaces a prefix at the beginning of a word with
142    /// an expanded form. Useful in combination with other filters
143    /// or as a shorthand.
144    pub fn prefix<S, E>(s: S, prefix: E, expand_to: E) -> Result<String>
145    where
146        S: fmt::Display,
147        E: AsRef<str>,
148    {
149        Ok(s.to_string()
150            .split_whitespace()
151            .map(|w| {
152                if w.starts_with(prefix.as_ref()) {
153                    w.replace(prefix.as_ref(), expand_to.as_ref())
154                } else {
155                    String::from(w)
156                }
157            })
158            .collect())
159    }
160
161    /// Replaces a postfix at the end of a word
162    /// with an expanded form. See `prefix`.
163    pub fn postfix<S, E>(s: S, postfix: E, expand_to: E) -> Result<String>
164    where
165        S: fmt::Display,
166        E: AsRef<str>,
167    {
168        Ok(s.to_string()
169            .split_whitespace()
170            .map(|w| {
171                if w.ends_with(postfix.as_ref()) {
172                    w.replace(postfix.as_ref(), expand_to.as_ref())
173                } else {
174                    String::from(w)
175                }
176            })
177            .collect())
178    }
179
180    /// Similar to prefix but instead creates an `a` tag with
181    /// the matched word as the text and the expanded as the `href` with
182    /// the text appended.
183    /// Useful for things like #hashtags.
184    pub fn tag<S, E>(s: S, prefix: E, expand_to: E) -> Result<String>
185    where
186        S: fmt::Display,
187        E: AsRef<str>,
188    {
189        Ok(s.to_string()
190            .split_whitespace()
191            .map(|w| {
192                if w.starts_with(prefix.as_ref()) {
193                    format!(
194                        "<a href=\"{}\">{}</a>",
195                        w.replace(prefix.as_ref(), expand_to.as_ref()),
196                        w
197                    )
198                } else {
199                    String::from(w)
200                }
201            })
202            .collect())
203    }
204
205    /// Formats a slice or `Vec` into an ordered or unordered list.
206    /// The `ordered` param controls if it will be an `ol` or `ul` tag.
207    pub fn list<T, L>(list: L, ordered: bool) -> Result<String>
208    where
209        T: fmt::Display,
210        L: AsRef<[T]>,
211    {
212        let ltag = if ordered { "ol" } else { "ul" };
213        Ok(format!(
214            "<{}>{}</{}>",
215            ltag,
216            list.as_ref()
217                .iter()
218                .map(|e| { format!("<li>{}</li>", e) })
219                .collect::<String>(),
220            ltag
221        ))
222    }
223
224}
225
226#[cfg(test)]
227mod tests {
228    use super::filters;
229
230    #[test]
231    fn smarty() {
232        assert_eq!(
233            filters::smarty(&"This is a \"smart\" 'string' don't... -- --- ").unwrap(),
234            "This is a &ldquo;smart&rdquo; &lsquo;string&rsquo; don't&#8230; &ndash; &mdash;"
235        );
236    }
237
238    #[test]
239    fn title() {
240        assert_eq!(
241            filters::title(&"title CASED sTrInG").unwrap(),
242            "Title Cased String"
243        );
244    }
245
246    #[test]
247    fn link() {
248        assert_eq!(
249            filters::link(&"https://rust-lang.org").unwrap(),
250            "<a href=\"https://rust-lang.org\">https://rust-lang.org</a>"
251        );
252    }
253
254    #[test]
255    fn regex() {
256        assert_eq!(
257            filters::regex(&"Test 12345 String", r"\d+", "numbers").unwrap(),
258            "Test numbers String"
259        );
260    }
261
262    #[test]
263    fn prefix() {
264        assert_eq!(
265            filters::prefix("#world", "#", "hello ").unwrap(),
266            "hello world"
267        );
268    }
269
270    #[test]
271    fn postfix() {
272        assert_eq!(
273            filters::postfix("hello#", "#", " world").unwrap(),
274            "hello world"
275        );
276    }
277
278    #[test]
279    fn tag() {
280        assert_eq!(
281            filters::tag("#rust", "#", "https://rust-lang.org/").unwrap(),
282            "<a href=\"https://rust-lang.org/rust\">#rust</a>"
283        );
284    }
285
286    #[test]
287    fn list() {
288        assert_eq!(
289            filters::list(vec![1, 2, 3], false).unwrap(),
290            "<ul><li>1</li><li>2</li><li>3</li></ul>"
291        );
292    }
293}