Skip to main content

stylex_css_parser/at_queries/
media_query.rs

1/*!
2Media query parsing and representation.
3
4Core functionality for parsing and representing CSS media queries.
5*/
6
7use crate::{
8  CssParseError,
9  css_types::{Length, calc::Calc},
10  token_parser::{TokenParser, tokens},
11  token_types::SimpleToken,
12};
13use rustc_hash::FxHashMap;
14use std::fmt::{self, Display};
15
16/// Fraction type for media query values like (aspect-ratio: 16/9)
17#[derive(Debug, Clone, PartialEq)]
18pub struct Fraction {
19  pub numerator: i32,
20  pub denominator: i32,
21}
22
23impl Display for Fraction {
24  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25    // Format with spaces for consistent output
26    write!(f, "{} / {}", self.numerator, self.denominator)
27  }
28}
29
30/// Word rule types for media queries
31#[derive(Debug, Clone, PartialEq)]
32pub enum WordRule {
33  Color,
34  Monochrome,
35  Grid,
36  ColorIndex,
37}
38
39impl Display for WordRule {
40  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41    match self {
42      WordRule::Color => write!(f, "color"),
43      WordRule::Monochrome => write!(f, "monochrome"),
44      WordRule::Grid => write!(f, "grid"),
45      WordRule::ColorIndex => write!(f, "color-index"),
46    }
47  }
48}
49
50/// Media rule values that can appear in media queries
51#[derive(Debug, Clone, PartialEq)]
52pub enum MediaRuleValue {
53  Number(f32),
54  Length(Length),
55  String(String),
56  Fraction(Fraction),
57}
58
59impl Display for MediaRuleValue {
60  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61    match self {
62      MediaRuleValue::Number(n) => write!(f, "{}", n),
63      MediaRuleValue::Length(l) => write!(f, "{}", l),
64      MediaRuleValue::String(s) => write!(f, "{}", s),
65      MediaRuleValue::Fraction(frac) => write!(f, "{}", frac),
66    }
67  }
68}
69
70/// Media keyword for CSS media queries
71#[derive(Debug, Clone, PartialEq)]
72pub struct MediaKeyword {
73  pub r#type: String, // Always "media-keyword"
74  pub key: String,    // 'screen' | 'print' | 'all'
75  pub not: bool,
76  pub only: bool, // Boolean field for CSS media queries
77}
78
79impl MediaKeyword {
80  pub fn new(key: String, not: bool, only: bool) -> Self {
81    Self {
82      r#type: "media-keyword".to_string(),
83      key,
84      not,
85      only,
86    }
87  }
88}
89
90impl Display for MediaKeyword {
91  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92    let mut parts = Vec::new();
93
94    if self.not {
95      parts.push("not".to_string());
96    }
97
98    if self.only {
99      parts.push("only".to_string());
100    }
101
102    parts.push(self.key.clone());
103    write!(f, "{}", parts.join(" "))
104  }
105}
106
107/// Media word rule for CSS media queries
108#[derive(Debug, Clone, PartialEq)]
109pub struct MediaWordRule {
110  pub r#type: String,    // Always "word-rule"
111  pub key_value: String, // The word rule value
112}
113
114impl MediaWordRule {
115  pub fn new(key_value: String) -> Self {
116    Self {
117      r#type: "word-rule".to_string(),
118      key_value,
119    }
120  }
121}
122
123impl Display for MediaWordRule {
124  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125    write!(f, "({})", self.key_value)
126  }
127}
128
129/// Media rule pair for CSS media queries
130#[derive(Debug, Clone, PartialEq)]
131pub struct MediaRulePair {
132  #[allow(dead_code)]
133  pub r#type: String, // Always "pair"
134  pub key: String,           // Property name
135  pub value: MediaRuleValue, // Property value
136}
137
138impl MediaRulePair {
139  pub fn new(key: String, value: MediaRuleValue) -> Self {
140    Self {
141      r#type: "pair".to_string(),
142      key,
143      value,
144    }
145  }
146}
147
148impl Display for MediaRulePair {
149  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150    write!(f, "({}: {})", self.key, self.value)
151  }
152}
153
154/// Media NOT rule for CSS media queries
155#[derive(Debug, Clone, PartialEq)]
156pub struct MediaNotRule {
157  #[allow(dead_code)]
158  pub r#type: String, // Always "not"
159  pub rule: Box<MediaQueryRule>, // Nested rule
160}
161
162impl MediaNotRule {
163  pub fn new(rule: MediaQueryRule) -> Self {
164    Self {
165      r#type: "not".to_string(),
166      rule: Box::new(rule),
167    }
168  }
169}
170
171impl Display for MediaNotRule {
172  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173    match self.rule.as_ref() {
174      MediaQueryRule::And(_) | MediaQueryRule::Or(_) => {
175        write!(f, "(not ({}))", self.rule)
176      },
177      _ => {
178        write!(f, "(not {})", self.rule)
179      },
180    }
181  }
182}
183
184/// Media AND rules for CSS media queries
185#[derive(Debug, Clone, PartialEq)]
186pub struct MediaAndRules {
187  pub r#type: String,             // Always "and"
188  pub rules: Vec<MediaQueryRule>, // Array of rules
189}
190
191impl MediaAndRules {
192  pub fn new(rules: Vec<MediaQueryRule>) -> Self {
193    Self {
194      r#type: "and".to_string(),
195      rules,
196    }
197  }
198}
199
200impl Display for MediaAndRules {
201  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202    let rule_strings: Vec<String> = self.rules.iter().map(|rule| rule.to_string()).collect();
203    write!(f, "{}", rule_strings.join(" and "))
204  }
205}
206
207/// Media OR rules for CSS media queries
208#[derive(Debug, Clone, PartialEq)]
209pub struct MediaOrRules {
210  pub r#type: String,             // Always "or"
211  pub rules: Vec<MediaQueryRule>, // Array of rules
212}
213
214impl MediaOrRules {
215  pub fn new(rules: Vec<MediaQueryRule>) -> Self {
216    Self {
217      r#type: "or".to_string(),
218      rules,
219    }
220  }
221}
222
223impl Display for MediaOrRules {
224  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225    let rule_strings: Vec<String> = self.rules.iter().map(|rule| rule.to_string()).collect();
226    write!(f, "{}", rule_strings.join(" or "))
227  }
228}
229
230/// All media query rules for CSS media queries
231#[derive(Debug, Clone, PartialEq)]
232#[allow(dead_code)]
233pub enum MediaQueryRule {
234  MediaKeyword(MediaKeyword),
235  WordRule(MediaWordRule),
236  Pair(MediaRulePair),
237  Not(MediaNotRule),
238  And(MediaAndRules),
239  Or(MediaOrRules),
240}
241
242/// Main MediaQuery struct for CSS media queries
243#[derive(Debug, Clone, PartialEq)]
244pub struct MediaQuery {
245  pub queries: MediaQueryRule,
246}
247
248impl MediaQuery {
249  pub fn new(queries: MediaQueryRule) -> Self {
250    Self {
251      queries: Self::normalize(queries),
252    }
253  }
254
255  pub fn new_from_rule(rule: MediaQueryRule) -> Self {
256    Self::new(rule)
257  }
258
259  pub fn normalize(rule: MediaQueryRule) -> MediaQueryRule {
260    match rule {
261      MediaQueryRule::And(ref and_rules) => {
262        let mut flattened: Vec<MediaQueryRule> = Vec::new();
263        for r in &and_rules.rules {
264          let norm = Self::normalize(r.clone());
265          match norm {
266            MediaQueryRule::And(inner_and) => {
267              flattened.extend(inner_and.rules);
268            },
269            _ => {
270              flattened.push(norm);
271            },
272          }
273        }
274
275        if flattened.is_empty() {
276          return MediaQueryRule::MediaKeyword(MediaKeyword::new("all".to_string(), true, false));
277        }
278
279        let merged = merge_and_simplify_ranges(flattened);
280        if merged.is_empty() {
281          return MediaQueryRule::And(MediaAndRules::new(vec![MediaQueryRule::MediaKeyword(
282            MediaKeyword::new("all".to_string(), true, false),
283          )]));
284        }
285        MediaQueryRule::And(MediaAndRules::new(merged))
286      },
287      MediaQueryRule::Or(ref or_rules) => {
288        let normalized_rules: Vec<MediaQueryRule> = or_rules
289          .rules
290          .iter()
291          .map(|r| Self::normalize(r.clone()))
292          .collect();
293        MediaQueryRule::Or(MediaOrRules::new(normalized_rules))
294      },
295      MediaQueryRule::Not(ref not_rule) => {
296        let normalized_operand = Self::normalize(not_rule.rule.as_ref().clone());
297
298        match normalized_operand {
299          MediaQueryRule::MediaKeyword(ref keyword) if keyword.key == "all" && keyword.not => {
300            return MediaQueryRule::MediaKeyword(MediaKeyword::new(
301              "all".to_string(),
302              false,
303              false,
304            ));
305          },
306          MediaQueryRule::And(ref and_rules) if and_rules.rules.len() == 1 => {
307            if let MediaQueryRule::MediaKeyword(keyword) = &and_rules.rules[0]
308              && keyword.key == "all"
309              && keyword.not
310            {
311              return MediaQueryRule::MediaKeyword(MediaKeyword::new(
312                "all".to_string(),
313                false,
314                false,
315              ));
316            }
317          },
318          MediaQueryRule::Not(inner_not) => {
319            return Self::normalize(inner_not.rule.as_ref().clone());
320          },
321          _ => {},
322        }
323
324        MediaQueryRule::Not(MediaNotRule::new(normalized_operand))
325      },
326      _ => rule,
327    }
328  }
329}
330
331/// Add Display implementation for MediaQueryRule
332impl Display for MediaQueryRule {
333  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334    // Use the format_queries logic instead of the individual Display implementations
335    write!(f, "{}", MediaQuery::format_queries(self, false))
336  }
337}
338
339impl Display for MediaQuery {
340  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341    write!(
342      f,
343      "@media {}",
344      MediaQuery::format_queries(&self.queries, true)
345    )
346  }
347}
348
349impl MediaQuery {
350  fn format_queries(queries: &MediaQueryRule, is_top_level: bool) -> String {
351    match queries {
352      MediaQueryRule::MediaKeyword(keyword) => {
353        let prefix = if keyword.not {
354          "not "
355        } else if keyword.only {
356          "only "
357        } else {
358          ""
359        };
360        format!(
361          "{}{}",
362          prefix,
363          if is_top_level {
364            keyword.key.clone()
365          } else {
366            format!("({})", keyword.key)
367          }
368        )
369      },
370      MediaQueryRule::WordRule(word_rule) => {
371        format!("({})", word_rule.key_value)
372      },
373      MediaQueryRule::Pair(pair) => match &pair.value {
374        MediaRuleValue::Fraction(frac) => {
375          format!("({}: {} / {})", pair.key, frac.numerator, frac.denominator)
376        },
377        MediaRuleValue::Length(len) => {
378          format!("({}: {})", pair.key, len)
379        },
380        MediaRuleValue::String(s) => {
381          format!("({}: {})", pair.key, s)
382        },
383        MediaRuleValue::Number(n) => {
384          format!("({}: {})", pair.key, n)
385        },
386      },
387      MediaQueryRule::Not(not_rule) => match not_rule.rule.as_ref() {
388        MediaQueryRule::And(_) | MediaQueryRule::Or(_) | MediaQueryRule::Not(_) => {
389          format!(
390            "(not ({}))",
391            MediaQuery::format_queries(not_rule.rule.as_ref(), false)
392          )
393        },
394        _ => {
395          format!(
396            "(not {})",
397            MediaQuery::format_queries(not_rule.rule.as_ref(), false)
398          )
399        },
400      },
401      MediaQueryRule::And(and_rules) => {
402        let rule_strings: Vec<String> = and_rules
403          .rules
404          .iter()
405          .map(|rule| MediaQuery::format_queries(rule, false))
406          .collect();
407        rule_strings.join(" and ")
408      },
409      MediaQueryRule::Or(or_rules) => {
410        // Filter out invalid rules (like empty or rules)
411        let valid_rules: Vec<&MediaQueryRule> = or_rules
412          .rules
413          .iter()
414          .filter(|r| !matches!(r, MediaQueryRule::Or(or) if or.rules.is_empty()))
415          .collect();
416
417        if valid_rules.is_empty() {
418          return "not all".to_string();
419        }
420
421        if valid_rules.len() == 1 {
422          return MediaQuery::format_queries(valid_rules[0], is_top_level);
423        }
424
425        let formatted_rules: Vec<String> = valid_rules
426          .iter()
427          .map(|rule| match rule {
428            MediaQueryRule::And(_) | MediaQueryRule::Or(_) => {
429              let rule_string = MediaQuery::format_queries(rule, false);
430              if !is_top_level {
431                format!("({})", rule_string)
432              } else {
433                rule_string
434              }
435            },
436            _ => MediaQuery::format_queries(rule, false),
437          })
438          .collect();
439
440        if is_top_level {
441          formatted_rules.join(", ")
442        } else {
443          formatted_rules.join(" or ")
444        }
445      },
446    }
447  }
448}
449
450impl MediaQuery {
451  pub fn parser() -> TokenParser<MediaQuery> {
452    TokenParser::new(
453      |tokens| {
454        if let Ok(Some(SimpleToken::AtKeyword(keyword))) = tokens.peek() {
455          if keyword == "media" {
456            tokens.consume_next_token()?; // consume "@media"
457
458            // Skip mandatory whitespace after "@media"
459            if let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
460              tokens.consume_next_token()?;
461            } else {
462              // "@media" without space or content should be a syntax error
463              return Err(CssParseError::ParseError {
464                message: "Expected whitespace or content after @media".to_string(),
465              });
466            }
467          } else {
468            return Err(CssParseError::ParseError {
469              message: "Expected @media at-keyword".to_string(),
470            });
471          }
472        } else {
473          // If no @media prefix, assume we're parsing just the query part (for backwards compatibility)
474          // This maintains compatibility with existing tests
475        }
476
477        let rule = (media_query_rule_parser().run)(tokens)?;
478        Ok(MediaQuery::new_from_rule(rule))
479      },
480      "media_query_parser",
481    )
482  }
483
484  /// Check if parentheses are balanced in a media query string
485  pub fn has_balanced_parens(input: &str) -> bool {
486    has_balanced_parens(input)
487  }
488}
489
490/// Validate media query string
491pub fn validate_media_query(input: &str) -> Result<MediaQuery, String> {
492  if !has_balanced_parens(input) {
493    return Err(crate::at_queries::messages::MediaQueryErrors::UNBALANCED_PARENS.to_string());
494  }
495
496  match MediaQuery::parser().parse_to_end(input) {
497    Ok(media_query) => Ok(media_query),
498    Err(_) => Err(crate::at_queries::messages::MediaQueryErrors::SYNTAX_ERROR.to_string()),
499  }
500}
501
502/// Check if parentheses are balanced
503fn has_balanced_parens(input: &str) -> bool {
504  let mut count = 0;
505  for ch in input.chars() {
506    match ch {
507      '(' => count += 1,
508      ')' => {
509        count -= 1;
510        if count < 0 {
511          return false;
512        }
513      },
514      _ => {},
515    }
516  }
517  count == 0
518}
519
520fn is_numeric_length(val: &MediaRuleValue) -> bool {
521  matches!(val, MediaRuleValue::Length(_))
522}
523
524fn merge_and_simplify_ranges(rules: Vec<MediaQueryRule>) -> Vec<MediaQueryRule> {
525  match merge_intervals_for_and(rules.clone()) {
526    Ok(merged) => {
527      if merged.is_empty() {
528        // Contradiction detected - return empty Vec, which the caller will handle
529        Vec::new()
530      } else {
531        merged
532      }
533    },
534    Err(_) => rules, // Return original rules on error
535  }
536}
537
538fn merge_intervals_for_and(rules: Vec<MediaQueryRule>) -> Result<Vec<MediaQueryRule>, String> {
539  const EPSILON: f32 = 0.01;
540  let dimensions = ["width", "height"];
541
542  // Track intervals for each dimension: [min, max]
543  let mut intervals: FxHashMap<&str, Vec<(f32, f32)>> = FxHashMap::default();
544  intervals.insert("width", Vec::new());
545  intervals.insert("height", Vec::new());
546
547  // Track units for each dimension
548  let mut units: FxHashMap<&str, String> = FxHashMap::default();
549  let mut has_any_unit_conflicts = false;
550
551  // Handle DeMorgan's law: not (A and B) = (not A) or (not B)
552  for rule in &rules {
553    if let MediaQueryRule::Not(not_rule) = rule
554      && let MediaQueryRule::And(and_rules) = not_rule.rule.as_ref()
555      && and_rules.rules.len() == 2
556    {
557      let left = &and_rules.rules[0];
558      let right = &and_rules.rules[1];
559
560      // Create left branch: all rules except current, plus (not left)
561      let mut left_branch_rules: Vec<MediaQueryRule> = rules
562        .iter()
563        .filter(|r| !std::ptr::eq(*r, rule))
564        .cloned()
565        .collect();
566      left_branch_rules.push(MediaQueryRule::Not(MediaNotRule::new(left.clone())));
567
568      // Create right branch: all rules except current, plus (not right)
569      let mut right_branch_rules: Vec<MediaQueryRule> = rules
570        .iter()
571        .filter(|r| !std::ptr::eq(*r, rule))
572        .cloned()
573        .collect();
574      right_branch_rules.push(MediaQueryRule::Not(MediaNotRule::new(right.clone())));
575
576      // Recursively process each branch
577      let left_branch = merge_intervals_for_and(left_branch_rules);
578      let right_branch = merge_intervals_for_and(right_branch_rules);
579
580      let mut or_rules = Vec::new();
581
582      // Add left branch if not empty
583      if let Ok(left_result) = left_branch
584        && !left_result.is_empty()
585      {
586        if left_result.len() == 1 {
587          or_rules.push(left_result.into_iter().next().unwrap());
588        } else {
589          or_rules.push(MediaQueryRule::And(MediaAndRules::new(left_result)));
590        }
591      }
592
593      // Add right branch if not empty
594      if let Ok(right_result) = right_branch
595        && !right_result.is_empty()
596      {
597        if right_result.len() == 1 {
598          or_rules.push(right_result.into_iter().next().unwrap());
599        } else {
600          or_rules.push(MediaQueryRule::And(MediaAndRules::new(right_result)));
601        }
602      }
603
604      if !or_rules.is_empty() {
605        return Ok(vec![MediaQueryRule::Or(MediaOrRules::new(or_rules))]);
606      }
607    }
608  }
609
610  for rule in &rules {
611    for dim in &dimensions {
612      match &rule {
613        // Handle min-width/min-height/max-width/max-height pairs
614        MediaQueryRule::Pair(pair)
615          if (pair.key == format!("min-{}", dim) || pair.key == format!("max-{}", dim))
616            && is_numeric_length(&pair.value) =>
617        {
618          if let MediaRuleValue::Length(length) = &pair.value {
619            let val = length.value;
620
621            // Track unit conflicts
622            let dim_intervals = intervals.get(dim).unwrap();
623            if dim_intervals.is_empty() {
624              units.insert(dim, length.unit.clone());
625            } else if units.get(dim) != Some(&length.unit) {
626              has_any_unit_conflicts = true;
627            }
628
629            let interval = if pair.key.starts_with("min-") {
630              (val, f32::INFINITY)
631            } else {
632              (f32::NEG_INFINITY, val)
633            };
634
635            intervals.get_mut(dim).unwrap().push(interval);
636            break;
637          }
638        },
639
640        // Handle NOT rules with min/max constraints
641        MediaQueryRule::Not(not_rule) => {
642          if let MediaQueryRule::Pair(pair) = not_rule.rule.as_ref()
643            && (pair.key == format!("min-{}", dim) || pair.key == format!("max-{}", dim))
644            && is_numeric_length(&pair.value)
645            && let MediaRuleValue::Length(length) = &pair.value
646          {
647            let val = length.value;
648
649            // Track unit conflicts
650            let dim_intervals = intervals.get(dim).unwrap();
651            if dim_intervals.is_empty() {
652              units.insert(dim, length.unit.clone());
653            } else if units.get(dim) != Some(&length.unit) {
654              has_any_unit_conflicts = true;
655            }
656
657            // NOT min-width becomes max-width with adjusted value, and vice versa
658            let interval = if pair.key.starts_with("min-") {
659              (f32::NEG_INFINITY, val - EPSILON)
660            } else {
661              (val + EPSILON, f32::INFINITY)
662            };
663
664            intervals.get_mut(dim).unwrap().push(interval);
665            break;
666          }
667        },
668
669        _ => {},
670      }
671    }
672
673    // Check if this rule contains only width/height media query rules
674    if !matches!(
675      rule,
676      MediaQueryRule::Pair(pair) if (
677        pair.key == "min-width" ||
678        pair.key == "max-width" ||
679        pair.key == "min-height" ||
680        pair.key == "max-height"
681      ) && is_numeric_length(&pair.value)
682    ) && !matches!(
683      rule,
684      MediaQueryRule::Not(not_rule) if matches!(
685        not_rule.rule.as_ref(),
686        MediaQueryRule::Pair(pair) if (
687          pair.key == "min-width" ||
688          pair.key == "max-width" ||
689          pair.key == "min-height" ||
690          pair.key == "max-height"
691        ) && is_numeric_length(&pair.value)
692      )
693    ) {
694      return Ok(rules);
695    }
696  }
697
698  let mut result = Vec::new();
699
700  // Return original rules if unit conflicts detected
701  if has_any_unit_conflicts {
702    return Ok(rules);
703  }
704
705  for dim in &dimensions {
706    let dim_intervals = intervals.get(dim).unwrap();
707    if dim_intervals.is_empty() {
708      continue;
709    }
710
711    let mut lower = f32::NEG_INFINITY;
712    let mut upper = f32::INFINITY;
713    for (l, u) in dim_intervals {
714      if *l > lower {
715        lower = *l;
716      }
717      if *u < upper {
718        upper = *u;
719      }
720    }
721
722    if lower > upper {
723      return Ok(Vec::new());
724    }
725
726    if lower != f32::NEG_INFINITY && lower.is_finite() {
727      result.push(MediaQueryRule::Pair(MediaRulePair::new(
728        format!("min-{}", dim),
729        MediaRuleValue::Length(Length::new(lower, units.get(dim).unwrap().clone())),
730      )));
731    }
732
733    if upper != f32::INFINITY && upper.is_finite() {
734      result.push(MediaQueryRule::Pair(MediaRulePair::new(
735        format!("max-{}", dim),
736        MediaRuleValue::Length(Length::new(upper, units.get(dim).unwrap().clone())),
737      )));
738    }
739  }
740  Ok(if result.is_empty() { rules } else { result })
741}
742
743/// Basic media type parser: screen | print | all
744fn basic_media_type_parser() -> TokenParser<String> {
745  tokens::ident()
746    .map(
747      |token| {
748        if let SimpleToken::Ident(value) = token {
749          value
750        } else {
751          "all".to_string()
752        }
753      },
754      Some("extract_media_type"),
755    )
756    .where_fn(
757      |value| matches!(value.as_str(), "screen" | "print" | "all"),
758      Some("valid_media_type"),
759    )
760}
761
762/// Media keyword parser with optional not/only modifiers
763fn media_keyword_parser() -> TokenParser<MediaQueryRule> {
764  TokenParser::new(
765    |tokens| {
766      let mut not_value = false;
767      let mut only_value = false; // Default to false instead of None
768
769      // Try to parse optional "not" at the beginning
770      if let Ok(Some(SimpleToken::Ident(val))) = tokens.peek()
771        && val == "not"
772      {
773        tokens.consume_next_token()?; // consume "not"
774        not_value = true;
775
776        // Consume whitespace after "not"
777        while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
778          tokens.consume_next_token()?;
779        }
780      }
781
782      // Try to parse optional "only" after "not" (or at beginning if no "not")
783      if let Ok(Some(SimpleToken::Ident(val))) = tokens.peek()
784        && val == "only"
785      {
786        tokens.consume_next_token()?; // consume "only"
787        only_value = true;
788
789        // Consume whitespace after "only"
790        while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
791          tokens.consume_next_token()?;
792        }
793      }
794
795      // Parse the media type (required)
796      let media_type = (basic_media_type_parser().run)(tokens)?;
797
798      Ok(MediaQueryRule::MediaKeyword(MediaKeyword::new(
799        media_type, not_value, only_value,
800      )))
801    },
802    "media_keyword_parser",
803  )
804}
805
806/// Media word rule parser for (color), (monochrome), etc.
807fn media_word_rule_parser() -> TokenParser<MediaQueryRule> {
808  tokens::ident()
809    .map(
810      |token| {
811        if let SimpleToken::Ident(value) = token {
812          value
813        } else {
814          "color".to_string()
815        }
816      },
817      Some("extract_word_rule"),
818    )
819    .where_fn(
820      |value| {
821        matches!(
822          value.as_str(),
823          "color" | "monochrome" | "grid" | "color-index"
824        )
825      },
826      Some("valid_word_rule"),
827    )
828    .surrounded_by(
829      TokenParser::<SimpleToken>::token(SimpleToken::LeftParen, Some("OpenParen")),
830      Some(TokenParser::<SimpleToken>::token(
831        SimpleToken::RightParen,
832        Some("CloseParen"),
833      )),
834    )
835    .map(
836      |keyword| MediaQueryRule::WordRule(MediaWordRule::new(keyword)),
837      Some("create_word_rule"),
838    )
839}
840
841fn media_rule_value_parser() -> TokenParser<MediaRuleValue> {
842  TokenParser::one_of(vec![
843    Calc::parser().map(
844      |calc| MediaRuleValue::String(calc.to_string()),
845      Some("calc_to_string"),
846    ),
847    // Dimensions (e.g., 768px)
848    tokens::dimension().map(
849      |token| {
850        if let SimpleToken::Dimension { value, unit } = token {
851          MediaRuleValue::Length(Length::new(value as f32, unit))
852        } else {
853          MediaRuleValue::Number(0.0)
854        }
855      },
856      Some("dimension_to_length"),
857    ),
858    tokens::ident().map(
859      |token| {
860        if let SimpleToken::Ident(value) = token {
861          MediaRuleValue::String(value)
862        } else {
863          MediaRuleValue::String("".to_string())
864        }
865      },
866      Some("ident_to_string"),
867    ),
868    // Fraction parsing (number / number) like aspect-ratio: 16/9
869    TokenParser::new(
870      |tokens| {
871        // Parse first number
872        let first_num = if let Ok(Some(SimpleToken::Number(value))) = tokens.consume_next_token() {
873          value as i32
874        } else {
875          return Err(CssParseError::ParseError {
876            message: "Expected first number in fraction".to_string(),
877          });
878        };
879
880        // Optional whitespace before slash
881        while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
882          tokens.consume_next_token()?;
883        }
884
885        // Parse slash delimiter
886        if let Ok(Some(SimpleToken::Delim(ch))) = tokens.consume_next_token() {
887          if ch != '/' {
888            return Err(CssParseError::ParseError {
889              message: "Expected '/' in fraction".to_string(),
890            });
891          }
892        } else {
893          return Err(CssParseError::ParseError {
894            message: "Expected '/' delimiter".to_string(),
895          });
896        }
897
898        // Optional whitespace after slash
899        while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
900          tokens.consume_next_token()?;
901        }
902
903        // Parse second number
904        let second_num = if let Ok(Some(SimpleToken::Number(value))) = tokens.consume_next_token() {
905          value as i32
906        } else {
907          return Err(CssParseError::ParseError {
908            message: "Expected second number in fraction".to_string(),
909          });
910        };
911
912        Ok(MediaRuleValue::Fraction(Fraction {
913          numerator: first_num,
914          denominator: second_num,
915        }))
916      },
917      "fraction_parser",
918    ),
919    // Numbers (must be last to avoid consuming numbers that are part of fractions)
920    tokens::number().map(
921      |token| {
922        if let SimpleToken::Number(value) = token {
923          MediaRuleValue::Number(value as f32)
924        } else {
925          MediaRuleValue::Number(0.0)
926        }
927      },
928      Some("number_to_value"),
929    ),
930  ])
931}
932
933/// Simple pair parser for (key: value) media features
934fn simple_pair_parser(value_parser: TokenParser<MediaRuleValue>) -> TokenParser<MediaQueryRule> {
935  let value_parser_rc = value_parser.run.clone(); // Clone the Rc<dyn Fn>
936
937  TokenParser::new(
938    move |tokens| {
939      // Parse opening parenthesis
940      if let Ok(Some(SimpleToken::LeftParen)) = tokens.consume_next_token() {
941        // Good, we have opening paren
942      } else {
943        return Err(CssParseError::ParseError {
944          message: "Expected opening parenthesis".to_string(),
945        });
946      }
947
948      // Optional whitespace after opening paren
949      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
950        tokens.consume_next_token()?;
951      }
952
953      // Parse key (identifier)
954      let key = if let Ok(Some(SimpleToken::Ident(key_name))) = tokens.consume_next_token() {
955        key_name
956      } else {
957        return Err(CssParseError::ParseError {
958          message: "Expected media feature name".to_string(),
959        });
960      };
961
962      // Optional whitespace before colon
963      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
964        tokens.consume_next_token()?;
965      }
966
967      // Parse colon
968      if let Ok(Some(SimpleToken::Colon)) = tokens.consume_next_token() {
969        // Good, we have colon
970      } else {
971        return Err(CssParseError::ParseError {
972          message: "Expected colon after media feature name".to_string(),
973        });
974      }
975
976      // Optional whitespace after colon
977      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
978        tokens.consume_next_token()?;
979      }
980
981      // Parse value using the cloned value parser
982      let value = (value_parser_rc)(tokens)?;
983
984      // Optional whitespace before closing paren
985      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
986        tokens.consume_next_token()?;
987      }
988
989      // Parse closing parenthesis
990      if let Ok(Some(SimpleToken::RightParen)) = tokens.consume_next_token() {
991        // Good, we have closing paren
992      } else {
993        return Err(CssParseError::ParseError {
994          message: "Expected closing parenthesis".to_string(),
995        });
996      }
997
998      Ok(MediaQueryRule::Pair(MediaRulePair::new(key, value)))
999    },
1000    "simple_pair_parser",
1001  )
1002}
1003
1004/// Combined inequality parser - handles both forward and reversed inequalities
1005fn combined_inequality_parser() -> TokenParser<MediaQueryRule> {
1006  TokenParser::one_of(vec![
1007    media_inequality_rule_parser(),          // Forward: (width <= 1250px)
1008    media_inequality_rule_parser_reversed(), // Reversed: (1250px >= width)
1009  ])
1010}
1011
1012/// Forward inequality parser: (width <= 1250px) or (width < 1250px)
1013fn media_inequality_rule_parser() -> TokenParser<MediaQueryRule> {
1014  TokenParser::new(
1015    |tokens| {
1016      // Expect opening paren
1017      let open_token = tokens
1018        .consume_next_token()?
1019        .ok_or(CssParseError::ParseError {
1020          message: "Expected opening parenthesis".to_string(),
1021        })?;
1022      if !matches!(open_token, SimpleToken::LeftParen) {
1023        return Err(CssParseError::ParseError {
1024          message: format!("Expected '(' token, got {:?}", open_token),
1025        });
1026      }
1027
1028      // Skip optional whitespace
1029      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1030        tokens.consume_next_token()?;
1031      }
1032
1033      // Parse property name (width or height)
1034      let key_token = tokens
1035        .consume_next_token()?
1036        .ok_or(CssParseError::ParseError {
1037          message: "Expected property name".to_string(),
1038        })?;
1039      let key = if let SimpleToken::Ident(name) = key_token {
1040        if name == "width" || name == "height" {
1041          name
1042        } else {
1043          return Err(CssParseError::ParseError {
1044            message: format!("Expected 'width' or 'height', got '{}'", name),
1045          });
1046        }
1047      } else {
1048        return Err(CssParseError::ParseError {
1049          message: format!("Expected identifier, got {:?}", key_token),
1050        });
1051      };
1052
1053      // Skip optional whitespace
1054      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1055        tokens.consume_next_token()?;
1056      }
1057
1058      // Parse operator (< or >)
1059      let op_token = tokens
1060        .consume_next_token()?
1061        .ok_or(CssParseError::ParseError {
1062          message: "Expected comparison operator".to_string(),
1063        })?;
1064      let op = if let SimpleToken::Delim(op_char) = op_token {
1065        if op_char == '<' || op_char == '>' {
1066          op_char
1067        } else {
1068          return Err(CssParseError::ParseError {
1069            message: format!("Expected '<' or '>', got '{}'", op_char),
1070          });
1071        }
1072      } else {
1073        return Err(CssParseError::ParseError {
1074          message: format!("Expected delimiter, got {:?}", op_token),
1075        });
1076      };
1077
1078      // Skip optional whitespace
1079      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1080        tokens.consume_next_token()?;
1081      }
1082
1083      // Parse optional equals sign
1084      let has_equals = if let Ok(Some(SimpleToken::Delim('='))) = tokens.peek() {
1085        tokens.consume_next_token()?;
1086        true
1087      } else {
1088        false
1089      };
1090
1091      // Skip optional whitespace
1092      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1093        tokens.consume_next_token()?;
1094      }
1095
1096      // Parse dimension value
1097      let dim_token = tokens
1098        .consume_next_token()?
1099        .ok_or(CssParseError::ParseError {
1100          message: "Expected dimension value".to_string(),
1101        })?;
1102      let mut dimension = if let SimpleToken::Dimension { value, unit } = dim_token {
1103        Length::new(value as f32, unit)
1104      } else {
1105        return Err(CssParseError::ParseError {
1106          message: format!("Expected dimension, got {:?}", dim_token),
1107        });
1108      };
1109
1110      // Skip optional whitespace
1111      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1112        tokens.consume_next_token()?;
1113      }
1114
1115      // Expect closing paren
1116      let close_token = tokens
1117        .consume_next_token()?
1118        .ok_or(CssParseError::ParseError {
1119          message: "Expected closing parenthesis".to_string(),
1120        })?;
1121      if !matches!(close_token, SimpleToken::RightParen) {
1122        return Err(CssParseError::ParseError {
1123          message: format!("Expected ')' token, got {:?}", close_token),
1124        });
1125      }
1126
1127      if !has_equals {
1128        const EPSILON: f32 = 0.01;
1129        if op == '>' {
1130          // (width > 400px) -> min-width: 400.01px
1131          dimension.value += EPSILON;
1132        } else {
1133          // (width < 400px) -> max-width: 399.99px
1134          dimension.value -= EPSILON;
1135        }
1136      }
1137
1138      // Convert to final key: (width < 1250px) becomes max-width
1139      let final_key = if op == '>' {
1140        format!("min-{}", key)
1141      } else {
1142        format!("max-{}", key)
1143      };
1144
1145      Ok(MediaQueryRule::Pair(MediaRulePair::new(
1146        final_key,
1147        MediaRuleValue::Length(dimension),
1148      )))
1149    },
1150    "media_inequality_rule_parser",
1151  )
1152}
1153
1154/// Reversed inequality parser: (1250px >= width) or (1250px > width)
1155fn media_inequality_rule_parser_reversed() -> TokenParser<MediaQueryRule> {
1156  TokenParser::new(
1157    |tokens| {
1158      // Expect opening paren
1159      let open_token = tokens
1160        .consume_next_token()?
1161        .ok_or(CssParseError::ParseError {
1162          message: "Expected opening parenthesis".to_string(),
1163        })?;
1164      if !matches!(open_token, SimpleToken::LeftParen) {
1165        return Err(CssParseError::ParseError {
1166          message: format!("Expected '(' token, got {:?}", open_token),
1167        });
1168      }
1169
1170      // Skip optional whitespace
1171      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1172        tokens.consume_next_token()?;
1173      }
1174
1175      // Parse dimension value first
1176      let dim_token = tokens
1177        .consume_next_token()?
1178        .ok_or(CssParseError::ParseError {
1179          message: "Expected dimension value".to_string(),
1180        })?;
1181      let dimension = if let SimpleToken::Dimension { value, unit } = dim_token {
1182        MediaRuleValue::Length(Length::new(value as f32, unit))
1183      } else {
1184        return Err(CssParseError::ParseError {
1185          message: format!("Expected dimension, got {:?}", dim_token),
1186        });
1187      };
1188
1189      // Skip optional whitespace
1190      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1191        tokens.consume_next_token()?;
1192      }
1193
1194      // Parse operator (< or >)
1195      let op_token = tokens
1196        .consume_next_token()?
1197        .ok_or(CssParseError::ParseError {
1198          message: "Expected comparison operator".to_string(),
1199        })?;
1200      let op = if let SimpleToken::Delim(op_char) = op_token {
1201        if op_char == '<' || op_char == '>' {
1202          op_char
1203        } else {
1204          return Err(CssParseError::ParseError {
1205            message: format!("Expected '<' or '>', got '{}'", op_char),
1206          });
1207        }
1208      } else {
1209        return Err(CssParseError::ParseError {
1210          message: format!("Expected delimiter, got {:?}", op_token),
1211        });
1212      };
1213
1214      // Skip optional whitespace
1215      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1216        tokens.consume_next_token()?;
1217      }
1218
1219      // Parse optional equals sign
1220      let has_equals = if let Ok(Some(SimpleToken::Delim('='))) = tokens.peek() {
1221        tokens.consume_next_token()?;
1222        true
1223      } else {
1224        false
1225      };
1226
1227      // Skip optional whitespace
1228      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1229        tokens.consume_next_token()?;
1230      }
1231
1232      // Parse property name (width or height)
1233      let key_token = tokens
1234        .consume_next_token()?
1235        .ok_or(CssParseError::ParseError {
1236          message: "Expected property name".to_string(),
1237        })?;
1238      let key = if let SimpleToken::Ident(name) = key_token {
1239        if name == "width" || name == "height" {
1240          name
1241        } else {
1242          return Err(CssParseError::ParseError {
1243            message: format!("Expected 'width' or 'height', got '{}'", name),
1244          });
1245        }
1246      } else {
1247        return Err(CssParseError::ParseError {
1248          message: format!("Expected identifier, got {:?}", key_token),
1249        });
1250      };
1251
1252      // Skip optional whitespace
1253      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1254        tokens.consume_next_token()?;
1255      }
1256
1257      // Expect closing paren
1258      let close_token = tokens
1259        .consume_next_token()?
1260        .ok_or(CssParseError::ParseError {
1261          message: "Expected closing parenthesis".to_string(),
1262        })?;
1263      if !matches!(close_token, SimpleToken::RightParen) {
1264        return Err(CssParseError::ParseError {
1265          message: format!("Expected ')' token, got {:?}", close_token),
1266        });
1267      }
1268
1269      let mut adjusted_dimension = dimension;
1270      if !has_equals {
1271        const EPSILON: f32 = 0.01;
1272        if let MediaRuleValue::Length(ref mut length) = adjusted_dimension {
1273          if op == '>' {
1274            // (1250px > width) -> max-width: 1249.99px
1275            length.value -= EPSILON;
1276          } else {
1277            // (1250px < width) -> min-width: 1250.01px
1278            length.value += EPSILON;
1279          }
1280        }
1281      }
1282
1283      // Convert to final key: (1250px > width) becomes max-width
1284      let final_key = if op == '>' {
1285        format!("max-{}", key)
1286      } else {
1287        format!("min-{}", key)
1288      };
1289
1290      Ok(MediaQueryRule::Pair(MediaRulePair::new(
1291        final_key,
1292        adjusted_dimension,
1293      )))
1294    },
1295    "media_inequality_rule_parser_reversed",
1296  )
1297}
1298
1299/// Double inequality parser: (500px <= width <= 1000px)
1300fn double_inequality_rule_parser() -> TokenParser<MediaQueryRule> {
1301  TokenParser::new(
1302    |tokens| {
1303      // Expect opening paren
1304      let open_token = tokens
1305        .consume_next_token()?
1306        .ok_or(CssParseError::ParseError {
1307          message: "Expected opening parenthesis".to_string(),
1308        })?;
1309      if !matches!(open_token, SimpleToken::LeftParen) {
1310        return Err(CssParseError::ParseError {
1311          message: format!("Expected '(' token, got {:?}", open_token),
1312        });
1313      }
1314
1315      // Skip optional whitespace
1316      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1317        tokens.consume_next_token()?;
1318      }
1319
1320      // Parse lower bound dimension
1321      let lower_dim_token = tokens
1322        .consume_next_token()?
1323        .ok_or(CssParseError::ParseError {
1324          message: "Expected lower bound dimension".to_string(),
1325        })?;
1326      let lower_dimension = if let SimpleToken::Dimension { value, unit } = lower_dim_token {
1327        MediaRuleValue::Length(Length::new(value as f32, unit))
1328      } else {
1329        return Err(CssParseError::ParseError {
1330          message: format!("Expected dimension, got {:?}", lower_dim_token),
1331        });
1332      };
1333
1334      // Skip optional whitespace
1335      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1336        tokens.consume_next_token()?;
1337      }
1338
1339      // Parse first operator (< or >)
1340      let op1_token = tokens
1341        .consume_next_token()?
1342        .ok_or(CssParseError::ParseError {
1343          message: "Expected first comparison operator".to_string(),
1344        })?;
1345      let _op1 = if let SimpleToken::Delim(op_char) = op1_token {
1346        if op_char == '<' || op_char == '>' {
1347          op_char
1348        } else {
1349          return Err(CssParseError::ParseError {
1350            message: format!("Expected '<' or '>', got '{}'", op_char),
1351          });
1352        }
1353      } else {
1354        return Err(CssParseError::ParseError {
1355          message: format!("Expected delimiter, got {:?}", op1_token),
1356        });
1357      };
1358
1359      // Skip optional whitespace
1360      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1361        tokens.consume_next_token()?;
1362      }
1363
1364      // Parse optional first equals sign
1365      let _eq1 = if let Ok(Some(SimpleToken::Delim('='))) = tokens.peek() {
1366        tokens.consume_next_token()?;
1367        true
1368      } else {
1369        false
1370      };
1371
1372      // Skip optional whitespace
1373      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1374        tokens.consume_next_token()?;
1375      }
1376
1377      // Parse property name (width or height)
1378      let key_token = tokens
1379        .consume_next_token()?
1380        .ok_or(CssParseError::ParseError {
1381          message: "Expected property name".to_string(),
1382        })?;
1383      let key = if let SimpleToken::Ident(name) = key_token {
1384        if name == "width" || name == "height" {
1385          name
1386        } else {
1387          return Err(CssParseError::ParseError {
1388            message: format!("Expected 'width' or 'height', got '{}'", name),
1389          });
1390        }
1391      } else {
1392        return Err(CssParseError::ParseError {
1393          message: format!("Expected identifier, got {:?}", key_token),
1394        });
1395      };
1396
1397      // Skip optional whitespace
1398      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1399        tokens.consume_next_token()?;
1400      }
1401
1402      // Parse second operator (< or >)
1403      let op2_token = tokens
1404        .consume_next_token()?
1405        .ok_or(CssParseError::ParseError {
1406          message: "Expected second comparison operator".to_string(),
1407        })?;
1408      let _op2 = if let SimpleToken::Delim(op_char) = op2_token {
1409        if op_char == '<' || op_char == '>' {
1410          op_char
1411        } else {
1412          return Err(CssParseError::ParseError {
1413            message: format!("Expected '<' or '>', got '{}'", op_char),
1414          });
1415        }
1416      } else {
1417        return Err(CssParseError::ParseError {
1418          message: format!("Expected delimiter, got {:?}", op2_token),
1419        });
1420      };
1421
1422      // Skip optional whitespace
1423      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1424        tokens.consume_next_token()?;
1425      }
1426
1427      // Parse optional second equals sign
1428      let _eq2 = if let Ok(Some(SimpleToken::Delim('='))) = tokens.peek() {
1429        tokens.consume_next_token()?;
1430        true
1431      } else {
1432        false
1433      };
1434
1435      // Skip optional whitespace
1436      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1437        tokens.consume_next_token()?;
1438      }
1439
1440      // Parse upper bound dimension
1441      let upper_dim_token = tokens
1442        .consume_next_token()?
1443        .ok_or(CssParseError::ParseError {
1444          message: "Expected upper bound dimension".to_string(),
1445        })?;
1446      let upper_dimension = if let SimpleToken::Dimension { value, unit } = upper_dim_token {
1447        MediaRuleValue::Length(Length::new(value as f32, unit))
1448      } else {
1449        return Err(CssParseError::ParseError {
1450          message: format!("Expected dimension, got {:?}", upper_dim_token),
1451        });
1452      };
1453
1454      // Skip optional whitespace
1455      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1456        tokens.consume_next_token()?;
1457      }
1458
1459      // Expect closing paren
1460      let close_token = tokens
1461        .consume_next_token()?
1462        .ok_or(CssParseError::ParseError {
1463          message: "Expected closing parenthesis".to_string(),
1464        })?;
1465      if !matches!(close_token, SimpleToken::RightParen) {
1466        return Err(CssParseError::ParseError {
1467          message: format!("Expected ')' token, got {:?}", close_token),
1468        });
1469      }
1470
1471      // Return an AND rule with min and max constraints
1472      // For (A op1 width op2 B), we need to determine min and max constraints
1473      let min_key = format!("min-{}", key);
1474      let max_key = format!("max-{}", key);
1475
1476      // Adjust values with epsilon only for strict inequalities
1477      const EPSILON: f32 = 0.01;
1478
1479      // Determine which dimension is min vs max based on the operators
1480      // For (A op1 width op2 B), we need to map to min/max constraints
1481
1482      let (min_value, max_value) = if !_eq1 && !_eq2 {
1483        // Both operators are strict
1484        if _op1 == '>' {
1485          (upper_dimension, lower_dimension) // (A > width > B): min = B, max = A
1486        } else {
1487          (lower_dimension, upper_dimension) // (A < width < B): min = A, max = B
1488        }
1489      } else if !_eq1 {
1490        // op1 is strict, op2 is inclusive
1491        if _op1 == '>' {
1492          (upper_dimension, lower_dimension) // (A > width >= B): min = B, max = A
1493        } else {
1494          (lower_dimension, upper_dimension) // (A < width >= B): min = A, max = B
1495        }
1496      } else if !_eq2 {
1497        // op1 is inclusive, op2 is strict
1498        if _op2 == '>' {
1499          (upper_dimension, lower_dimension) // (A >= width > B): min = B, max = A
1500        } else {
1501          (lower_dimension, upper_dimension) // (A >= width < B): min = A, max = B
1502        }
1503      } else {
1504        // Both operators are inclusive - determine assignment based on operator type
1505        if _op1 == '>' && _eq1 {
1506          // Reverse inclusive: (A >= width >= B)
1507          (upper_dimension, lower_dimension) // min = B, max = A
1508        } else if _op1 == '<' && _eq1 {
1509          // Forward inclusive: (A <= width <= B)
1510          (lower_dimension, upper_dimension) // min = A, max = B
1511        } else {
1512          // Fallback
1513          (lower_dimension, upper_dimension)
1514        }
1515      };
1516      let mut min_value = min_value;
1517      let mut max_value = max_value;
1518
1519      // Apply epsilon based on whether operators are strict or inclusive
1520      if let MediaRuleValue::Length(ref mut length) = min_value {
1521        // For min_value: if either operator is strict and would create a greater-than constraint, add epsilon
1522        if (_op1 == '<' && !_eq1) || (_op2 == '>' && !_eq2) {
1523          length.value += EPSILON; // width > min_value → min-width: min_value + epsilon
1524        }
1525      }
1526      if let MediaRuleValue::Length(ref mut length) = max_value {
1527        // For max_value: if either operator is strict and would create a less-than constraint, subtract epsilon
1528        if (_op1 == '>' && !_eq1) || (_op2 == '<' && !_eq2) {
1529          length.value -= EPSILON; // width < max_value → max-width: max_value - epsilon
1530        }
1531      }
1532
1533      Ok(MediaQueryRule::And(MediaAndRules::new(vec![
1534        MediaQueryRule::Pair(MediaRulePair::new(min_key, min_value)),
1535        MediaQueryRule::Pair(MediaRulePair::new(max_key, max_value)),
1536      ])))
1537    },
1538    "double_inequality_rule_parser",
1539  )
1540}
1541
1542/// Enhanced NOT parser that handles complex nested expressions
1543fn leading_not_parser() -> TokenParser<MediaQueryRule> {
1544  TokenParser::new(
1545    |tokens| {
1546      // Expect "not" keyword
1547      let not_token = tokens
1548        .consume_next_token()?
1549        .ok_or(CssParseError::ParseError {
1550          message: "Expected 'not' keyword".to_string(),
1551        })?;
1552      if let SimpleToken::Ident(keyword) = not_token {
1553        if keyword != "not" {
1554          return Err(CssParseError::ParseError {
1555            message: format!("Expected 'not', got '{}'", keyword),
1556          });
1557        }
1558      } else {
1559        return Err(CssParseError::ParseError {
1560          message: format!("Expected identifier, got {:?}", not_token),
1561        });
1562      }
1563
1564      // Skip whitespace after "not"
1565      let whitespace_token = tokens
1566        .consume_next_token()?
1567        .ok_or(CssParseError::ParseError {
1568          message: "Expected whitespace after 'not'".to_string(),
1569        })?;
1570      if !matches!(whitespace_token, SimpleToken::Whitespace) {
1571        return Err(CssParseError::ParseError {
1572          message: format!("Expected whitespace, got {:?}", whitespace_token),
1573        });
1574      }
1575
1576      // Parse the rule that follows "not" using normal rule parser
1577      let inner_rule = (normal_rule_parser().run)(tokens)?;
1578      Ok(MediaQueryRule::Not(MediaNotRule::new(inner_rule)))
1579    },
1580    "leading_not_parser",
1581  )
1582}
1583
1584/// This parser specifically handles "(not ...)" patterns
1585fn parenthesized_not_parser() -> TokenParser<MediaQueryRule> {
1586  TokenParser::new(
1587    |tokens| {
1588      // Expect opening parenthesis
1589      if let Ok(Some(SimpleToken::LeftParen)) = tokens.peek() {
1590        tokens.consume_next_token()?; // consume '('
1591
1592        // Skip optional whitespace
1593        while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1594          tokens.consume_next_token()?;
1595        }
1596
1597        // Expect "not" keyword
1598        if let Ok(Some(SimpleToken::Ident(keyword))) = tokens.peek() {
1599          if keyword == "not" {
1600            tokens.consume_next_token()?; // consume "not"
1601
1602            // Skip mandatory whitespace after "not"
1603            if let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1604              tokens.consume_next_token()?;
1605            } else {
1606              return Err(CssParseError::ParseError {
1607                message: "Expected whitespace after 'not' in parenthesized expression".to_string(),
1608              });
1609            }
1610
1611            // Parse the rule after "not" using the normal rule parser
1612            let inner_rule = (normal_rule_parser().run)(tokens)?;
1613
1614            // Skip optional whitespace before closing
1615            while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1616              tokens.consume_next_token()?;
1617            }
1618
1619            // Expect closing parenthesis
1620            if let Ok(Some(SimpleToken::RightParen)) = tokens.peek() {
1621              tokens.consume_next_token()?; // consume ')'
1622              Ok(MediaQueryRule::Not(MediaNotRule::new(inner_rule)))
1623            } else {
1624              Err(CssParseError::ParseError {
1625                message: "Expected closing parenthesis after parenthesized NOT expression"
1626                  .to_string(),
1627              })
1628            }
1629          } else {
1630            Err(CssParseError::ParseError {
1631              message: "Expected 'not' keyword in parenthesized NOT expression".to_string(),
1632            })
1633          }
1634        } else {
1635          Err(CssParseError::ParseError {
1636            message: "Expected 'not' keyword in parenthesized NOT expression".to_string(),
1637          })
1638        }
1639      } else {
1640        Err(CssParseError::ParseError {
1641          message: "Expected opening parenthesis for parenthesized NOT expression".to_string(),
1642        })
1643      }
1644    },
1645    "parenthesized_not_parser",
1646  )
1647}
1648
1649fn media_query_rule_parser() -> TokenParser<MediaQueryRule> {
1650  // Parse OR-separated rules (comma-separated)
1651  or_combinator_parser()
1652}
1653
1654/// Parse OR-separated media query rules (comma-separated OR "or" keyword)
1655fn or_combinator_parser() -> TokenParser<MediaQueryRule> {
1656  TokenParser::new(
1657    |tokens| {
1658      let mut rules = Vec::new();
1659
1660      // Parse the first rule
1661      let first_rule = (and_combinator_parser().run)(tokens)?;
1662      rules.push(first_rule);
1663
1664      // Parse additional OR rules (comma-separated OR "or" keyword)
1665      loop {
1666        let checkpoint = tokens.save_position();
1667
1668        // Skip optional whitespace
1669        while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1670          tokens.consume_next_token()?;
1671        }
1672
1673        // Check for comma-separated OR
1674        if let Ok(Some(SimpleToken::Comma)) = tokens.peek() {
1675          tokens.consume_next_token()?; // consume comma
1676
1677          // Skip optional whitespace after comma
1678          while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1679            tokens.consume_next_token()?;
1680          }
1681
1682          let rule = (and_combinator_parser().run)(tokens)?;
1683          rules.push(rule);
1684          continue;
1685        }
1686
1687        // Check for "or" keyword
1688        if let Ok(Some(SimpleToken::Ident(keyword))) = tokens.peek()
1689          && keyword == "or"
1690        {
1691          tokens.consume_next_token()?; // consume "or"
1692
1693          // Skip whitespace after "or"
1694          while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1695            tokens.consume_next_token()?;
1696          }
1697
1698          let rule = (and_combinator_parser().run)(tokens)?;
1699          rules.push(rule);
1700          continue;
1701        }
1702
1703        // No more OR patterns found, restore position and break
1704        tokens.restore_position(checkpoint)?;
1705        break;
1706      }
1707
1708      // If we only have one rule, return it directly
1709      if rules.len() == 1 {
1710        Ok(rules.into_iter().next().unwrap())
1711      } else {
1712        Ok(MediaQueryRule::Or(MediaOrRules::new(rules)))
1713      }
1714    },
1715    "or_combinator_parser",
1716  )
1717}
1718
1719/// Parse AND-separated media query rules
1720fn and_combinator_parser() -> TokenParser<MediaQueryRule> {
1721  TokenParser::new(
1722    |tokens| {
1723      let mut rules = Vec::new();
1724
1725      // Parse the first rule
1726      let first_rule = (normal_rule_parser().run)(tokens)?;
1727      rules.push(first_rule);
1728
1729      // Parse additional AND rules
1730      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1731        // Check if next non-whitespace token is "and"
1732        let checkpoint = tokens.save_position();
1733
1734        // Skip whitespace
1735        while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1736          tokens.consume_next_token()?;
1737        }
1738
1739        // Check for "and" keyword
1740        if let Ok(Some(SimpleToken::Ident(keyword))) = tokens.peek() {
1741          if keyword == "and" {
1742            tokens.consume_next_token()?; // consume "and"
1743
1744            // Skip whitespace after "and"
1745            while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1746              tokens.consume_next_token()?;
1747            }
1748
1749            let rule = (normal_rule_parser().run)(tokens)?;
1750            rules.push(rule);
1751          } else {
1752            // Not an "and", restore position and break
1753            tokens.restore_position(checkpoint)?;
1754            break;
1755          }
1756        } else {
1757          // No identifier after whitespace, restore position and break
1758          tokens.restore_position(checkpoint)?;
1759          break;
1760        }
1761      }
1762
1763      // If we only have one rule, return it directly
1764      if rules.len() == 1 {
1765        Ok(rules.into_iter().next().unwrap())
1766      } else {
1767        Ok(MediaQueryRule::And(MediaAndRules::new(rules)))
1768      }
1769    },
1770    "and_combinator_parser",
1771  )
1772}
1773
1774/// Normal rule parser that combines all rule types
1775fn normal_rule_parser() -> TokenParser<MediaQueryRule> {
1776  TokenParser::one_of(vec![
1777    // Media keyword parser must come first to handle "not screen", "only print" etc.
1778    // as MediaKeyword rules, not as separate NOT rules
1779    media_keyword_parser(),
1780    // Parenthesized NOT parser for "(not ...)" patterns
1781    parenthesized_not_parser(),
1782    // Leading not parser for cases where NOT is not part of media keywords
1783    leading_not_parser(),
1784    // Parenthesized expressions parser for complex nested cases
1785    parenthesized_expression_parser(),
1786    // Double inequality parser: (500px <= width <= 1000px)
1787    double_inequality_rule_parser(),
1788    // Combined inequality parser: (width <= 1250px) and (1250px >= width)
1789    combined_inequality_parser(),
1790    // Word rule parser for (color), (monochrome), (grid), (color-index)
1791    media_word_rule_parser(),
1792    // Pair parser for (key: value) patterns like (min-width: 768px)
1793    simple_pair_parser(media_rule_value_parser()),
1794  ])
1795}
1796
1797/// Parse parenthesized expressions, including complex NOT expressions
1798/// Handles: (not (max-width: 1024px)), ((min-width: 500px) and (max-width: 600px))
1799fn parenthesized_expression_parser() -> TokenParser<MediaQueryRule> {
1800  TokenParser::new(
1801    |tokens| {
1802      // Expect opening parenthesis
1803      if let Ok(Some(SimpleToken::LeftParen)) = tokens.peek() {
1804        tokens.consume_next_token()?; // consume '('
1805
1806        // Skip optional whitespace
1807        while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1808          tokens.consume_next_token()?;
1809        }
1810
1811        // Try to parse a NOT expression first
1812        if let Ok(Some(SimpleToken::Ident(keyword))) = tokens.peek()
1813          && keyword == "not"
1814        {
1815          // Parse NOT expression within parentheses
1816          let not_rule = (leading_not_parser().run)(tokens)?;
1817
1818          // Skip optional whitespace before closing
1819          while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1820            tokens.consume_next_token()?;
1821          }
1822
1823          // Expect closing parenthesis
1824          if let Ok(Some(SimpleToken::RightParen)) = tokens.peek() {
1825            tokens.consume_next_token()?; // consume ')'
1826            return Ok(not_rule);
1827          } else {
1828            return Err(CssParseError::ParseError {
1829              message: "Expected closing parenthesis after parenthesized NOT expression"
1830                .to_string(),
1831            });
1832          }
1833        }
1834
1835        // Parse complex expression using full combinator parser
1836        let inner_expression = (and_combinator_parser().run)(tokens)?;
1837
1838        // Skip optional whitespace before closing
1839        while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
1840          tokens.consume_next_token()?;
1841        }
1842
1843        // Expect closing parenthesis
1844        if let Ok(Some(SimpleToken::RightParen)) = tokens.peek() {
1845          tokens.consume_next_token()?; // consume ')'
1846          Ok(inner_expression)
1847        } else {
1848          Err(CssParseError::ParseError {
1849            message: "Expected closing parenthesis after parenthesized expression".to_string(),
1850          })
1851        }
1852      } else {
1853        Err(CssParseError::ParseError {
1854          message: "Expected opening parenthesis for parenthesized expression".to_string(),
1855        })
1856      }
1857    },
1858    "parenthesized_expression_parser",
1859  )
1860}
1861
1862#[cfg(test)]
1863mod tests {
1864  use stylex_macros::stylex_panic;
1865
1866  use super::*;
1867
1868  #[test]
1869  fn test_media_query_creation() {
1870    let query = MediaQuery::parser().parse_to_end("@media screen").unwrap();
1871    assert_eq!(query.to_string(), "@media screen");
1872  }
1873
1874  #[test]
1875  fn test_media_query_display() {
1876    let query = MediaQuery::parser()
1877      .parse_to_end("@media (min-width: 768px)")
1878      .unwrap();
1879    assert_eq!(format!("{}", query), "@media (min-width: 768px)");
1880  }
1881
1882  #[test]
1883  fn test_has_balanced_parens() {
1884    assert!(has_balanced_parens("(min-width: 768px)"));
1885    assert!(has_balanced_parens(
1886      "(min-width: 768px) and (max-width: 1200px)"
1887    ));
1888    assert!(has_balanced_parens("screen"));
1889    assert!(has_balanced_parens(""));
1890
1891    assert!(!has_balanced_parens("(min-width: 768px"));
1892    assert!(!has_balanced_parens("min-width: 768px)"));
1893    assert!(!has_balanced_parens("((min-width: 768px)"));
1894  }
1895
1896  #[test]
1897  fn test_validate_media_query_success() {
1898    let result = validate_media_query("@media (min-width: 768px)");
1899    assert!(result.is_ok());
1900
1901    let query = result.unwrap();
1902    assert_eq!(query.to_string(), "@media (min-width: 768px)");
1903  }
1904
1905  #[test]
1906  fn test_validate_media_query_unbalanced_parens() {
1907    let result = validate_media_query("@media (min-width: 768px");
1908    assert!(result.is_err());
1909    assert!(result.unwrap_err().contains("parentheses"));
1910  }
1911
1912  #[test]
1913  fn test_media_query_parser_creation() {
1914    // Test that parser can be created (even if it's a placeholder)
1915    let _parser = MediaQuery::parser();
1916  }
1917
1918  #[test]
1919  fn test_media_query_equality() {
1920    let query1 = MediaQuery::parser().parse_to_end("@media screen").unwrap();
1921    let query2 = MediaQuery::parser().parse_to_end("@media screen").unwrap();
1922    let query3 = MediaQuery::parser().parse_to_end("@media print").unwrap();
1923
1924    assert_eq!(query1, query2);
1925    assert_ne!(query1, query3);
1926  }
1927
1928  #[test]
1929  fn test_media_query_clone() {
1930    let query = MediaQuery::parser()
1931      .parse_to_end("@media (orientation: landscape)")
1932      .unwrap();
1933    let cloned = query.clone();
1934
1935    assert_eq!(query, cloned);
1936  }
1937
1938  #[test]
1939  fn test_common_media_queries() {
1940    // Test currently implemented media query features
1941    let implemented_queries = vec![
1942      "@media screen",
1943      "@media print",
1944      "@media (min-width: 768px)",
1945      "@media screen and (min-width: 768px)", // Now implemented!
1946      "@media (min-width: 768px) and (max-width: 1024px)", // Now implemented!
1947      "@media not screen",
1948      "@media only screen and (min-width: 768px)", // Now implemented!
1949    ];
1950
1951    for query_str in implemented_queries {
1952      let result = validate_media_query(query_str);
1953      assert!(result.is_ok(), "Failed to validate: {}", query_str);
1954
1955      let query = result.unwrap();
1956      assert_eq!(
1957        query.to_string(),
1958        query_str.replace(" screen and", " (screen) and")
1959      );
1960      println!("✅ Validated: {}", query_str);
1961    }
1962
1963    // All AND combinators are now implemented - test any remaining edge cases
1964    let edge_case_queries = vec![
1965      // Complex nested NOT expressions might still have issues
1966      // Add any edge cases here as they're discovered
1967    ];
1968
1969    for query_str in edge_case_queries {
1970      let result = validate_media_query(query_str);
1971      if result.is_err() {
1972        println!("✅ Correctly rejecting edge case: {}", query_str);
1973      } else {
1974        println!("⚠️  Unexpectedly accepting edge case: {}", query_str);
1975      }
1976    }
1977  }
1978
1979  #[test]
1980  fn test_complex_parentheses() {
1981    let supported_query = "@media (min-width: 768px)";
1982    let result = validate_media_query(supported_query);
1983    assert!(
1984      result.is_ok(),
1985      "Simple parentheses should work: {:?}",
1986      result
1987    );
1988
1989    // Test complex query with AND combinators - now implemented and should work!
1990    let and_combinator_query = "@media screen and ((min-width: 768px) and (max-width: 1024px))";
1991    let result = validate_media_query(and_combinator_query);
1992    assert!(
1993      result.is_ok(),
1994      "Complex AND combinators should now work: {:?}",
1995      result
1996    );
1997    println!(
1998      "✅ Complex parentheses with AND combinators now working: {}",
1999      and_combinator_query
2000    );
2001  }
2002
2003  #[test]
2004  fn test_media_query_normalization() {
2005    let input = "@media not (not (not (min-width: 400px)))";
2006    let parsed = MediaQuery::parser().parse_to_end(input).unwrap();
2007    println!("Triple NOT input: {}", input);
2008    println!("Triple NOT output: {}", parsed);
2009
2010    // Should be normalized to single NOT
2011    match &parsed.queries {
2012      MediaQueryRule::Not(not_rule) => match &not_rule.rule.as_ref() {
2013        MediaQueryRule::Pair(pair) => {
2014          assert_eq!(pair.key, "min-width");
2015          println!("✅ Triple NOT correctly normalized to single NOT");
2016        },
2017        _ => stylex_panic!("Expected Pair rule inside NOT, got: {:?}", not_rule.rule),
2018      },
2019      _ => stylex_panic!("Expected NOT rule at top level, got: {:?}", parsed.queries),
2020    }
2021
2022    // Test quadruple NOT normalization (should cancel out completely)
2023    let input_quad = "@media not (not (not (not (max-width: 500px))))";
2024    let parsed_quad = MediaQuery::parser().parse_to_end(input_quad).unwrap();
2025    println!("Quadruple NOT input: {}", input_quad);
2026    println!("Quadruple NOT output: {}", parsed_quad);
2027
2028    // Should be normalized to no NOT (just the pair)
2029    match &parsed_quad.queries {
2030      MediaQueryRule::Pair(pair) => {
2031        assert_eq!(pair.key, "max-width");
2032        println!("✅ Quadruple NOT correctly canceled out");
2033      },
2034      _ => stylex_panic!(
2035        "Expected Pair rule (no NOT), got: {:?}",
2036        parsed_quad.queries
2037      ),
2038    }
2039
2040    let complex_input = "@media (max-width: 1440px) and (not (max-width: 1024px)) and (not (max-width: 768px)) and (not (max-width: 458px))";
2041    let parsed_complex = MediaQuery::parser().parse_to_end(complex_input).unwrap();
2042    println!("Complex input: {}", complex_input);
2043    println!("Complex output: {}", parsed_complex);
2044
2045    match &parsed_complex.queries {
2046      MediaQueryRule::And(and_rules) => {
2047        println!(
2048          "✅ Complex NOT-AND expression normalized to AND with {} rules",
2049          and_rules.rules.len()
2050        );
2051        // Verify it contains both min and max constraints
2052        let has_min = and_rules
2053          .rules
2054          .iter()
2055          .any(|r| matches!(r, MediaQueryRule::Pair(pair) if pair.key.starts_with("min-")));
2056        let has_max = and_rules
2057          .rules
2058          .iter()
2059          .any(|r| matches!(r, MediaQueryRule::Pair(pair) if pair.key.starts_with("max-")));
2060        assert!(
2061          has_min && has_max,
2062          "Should contain both min and max constraints"
2063        );
2064      },
2065      _ => {
2066        // Might be a single constraint if merging results in one rule
2067        println!(
2068          "ℹ️  Complex expression normalized to single rule: {:?}",
2069          parsed_complex.queries
2070        );
2071      },
2072    }
2073  }
2074
2075  #[test]
2076  fn test_nested_unbalanced_parentheses() {
2077    let invalid_queries = vec![
2078      "@media ((min-width: 768px)",
2079      "@media (min-width: 768px))",
2080      "@media (((min-width: 768px)",
2081      "@media (min-width: 768px)))",
2082    ];
2083
2084    for query_str in invalid_queries {
2085      let result = validate_media_query(query_str);
2086      assert!(result.is_err(), "Should have failed: {}", query_str);
2087    }
2088  }
2089}