Skip to main content

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}