1use std::fmt;
26
27use crate::engine::{encode_loop, is_unicode_noncharacter};
28
29pub fn for_css_string(input: &str) -> String {
59 let mut out = String::with_capacity(input.len());
60 write_css_string(&mut out, input).expect("writing to string cannot fail");
61 out
62}
63
64pub fn write_css_string<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
68 encode_loop(out, input, needs_css_string_encoding, write_css_encoded)
69}
70
71fn needs_css_string_encoding(c: char) -> bool {
72 needs_css_common_encoding(c) || matches!(c, '(' | ')')
73}
74
75pub fn for_css_url(input: &str) -> String {
100 let mut out = String::with_capacity(input.len());
101 write_css_url(&mut out, input).expect("writing to string cannot fail");
102 out
103}
104
105pub fn write_css_url<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
109 encode_loop(out, input, needs_css_url_encoding, write_css_encoded)
110}
111
112fn needs_css_url_encoding(c: char) -> bool {
113 needs_css_string_encoding(c) || c == ' '
115}
116
117fn needs_css_common_encoding(c: char) -> bool {
118 let cp = c as u32;
119 cp <= 0x1F
120 || matches!(c, '"' | '\'' | '\\' | '<' | '&' | '/' | '>')
121 || (0x7F..=0x9F).contains(&cp) || cp == 0x2028
123 || cp == 0x2029
124 || (0x202A..=0x202E).contains(&cp) || (0x2066..=0x2069).contains(&cp) || is_unicode_noncharacter(cp)
127}
128
129fn write_css_encoded<W: fmt::Write>(out: &mut W, c: char, next: Option<char>) -> fmt::Result {
130 let cp = c as u32;
131
132 if is_unicode_noncharacter(cp) {
134 return out.write_char('_');
135 }
136
137 write!(out, "\\{:x}", cp)?;
139
140 if needs_css_separator(next) {
142 out.write_char(' ')?;
143 }
144
145 Ok(())
146}
147
148fn needs_css_separator(next: Option<char>) -> bool {
152 match next {
153 Some(c) => c.is_ascii_hexdigit() || matches!(c, ' ' | '\t' | '\n' | '\x0C' | '\r'),
154 None => true,
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 const TEXT_DIRECTION: [(&str, &str); 9] = [
164 ("\u{202A}", r"\202a"),
165 ("\u{202B}", r"\202b"),
166 ("\u{202C}", r"\202c"),
167 ("\u{202D}", r"\202d"),
168 ("\u{202E}", r"\202e"),
169 ("\u{2066}", r"\2066"),
170 ("\u{2067}", r"\2067"),
171 ("\u{2068}", r"\2068"),
172 ("\u{2069}", r"\2069"),
173 ];
174
175 #[test]
178 fn css_string_no_encoding_needed() {
179 assert_eq!(for_css_string("hello"), "hello");
180 assert_eq!(for_css_string(""), "");
181 }
182
183 #[test]
184 fn css_string_encodes_double_quote() {
185 assert_eq!(for_css_string(r#"a"b"#), r"a\22 b");
187 assert_eq!(for_css_string(r#"a""#), r"a\22 ");
189 }
190
191 #[test]
192 fn css_string_encodes_single_quote() {
193 assert_eq!(for_css_string("a'z"), r"a\27z");
195 assert_eq!(for_css_string("a'1"), r"a\27 1");
197 }
198
199 #[test]
200 fn css_string_encodes_backslash() {
201 assert_eq!(for_css_string(r"a\b"), r"a\5c b");
202 }
203
204 #[test]
205 fn css_string_encodes_angle_brackets() {
206 assert_eq!(for_css_string("<x>"), r"\3cx\3e ");
208 }
209
210 #[test]
211 fn css_string_encodes_ampersand() {
212 assert_eq!(for_css_string("a&b"), r"a\26 b");
213 }
214
215 #[test]
216 fn css_string_encodes_parens() {
217 assert_eq!(for_css_string("a(b)"), r"a\28 b\29 ");
218 }
219
220 #[test]
221 fn css_string_encodes_slash() {
222 assert_eq!(for_css_string("a/b"), r"a\2f b");
223 }
224
225 #[test]
226 fn css_string_encodes_control_chars() {
227 assert_eq!(for_css_string("\x00"), r"\0 ");
228 assert_eq!(for_css_string("\x01x"), r"\1x");
229 assert_eq!(for_css_string("\x1F"), r"\1f ");
230 }
231
232 #[test]
233 fn css_string_encodes_del() {
234 assert_eq!(for_css_string("\x7F"), r"\7f ");
235 }
236
237 #[test]
238 fn css_string_encodes_c1_controls() {
239 assert_eq!(for_css_string("\u{0080}"), r"\80 ");
240 assert_eq!(for_css_string("\u{0085}"), r"\85 ");
241 assert_eq!(for_css_string("\u{009F}"), r"\9f ");
242 assert_eq!(for_css_string("\u{0080}a"), r"\80 a");
244 assert_eq!(for_css_string("\u{0080}z"), r"\80z");
246 }
247
248 #[test]
249 fn css_string_encodes_line_separators() {
250 assert_eq!(for_css_string("\u{2028}"), r"\2028 ");
251 assert_eq!(for_css_string("\u{2029}"), r"\2029 ");
252 }
253
254 #[test]
255 fn css_string_encodes_text_direction_controls() {
256 for (raw, escaped) in TEXT_DIRECTION {
257 assert_eq!(for_css_string(&format!("a{raw}z")), format!("a{escaped}z"));
258 }
259 }
260
261 #[test]
262 fn css_string_text_direction_separator() {
263 for (raw, escaped) in TEXT_DIRECTION {
264 assert_eq!(for_css_string(&format!("{raw}b")), format!("{escaped} b"));
266 assert_eq!(for_css_string(&format!("{raw}z")), format!("{escaped}z"));
267 assert_eq!(for_css_string(raw), format!("{escaped} "));
268 }
269 }
270
271 #[test]
272 fn css_string_replaces_nonchars_with_underscore() {
273 assert_eq!(for_css_string("\u{FDD0}"), "_");
274 assert_eq!(for_css_string("\u{FFFE}"), "_");
275 assert_eq!(for_css_string("\u{FFFF}"), "_");
276 }
277
278 #[test]
279 fn css_string_separator_before_whitespace() {
280 assert_eq!(for_css_string("' "), r"\27 ");
283 }
284
285 #[test]
286 fn css_string_preserves_non_ascii() {
287 assert_eq!(for_css_string("café"), "café");
288 }
289
290 #[test]
291 fn css_string_writer_variant() {
292 let mut out = String::new();
293 write_css_string(&mut out, "a'b").unwrap();
295 assert_eq!(out, r"a\27 b");
296 }
297
298 #[test]
301 fn css_url_encodes_parens() {
302 assert_eq!(for_css_url("a(b)c"), r"a\28 b\29 c");
303 }
304
305 #[test]
306 fn css_url_encodes_space() {
307 assert_eq!(for_css_url("a b"), r"a\20 b");
308 assert_eq!(for_css_url(" "), r"\20 ");
309 }
310
311 #[test]
312 fn css_url_preserves_unicode_spaces() {
313 assert_eq!(for_css_url("a\u{a0}b"), "a\u{a0}b");
314 assert_eq!(for_css_url("a\u{2003}b"), "a\u{2003}b");
315 assert_eq!(for_css_url("a\u{3000}b"), "a\u{3000}b");
316 }
317
318 #[test]
319 fn css_url_encodes_quotes() {
320 assert_eq!(for_css_url("a'b"), r"a\27 b");
322 }
323
324 #[test]
325 fn css_url_encodes_backslash() {
326 assert_eq!(for_css_url(r"a\b"), r"a\5c b");
327 }
328
329 #[test]
330 fn css_url_encodes_c1_controls() {
331 assert_eq!(for_css_url("\u{0080}"), r"\80 ");
332 assert_eq!(for_css_url("\u{0085}"), r"\85 ");
333 assert_eq!(for_css_url("\u{009F}"), r"\9f ");
334 }
335
336 #[test]
337 fn css_url_encodes_text_direction_controls() {
338 for (raw, escaped) in TEXT_DIRECTION {
339 assert_eq!(for_css_url(&format!("a{raw}z")), format!("a{escaped}z"));
340 assert_eq!(for_css_url(&format!("{raw}b")), format!("{escaped} b"));
341 assert_eq!(for_css_url(raw), format!("{escaped} "));
342 }
343 }
344}