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 {
84 let mut out = String::with_capacity(input.len());
85 write_css_url(&mut out, input).expect("writing to string cannot fail");
86 out
87}
88
89pub fn write_css_url<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
93 encode_loop(out, input, needs_css_url_encoding, write_css_encoded)
94}
95
96fn needs_css_url_encoding(c: char) -> bool {
97 needs_css_common_encoding(c)
98 }
100
101fn needs_css_common_encoding(c: char) -> bool {
102 let cp = c as u32;
103 cp <= 0x1F
104 || matches!(c, '"' | '\'' | '\\' | '<' | '&' | '/' | '>')
105 || (0x7F..=0x9F).contains(&cp) || cp == 0x2028
107 || cp == 0x2029
108 || is_unicode_noncharacter(cp)
109}
110
111fn write_css_encoded<W: fmt::Write>(out: &mut W, c: char, next: Option<char>) -> fmt::Result {
112 let cp = c as u32;
113
114 if is_unicode_noncharacter(cp) {
116 return out.write_char('_');
117 }
118
119 write!(out, "\\{:x}", cp)?;
121
122 if needs_css_separator(next) {
124 out.write_char(' ')?;
125 }
126
127 Ok(())
128}
129
130fn needs_css_separator(next: Option<char>) -> bool {
133 match next {
134 Some(c) => c.is_ascii_hexdigit() || matches!(c, ' ' | '\t' | '\n' | '\x0C' | '\r'),
135 None => false,
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
146 fn css_string_no_encoding_needed() {
147 assert_eq!(for_css_string("hello"), "hello");
148 assert_eq!(for_css_string(""), "");
149 }
150
151 #[test]
152 fn css_string_encodes_double_quote() {
153 assert_eq!(for_css_string(r#"a"b"#), r"a\22 b");
155 assert_eq!(for_css_string(r#"a""#), r"a\22");
157 }
158
159 #[test]
160 fn css_string_encodes_single_quote() {
161 assert_eq!(for_css_string("a'z"), r"a\27z");
163 assert_eq!(for_css_string("a'1"), r"a\27 1");
165 }
166
167 #[test]
168 fn css_string_encodes_backslash() {
169 assert_eq!(for_css_string(r"a\b"), r"a\5c b");
170 }
171
172 #[test]
173 fn css_string_encodes_angle_brackets() {
174 assert_eq!(for_css_string("<x>"), r"\3cx\3e");
176 }
177
178 #[test]
179 fn css_string_encodes_ampersand() {
180 assert_eq!(for_css_string("a&b"), r"a\26 b");
181 }
182
183 #[test]
184 fn css_string_encodes_parens() {
185 assert_eq!(for_css_string("a(b)"), r"a\28 b\29");
186 }
187
188 #[test]
189 fn css_string_encodes_slash() {
190 assert_eq!(for_css_string("a/b"), r"a\2f b");
191 }
192
193 #[test]
194 fn css_string_encodes_control_chars() {
195 assert_eq!(for_css_string("\x00"), r"\0");
196 assert_eq!(for_css_string("\x01x"), r"\1x");
197 assert_eq!(for_css_string("\x1F"), r"\1f");
198 }
199
200 #[test]
201 fn css_string_encodes_del() {
202 assert_eq!(for_css_string("\x7F"), r"\7f");
203 }
204
205 #[test]
206 fn css_string_encodes_c1_controls() {
207 assert_eq!(for_css_string("\u{0080}"), r"\80");
208 assert_eq!(for_css_string("\u{0085}"), r"\85");
209 assert_eq!(for_css_string("\u{009F}"), r"\9f");
210 assert_eq!(for_css_string("\u{0080}a"), r"\80 a");
212 assert_eq!(for_css_string("\u{0080}z"), r"\80z");
214 }
215
216 #[test]
217 fn css_string_encodes_line_separators() {
218 assert_eq!(for_css_string("\u{2028}"), r"\2028");
219 assert_eq!(for_css_string("\u{2029}"), r"\2029");
220 }
221
222 #[test]
223 fn css_string_replaces_nonchars_with_underscore() {
224 assert_eq!(for_css_string("\u{FDD0}"), "_");
225 assert_eq!(for_css_string("\u{FFFE}"), "_");
226 assert_eq!(for_css_string("\u{FFFF}"), "_");
227 }
228
229 #[test]
230 fn css_string_separator_before_whitespace() {
231 assert_eq!(for_css_string("' "), r"\27 ");
234 }
235
236 #[test]
237 fn css_string_preserves_non_ascii() {
238 assert_eq!(for_css_string("café"), "café");
239 }
240
241 #[test]
242 fn css_string_writer_variant() {
243 let mut out = String::new();
244 write_css_string(&mut out, "a'b").unwrap();
246 assert_eq!(out, r"a\27 b");
247 }
248
249 #[test]
252 fn css_url_does_not_encode_parens() {
253 assert_eq!(for_css_url("a(b)c"), "a(b)c");
254 }
255
256 #[test]
257 fn css_url_encodes_quotes() {
258 assert_eq!(for_css_url("a'b"), r"a\27 b");
260 }
261
262 #[test]
263 fn css_url_encodes_backslash() {
264 assert_eq!(for_css_url(r"a\b"), r"a\5c b");
265 }
266
267 #[test]
268 fn css_url_encodes_c1_controls() {
269 assert_eq!(for_css_url("\u{0080}"), r"\80");
270 assert_eq!(for_css_url("\u{0085}"), r"\85");
271 assert_eq!(for_css_url("\u{009F}"), r"\9f");
272 }
273}