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