stylex_transform/shared/utils/css/
common.rs1use crate::shared::utils::common::round_to_decimal_places;
2use crate::shared::utils::css::normalizers::base::base_normalizer;
3use crate::shared::utils::css::validators::unprefixed_custom_properties::unprefixed_custom_properties_validator;
4use crate::shared::{structures::state_manager::StateManager, utils::common::dashify};
5use stylex_constants::constants::common::{
6 COLOR_FUNCTION_LISTED_NORMALIZED_PROPERTY_VALUES,
7 COLOR_RELATIVE_VALUES_LISTED_NORMALIZED_PROPERTY_VALUES, CSS_CONTENT_FUNCTIONS,
8 CSS_CONTENT_KEYWORDS,
9};
10use stylex_constants::constants::long_hand_logical::LONG_HAND_LOGICAL;
11use stylex_constants::constants::long_hand_physical::LONG_HAND_PHYSICAL;
12use stylex_constants::constants::messages::LINT_UNCLOSED_FUNCTION;
13use stylex_constants::constants::number_properties::NUMBER_PROPERTY_SUFFIXIES;
14use stylex_constants::constants::priorities::{
15 AT_RULE_PRIORITIES, CAMEL_CASE_PRIORITIES, PSEUDO_CLASS_PRIORITIES, PSEUDO_ELEMENT_PRIORITY,
16};
17use stylex_constants::constants::shorthands_of_longhands::SHORTHANDS_OF_LONGHANDS;
18use stylex_constants::constants::shorthands_of_shorthands::SHORTHANDS_OF_SHORTHANDS;
19use stylex_constants::constants::unitless_number_properties::UNITLESS_NUMBER_PROPERTIES;
20use stylex_css::css::generate_ltr::generate_ltr;
21use stylex_css::css::generate_rtl::generate_rtl;
22use stylex_css::css::normalizers::whitespace_normalizer::whitespace_normalizer;
23use stylex_regex::regex::{
24 ANCESTOR_SELECTOR, ANY_SIBLING_SELECTOR, CLEAN_CSS_VAR, DESCENDANT_SELECTOR, MANY_SPACES,
25 PSEUDO_PART_REGEX, SIBLING_AFTER_SELECTOR, SIBLING_BEFORE_SELECTOR,
26};
27use stylex_structures::pair::Pair;
28use stylex_structures::stylex_state_options::StyleXStateOptions;
29use stylex_types::structures::injectable_style::InjectableStyle;
30
31use stylex_macros::stylex_panic;
32use swc_core::{
33 common::{BytePos, input::StringInput, source_map::SmallPos},
34 css::{
35 ast::{Ident, Stylesheet},
36 codegen::{
37 CodeGenerator, CodegenConfig, Emit,
38 writer::basic::{BasicCssWriter, BasicCssWriterConfig},
39 },
40 parser::{error::Error, parse_string_input, parser::ParserConfig},
41 },
42};
43
44#[allow(unused_imports)]
45pub(crate) use stylex_css::values::common::split_value_required;
46
47#[allow(unused_imports)]
48pub(crate) use stylex_css::values::common::split_value;
49
50const THUMB_VARIANTS: [&str; 3] = [
51 "::-webkit-slider-thumb",
52 "::-moz-range-thumb",
53 "::-ms-thumb",
54];
55
56pub(crate) fn build_nested_css_rule(
57 class_name: &str,
58 decls: String,
59 pseudos: &mut [String],
60 at_rules: &mut [String],
61 const_rules: &mut [String],
62) -> String {
63 let pseudo = pseudos
64 .iter()
65 .filter(|&p| p != "::thumb")
66 .collect::<Vec<&String>>();
67 let pseudo_strs: Vec<&str> = pseudo.iter().map(|s| s.as_str()).collect();
68 let pseudo = pseudo_strs.join("");
69
70 let mut combined_at_rules = Vec::with_capacity(at_rules.len() + const_rules.len());
71
72 combined_at_rules.extend_from_slice(at_rules);
73 combined_at_rules.extend_from_slice(const_rules);
74
75 let has_where = pseudo.contains(":where(");
77 let extra_class_for_where = if has_where {
78 format!(".{}", class_name)
79 } else {
80 String::default()
81 };
82
83 let mut selector_for_at_rules = format!(
84 ".{class_name}{extra_class_for_where}{}{pseudo}",
85 combined_at_rules
86 .iter()
87 .map(|_| format!(".{}", class_name))
88 .collect::<Vec<String>>()
89 .join(""),
90 );
91
92 if pseudos.contains(&"::thumb".to_string()) {
93 selector_for_at_rules = THUMB_VARIANTS
94 .iter()
95 .map(|suffix| format!("{}{}", selector_for_at_rules, suffix))
96 .collect::<Vec<String>>()
97 .join(", ");
98 }
99
100 combined_at_rules.iter().fold(
101 format!("{}{{{}}}", selector_for_at_rules, decls),
102 |acc, at_rule| format!("{}{{{}}}", at_rule, acc),
103 )
104}
105
106pub(crate) fn generate_css_rule(
107 class_name: &str,
108 key: &str,
109 values: &[String],
110 pseudos: &mut [String],
111 at_rules: &mut [String],
112 const_rules: &mut [String],
113 options: &StyleXStateOptions,
114) -> InjectableStyle {
115 let mut pairs: Vec<Pair> = vec![];
116
117 for value in values {
118 pairs.push(Pair::new(key.to_string(), value.clone()));
119 }
120
121 let ltr_pairs: Vec<Pair> = pairs
122 .iter()
123 .map(|pair| generate_ltr(pair, options))
124 .collect::<Vec<Pair>>();
125
126 let rtl_pairs: Vec<Pair> = pairs
127 .iter()
128 .filter_map(|pair| generate_rtl(pair, options))
129 .collect::<Vec<Pair>>();
130
131 let ltr_decls = ltr_pairs
132 .iter()
133 .map(|pair| format!("{}:{}", pair.key, pair.value))
134 .collect::<Vec<String>>()
135 .join(";");
136
137 let rtl_decls = rtl_pairs
138 .iter()
139 .map(|pair| format!("{}:{}", pair.key, pair.value))
140 .collect::<Vec<String>>()
141 .join(";");
142
143 let ltr_rule = build_nested_css_rule(class_name, ltr_decls, pseudos, at_rules, const_rules);
144 let rtl_rule = if rtl_decls.is_empty() {
145 None
146 } else {
147 Some(build_nested_css_rule(
148 class_name,
149 rtl_decls,
150 pseudos,
151 at_rules,
152 const_rules,
153 ))
154 };
155
156 let priority = get_priority(key)
157 + pseudos.iter().map(|p| get_priority(p)).sum::<f64>()
158 + at_rules.iter().map(|a| get_priority(a)).sum::<f64>()
159 + const_rules.iter().map(|c| get_priority(c)).sum::<f64>();
160
161 InjectableStyle {
162 priority: Some(priority),
163 rtl: rtl_rule,
164 ltr: ltr_rule,
165 }
166}
167
168fn get_compound_pseudo_priority(key: &str) -> Option<f64> {
170 let mut parts: Vec<String> = vec![];
171
172 for mat in PSEUDO_PART_REGEX.find_iter(key).flatten() {
173 parts.push(mat.as_str().to_string());
174 }
175
176 if parts.len() <= 1 || parts.iter().any(|p| p.contains('(')) {
179 return None;
180 }
181
182 let mut total: f64 = 0.0;
183
184 for part in parts.iter() {
185 total += if part.starts_with("::") {
186 PSEUDO_ELEMENT_PRIORITY
187 } else {
188 **PSEUDO_CLASS_PRIORITIES.get(part.as_str()).unwrap_or(&&40.0)
189 };
190 }
191
192 Some(total)
193}
194
195fn get_at_rule_priority(key: &str) -> Option<f64> {
196 if key.starts_with("--") {
197 return Some(1.0);
198 }
199
200 if key.starts_with("@supports") {
201 return AT_RULE_PRIORITIES.get("@supports").map(|v| **v);
202 }
203
204 if key.starts_with("@media") {
205 return AT_RULE_PRIORITIES.get("@media").map(|v| **v);
206 }
207
208 if key.starts_with("@container") {
209 return AT_RULE_PRIORITIES.get("@container").map(|v| **v);
210 }
211
212 None
213}
214
215fn get_pseudo_element_priority(key: &str) -> Option<f64> {
216 if key.starts_with("::") {
217 return Some(PSEUDO_ELEMENT_PRIORITY);
218 }
219
220 None
221}
222
223fn get_pseudo_class_priority(key: &str) -> Option<f64> {
224 let pseudo_base = |p: &str| -> f64 { **PSEUDO_CLASS_PRIORITIES.get(p).unwrap_or(&&40.0) / 100.0 };
225
226 if let Ok(Some(captures)) = ANCESTOR_SELECTOR.captures(key)
228 && let Some(pseudo) = captures.get(1)
229 {
230 return Some(10.0 + pseudo_base(pseudo.as_str()));
231 }
232
233 if let Ok(Some(captures)) = DESCENDANT_SELECTOR.captures(key)
235 && let Some(pseudo) = captures.get(1)
236 {
237 return Some(15.0 + pseudo_base(pseudo.as_str()));
238 }
239
240 if let Ok(Some(captures)) = ANY_SIBLING_SELECTOR.captures(key)
242 && let (Some(pseudo1), Some(pseudo2)) = (captures.get(1), captures.get(2))
243 {
244 return Some(20.0 + pseudo_base(pseudo1.as_str()).max(pseudo_base(pseudo2.as_str())));
245 }
246
247 if let Ok(Some(captures)) = SIBLING_BEFORE_SELECTOR.captures(key)
249 && let Some(pseudo) = captures.get(1)
250 {
251 return Some(30.0 + pseudo_base(pseudo.as_str()));
252 }
253
254 if let Ok(Some(captures)) = SIBLING_AFTER_SELECTOR.captures(key)
256 && let Some(pseudo) = captures.get(1)
257 {
258 return Some(40.0 + pseudo_base(pseudo.as_str()));
259 }
260
261 if key.starts_with(':') {
262 let prop: &str = key.split('(').next().unwrap_or(key);
263
264 return Some(**PSEUDO_CLASS_PRIORITIES.get(prop).unwrap_or(&&40.0));
265 }
266
267 None
268}
269
270fn get_default_priority(key: &str) -> Option<f64> {
271 if SHORTHANDS_OF_SHORTHANDS.contains(key) {
272 return Some(1000.0);
273 }
274
275 if SHORTHANDS_OF_LONGHANDS.contains(key) {
276 return Some(2000.0);
277 }
278
279 if LONG_HAND_LOGICAL.contains(key) {
280 return Some(3000.0);
281 }
282
283 if LONG_HAND_PHYSICAL.contains(key) {
284 return Some(4000.0);
285 }
286
287 None
288}
289
290pub(crate) fn get_priority(key: &str) -> f64 {
291 if let Some(at_rule_priority) = get_at_rule_priority(key) {
292 return at_rule_priority;
293 }
294
295 if let Some(compound_priority) = get_compound_pseudo_priority(key) {
296 return compound_priority;
297 }
298
299 if let Some(pseudo_element_priority) = get_pseudo_element_priority(key) {
300 return pseudo_element_priority;
301 }
302
303 if let Some(pseudo_class_priority) = get_pseudo_class_priority(key) {
304 return pseudo_class_priority;
305 }
306
307 if let Some(default_priority) = get_default_priority(key) {
308 return default_priority;
309 }
310
311 3000.0
312}
313
314pub(crate) fn transform_value(key: &str, value: &str, state: &StateManager) -> String {
315 let css_property_value = value.trim();
316
317 let value = match &css_property_value.parse::<f64>() {
318 Ok(value) => format!(
319 "{0}{1}",
320 round_to_decimal_places(*value, 4),
321 get_number_suffix(key)
322 ),
323 Err(_) => css_property_value.to_string(),
324 };
325
326 if key == "content" || key == "hyphenateCharacter" || key == "hyphenate-character" {
327 let is_css_function = CSS_CONTENT_FUNCTIONS
328 .iter()
329 .any(|func| value.contains(func));
330
331 let is_keyword = CSS_CONTENT_KEYWORDS.contains(&value.as_str());
332
333 let double_quote_count = value.matches('"').count();
334 let single_quote_count = value.matches('\'').count();
335
336 let has_matching_quotes = double_quote_count >= 2 || single_quote_count >= 2;
337
338 if is_css_function || is_keyword || has_matching_quotes {
339 return value.to_string();
340 }
341
342 return format!("\"{}\"", value);
343 }
344
345 normalize_css_property_value(key, value.as_ref(), &state.options)
346}
347
348pub(crate) fn transform_value_cached(key: &str, value: &str, state: &mut StateManager) -> String {
349 let cache_key: String = format!("{}:{}", key, value);
350
351 let cache = state.css_property_seen.get(&cache_key);
352
353 if let Some(result) = cache {
354 return result.to_string();
355 }
356
357 let result = transform_value(key, value, state);
358
359 state.css_property_seen.insert(cache_key, result.clone());
360
361 result
362}
363
364pub fn swc_parse_css(source: &str) -> (Result<Stylesheet, Error>, Vec<Error>) {
365 let config = ParserConfig {
366 allow_wrong_line_comments: false,
367 css_modules: false,
368 legacy_nesting: false,
369 legacy_ie: false,
370 };
371
372 let input = StringInput::new(
373 source,
374 BytePos::from_usize(0),
375 BytePos::from_usize(source.len()),
376 );
377 let mut errors: Vec<Error> = vec![];
378
379 (parse_string_input(input, None, config, &mut errors), errors)
380}
381
382pub(crate) fn normalize_css_property_value(
383 css_property: &str,
384 css_property_value: &str,
385 options: &StyleXStateOptions,
386) -> String {
387 if COLOR_FUNCTION_LISTED_NORMALIZED_PROPERTY_VALUES
388 .iter()
389 .chain(COLOR_RELATIVE_VALUES_LISTED_NORMALIZED_PROPERTY_VALUES.iter())
390 .any(|css_fnc| {
391 css_property_value.contains(format!("{}(", css_fnc).as_str())
392 || css_property_value.to_lowercase().contains(css_fnc)
393 })
394 {
395 return MANY_SPACES.replace_all(css_property_value, " ").to_string();
396 }
397
398 let is_css_variable = css_property.starts_with("--");
399
400 let css_property_for_parsing = if is_css_variable {
401 "color"
402 } else {
403 css_property
404 };
405
406 let css_rule = if css_property_for_parsing.starts_with(':') {
407 format!("{0} {1}", css_property_for_parsing, css_property_value)
408 } else {
409 format!(
410 "* {{ {0}: {1} }}",
411 css_property_for_parsing, css_property_value
412 )
413 };
414
415 let (parsed_css, errors) = swc_parse_css(css_rule.as_str());
416
417 if !errors.is_empty() {
418 let mut error_message = match errors.first() {
419 Some(e) => e.message().to_string(),
420 None => stylex_panic!("CSS parsing failed but no error details were available."),
421 };
422
423 if error_message.ends_with("expected ')'") || error_message.ends_with("expected '('") {
424 error_message = LINT_UNCLOSED_FUNCTION.to_string();
425 }
426
427 stylex_panic!("{}, css rule: {}", error_message, css_rule)
428 }
429
430 match parsed_css {
431 Ok(parsed_css_property_value) => {
432 unprefixed_custom_properties_validator(&parsed_css_property_value);
447
448 let parsed_ast = base_normalizer(
449 parsed_css_property_value,
450 options.enable_font_size_px_to_rem,
451 Some(css_property),
452 );
453
454 let result = whitespace_normalizer(stringify(&parsed_ast));
459
460 convert_css_function_to_camel_case(result.as_str())
461 },
462 Err(err) => {
463 stylex_panic!("{}", err.message())
464 },
465 }
466}
467
468pub(crate) fn get_number_suffix(key: &str) -> String {
472 if UNITLESS_NUMBER_PROPERTIES.contains(key) || key.starts_with("--") {
473 return String::default();
474 }
475
476 let result = match NUMBER_PROPERTY_SUFFIXIES.get(key) {
477 Some(suffix) => suffix,
478 None => "px",
479 };
480
481 result.to_string()
482}
483
484pub(crate) fn get_value_from_ident(ident: &Ident) -> String {
485 ident.value.to_string()
486}
487
488fn convert_css_function_to_camel_case(function: &str) -> String {
489 let Some(items) = function.find('(') else {
490 return function.to_string();
491 };
492
493 let (name, args) = function.split_at(items);
494
495 let Some(camel_case_name) = CAMEL_CASE_PRIORITIES.get(name) else {
496 return function.to_string();
497 };
498
499 format!("{}{}", camel_case_name, args)
500}
501
502pub fn stringify(node: &Stylesheet) -> String {
503 let mut buf = String::new();
504 let writer = BasicCssWriter::new(&mut buf, None, BasicCssWriterConfig::default());
505 let mut codegen = CodeGenerator::new(writer, CodegenConfig { minify: true });
506
507 match Emit::emit(&mut codegen, node) {
508 Ok(_) => {},
509 Err(e) => stylex_panic!("CSS codegen emit failed: {}", e),
510 };
511
512 let mut result = buf.replace('\'', "");
513
514 if result.contains("--\\") {
515 result = CLEAN_CSS_VAR
527 .replace_all(buf.as_str(), |caps: &fancy_regex::Captures| {
528 caps
529 .get(1)
530 .map_or(String::default(), |m| m.as_str().to_string())
531 })
532 .to_string();
533 }
534
535 result
536}
537
538pub(crate) fn normalize_css_property_name(prop: &str) -> String {
544 if prop.starts_with("--") {
545 return prop.to_string();
546 }
547 dashify(prop)
548}
549
550pub(crate) fn inline_style_to_css_string(pairs: &[Pair]) -> String {
554 pairs
555 .iter()
556 .map(|p| format!("{}:{}", normalize_css_property_name(&p.key), p.value))
557 .collect::<Vec<_>>()
558 .join(";")
559}