Skip to main content

style/queries/
feature_expression.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Parsing for query feature expressions, like `(foo: bar)` or
6//! `(width >= 400px)`.
7
8use super::feature::{Evaluator, QueryFeatureDescription};
9use super::feature::{FeatureFlags, KeywordDiscriminant};
10use crate::context::QuirksMode;
11use crate::custom_properties::{
12    self, ComputedSubstitutionFunctions, SubstitutionFunctionKind,
13    VariableValue as CustomVariableValue,
14};
15use crate::derives::*;
16use crate::dom::AttributeTracker;
17use crate::parser::{Parse, ParserContext};
18use crate::properties::CSSWideKeyword;
19use crate::properties_and_values::value::{ComputedValueComponent as Component, ValueInner};
20use crate::selector_map::PrecomputedHashSet;
21use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase};
22use crate::stylesheets::container_rule::AttrReferenceSet;
23use crate::stylesheets::{CssRuleType, Origin, UrlExtraData};
24use crate::values::computed::{self, CSSPixelLength, ToComputedValue};
25use crate::values::specified::{
26    Angle, Integer, Length, Number, Percentage, Ratio, Resolution, Time,
27};
28use crate::values::DashedIdent;
29use crate::{Atom, Zero};
30use cssparser::{Parser, Token};
31use selectors::kleene_value::KleeneValue;
32use std::cmp::Ordering;
33use std::fmt::{self, Write};
34use style_traits::{CssWriter, ParseError, ParsingMode, StyleParseErrorKind, ToCss};
35
36/// Whether we're parsing a media or container query feature.
37#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
38pub enum FeatureType {
39    /// We're parsing a media feature.
40    Media,
41    /// We're parsing a container feature.
42    Container,
43}
44
45impl FeatureType {
46    fn features(&self) -> &'static [QueryFeatureDescription] {
47        #[cfg(feature = "gecko")]
48        use crate::gecko::media_features::MEDIA_FEATURES;
49        #[cfg(feature = "servo")]
50        use crate::servo::media_features::MEDIA_FEATURES;
51
52        use crate::stylesheets::container_rule::CONTAINER_FEATURES;
53
54        match *self {
55            FeatureType::Media => &MEDIA_FEATURES,
56            FeatureType::Container => &CONTAINER_FEATURES,
57        }
58    }
59
60    fn find_feature(&self, name: &Atom) -> Option<(usize, &'static QueryFeatureDescription)> {
61        self.features()
62            .iter()
63            .enumerate()
64            .find(|(_, f)| f.name == *name)
65    }
66}
67
68/// The kind of matching that should be performed on a feature value.
69#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
70enum LegacyRange {
71    /// At least the specified value.
72    Min,
73    /// At most the specified value.
74    Max,
75}
76
77/// The operator that was specified in this feature.
78#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
79pub enum Operator {
80    /// =
81    Equal,
82    /// >
83    GreaterThan,
84    /// >=
85    GreaterThanEqual,
86    /// <
87    LessThan,
88    /// <=
89    LessThanEqual,
90}
91
92impl ToCss for Operator {
93    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
94    where
95        W: fmt::Write,
96    {
97        dest.write_str(match *self {
98            Self::Equal => "=",
99            Self::LessThan => "<",
100            Self::LessThanEqual => "<=",
101            Self::GreaterThan => ">",
102            Self::GreaterThanEqual => ">=",
103        })
104    }
105}
106
107impl Operator {
108    fn is_compatible_with(self, right_op: Self) -> bool {
109        // Some operators are not compatible with each other in multi-range
110        // context.
111        match self {
112            Self::Equal => false,
113            Self::GreaterThan | Self::GreaterThanEqual => {
114                matches!(right_op, Self::GreaterThan | Self::GreaterThanEqual)
115            },
116            Self::LessThan | Self::LessThanEqual => {
117                matches!(right_op, Self::LessThan | Self::LessThanEqual)
118            },
119        }
120    }
121
122    fn evaluate(&self, cmp: Ordering) -> bool {
123        match *self {
124            Self::Equal => cmp == Ordering::Equal,
125            Self::GreaterThan => cmp == Ordering::Greater,
126            Self::GreaterThanEqual => cmp == Ordering::Equal || cmp == Ordering::Greater,
127            Self::LessThan => cmp == Ordering::Less,
128            Self::LessThanEqual => cmp == Ordering::Equal || cmp == Ordering::Less,
129        }
130    }
131
132    fn parse(input: &mut Parser) -> Result<Self, ParseError> {
133        let operator = match *input.next()? {
134            Token::Delim('=') => return Ok(Operator::Equal),
135            Token::Delim('>') => Operator::GreaterThan,
136            Token::Delim('<') => Operator::LessThan,
137            _ => return Err(ParseError::unexpected_token()),
138        };
139
140        // https://drafts.csswg.org/mediaqueries-4/#mq-syntax:
141        //
142        //     No whitespace is allowed between the “<” or “>”
143        //     <delim-token>s and the following “=” <delim-token>, if it’s
144        //     present.
145        //
146        // TODO(emilio): Maybe we should ignore comments as well?
147        // https://github.com/w3c/csswg-drafts/issues/6248
148        let parsed_equal = input
149            .try_parse(|i| {
150                let t = i.next_including_whitespace().map_err(|_| ())?;
151                if !matches!(t, Token::Delim('=')) {
152                    return Err(());
153                }
154                Ok(())
155            })
156            .is_ok();
157
158        if !parsed_equal {
159            return Ok(operator);
160        }
161
162        Ok(match operator {
163            Operator::GreaterThan => Operator::GreaterThanEqual,
164            Operator::LessThan => Operator::LessThanEqual,
165            _ => unreachable!(),
166        })
167    }
168}
169
170#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)]
171enum QueryFeatureExpressionKind {
172    /// Just the media feature name.
173    Empty,
174
175    /// A single value.
176    Single(QueryExpressionValue),
177
178    /// Legacy range syntax (min-*: value) or so.
179    LegacyRange(LegacyRange, QueryExpressionValue),
180
181    /// Modern range context syntax:
182    /// https://drafts.csswg.org/mediaqueries-5/#mq-range-context
183    Range {
184        left: Option<(Operator, QueryExpressionValue)>,
185        right: Option<(Operator, QueryExpressionValue)>,
186    },
187}
188
189impl QueryFeatureExpressionKind {
190    /// Evaluate a given range given an optional query value and a value from
191    /// the browser.
192    fn evaluate<T>(
193        &self,
194        context_value: T,
195        mut compute: impl FnMut(&QueryExpressionValue) -> T,
196    ) -> bool
197    where
198        T: PartialOrd + Zero,
199    {
200        match *self {
201            Self::Empty => !context_value.is_zero(),
202            Self::Single(ref value) => {
203                let value = compute(value);
204                let cmp = match context_value.partial_cmp(&value) {
205                    Some(c) => c,
206                    None => return false,
207                };
208                cmp == Ordering::Equal
209            },
210            Self::LegacyRange(ref range, ref value) => {
211                let value = compute(value);
212                let cmp = match context_value.partial_cmp(&value) {
213                    Some(c) => c,
214                    None => return false,
215                };
216                cmp == Ordering::Equal
217                    || match range {
218                        LegacyRange::Min => cmp == Ordering::Greater,
219                        LegacyRange::Max => cmp == Ordering::Less,
220                    }
221            },
222            Self::Range {
223                ref left,
224                ref right,
225            } => {
226                debug_assert!(left.is_some() || right.is_some());
227                if let Some((op, value)) = left {
228                    let value = compute(value);
229                    let cmp = match value.partial_cmp(&context_value) {
230                        Some(c) => c,
231                        None => return false,
232                    };
233                    if !op.evaluate(cmp) {
234                        return false;
235                    }
236                }
237                if let Some((op, value)) = right {
238                    let value = compute(value);
239                    let cmp = match context_value.partial_cmp(&value) {
240                        Some(c) => c,
241                        None => return false,
242                    };
243                    if !op.evaluate(cmp) {
244                        return false;
245                    }
246                }
247                true
248            },
249        }
250    }
251
252    /// Non-ranged features only need to compare to one value at most.
253    fn non_ranged_value(&self) -> Option<&QueryExpressionValue> {
254        match *self {
255            Self::Empty => None,
256            Self::Single(ref v) => Some(v),
257            Self::LegacyRange(..) | Self::Range { .. } => {
258                debug_assert!(false, "Unexpected ranged value in non-ranged feature!");
259                None
260            },
261        }
262    }
263}
264
265/// A feature expression contains a reference to the feature, the value the
266/// query contained, and the range to evaluate.
267#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)]
268pub struct QueryFeatureExpression {
269    feature_type: FeatureType,
270    feature_index: usize,
271    kind: QueryFeatureExpressionKind,
272}
273
274impl ToCss for QueryFeatureExpression {
275    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
276    where
277        W: fmt::Write,
278    {
279        dest.write_char('(')?;
280
281        match self.kind {
282            QueryFeatureExpressionKind::Empty => self.write_name(dest)?,
283            QueryFeatureExpressionKind::Single(ref v)
284            | QueryFeatureExpressionKind::LegacyRange(_, ref v) => {
285                self.write_name(dest)?;
286                dest.write_str(": ")?;
287                v.to_css(dest, Some(self))?;
288            },
289            QueryFeatureExpressionKind::Range {
290                ref left,
291                ref right,
292            } => {
293                if let Some((op, val)) = left {
294                    val.to_css(dest, Some(self))?;
295                    dest.write_char(' ')?;
296                    op.to_css(dest)?;
297                    dest.write_char(' ')?;
298                }
299                self.write_name(dest)?;
300                if let Some((op, val)) = right {
301                    dest.write_char(' ')?;
302                    op.to_css(dest)?;
303                    dest.write_char(' ')?;
304                    val.to_css(dest, Some(self))?;
305                }
306            },
307        }
308        dest.write_char(')')
309    }
310}
311
312fn consume_operation_or_colon(input: &mut Parser) -> Result<Option<Operator>, ParseError> {
313    if input.try_parse(|input| input.expect_colon()).is_ok() {
314        return Ok(None);
315    }
316    Operator::parse(input).map(Some)
317}
318
319#[allow(unused_variables)]
320fn disabled_by_pref(feature: &Atom, context: &ParserContext) -> bool {
321    #[cfg(feature = "gecko")]
322    {
323        // prefers-reduced-transparency is always enabled in the ua and chrome. On
324        // the web it is hidden behind a preference (see Bug 1822176).
325        if *feature == atom!("prefers-reduced-transparency") {
326            return !context.chrome_rules_enabled()
327                && !crate::pref!("layout.css.prefers-reduced-transparency.enabled");
328        }
329
330        // inverted-colors is always enabled in the ua and chrome. On
331        // the web it is hidden behind a preference.
332        if *feature == atom!("inverted-colors") {
333            return !context.chrome_rules_enabled()
334                && !crate::pref!("layout.css.inverted-colors.enabled");
335        }
336    }
337    false
338}
339
340impl QueryFeatureExpression {
341    fn new(
342        feature_type: FeatureType,
343        feature_index: usize,
344        kind: QueryFeatureExpressionKind,
345    ) -> Self {
346        debug_assert!(feature_index < feature_type.features().len());
347        Self {
348            feature_type,
349            feature_index,
350            kind,
351        }
352    }
353
354    fn write_name<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
355    where
356        W: fmt::Write,
357    {
358        let feature = self.feature();
359        if feature.flags.contains(FeatureFlags::WEBKIT_PREFIX) {
360            dest.write_str("-webkit-")?;
361        }
362
363        if let QueryFeatureExpressionKind::LegacyRange(range, _) = self.kind {
364            match range {
365                LegacyRange::Min => dest.write_str("min-")?,
366                LegacyRange::Max => dest.write_str("max-")?,
367            }
368        }
369
370        // NB: CssStringWriter not needed, feature names are under control.
371        write!(dest, "{}", feature.name)?;
372
373        Ok(())
374    }
375
376    fn feature(&self) -> &'static QueryFeatureDescription {
377        &self.feature_type.features()[self.feature_index]
378    }
379
380    /// Returns the feature flags for our feature.
381    pub fn feature_flags(&self) -> FeatureFlags {
382        self.feature().flags
383    }
384
385    fn parse_feature_name(
386        context: &ParserContext,
387        input: &mut Parser,
388        feature_type: FeatureType,
389    ) -> Result<(usize, Option<LegacyRange>), ParseError> {
390        let mut flags = FeatureFlags::empty();
391        let ident = input.expect_ident()?;
392
393        if context.chrome_rules_enabled() {
394            flags.insert(FeatureFlags::CHROME_AND_UA_ONLY);
395        }
396
397        let mut feature_name = &**ident;
398        if starts_with_ignore_ascii_case(feature_name, "-webkit-") {
399            feature_name = &feature_name[8..];
400            flags.insert(FeatureFlags::WEBKIT_PREFIX);
401        }
402
403        let range = if starts_with_ignore_ascii_case(feature_name, "min-") {
404            feature_name = &feature_name[4..];
405            Some(LegacyRange::Min)
406        } else if starts_with_ignore_ascii_case(feature_name, "max-") {
407            feature_name = &feature_name[4..];
408            Some(LegacyRange::Max)
409        } else {
410            None
411        };
412
413        let atom = Atom::from(string_as_ascii_lowercase(feature_name));
414        let (feature_index, feature) = match feature_type.find_feature(&atom) {
415            Some((i, f)) => (i, f),
416            None => {
417                return Err(ParseError::custom(
418                    StyleParseErrorKind::MediaQueryExpectedFeatureName,
419                ))
420            },
421        };
422
423        if disabled_by_pref(&feature.name, context)
424            || !flags.contains(feature.flags.parsing_requirements())
425            || (range.is_some() && !feature.allows_ranges())
426        {
427            return Err(ParseError::custom(
428                StyleParseErrorKind::MediaQueryExpectedFeatureName,
429            ));
430        }
431
432        Ok((feature_index, range))
433    }
434
435    /// Parses the following range syntax:
436    ///
437    ///   (feature-value <operator> feature-name)
438    ///   (feature-value <operator> feature-name <operator> feature-value)
439    fn parse_multi_range_syntax(
440        context: &ParserContext,
441        input: &mut Parser,
442        feature_type: FeatureType,
443    ) -> Result<Self, ParseError> {
444        let start = input.state();
445
446        // To parse the values, we first need to find the feature name. We rely
447        // on feature values for ranged features not being able to be top-level
448        // <ident>s, which holds.
449        let feature_index = loop {
450            // NOTE: parse_feature_name advances the input.
451            if let Ok((index, range)) = Self::parse_feature_name(context, input, feature_type) {
452                if range.is_some() {
453                    // Ranged names are not allowed here.
454                    return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
455                }
456                break index;
457            }
458            if input.is_exhausted() {
459                return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
460            }
461        };
462
463        input.reset(&start);
464
465        let feature = &feature_type.features()[feature_index];
466        let left_val = QueryExpressionValue::parse(feature, context, input)?;
467        let left_op = Operator::parse(input)?;
468
469        {
470            let (parsed_index, _) = Self::parse_feature_name(context, input, feature_type)?;
471            debug_assert_eq!(
472                parsed_index, feature_index,
473                "How did we find a different feature?"
474            );
475        }
476
477        let right_op = input.try_parse(Operator::parse).ok();
478        let right = match right_op {
479            Some(op) => {
480                if !left_op.is_compatible_with(op) {
481                    return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
482                }
483                Some((op, QueryExpressionValue::parse(feature, context, input)?))
484            },
485            None => None,
486        };
487        Ok(Self::new(
488            feature_type,
489            feature_index,
490            QueryFeatureExpressionKind::Range {
491                left: Some((left_op, left_val)),
492                right,
493            },
494        ))
495    }
496
497    /// Parse a feature expression where we've already consumed the parenthesis.
498    pub fn parse_in_parenthesis_block(
499        context: &ParserContext,
500        input: &mut Parser,
501        feature_type: FeatureType,
502    ) -> Result<Self, ParseError> {
503        let (feature_index, range) =
504            match input.try_parse(|input| Self::parse_feature_name(context, input, feature_type)) {
505                Ok(v) => v,
506                Err(e) => {
507                    if let Ok(expr) = Self::parse_multi_range_syntax(context, input, feature_type) {
508                        return Ok(expr);
509                    }
510                    return Err(e);
511                },
512            };
513        let operator = input.try_parse(consume_operation_or_colon);
514        let operator = match operator {
515            Err(..) => {
516                // If there's no colon, this is a query of the form
517                // '(<feature>)', that is, there's no value specified.
518                //
519                // Gecko doesn't allow ranged expressions without a
520                // value, so just reject them here too.
521                if range.is_some() {
522                    return Err(ParseError::custom(
523                        StyleParseErrorKind::RangedExpressionWithNoValue,
524                    ));
525                }
526
527                return Ok(Self::new(
528                    feature_type,
529                    feature_index,
530                    QueryFeatureExpressionKind::Empty,
531                ));
532            },
533            Ok(operator) => operator,
534        };
535
536        let feature = &feature_type.features()[feature_index];
537
538        let value = QueryExpressionValue::parse(feature, context, input)
539            .map_err(|_| ParseError::custom(StyleParseErrorKind::MediaQueryExpectedFeatureValue))?;
540
541        let kind = match range {
542            Some(range) => {
543                if operator.is_some() {
544                    return Err(ParseError::custom(
545                        StyleParseErrorKind::MediaQueryUnexpectedOperator,
546                    ));
547                }
548                QueryFeatureExpressionKind::LegacyRange(range, value)
549            },
550            None => match operator {
551                Some(operator) => {
552                    if !feature.allows_ranges() {
553                        return Err(ParseError::custom(
554                            StyleParseErrorKind::MediaQueryUnexpectedOperator,
555                        ));
556                    }
557                    QueryFeatureExpressionKind::Range {
558                        left: None,
559                        right: Some((operator, value)),
560                    }
561                },
562                None => QueryFeatureExpressionKind::Single(value),
563            },
564        };
565
566        Ok(Self::new(feature_type, feature_index, kind))
567    }
568
569    /// Returns whether this "plain" feature query evaluates to true for the given device.
570    pub fn matches(&self, context: &computed::Context) -> KleeneValue {
571        macro_rules! expect {
572            ($variant:ident, $v:expr) => {
573                match *$v {
574                    QueryExpressionValue::$variant(ref v) => v,
575                    _ => unreachable!("Unexpected QueryExpressionValue"),
576                }
577            };
578        }
579
580        KleeneValue::from(match self.feature().evaluator {
581            Evaluator::Length(eval) => {
582                let v = eval(context);
583                self.kind
584                    .evaluate(v, |v| expect!(Length, v).to_computed_value(context))
585            },
586            Evaluator::OptionalLength(eval) => {
587                let v = match eval(context) {
588                    Some(v) => v,
589                    None => return KleeneValue::Unknown,
590                };
591                self.kind
592                    .evaluate(v, |v| expect!(Length, v).to_computed_value(context))
593            },
594            Evaluator::Integer(eval) => {
595                let v = eval(context);
596                self.kind
597                    .evaluate(v, |v| expect!(Integer, v).to_computed_value(context))
598            },
599            Evaluator::Float(eval) => {
600                let v = eval(context);
601                self.kind
602                    .evaluate(v, |v| expect!(Float, v).to_computed_value(context))
603            },
604            Evaluator::NumberRatio(eval) => {
605                let ratio = eval(context);
606                // A ratio of 0/0 behaves as the ratio 1/0, so we need to call used_value()
607                // to convert it if necessary.
608                // FIXME: we may need to update here once
609                // https://github.com/w3c/csswg-drafts/issues/4954 got resolved.
610                self.kind.evaluate(ratio, |v| {
611                    expect!(NumberRatio, v)
612                        .to_computed_value(context)
613                        .used_value()
614                })
615            },
616            Evaluator::OptionalNumberRatio(eval) => {
617                let ratio = match eval(context) {
618                    Some(v) => v,
619                    None => return KleeneValue::Unknown,
620                };
621                // See above for subtleties here.
622                self.kind.evaluate(ratio, |v| {
623                    expect!(NumberRatio, v)
624                        .to_computed_value(context)
625                        .used_value()
626                })
627            },
628            Evaluator::Resolution(eval) => {
629                let v = eval(context).dppx();
630                self.kind.evaluate(v, |v| {
631                    expect!(Resolution, v).to_computed_value(context).dppx()
632                })
633            },
634            Evaluator::Enumerated { evaluator, .. } => {
635                let computed = self
636                    .kind
637                    .non_ranged_value()
638                    .map(|v| *expect!(Enumerated, v));
639                return evaluator(context, computed);
640            },
641            Evaluator::BoolInteger(eval) => {
642                let computed = self
643                    .kind
644                    .non_ranged_value()
645                    .map(|v| expect!(BoolInteger, v).to_computed_value(context));
646                let boolean = eval(context);
647                computed.map_or(boolean, |v| v == boolean as i32)
648            },
649        })
650    }
651}
652
653/// A value found or expected in a expression.
654///
655/// FIXME(emilio): How should calc() serialize in the Number / Integer /
656/// BoolInteger / NumberRatio case, as computed or as specified value?
657///
658/// If the first, this would need to store the relevant values.
659///
660/// See: https://github.com/w3c/csswg-drafts/issues/1968
661#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
662pub enum QueryExpressionValue {
663    /// A length.
664    Length(Length),
665    /// An integer.
666    Integer(Integer),
667    /// A floating point value.
668    Float(Number),
669    /// A boolean value, specified as an integer (i.e., either 0 or 1).
670    BoolInteger(Integer),
671    /// A single non-negative number or two non-negative numbers separated by '/',
672    /// with optional whitespace on either side of the '/'.
673    NumberRatio(Ratio),
674    /// A resolution.
675    Resolution(Resolution),
676    /// An enumerated value, defined by the variant keyword table in the
677    /// feature's `mData` member.
678    Enumerated(KeywordDiscriminant),
679    /// Value types only used by style-range query expressions, not feature queries.
680    /// A CSS-wide keyword.
681    Keyword(CSSWideKeyword),
682    /// A percentage.
683    Percentage(Percentage),
684    /// An angle.
685    Angle(Angle),
686    /// A time value.
687    Time(Time),
688    /// A custom property name.
689    Custom(DashedIdent),
690    /// An arbitrary substitution function (var(), attr(), env()), stored as a string
691    /// for later evaluation. We store this as a custom-property value to make it easy
692    /// to resolve later.
693    Function(Box<CustomVariableValue>),
694}
695
696impl QueryExpressionValue {
697    fn to_css<W>(
698        &self,
699        dest: &mut CssWriter<W>,
700        for_expr: Option<&QueryFeatureExpression>,
701    ) -> fmt::Result
702    where
703        W: fmt::Write,
704    {
705        match *self {
706            QueryExpressionValue::Length(ref l) => l.to_css(dest),
707            QueryExpressionValue::Integer(ref v) => v.to_css(dest),
708            QueryExpressionValue::Float(ref v) => v.to_css(dest),
709            QueryExpressionValue::BoolInteger(ref v) => v.to_css(dest),
710            QueryExpressionValue::NumberRatio(ref ratio) => ratio.to_css(dest),
711            QueryExpressionValue::Resolution(ref r) => r.to_css(dest),
712            QueryExpressionValue::Keyword(k) => k.to_css(dest),
713            QueryExpressionValue::Percentage(ref v) => v.to_css(dest),
714            QueryExpressionValue::Angle(ref v) => v.to_css(dest),
715            QueryExpressionValue::Time(ref v) => v.to_css(dest),
716            QueryExpressionValue::Custom(ref v) => v.to_css(dest),
717            QueryExpressionValue::Function(ref f) => f.to_css(dest),
718            QueryExpressionValue::Enumerated(value) => match for_expr
719                .expect("caller should have passed for_expr")
720                .feature()
721                .evaluator
722            {
723                Evaluator::Enumerated { serializer, .. } => dest.write_str(&serializer(value)),
724                _ => unreachable!(),
725            },
726        }
727    }
728
729    fn parse(
730        for_feature: &QueryFeatureDescription,
731        context: &ParserContext,
732        input: &mut Parser,
733    ) -> Result<QueryExpressionValue, ParseError> {
734        Ok(match for_feature.evaluator {
735            Evaluator::OptionalLength(..) | Evaluator::Length(..) => {
736                let length = Length::parse(context, input)?;
737                QueryExpressionValue::Length(length)
738            },
739            Evaluator::Integer(..) => {
740                let integer = Integer::parse(context, input)?;
741                QueryExpressionValue::Integer(integer)
742            },
743            Evaluator::BoolInteger(..) => {
744                let integer = Integer::parse(context, input)?;
745                if matches!(integer.resolve(), Some(v) if v != 0 && v != 1) {
746                    return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
747                }
748                QueryExpressionValue::BoolInteger(integer)
749            },
750            Evaluator::Float(..) => {
751                let number = Number::parse(context, input)?;
752                QueryExpressionValue::Float(number)
753            },
754            Evaluator::OptionalNumberRatio(..) | Evaluator::NumberRatio(..) => {
755                use crate::values::specified::Ratio as SpecifiedRatio;
756                let ratio = SpecifiedRatio::parse(context, input)?;
757                QueryExpressionValue::NumberRatio(ratio)
758            },
759            Evaluator::Resolution(..) => {
760                QueryExpressionValue::Resolution(Resolution::parse(context, input)?)
761            },
762            Evaluator::Enumerated { parser, .. } => {
763                QueryExpressionValue::Enumerated(parser(context, input)?)
764            },
765        })
766    }
767
768    // Parse any of the types that can occur in a <style-range> query:
769    // <number>, <percentage>, <length>, <angle>, <time>, <frequency> or <resolution>,
770    // or a custom property name.
771    // NB: we don't currently implement the <frequency> type anywhere, so it is not
772    // parsed here.
773    fn parse_for_style_range(
774        context: &ParserContext,
775        input: &mut Parser,
776    ) -> Result<Self, ParseError> {
777        if let Ok(number) = input.try_parse(|i| Number::parse(context, i)) {
778            return Ok(Self::Float(number));
779        }
780        if let Ok(percent) = input.try_parse(|i| Percentage::parse(context, i)) {
781            return Ok(Self::Percentage(percent));
782        }
783        if let Ok(length) = input.try_parse(|i| Length::parse(context, i)) {
784            return Ok(Self::Length(length));
785        }
786        if let Ok(angle) = input.try_parse(|i| Angle::parse(context, i)) {
787            return Ok(Self::Angle(angle));
788        }
789        if let Ok(time) = input.try_parse(|i| Time::parse(context, i)) {
790            return Ok(Self::Time(time));
791        }
792        if let Ok(resolution) = input.try_parse(|i| Resolution::parse(context, i)) {
793            return Ok(Self::Resolution(resolution));
794        }
795        if let Ok(ident) = input.try_parse(|i| DashedIdent::parse(context, i)) {
796            return Ok(Self::Custom(ident));
797        }
798        if let Ok(keyword) = input.try_parse(|i| CSSWideKeyword::parse(i)) {
799            return Ok(Self::Keyword(keyword));
800        }
801        input.skip_whitespace();
802        let start = input.position();
803        if let Ok(Token::Function(name)) = input.next() {
804            // Helper to parse the function arg and store the complete expression (function
805            // name and parenthesized argument) into a CustomVariableValue.
806            let parse_func = |input: &mut Parser| -> Result<CustomVariableValue, ParseError> {
807                input.parse_nested_block(|i| i.expect_no_error_token().map_err(Into::into))?;
808                CustomVariableValue::parse(
809                    &mut Parser::new(input.slice_from(start)),
810                    Some(&context.namespaces.prefixes),
811                    context.url_data,
812                )
813            };
814
815            if SubstitutionFunctionKind::from_ident(name).is_ok() {
816                return Ok(Self::Function(Box::new(parse_func(input)?)));
817            }
818        }
819        Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError))
820    }
821
822    fn collect_attribute_references(&self, references: &mut AttrReferenceSet) {
823        if let Self::Function(f) = self {
824            f.collect_attribute_references(references)
825        }
826    }
827}
828
829/// https://drafts.csswg.org/css-conditional-5/#typedef-style-range
830#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)]
831pub enum QueryStyleRange {
832    /// A style-range for style container queries with two values
833    /// (val1 OP val2).
834    #[allow(missing_docs)]
835    StyleRange2 {
836        value1: QueryExpressionValue,
837        op1: Operator,
838        value2: QueryExpressionValue,
839    },
840
841    /// A style-range for style container queries with three values
842    /// (val1 OP val2 OP val3).
843    #[allow(missing_docs)]
844    StyleRange3 {
845        value1: QueryExpressionValue,
846        op1: Operator,
847        value2: QueryExpressionValue,
848        op2: Operator,
849        value3: QueryExpressionValue,
850    },
851}
852
853impl ToCss for QueryStyleRange {
854    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
855    where
856        W: fmt::Write,
857    {
858        match self {
859            Self::StyleRange2 {
860                value1,
861                op1,
862                value2,
863            } => {
864                value1.to_css(dest, None)?;
865                dest.write_char(' ')?;
866                op1.to_css(dest)?;
867                dest.write_char(' ')?;
868                value2.to_css(dest, None)
869            },
870            Self::StyleRange3 {
871                value1,
872                op1,
873                value2,
874                op2,
875                value3,
876            } => {
877                value1.to_css(dest, None)?;
878                dest.write_char(' ')?;
879                op1.to_css(dest)?;
880                dest.write_char(' ')?;
881                value2.to_css(dest, None)?;
882                dest.write_char(' ')?;
883                op2.to_css(dest)?;
884                dest.write_char(' ')?;
885                value3.to_css(dest, None)
886            },
887        }
888    }
889}
890
891impl QueryStyleRange {
892    /// Parses the following range syntax:
893    ///
894    ///   value <operator> value
895    ///   value <operator> value <operator> value
896    ///
897    /// This is only used when parsing @container style() queries; the feature_type
898    /// and index is hardcoded (and ignored).
899    pub fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
900        let value1 = QueryExpressionValue::parse_for_style_range(context, input)?;
901        let op1 = Operator::parse(input)?;
902        let value2 = QueryExpressionValue::parse_for_style_range(context, input)?;
903
904        if let Ok(op2) = input.try_parse(|i| Operator::parse(i)) {
905            if op1.is_compatible_with(op2) {
906                let value3 = QueryExpressionValue::parse_for_style_range(context, input)?;
907                return Ok(Self::StyleRange3 {
908                    value1,
909                    op1,
910                    value2,
911                    op2,
912                    value3,
913                });
914            }
915        }
916
917        Ok(Self::StyleRange2 {
918            value1,
919            op1,
920            value2,
921        })
922    }
923
924    /// Returns whether this style-range query evaluates to true for the given context.
925    pub fn evaluate(
926        &self,
927        context: &computed::Context,
928        attribute_tracker: &mut AttributeTracker,
929    ) -> KleeneValue {
930        match self {
931            QueryStyleRange::StyleRange2 {
932                value1,
933                op1,
934                value2,
935            } => Self::compare_values(
936                Self::resolve_value(
937                    value1,
938                    context,
939                    attribute_tracker,
940                    &mut PrecomputedHashSet::default(),
941                )
942                .as_ref(),
943                Self::resolve_value(
944                    value2,
945                    context,
946                    attribute_tracker,
947                    &mut PrecomputedHashSet::default(),
948                )
949                .as_ref(),
950            )
951            .is_some_and(|c| op1.evaluate(c))
952            .into(),
953
954            QueryStyleRange::StyleRange3 {
955                value1,
956                op1,
957                value2,
958                op2,
959                value3,
960            } => {
961                let v1 = Self::resolve_value(
962                    value1,
963                    context,
964                    attribute_tracker,
965                    &mut PrecomputedHashSet::default(),
966                );
967                let v2 = Self::resolve_value(
968                    value2,
969                    context,
970                    attribute_tracker,
971                    &mut PrecomputedHashSet::default(),
972                );
973                Self::compare_values(v1.as_ref(), v2.as_ref())
974                    .is_some_and(|c1| {
975                        op1.evaluate(c1)
976                            && Self::compare_values(
977                                v2.as_ref(),
978                                Self::resolve_value(
979                                    value3,
980                                    context,
981                                    attribute_tracker,
982                                    &mut PrecomputedHashSet::default(),
983                                )
984                                .as_ref(),
985                            )
986                            .is_some_and(|c2| op2.evaluate(c2))
987                    })
988                    .into()
989            },
990        }
991    }
992
993    // Resolve a QueryExpressionValue to its computed value for comparison.
994    fn resolve_value(
995        value: &QueryExpressionValue,
996        context: &computed::Context,
997        attribute_tracker: &mut AttributeTracker,
998        visited_set: &mut PrecomputedHashSet<DashedIdent>,
999    ) -> Option<Component> {
1000        match value {
1001            QueryExpressionValue::Custom(ident) => {
1002                // `ident` is the dashed ident, but we need the name
1003                // without "--" for custom-property lookup.
1004                let name = ident.undashed();
1005                let stylist = context
1006                    .builder
1007                    .stylist
1008                    .expect("container queries should have a stylist around");
1009                let registration = stylist.get_custom_property_registration(&name);
1010                let current_value = context
1011                    .inherited_custom_properties()
1012                    .get(registration, &name)?;
1013                match &current_value.v {
1014                    ValueInner::Component(component) => Some(component.clone()),
1015                    ValueInner::Universal(v) => {
1016                        // If visited_set.insert() returns false, ident was already seen
1017                        // and we risk infinite recursion, so instead return None
1018                        // (i.e. the value cannot be resolved).
1019                        if visited_set.insert(ident.clone()) {
1020                            Self::resolve_universal(
1021                                &v.css,
1022                                &v.url_data,
1023                                context,
1024                                attribute_tracker,
1025                                visited_set,
1026                            )
1027                        } else {
1028                            None
1029                        }
1030                    },
1031                    ValueInner::List(_) => {
1032                        debug_assert!(false, "We don't parse list values in style queries");
1033                        None
1034                    },
1035                }
1036            },
1037            QueryExpressionValue::Function(value) => {
1038                let sub_funcs = ComputedSubstitutionFunctions::new(
1039                    Some(context.inherited_custom_properties().clone()),
1040                    None,
1041                );
1042                let stylist = context
1043                    .builder
1044                    .stylist
1045                    .expect("container queries should have a stylist around");
1046                let substituted = custom_properties::substitute(
1047                    value,
1048                    &sub_funcs,
1049                    stylist,
1050                    context,
1051                    attribute_tracker,
1052                )
1053                .ok()?;
1054                Self::resolve_universal(
1055                    &substituted.css,
1056                    &value.url_data,
1057                    context,
1058                    attribute_tracker,
1059                    visited_set,
1060                )
1061            },
1062            QueryExpressionValue::Length(v) => {
1063                Some(Component::Length(v.to_computed_value(context)))
1064            },
1065            QueryExpressionValue::Float(v) => Some(Component::Number(v.to_computed_value(context))),
1066            QueryExpressionValue::Resolution(v) => {
1067                Some(Component::Resolution(v.to_computed_value(context)))
1068            },
1069            QueryExpressionValue::Percentage(v) => {
1070                Some(Component::Percentage(v.to_computed_value(context)))
1071            },
1072            QueryExpressionValue::Angle(v) => Some(Component::Angle(v.to_computed_value(context))),
1073            QueryExpressionValue::Time(v) => Some(Component::Time(v.to_computed_value(context))),
1074            // It's unclear to me what CSS-wide keywords would mean in a style-range query;
1075            // for now, at least, they'll just fail to resolve.
1076            QueryExpressionValue::Keyword(_) => None,
1077            _ => {
1078                debug_assert!(false, "unexpected value type in style range");
1079                None
1080            },
1081        }
1082    }
1083
1084    // If a custom-property QueryExpressionValue has a "universal-syntax" value, we need to
1085    // send the current CSS text of the value to QueryExpressionValue::parse_for_style_range
1086    // to try and resolve to a specific typed value.
1087    // After parsing, this will call back to QueryExpressionValue::resolve_value with the
1088    // parsed result, which has the potential for mutual recursion; we keep track of a
1089    // visited_set of custom property names to protect against this.
1090    fn resolve_universal(
1091        css_text: &str,
1092        url_data: &UrlExtraData,
1093        context: &computed::Context,
1094        attribute_tracker: &mut AttributeTracker,
1095        visited_set: &mut PrecomputedHashSet<DashedIdent>,
1096    ) -> Option<Component> {
1097        let parser_context = ParserContext::new(
1098            Origin::Author,
1099            url_data,
1100            Some(CssRuleType::Container),
1101            ParsingMode::DEFAULT,
1102            QuirksMode::NoQuirks,
1103            /* namespaces = */ Default::default(),
1104            /* error_reporter = */ None,
1105            /* use_counters = */ None,
1106            /* attr_taint */ Default::default(),
1107        );
1108        QueryExpressionValue::parse_for_style_range(&parser_context, &mut Parser::new(css_text))
1109            .ok()
1110            .and_then(|parsed| {
1111                Self::resolve_value(&parsed, context, attribute_tracker, visited_set)
1112            })
1113    }
1114
1115    fn compare_values(value1: Option<&Component>, value2: Option<&Component>) -> Option<Ordering> {
1116        let value1 = value1?;
1117        let value2 = value2?;
1118        match (value1, value2) {
1119            (Component::Length(v1), Component::Length(v2)) => v1.partial_cmp(v2),
1120            (Component::Number(v1), Component::Number(v2)) => v1.partial_cmp(v2),
1121            (Component::Resolution(v1), Component::Resolution(v2)) => {
1122                v1.dppx().partial_cmp(&v2.dppx())
1123            },
1124            (Component::Percentage(v1), Component::Percentage(v2)) => v1.partial_cmp(v2),
1125            (Component::Angle(v1), Component::Angle(v2)) => v1.partial_cmp(v2),
1126            (Component::Time(v1), Component::Time(v2)) => v1.partial_cmp(v2),
1127            (Component::Length(v1), Component::Number(v2)) => {
1128                if v2.is_zero() {
1129                    v1.partial_cmp(&CSSPixelLength::zero())
1130                } else {
1131                    None
1132                }
1133            },
1134            (Component::Number(v1), Component::Length(v2)) => {
1135                if v1.is_zero() {
1136                    CSSPixelLength::zero().partial_cmp(v2)
1137                } else {
1138                    None
1139                }
1140            },
1141            _ => None,
1142        }
1143    }
1144
1145    /// Get all the attributes referenced inside of an attr() function within
1146    /// all our QueryExpressionValues.
1147    pub fn collect_attribute_references(&self, references: &mut AttrReferenceSet) {
1148        match self {
1149            QueryStyleRange::StyleRange2 {
1150                value1,
1151                op1: _,
1152                value2,
1153            } => {
1154                value1.collect_attribute_references(references);
1155                value2.collect_attribute_references(references);
1156            },
1157            QueryStyleRange::StyleRange3 {
1158                value1,
1159                op1: _,
1160                value2,
1161                op2: _,
1162                value3,
1163            } => {
1164                value1.collect_attribute_references(references);
1165                value2.collect_attribute_references(references);
1166                value3.collect_attribute_references(references);
1167            },
1168        }
1169    }
1170}