stylex_css/values/parser.rs
1use cssparser::{
2 ParseError, Parser, ParserInput, SourcePosition, Token, serialize_identifier, serialize_string,
3};
4use stylex_macros::{stylex_panic, stylex_unimplemented, stylex_unreachable};
5
6pub fn format_ident(ident: &str) -> String {
7 let mut res: String = String::default();
8 let _ = serialize_identifier(ident, &mut res);
9 res = res.trim_end().to_string();
10 res
11}
12
13pub fn _format_quoted_string(string: &str) -> String {
14 let mut res: String = String::default();
15 let _ = serialize_string(string, &mut res);
16 res
17}
18
19// const CSS_PROPS_WITH_IMAGE_URLS: &[&str] = &[
20// // Universal
21// "background",
22// "background-image",
23// "border-image",
24// "border-image-source",
25// "content",
26// "cursor",
27// "list-style",
28// "list-style-image",
29// "mask",
30// "mask-image",
31// // Specific to @counter-style
32// "additive-symbols",
33// "negative",
34// "pad",
35// "prefix",
36// "suffix",
37// "symbols",
38// ];
39
40// pub fn is_image_url_prop(prop_name: &str) -> bool {
41// CSS_PROPS_WITH_IMAGE_URLS
42// .iter()
43// .find(|p| prop_name.eq_ignore_ascii_case(p))
44// .is_some()
45// }
46
47pub fn parse_css_inner<'a>(
48 parser: &mut Parser,
49 rule_name: &str,
50 prop_name: &str,
51) -> Result<Vec<String>, ParseError<'a, Vec<String>>> {
52 let mut result: Vec<String> = vec![];
53
54 let mut curr_rule: String = rule_name.to_string();
55 let mut curr_prop: String = prop_name.to_string();
56 let mut token: &Token;
57 let mut token_offset: SourcePosition;
58
59 loop {
60 let mut iter_result: String = String::default();
61
62 token_offset = parser.position();
63 token = match parser.next_including_whitespace_and_comments() {
64 Ok(token) => token,
65 Err(_) => {
66 break;
67 },
68 };
69
70 match *token {
71 Token::Comment(_) => {
72 let token_slice = parser.slice_from(token_offset);
73 iter_result.push_str(token_slice);
74 },
75 Token::Semicolon => iter_result.push(';'),
76 Token::Colon => iter_result.push(':'),
77 Token::Comma => iter_result.push(','),
78 Token::ParenthesisBlock | Token::SquareBracketBlock | Token::CurlyBracketBlock => {
79 // if options.no_fonts && curr_rule == "font-face" {
80 // continue;
81 // }
82
83 let closure: &str;
84 if token == &Token::ParenthesisBlock {
85 iter_result.push('(');
86 closure = ")";
87 } else if token == &Token::SquareBracketBlock {
88 iter_result.push('[');
89 closure = "]";
90 } else {
91 iter_result.push('{');
92 closure = "}";
93 }
94
95 let block_css: Vec<String> = match parser.parse_nested_block(|parser| {
96 parse_css_inner(
97 // cache,
98 // client,
99 // document_url,
100 parser,
101 // options,
102 // depth,
103 rule_name,
104 curr_prop.as_str(),
105 // func_name,
106 )
107 }) {
108 Ok(css) => css,
109 Err(e) => stylex_panic!("Failed to parse nested CSS block: {:?}", e),
110 };
111
112 iter_result.push_str(join_css(&block_css).as_str());
113
114 iter_result.push_str(closure);
115 },
116 Token::CloseParenthesis => iter_result.push(')'),
117 Token::CloseSquareBracket => iter_result.push(']'),
118 Token::CloseCurlyBracket => iter_result.push('}'),
119 Token::IncludeMatch => iter_result.push_str("~="),
120 Token::DashMatch => iter_result.push_str("|="),
121 Token::PrefixMatch => iter_result.push_str("^="),
122 Token::SuffixMatch => iter_result.push_str("$="),
123 Token::SubstringMatch => iter_result.push_str("*="),
124 Token::CDO => iter_result.push_str("<!--"),
125 Token::CDC => iter_result.push_str("-->"),
126 Token::WhiteSpace(value) => {
127 iter_result.push_str(value);
128 },
129 // div...
130 Token::Ident(ref value) => {
131 curr_rule = String::default();
132 curr_prop = value.to_string();
133 iter_result.push_str(&format_ident(value));
134 },
135 // @import, @font-face, @charset, @media...
136 Token::AtKeyword(ref value) => {
137 curr_rule = value.to_string();
138 // if options.no_fonts && curr_rule == "font-face" {
139 // continue;
140 // }
141 iter_result.push('@');
142 iter_result.push_str(value);
143 },
144 Token::Hash(ref value) => {
145 iter_result.push('#');
146 iter_result.push_str(value);
147 },
148 Token::QuotedString(ref value) => {
149 // Add the quoted string with quotes preserved
150 iter_result.push_str(&_format_quoted_string(value));
151
152 // if curr_rule == "import" {
153 // // Reset current at-rule value
154 // curr_rule = String::default();
155
156 // // Skip empty import values
157 // if value.len() == 0 {
158 // result.push_str("''");
159 // continue;
160 // }
161
162 // let import_full_url: Url = resolve_url(&document_url, value);
163 // match retrieve_asset(
164 // cache,
165 // client,
166 // &document_url,
167 // &import_full_url,
168 // options,
169 // depth + 1,
170 // ) {
171 // Ok((import_contents, import_final_url, import_media_type, import_charset)) => {
172 // let mut import_data_url = create_data_url(
173 // &import_media_type,
174 // &import_charset,
175 // embed_css(
176 // cache,
177 // client,
178 // &import_final_url,
179 // &String::from_utf8_lossy(&import_contents),
180 // options,
181 // depth + 1,
182 // )
183 // .as_bytes(),
184 // &import_final_url,
185 // );
186 // import_data_url.set_fragment(import_full_url.fragment());
187 // result.push_str(format_quoted_string(&import_data_url.to_string()).as_str());
188 // }
189 // Err(_) => {
190 // // Keep remote reference if unable to retrieve the asset
191 // if import_full_url.scheme() == "http" || import_full_url.scheme() == "https" {
192 // result.push_str(format_quoted_string(&import_full_url.to_string()).as_str());
193 // }
194 // }
195 // }
196 // } else {
197 // if func_name == "url" {
198 // // Skip empty url()'s
199 // if value.len() == 0 {
200 // continue;
201 // }
202
203 // if options.no_images && is_image_url_prop(curr_prop.as_str()) {
204 // result.push_str(format_quoted_string(EMPTY_IMAGE_DATA_URL).as_str());
205 // } else {
206 // let resolved_url: Url = resolve_url(&document_url, value);
207 // match retrieve_asset(
208 // cache,
209 // client,
210 // &document_url,
211 // &resolved_url,
212 // options,
213 // depth + 1,
214 // ) {
215 // Ok((data, final_url, media_type, charset)) => {
216 // let mut data_url = create_data_url(&media_type, &charset, &data, &final_url);
217 // data_url.set_fragment(resolved_url.fragment());
218 // result.push_str(format_quoted_string(&data_url.to_string()).as_str());
219 // }
220 // Err(_) => {
221 // // Keep remote reference if unable to retrieve the asset
222 // if resolved_url.scheme() == "http" || resolved_url.scheme() == "https" {
223 // result.push_str(format_quoted_string(&resolved_url.to_string()).as_str());
224 // }
225 // }
226 // }
227 // }
228 // } else {
229 // result.push_str(format_quoted_string(value).as_str());
230 // }
231 // }
232 },
233 Token::Number {
234 ref has_sign,
235 ref value,
236 ..
237 } => {
238 if *has_sign && *value >= 0. {
239 iter_result.push('+');
240 }
241 iter_result.push_str(&value.to_string())
242 },
243 Token::Percentage {
244 ref has_sign,
245 ref unit_value,
246 ..
247 } => {
248 if *has_sign && *unit_value >= 0. {
249 iter_result.push('+');
250 }
251 iter_result.push_str(&(unit_value * 100.0).to_string());
252 iter_result.push('%');
253 },
254 Token::Dimension {
255 ref has_sign,
256 ref value,
257 ref unit,
258 ..
259 } => {
260 if *has_sign && *value >= 0. {
261 iter_result.push('+');
262 }
263 iter_result.push_str(&value.to_string());
264 iter_result.push_str(unit.as_ref());
265 },
266 // #selector, #id...
267 Token::IDHash(ref value) => {
268 curr_rule = String::default();
269 iter_result.push('#');
270 iter_result.push_str(&format_ident(value));
271 },
272 // url()
273 Token::UnquotedUrl(ref _value) => {
274 stylex_unimplemented!(
275 "Unquoted URL values in CSS are not supported. Use url(\"...\") with quotes instead."
276 );
277 // let is_import: bool = curr_rule == "import";
278
279 // if is_import {
280 // // Reset current at-rule value
281 // curr_rule = String::default();
282 // }
283
284 // // Skip empty url()'s
285 // if value.len() < 1 {
286 // result.push_str("url()");
287 // continue;
288 // } else if value.starts_with("#") {
289 // result.push_str("url(");
290 // result.push_str(value);
291 // result.push_str(")");
292 // continue;
293 // }
294
295 // result.push_str("url(");
296 // if is_import {
297 // let full_url: Url = resolve_url(&document_url, value);
298 // match retrieve_asset(cache, client, &document_url, &full_url, options, depth + 1) {
299 // Ok((css, final_url, media_type, charset)) => {
300 // let mut data_url = create_data_url(
301 // &media_type,
302 // &charset,
303 // embed_css(
304 // cache,
305 // client,
306 // &final_url,
307 // &String::from_utf8_lossy(&css),
308 // options,
309 // depth + 1,
310 // )
311 // .as_bytes(),
312 // &final_url,
313 // );
314 // data_url.set_fragment(full_url.fragment());
315 // result.push_str(format_quoted_string(&data_url.to_string()).as_str());
316 // }
317 // Err(_) => {
318 // // Keep remote reference if unable to retrieve the asset
319 // if full_url.scheme() == "http" || full_url.scheme() == "https" {
320 // result.push_str(format_quoted_string(&full_url.to_string()).as_str());
321 // }
322 // }
323 // }
324 // } else {
325 // if is_image_url_prop(curr_prop.as_str()) && options.no_images {
326 // result.push_str(format_quoted_string(EMPTY_IMAGE_DATA_URL).as_str());
327 // } else {
328 // let full_url: Url = resolve_url(&document_url, value);
329 // match retrieve_asset(cache, client, &document_url, &full_url, options, depth + 1) {
330 // Ok((data, final_url, media_type, charset)) => {
331 // let mut data_url = create_data_url(&media_type, &charset, &data, &final_url);
332 // data_url.set_fragment(full_url.fragment());
333 // result.push_str(format_quoted_string(&data_url.to_string()).as_str());
334 // }
335 // Err(_) => {
336 // // Keep remote reference if unable to retrieve the asset
337 // if full_url.scheme() == "http" || full_url.scheme() == "https" {
338 // result.push_str(format_quoted_string(&full_url.to_string()).as_str());
339 // }
340 // }
341 // }
342 // }
343 // }
344 // result.push_str(")");
345 },
346 Token::Delim(ref value) => iter_result.push(*value),
347 Token::Function(ref name) => {
348 iter_result.push_str(name);
349 iter_result.push('(');
350
351 let block_css: Vec<String> = match parser.parse_nested_block(|parser| {
352 parse_css_inner(parser, curr_rule.as_str(), curr_prop.as_str())
353 }) {
354 Ok(css) => css,
355 Err(e) => stylex_panic!("Failed to parse CSS function block: {:?}", e),
356 };
357
358 iter_result.push_str(join_css(&block_css).as_str());
359
360 iter_result.push(')');
361 },
362 _ => {},
363 }
364
365 // Ensure empty CSS is really empty
366 if !iter_result.is_empty() && iter_result.trim().is_empty() {
367 iter_result = iter_result.trim().to_string()
368 }
369
370 if !iter_result.is_empty() {
371 result.push(iter_result);
372 }
373 }
374
375 Ok(result)
376}
377
378pub fn parse_css(css_string: &str) -> Vec<String> {
379 let mut input = ParserInput::new(css_string);
380
381 let mut parser = Parser::new(&mut input);
382 let rule_name = "";
383 let prop_name = "";
384
385 match parse_css_inner(&mut parser, rule_name, prop_name) {
386 Ok(nodes) => nodes
387 .into_iter()
388 .filter_map(|s| {
389 if !s.is_empty() && s != "," {
390 Some(s)
391 } else {
392 None
393 }
394 })
395 .collect::<Vec<String>>(),
396 Err(_) => stylex_unreachable!("parse_css_inner returned Err, which should not happen"),
397 }
398}
399
400fn join_css(nodes: &[String]) -> String {
401 let mut result = String::new();
402 let mut needs_space = false;
403
404 for node in nodes.iter() {
405 if node == "/" || node == "," {
406 needs_space = false;
407 } else {
408 if needs_space {
409 result.push(' ');
410 }
411 needs_space = true;
412 }
413 result.push_str(node);
414 }
415
416 result
417}