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