askama/filters/
urlencode.rs

1use std::convert::Infallible;
2use std::fmt;
3use std::fmt::Write;
4
5use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, utf8_percent_encode};
6
7use crate::filters::{FastWritable, HtmlSafeOutput};
8
9// Urlencode char encoding set. Only the characters in the unreserved set don't
10// have any special purpose in any part of a URI and can be safely left
11// unencoded as specified in https://tools.ietf.org/html/rfc3986.html#section-2.3
12const URLENCODE_STRICT_SET: &AsciiSet = &NON_ALPHANUMERIC
13    .remove(b'_')
14    .remove(b'.')
15    .remove(b'-')
16    .remove(b'~');
17
18// Same as URLENCODE_STRICT_SET, but preserves forward slashes for encoding paths
19const URLENCODE_SET: &AsciiSet = &URLENCODE_STRICT_SET.remove(b'/');
20
21/// Percent-encodes the argument for safe use in URI; does not encode `/`.
22///
23/// This should be safe for all parts of URI (paths segments, query keys, query
24/// values). In the rare case that the server can't deal with forward slashes in
25/// the query string, use [`urlencode_strict`], which encodes them as well.
26///
27/// Encodes all characters except ASCII letters, digits, and `_.-~/`. In other
28/// words, encodes all characters which are not in the unreserved set,
29/// as specified by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.3),
30/// with the exception of `/`.
31///
32/// ```none,ignore
33/// <a href="/metro{{ "/stations/Château d'Eau"|urlencode }}">Station</a>
34/// <a href="/page?text={{ "look, unicode/emojis ✨"|urlencode }}">Page</a>
35/// ```
36///
37/// To encode `/` as well, see [`urlencode_strict`](./fn.urlencode_strict.html).
38///
39/// [`urlencode_strict`]: ./fn.urlencode_strict.html
40///
41/// ```
42/// # #[cfg(feature = "code-in-doc")] {
43/// # use askama::Template;
44/// /// ```jinja
45/// /// <div>{{ example|urlencode }}</div>
46/// /// ```
47/// #[derive(Template)]
48/// #[template(ext = "html", in_doc = true)]
49/// struct Example<'a> {
50///     example: &'a str,
51/// }
52///
53/// assert_eq!(
54///     Example { example: "hello?world" }.to_string(),
55///     "<div>hello%3Fworld</div>"
56/// );
57/// # }
58/// ```
59#[inline]
60pub fn urlencode<T>(s: T) -> Result<HtmlSafeOutput<UrlencodeFilter<T>>, Infallible> {
61    Ok(HtmlSafeOutput(UrlencodeFilter(s, URLENCODE_SET)))
62}
63
64/// Percent-encodes the argument for safe use in URI; encodes `/`.
65///
66/// Use this filter for encoding query keys and values in the rare case that
67/// the server can't process them unencoded.
68///
69/// Encodes all characters except ASCII letters, digits, and `_.-~`. In other
70/// words, encodes all characters which are not in the unreserved set,
71/// as specified by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.3).
72///
73/// ```none,ignore
74/// <a href="/page?text={{ "look, unicode/emojis ✨"|urlencode_strict }}">Page</a>
75/// ```
76///
77/// If you want to preserve `/`, see [`urlencode`](./fn.urlencode.html).
78///
79/// ```
80/// # #[cfg(feature = "code-in-doc")] {
81/// # use askama::Template;
82/// /// ```jinja
83/// /// <a href='{{ example|urlencode_strict }}'>Example</a>
84/// /// ```
85/// #[derive(Template)]
86/// #[template(ext = "html", in_doc = true)]
87/// struct Example<'a> {
88///     example: &'a str,
89/// }
90///
91/// assert_eq!(
92///     Example { example: "/hello/world" }.to_string(),
93///     "<a href='%2Fhello%2Fworld'>Example</a>"
94/// );
95/// # }
96/// ```
97#[inline]
98pub fn urlencode_strict<T>(s: T) -> Result<HtmlSafeOutput<UrlencodeFilter<T>>, Infallible> {
99    Ok(HtmlSafeOutput(UrlencodeFilter(s, URLENCODE_STRICT_SET)))
100}
101
102pub struct UrlencodeFilter<T>(pub T, pub &'static AsciiSet);
103
104impl<T: fmt::Display> fmt::Display for UrlencodeFilter<T> {
105    #[inline]
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        write!(UrlencodeWriter(f, self.1), "{}", self.0)
108    }
109}
110
111impl<T: FastWritable> FastWritable for UrlencodeFilter<T> {
112    #[inline]
113    fn write_into<W: fmt::Write + ?Sized>(&self, f: &mut W) -> crate::Result<()> {
114        self.0.write_into(&mut UrlencodeWriter(f, self.1))
115    }
116}
117
118struct UrlencodeWriter<W>(W, &'static AsciiSet);
119
120impl<W: fmt::Write> fmt::Write for UrlencodeWriter<W> {
121    fn write_str(&mut self, s: &str) -> fmt::Result {
122        for s in utf8_percent_encode(s, self.1) {
123            self.0.write_str(s)?;
124        }
125        Ok(())
126    }
127}
128
129#[test]
130#[cfg(feature = "alloc")]
131fn test_urlencoding() {
132    use alloc::string::ToString;
133
134    // Unreserved (https://tools.ietf.org/html/rfc3986.html#section-2.3)
135    // alpha / digit
136    assert_eq!(urlencode("AZaz09").unwrap().to_string(), "AZaz09");
137    assert_eq!(urlencode_strict("AZaz09").unwrap().to_string(), "AZaz09");
138    // other
139    assert_eq!(urlencode("_.-~").unwrap().to_string(), "_.-~");
140    assert_eq!(urlencode_strict("_.-~").unwrap().to_string(), "_.-~");
141
142    // Reserved (https://tools.ietf.org/html/rfc3986.html#section-2.2)
143    // gen-delims
144    assert_eq!(
145        urlencode(":/?#[]@").unwrap().to_string(),
146        "%3A/%3F%23%5B%5D%40"
147    );
148    assert_eq!(
149        urlencode_strict(":/?#[]@").unwrap().to_string(),
150        "%3A%2F%3F%23%5B%5D%40"
151    );
152    // sub-delims
153    assert_eq!(
154        urlencode("!$&'()*+,;=").unwrap().to_string(),
155        "%21%24%26%27%28%29%2A%2B%2C%3B%3D"
156    );
157    assert_eq!(
158        urlencode_strict("!$&'()*+,;=").unwrap().to_string(),
159        "%21%24%26%27%28%29%2A%2B%2C%3B%3D"
160    );
161
162    // Other
163    assert_eq!(
164        urlencode("žŠďŤňĚáÉóŮ").unwrap().to_string(),
165        "%C5%BE%C5%A0%C4%8F%C5%A4%C5%88%C4%9A%C3%A1%C3%89%C3%B3%C5%AE"
166    );
167    assert_eq!(
168        urlencode_strict("žŠďŤňĚáÉóŮ").unwrap().to_string(),
169        "%C5%BE%C5%A0%C4%8F%C5%A4%C5%88%C4%9A%C3%A1%C3%89%C3%B3%C5%AE"
170    );
171
172    // Ferris
173    assert_eq!(urlencode("🦀").unwrap().to_string(), "%F0%9F%A6%80");
174    assert_eq!(urlencode_strict("🦀").unwrap().to_string(), "%F0%9F%A6%80");
175}