stylex_css_parser/at_queries/
media_query_transform.rs1use super::media_query::{
14 MediaAndRules, MediaNotRule, MediaOrRules, MediaQuery, MediaQueryRule, MediaRuleValue,
15};
16use rustc_hash::FxHashMap;
17use swc_core::atoms::Wtf8Atom;
18use swc_core::common::DUMMY_SP;
19use swc_core::ecma::ast::{Expr, KeyValueProp, ObjectLit, Prop, PropName, PropOrSpread, Str};
20
21fn key_value_to_str(key_value: &KeyValueProp) -> String {
23 match &key_value.key {
24 PropName::Str(s) => s
25 .value
26 .as_atom()
27 .expect("Failed to convert Str to Atom")
28 .to_string(),
29 PropName::Ident(id) => id.sym.to_string(),
30 _ => String::new(),
31 }
32}
33
34pub fn last_media_query_wins_transform(styles: &[KeyValueProp]) -> Vec<KeyValueProp> {
36 dfs_process_queries_with_depth(styles, 0)
37}
38
39pub fn last_media_query_wins_transform_internal(queries: Vec<MediaQuery>) -> Vec<MediaQuery> {
42 queries
45}
46
47fn create_object_from_key_values(key_values: Vec<KeyValueProp>) -> ObjectLit {
49 let props = key_values
50 .into_iter()
51 .map(|kv| PropOrSpread::Prop(Box::new(Prop::KeyValue(kv))))
52 .collect();
53
54 ObjectLit {
55 span: DUMMY_SP,
56 props,
57 }
58}
59
60fn dfs_process_queries_with_depth(obj: &[KeyValueProp], depth: u32) -> Vec<KeyValueProp> {
62 let mut result = Vec::new();
63
64 for prop in obj {
65 match &*prop.value {
66 Expr::Array(_) => {
67 result.push(prop.clone());
69 },
70 Expr::Object(obj_lit) => {
71 let mut key_values = Vec::new();
73 for obj_prop in &obj_lit.props {
74 if let PropOrSpread::Prop(p) = obj_prop
75 && let Prop::KeyValue(kv) = &**p
76 {
77 key_values.push(kv.clone());
78 }
79 }
80
81 let processed_values = dfs_process_queries_with_depth(&key_values, depth + 1);
83 let transformed_obj = create_object_from_key_values(processed_values);
84
85 result.push(KeyValueProp {
86 key: prop.key.clone(),
87 value: Box::new(Expr::Object(transformed_obj)),
88 });
89 },
90 _ => {
91 result.push(prop.clone());
93 },
94 }
95 }
96
97 if depth >= 1 {
99 transform_media_queries_in_result(result)
100 } else {
101 result
102 }
103}
104
105fn transform_media_queries_in_result(result: Vec<KeyValueProp>) -> Vec<KeyValueProp> {
107 let has_media_queries = result.iter().any(|kv| {
109 let key = key_value_to_str(kv);
110 key.starts_with("@media ")
111 });
112
113 if !has_media_queries {
114 return result;
115 }
116
117 let media_keys: Vec<String> = result
119 .iter()
120 .filter_map(|kv| {
121 let key = key_value_to_str(kv);
122 if key.starts_with("@media ") {
123 Some(key)
124 } else {
125 None
126 }
127 })
128 .collect();
129
130 if media_keys.len() <= 1 {
131 return result;
132 }
133
134 if are_media_queries_disjoint(&media_keys) {
136 return normalize_media_query_syntax(result);
137 }
138
139 let mut negations = Vec::new();
141 let mut accumulated_negations: Vec<Vec<MediaQuery>> = Vec::new();
142
143 for i in (1..media_keys.len()).rev() {
144 if let Ok(mq) = MediaQuery::parser().parse_to_end(&media_keys[i]) {
145 negations.push(mq);
146 accumulated_negations.push(negations.clone());
147 }
148 }
149 accumulated_negations.reverse();
150 accumulated_negations.push(Vec::new()); let mut result_map = FxHashMap::default();
154
155 for kv in &result {
157 let key = key_value_to_str(kv);
158 if !key.starts_with("@media ") {
159 result_map.insert(key, kv.clone());
160 }
161 }
162
163 for (i, media_key) in media_keys.iter().enumerate() {
165 if let Some(original_kv) = result.iter().find(|kv| key_value_to_str(kv) == *media_key)
166 && let Ok(base_mq) = MediaQuery::parser().parse_to_end(media_key)
167 {
168 let mut reversed_negations = accumulated_negations[i].clone();
169 reversed_negations.reverse();
170
171 let combined_query = combine_media_query_with_negations(base_mq, reversed_negations);
172 let new_media_key = combined_query.to_string();
173
174 result_map.insert(
175 new_media_key,
176 KeyValueProp {
177 key: PropName::Str(Str {
178 span: DUMMY_SP,
179 value: Wtf8Atom::from(combined_query.to_string()),
180 raw: None,
181 }),
182 value: original_kv.value.clone(),
183 },
184 );
185 }
186 }
187
188 let mut final_result = Vec::new();
190
191 for kv in &result {
193 let key = key_value_to_str(kv);
194 if !key.starts_with("@media ") {
195 final_result.push(kv.clone());
196 }
197 }
198
199 for media_key in &media_keys {
201 if let Ok(base_mq) = MediaQuery::parser().parse_to_end(media_key) {
202 let i = media_keys.iter().position(|k| k == media_key).unwrap();
203 let mut reversed_negations = accumulated_negations[i].clone();
204 reversed_negations.reverse();
205
206 let combined_query = combine_media_query_with_negations(base_mq, reversed_negations);
207 let new_media_key = combined_query.to_string();
208
209 if let Some(original_kv) = result.iter().find(|kv| key_value_to_str(kv) == *media_key) {
210 final_result.push(KeyValueProp {
211 key: PropName::Str(Str {
212 span: DUMMY_SP,
213 value: Wtf8Atom::from(new_media_key),
214 raw: None,
215 }),
216 value: original_kv.value.clone(),
217 });
218 }
219 }
220 }
221
222 final_result
223}
224
225fn combine_media_query_with_negations(
227 current: MediaQuery,
228 negations: Vec<MediaQuery>,
229) -> MediaQuery {
230 if negations.is_empty() {
231 return current;
232 }
233
234 let not_rules: Vec<MediaQueryRule> = negations
236 .into_iter()
237 .map(|mq| MediaQueryRule::Not(MediaNotRule::new(mq.queries)))
238 .collect();
239
240 let combined_ast = match current.queries {
242 MediaQueryRule::Or(or_rules) => {
243 let new_rules = or_rules
244 .rules
245 .into_iter()
246 .map(|rule| {
247 let mut and_rules = vec![rule];
248 and_rules.extend(not_rules.clone());
249 MediaQueryRule::And(MediaAndRules::new(and_rules))
250 })
251 .collect();
252 MediaQueryRule::Or(MediaOrRules::new(new_rules))
253 },
254 other => {
255 let mut rules = vec![other];
256 rules.extend(not_rules);
257 MediaQueryRule::And(MediaAndRules::new(rules))
258 },
259 };
260
261 MediaQuery::new_from_rule(combined_ast)
262}
263
264fn are_media_queries_disjoint(media_keys: &[String]) -> bool {
266 let mut ranges = Vec::new();
267
268 for media_key in media_keys {
269 if let Ok(mq) = MediaQuery::parser().parse_to_end(media_key) {
270 if let Some(range) = extract_width_height_range(&mq) {
271 ranges.push(range);
272 } else {
273 return false;
275 }
276 } else {
277 return false;
278 }
279 }
280
281 for i in 0..ranges.len() {
283 for j in (i + 1)..ranges.len() {
284 if ranges_overlap(&ranges[i], &ranges[j]) {
285 return false;
286 }
287 }
288 }
289
290 true
291}
292
293fn extract_width_height_range(mq: &MediaQuery) -> Option<(String, f32, f32)> {
295 match &mq.queries {
296 MediaQueryRule::And(and_rules) if and_rules.rules.len() == 2 => {
297 let mut min_val = None;
298 let mut max_val = None;
299 let mut dimension = None;
300
301 for rule in &and_rules.rules {
302 if let MediaQueryRule::Pair(pair) = rule {
303 if pair.key.starts_with("min-width") || pair.key.starts_with("max-width") {
304 if dimension.is_none() {
305 dimension = Some("width".to_string());
306 } else if dimension.as_ref() != Some(&"width".to_string()) {
307 return None; }
309
310 if let MediaRuleValue::Length(length) = &pair.value {
311 if pair.key.starts_with("min-") {
312 min_val = Some(length.value);
313 } else {
314 max_val = Some(length.value);
315 }
316 } else {
317 return None; }
319 } else if pair.key.starts_with("min-height") || pair.key.starts_with("max-height") {
320 if dimension.is_none() {
321 dimension = Some("height".to_string());
322 } else if dimension.as_ref() != Some(&"height".to_string()) {
323 return None; }
325
326 if let MediaRuleValue::Length(length) = &pair.value {
327 if pair.key.starts_with("min-") {
328 min_val = Some(length.value);
329 } else {
330 max_val = Some(length.value);
331 }
332 } else {
333 return None; }
335 } else {
336 return None; }
338 } else {
339 return None; }
341 }
342
343 if let (Some(dim), Some(min), Some(max)) = (dimension, min_val, max_val) {
344 Some((dim, min, max))
345 } else {
346 None
347 }
348 },
349 _ => None,
350 }
351}
352
353fn ranges_overlap(range1: &(String, f32, f32), range2: &(String, f32, f32)) -> bool {
355 if range1.0 != range2.0 {
357 return false;
358 }
359
360 let (_, min1, max1) = range1;
361 let (_, min2, max2) = range2;
362
363 min1 <= max2 && min2 <= max1
366}
367
368fn normalize_media_query_syntax(result: Vec<KeyValueProp>) -> Vec<KeyValueProp> {
370 result
371 .into_iter()
372 .map(|kv| {
373 let key = key_value_to_str(&kv);
374 if key.starts_with("@media ") {
375 if let Ok(mq) = MediaQuery::parser().parse_to_end(&key) {
376 let normalized_key = mq.to_string();
377 KeyValueProp {
378 key: PropName::Str(Str {
379 span: DUMMY_SP,
380 value: Wtf8Atom::from(normalized_key),
381 raw: None,
382 }),
383 value: kv.value,
384 }
385 } else {
386 kv
387 }
388 } else {
389 kv
390 }
391 })
392 .collect()
393}