1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
//! # Askama Filters
//!
//! A collection of additional template filters for the
//! [Askama templating engine](https://github.com/djc/askama)
//!
//! ## Using
//!
//! This library is intended to be used alongside Askama and is
//! effectively useless without it.
//!
//! Inside any module that is defining an Askama `Template`
//! just `use askama_filters::filters;` and they will be available
//! in the HTML template.
//!
//! If you wish to use this library in addition to your own filters
//! create a module named `filters` and add `use askama_filters::filters::*`
//! to it. Then import that module wherever you are creating `Template`s
//! and both sets of filters should be available.
//!
//! A number of filters in this library return HTML markup.
//! For security reasons these are not automatically considered safe
//! and you most likely will want to chain them with the `safe` filter
//! like so `var|markdown|safe`. Escaping inbetween is also recommeneded
//! such as `var|e|md|safe`.
//!
//! ## Features
//!
//! There are a few optional features that can be enabled:
//!
//! - `markdown`: Markdown parsing using a filter.
//! - `date`: Parse and format date/time strings.

mod error;
pub use crate::error::{Error, Result};

#[cfg(feature = "markdown")]
mod md;

#[cfg(feature = "date")]
mod date;

pub mod filters {
    //! The main collection of filters.
    //! These filters do not require a feature flag.

    use super::error::Result;
    use regex as re;
    use std::fmt;

    #[cfg(feature = "markdown")]
    pub use crate::md::*;

    #[cfg(feature = "date")]
    pub use crate::date::*;

    /// Replaces straight quotes with curly quotes,
    /// and turns `--` and `---` to en- and em-dash
    /// and ... into an ellipsis.
    pub fn smarty(s: &dyn fmt::Display) -> Result<String> {
        Ok(s.to_string()
            // Smart Quotes
            .split_whitespace()
            .map(|w| {
                let mut w = String::from(w);
                if w.starts_with('\'') {
                    w = w.replacen('\'', "&lsquo;", 1);
                } else if w.starts_with('"') {
                    w = w.replacen('"', "&ldquo;", 1);
                }
                if w.ends_with('\'') {
                    w.pop();
                    w.push_str("&rsquo;");
                } else if w.ends_with('"') {
                    w.pop();
                    w.push_str("&rdquo;");
                }
                w
            })
            .collect::<Vec<String>>()
            .join(" ")
            // Em-dash first
            .replace("---", "&mdash;")
            // then En-dash
            .replace("--", "&ndash;")
            // Ellipsis
            .replace("...", "&#8230;"))
    }

    /// Converts text into title case.
    pub fn title(s: &dyn fmt::Display) -> Result<String> {
        // Taken from https://gist.github.com/jpastuszek/2704f3c5a3864b05c48ee688d0fd21d7#gistcomment-2125912
        Ok(s.to_string()
            .split_whitespace()
            .map(|w| w.chars())
            .map(|mut c| {
                c.next()
                    .into_iter()
                    .flat_map(|c| c.to_uppercase())
                    .chain(c.flat_map(|c| c.to_lowercase()))
            })
            .map(|c| c.collect::<String>())
            .collect::<Vec<String>>()
            .join(" "))
    }

    /// Expands links into `a` tags to the link
    /// with the same text.
    pub fn link(s: &dyn fmt::Display) -> Result<String> {
        // Taken from https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
        let r = re::Regex::new(
            r"[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)",
        )?;
        Ok(s.to_string()
            .split_whitespace()
            .map(|w| {
                if r.is_match(w) {
                    format!("<a href=\"{}\">{}</a>", w, w)
                } else {
                    String::from(w)
                }
            })
            .collect::<Vec<String>>()
            .join(" "))
    }

    /// Applies a Regular Expression replacement to the text.
    /// Will throw an error if the supplied regex string is invalid.
    /// See the [`regex` crate docs](https://docs.rs/regex) for more info.
    /// The second argument the regex should be a raw string like `r"\d+"`.
    pub fn regex<S, R>(s: S, regex: R, replace: R) -> Result<String>
    where
        S: fmt::Display,
        R: AsRef<str>,
    {
        let r = re::Regex::new(regex.as_ref())?;
        Ok(String::from(
            r.replace_all(&s.to_string(), replace.as_ref()),
        ))
    }

    /// Replaces a prefix at the beginning of a word with
    /// an expanded form. Useful in combination with other filters
    /// or as a shorthand.
    pub fn prefix<S, E>(s: S, prefix: E, expand_to: E) -> Result<String>
    where
        S: fmt::Display,
        E: AsRef<str>,
    {
        Ok(s.to_string()
            .split_whitespace()
            .map(|w| {
                if w.starts_with(prefix.as_ref()) {
                    w.replace(prefix.as_ref(), expand_to.as_ref())
                } else {
                    String::from(w)
                }
            })
            .collect())
    }

    /// Replaces a postfix at the end of a word
    /// with an expanded form. See `prefix`.
    pub fn postfix<S, E>(s: S, postfix: E, expand_to: E) -> Result<String>
    where
        S: fmt::Display,
        E: AsRef<str>,
    {
        Ok(s.to_string()
            .split_whitespace()
            .map(|w| {
                if w.ends_with(postfix.as_ref()) {
                    w.replace(postfix.as_ref(), expand_to.as_ref())
                } else {
                    String::from(w)
                }
            })
            .collect())
    }

    /// Similar to prefix but instead creates an `a` tag with
    /// the matched word as the text and the expanded as the `href` with
    /// the text appended.
    /// Useful for things like #hashtags.
    pub fn tag<S, E>(s: S, prefix: E, expand_to: E) -> Result<String>
    where
        S: fmt::Display,
        E: AsRef<str>,
    {
        Ok(s.to_string()
            .split_whitespace()
            .map(|w| {
                if w.starts_with(prefix.as_ref()) {
                    format!(
                        "<a href=\"{}\">{}</a>",
                        w.replace(prefix.as_ref(), expand_to.as_ref()),
                        w
                    )
                } else {
                    String::from(w)
                }
            })
            .collect())
    }

    /// Formats a slice or `Vec` into an ordered or unordered list.
    /// The `ordered` param controls if it will be an `ol` or `ul` tag.
    pub fn list<T, L>(list: L, ordered: bool) -> Result<String>
    where
        T: fmt::Display,
        L: AsRef<[T]>,
    {
        let ltag = if ordered { "ol" } else { "ul" };
        Ok(format!(
            "<{}>{}</{}>",
            ltag,
            list.as_ref()
                .iter()
                .map(|e| { format!("<li>{}</li>", e) })
                .collect::<String>(),
            ltag
        ))
    }

}

#[cfg(test)]
mod tests {
    use super::filters;

    #[test]
    fn smarty() {
        assert_eq!(
            filters::smarty(&"This is a \"smart\" 'string' don't... -- --- ").unwrap(),
            "This is a &ldquo;smart&rdquo; &lsquo;string&rsquo; don't&#8230; &ndash; &mdash;"
        );
    }

    #[test]
    fn title() {
        assert_eq!(
            filters::title(&"title CASED sTrInG").unwrap(),
            "Title Cased String"
        );
    }

    #[test]
    fn link() {
        assert_eq!(
            filters::link(&"https://rust-lang.org").unwrap(),
            "<a href=\"https://rust-lang.org\">https://rust-lang.org</a>"
        );
    }

    #[test]
    fn regex() {
        assert_eq!(
            filters::regex(&"Test 12345 String", r"\d+", "numbers").unwrap(),
            "Test numbers String"
        );
    }

    #[test]
    fn prefix() {
        assert_eq!(
            filters::prefix("#world", "#", "hello ").unwrap(),
            "hello world"
        );
    }

    #[test]
    fn postfix() {
        assert_eq!(
            filters::postfix("hello#", "#", " world").unwrap(),
            "hello world"
        );
    }

    #[test]
    fn tag() {
        assert_eq!(
            filters::tag("#rust", "#", "https://rust-lang.org/").unwrap(),
            "<a href=\"https://rust-lang.org/rust\">#rust</a>"
        );
    }

    #[test]
    fn list() {
        assert_eq!(
            filters::list(vec![1, 2, 3], false).unwrap(),
            "<ul><li>1</li><li>2</li><li>3</li></ul>"
        );
    }
}