contextual_encoder/
css.rs1use std::fmt;
23
24use crate::engine::{encode_loop, is_unicode_noncharacter};
25
26pub fn for_css_string(input: &str) -> String {
54 let mut out = String::with_capacity(input.len());
55 write_css_string(&mut out, input).expect("writing to string cannot fail");
56 out
57}
58
59pub fn write_css_string<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
63 encode_loop(out, input, needs_css_string_encoding, write_css_encoded)
64}
65
66fn needs_css_string_encoding(c: char) -> bool {
67 needs_css_common_encoding(c) || matches!(c, '(' | ')')
68}
69
70pub fn for_css_url(input: &str) -> String {
95 let mut out = String::with_capacity(input.len());
96 write_css_url(&mut out, input).expect("writing to string cannot fail");
97 out
98}
99
100pub fn write_css_url<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
104 encode_loop(out, input, needs_css_url_encoding, write_css_encoded)
105}
106
107fn needs_css_url_encoding(c: char) -> bool {
108 needs_css_string_encoding(c) || c == ' '
110}
111
112fn needs_css_common_encoding(c: char) -> bool {
113 let cp = c as u32;
114 cp <= 0x1F
115 || matches!(c, '"' | '\'' | '\\' | '<' | '&' | '/' | '>')
116 || (0x7F..=0x9F).contains(&cp) || cp == 0x2028
118 || cp == 0x2029
119 || is_unicode_noncharacter(cp)
120}
121
122fn write_css_encoded<W: fmt::Write>(out: &mut W, c: char, next: Option<char>) -> fmt::Result {
123 let cp = c as u32;
124
125 if is_unicode_noncharacter(cp) {
127 return out.write_char('_');
128 }
129
130 write!(out, "\\{:x}", cp)?;
132
133 if needs_css_separator(next) {
135 out.write_char(' ')?;
136 }
137
138 Ok(())
139}
140
141fn needs_css_separator(next: Option<char>) -> bool {
145 match next {
146 Some(c) => c.is_ascii_hexdigit() || matches!(c, ' ' | '\t' | '\n' | '\x0C' | '\r'),
147 None => true,
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
158 fn css_string_no_encoding_needed() {
159 assert_eq!(for_css_string("hello"), "hello");
160 assert_eq!(for_css_string(""), "");
161 }
162
163 #[test]
164 fn css_string_encodes_double_quote() {
165 assert_eq!(for_css_string(r#"a"b"#), r"a\22 b");
167 assert_eq!(for_css_string(r#"a""#), r"a\22 ");
169 }
170
171 #[test]
172 fn css_string_encodes_single_quote() {
173 assert_eq!(for_css_string("a'z"), r"a\27z");
175 assert_eq!(for_css_string("a'1"), r"a\27 1");
177 }
178
179 #[test]
180 fn css_string_encodes_backslash() {
181 assert_eq!(for_css_string(r"a\b"), r"a\5c b");
182 }
183
184 #[test]
185 fn css_string_encodes_angle_brackets() {
186 assert_eq!(for_css_string("<x>"), r"\3cx\3e ");
188 }
189
190 #[test]
191 fn css_string_encodes_ampersand() {
192 assert_eq!(for_css_string("a&b"), r"a\26 b");
193 }
194
195 #[test]
196 fn css_string_encodes_parens() {
197 assert_eq!(for_css_string("a(b)"), r"a\28 b\29 ");
198 }
199
200 #[test]
201 fn css_string_encodes_slash() {
202 assert_eq!(for_css_string("a/b"), r"a\2f b");
203 }
204
205 #[test]
206 fn css_string_encodes_control_chars() {
207 assert_eq!(for_css_string("\x00"), r"\0 ");
208 assert_eq!(for_css_string("\x01x"), r"\1x");
209 assert_eq!(for_css_string("\x1F"), r"\1f ");
210 }
211
212 #[test]
213 fn css_string_encodes_del() {
214 assert_eq!(for_css_string("\x7F"), r"\7f ");
215 }
216
217 #[test]
218 fn css_string_encodes_c1_controls() {
219 assert_eq!(for_css_string("\u{0080}"), r"\80 ");
220 assert_eq!(for_css_string("\u{0085}"), r"\85 ");
221 assert_eq!(for_css_string("\u{009F}"), r"\9f ");
222 assert_eq!(for_css_string("\u{0080}a"), r"\80 a");
224 assert_eq!(for_css_string("\u{0080}z"), r"\80z");
226 }
227
228 #[test]
229 fn css_string_encodes_line_separators() {
230 assert_eq!(for_css_string("\u{2028}"), r"\2028 ");
231 assert_eq!(for_css_string("\u{2029}"), r"\2029 ");
232 }
233
234 #[test]
235 fn css_string_replaces_nonchars_with_underscore() {
236 assert_eq!(for_css_string("\u{FDD0}"), "_");
237 assert_eq!(for_css_string("\u{FFFE}"), "_");
238 assert_eq!(for_css_string("\u{FFFF}"), "_");
239 }
240
241 #[test]
242 fn css_string_separator_before_whitespace() {
243 assert_eq!(for_css_string("' "), r"\27 ");
246 }
247
248 #[test]
249 fn css_string_preserves_non_ascii() {
250 assert_eq!(for_css_string("café"), "café");
251 }
252
253 #[test]
254 fn css_string_writer_variant() {
255 let mut out = String::new();
256 write_css_string(&mut out, "a'b").unwrap();
258 assert_eq!(out, r"a\27 b");
259 }
260
261 #[test]
264 fn css_url_encodes_parens() {
265 assert_eq!(for_css_url("a(b)c"), r"a\28 b\29 c");
266 }
267
268 #[test]
269 fn css_url_encodes_space() {
270 assert_eq!(for_css_url("a b"), r"a\20 b");
271 assert_eq!(for_css_url(" "), r"\20 ");
272 }
273
274 #[test]
275 fn css_url_preserves_unicode_spaces() {
276 assert_eq!(for_css_url("a\u{a0}b"), "a\u{a0}b");
277 assert_eq!(for_css_url("a\u{2003}b"), "a\u{2003}b");
278 assert_eq!(for_css_url("a\u{3000}b"), "a\u{3000}b");
279 }
280
281 #[test]
282 fn css_url_encodes_quotes() {
283 assert_eq!(for_css_url("a'b"), r"a\27 b");
285 }
286
287 #[test]
288 fn css_url_encodes_backslash() {
289 assert_eq!(for_css_url(r"a\b"), r"a\5c b");
290 }
291
292 #[test]
293 fn css_url_encodes_c1_controls() {
294 assert_eq!(for_css_url("\u{0080}"), r"\80 ");
295 assert_eq!(for_css_url("\u{0085}"), r"\85 ");
296 assert_eq!(for_css_url("\u{009F}"), r"\9f ");
297 }
298}