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, VariableValue as CustomVariableValue,
13};
14use crate::derives::*;
15use crate::dom::AttributeTracker;
16use crate::parser::{Parse, ParserContext};
17use crate::properties::{self, CSSWideKeyword};
18use crate::properties_and_values::value::{ComputedValueComponent as Component, ValueInner};
19use crate::selector_map::PrecomputedHashSet;
20use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase};
21use crate::stylesheets::{CssRuleType, Origin, UrlExtraData};
22use crate::values::computed::{self, CSSPixelLength, Ratio, ToComputedValue};
23use crate::values::specified::{Angle, Integer, Length, Number, Percentage, Resolution, Time};
24use crate::values::{CSSFloat, DashedIdent};
25use crate::{Atom, Zero};
26use cssparser::{Parser, ParserInput, Token};
27use selectors::kleene_value::KleeneValue;
28use std::cmp::Ordering;
29use std::fmt::{self, Write};
30use style_traits::{CssWriter, ParseError, ParsingMode, StyleParseErrorKind, ToCss};
31
32/// Whether we're parsing a media or container query feature.
33#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
34pub enum FeatureType {
35    /// We're parsing a media feature.
36    Media,
37    /// We're parsing a container feature.
38    Container,
39}
40
41impl FeatureType {
42    fn features(&self) -> &'static [QueryFeatureDescription] {
43        #[cfg(feature = "gecko")]
44        use crate::gecko::media_features::MEDIA_FEATURES;
45        #[cfg(feature = "servo")]
46        use crate::servo::media_features::MEDIA_FEATURES;
47
48        use crate::stylesheets::container_rule::CONTAINER_FEATURES;
49
50        match *self {
51            FeatureType::Media => &MEDIA_FEATURES,
52            FeatureType::Container => &CONTAINER_FEATURES,
53        }
54    }
55
56    fn find_feature(&self, name: &Atom) -> Option<(usize, &'static QueryFeatureDescription)> {
57        self.features()
58            .iter()
59            .enumerate()
60            .find(|(_, f)| f.name == *name)
61    }
62}
63
64/// The kind of matching that should be performed on a feature value.
65#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
66enum LegacyRange {
67    /// At least the specified value.
68    Min,
69    /// At most the specified value.
70    Max,
71}
72
73/// The operator that was specified in this feature.
74#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
75pub enum Operator {
76    /// =
77    Equal,
78    /// >
79    GreaterThan,
80    /// >=
81    GreaterThanEqual,
82    /// <
83    LessThan,
84    /// <=
85    LessThanEqual,
86}
87
88impl ToCss for Operator {
89    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
90    where
91        W: fmt::Write,
92    {
93        dest.write_str(match *self {
94            Self::Equal => "=",
95            Self::LessThan => "<",
96            Self::LessThanEqual => "<=",
97            Self::GreaterThan => ">",
98            Self::GreaterThanEqual => ">=",
99        })
100    }
101}
102
103impl Operator {
104    fn is_compatible_with(self, right_op: Self) -> bool {
105        // Some operators are not compatible with each other in multi-range
106        // context.
107        match self {
108            Self::Equal => false,
109            Self::GreaterThan | Self::GreaterThanEqual => {
110                matches!(right_op, Self::GreaterThan | Self::GreaterThanEqual)
111            },
112            Self::LessThan | Self::LessThanEqual => {
113                matches!(right_op, Self::LessThan | Self::LessThanEqual)
114            },
115        }
116    }
117
118    fn evaluate(&self, cmp: Ordering) -> bool {
119        match *self {
120            Self::Equal => cmp == Ordering::Equal,
121            Self::GreaterThan => cmp == Ordering::Greater,
122            Self::GreaterThanEqual => cmp == Ordering::Equal || cmp == Ordering::Greater,
123            Self::LessThan => cmp == Ordering::Less,
124            Self::LessThanEqual => cmp == Ordering::Equal || cmp == Ordering::Less,
125        }
126    }
127
128    fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
129        let location = input.current_source_location();
130        let operator = match *input.next()? {
131            Token::Delim('=') => return Ok(Operator::Equal),
132            Token::Delim('>') => Operator::GreaterThan,
133            Token::Delim('<') => Operator::LessThan,
134            ref t => return Err(location.new_unexpected_token_error(t.clone())),
135        };
136
137        // https://drafts.csswg.org/mediaqueries-4/#mq-syntax:
138        //
139        //     No whitespace is allowed between the “<” or “>”
140        //     <delim-token>s and the following “=” <delim-token>, if it’s
141        //     present.
142        //
143        // TODO(emilio): Maybe we should ignore comments as well?
144        // https://github.com/w3c/csswg-drafts/issues/6248
145        let parsed_equal = input
146            .try_parse(|i| {
147                let t = i.next_including_whitespace().map_err(|_| ())?;
148                if !matches!(t, Token::Delim('=')) {
149                    return Err(());
150                }
151                Ok(())
152            })
153            .is_ok();
154
155        if !parsed_equal {
156            return Ok(operator);
157        }
158
159        Ok(match operator {
160            Operator::GreaterThan => Operator::GreaterThanEqual,
161            Operator::LessThan => Operator::LessThanEqual,
162            _ => unreachable!(),
163        })
164    }
165}
166
167#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)]
168enum QueryFeatureExpressionKind {
169    /// Just the media feature name.
170    Empty,
171
172    /// A single value.
173    Single(QueryExpressionValue),
174
175    /// Legacy range syntax (min-*: value) or so.
176    LegacyRange(LegacyRange, QueryExpressionValue),
177
178    /// Modern range context syntax:
179    /// https://drafts.csswg.org/mediaqueries-5/#mq-range-context
180    Range {
181        left: Option<(Operator, QueryExpressionValue)>,
182        right: Option<(Operator, QueryExpressionValue)>,
183    },
184}
185
186impl QueryFeatureExpressionKind {
187    /// Evaluate a given range given an optional query value and a value from
188    /// the browser.
189    fn evaluate<T>(
190        &self,
191        context_value: T,
192        mut compute: impl FnMut(&QueryExpressionValue) -> T,
193    ) -> bool
194    where
195        T: PartialOrd + Zero,
196    {
197        match *self {
198            Self::Empty => return !context_value.is_zero(),
199            Self::Single(ref value) => {
200                let value = compute(value);
201                let cmp = match context_value.partial_cmp(&value) {
202                    Some(c) => c,
203                    None => return false,
204                };
205                cmp == Ordering::Equal
206            },
207            Self::LegacyRange(ref range, ref value) => {
208                let value = compute(value);
209                let cmp = match context_value.partial_cmp(&value) {
210                    Some(c) => c,
211                    None => return false,
212                };
213                cmp == Ordering::Equal
214                    || match range {
215                        LegacyRange::Min => cmp == Ordering::Greater,
216                        LegacyRange::Max => cmp == Ordering::Less,
217                    }
218            },
219            Self::Range {
220                ref left,
221                ref right,
222            } => {
223                debug_assert!(left.is_some() || right.is_some());
224                if let Some((ref op, ref value)) = left {
225                    let value = compute(value);
226                    let cmp = match value.partial_cmp(&context_value) {
227                        Some(c) => c,
228                        None => return false,
229                    };
230                    if !op.evaluate(cmp) {
231                        return false;
232                    }
233                }
234                if let Some((ref op, ref value)) = right {
235                    let value = compute(value);
236                    let cmp = match context_value.partial_cmp(&value) {
237                        Some(c) => c,
238                        None => return false,
239                    };
240                    if !op.evaluate(cmp) {
241                        return false;
242                    }
243                }
244                true
245            },
246        }
247    }
248
249    /// Non-ranged features only need to compare to one value at most.
250    fn non_ranged_value(&self) -> Option<&QueryExpressionValue> {
251        match *self {
252            Self::Empty => None,
253            Self::Single(ref v) => Some(v),
254            Self::LegacyRange(..) | Self::Range { .. } => {
255                debug_assert!(false, "Unexpected ranged value in non-ranged feature!");
256                None
257            },
258        }
259    }
260}
261
262/// A feature expression contains a reference to the feature, the value the
263/// query contained, and the range to evaluate.
264#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)]
265pub struct QueryFeatureExpression {
266    feature_type: FeatureType,
267    feature_index: usize,
268    kind: QueryFeatureExpressionKind,
269}
270
271impl ToCss for QueryFeatureExpression {
272    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
273    where
274        W: fmt::Write,
275    {
276        dest.write_char('(')?;
277
278        match self.kind {
279            QueryFeatureExpressionKind::Empty => self.write_name(dest)?,
280            QueryFeatureExpressionKind::Single(ref v)
281            | QueryFeatureExpressionKind::LegacyRange(_, ref v) => {
282                self.write_name(dest)?;
283                dest.write_str(": ")?;
284                v.to_css(dest, Some(self))?;
285            },
286            QueryFeatureExpressionKind::Range {
287                ref left,
288                ref right,
289            } => {
290                if let Some((ref op, ref val)) = left {
291                    val.to_css(dest, Some(self))?;
292                    dest.write_char(' ')?;
293                    op.to_css(dest)?;
294                    dest.write_char(' ')?;
295                }
296                self.write_name(dest)?;
297                if let Some((ref op, ref val)) = right {
298                    dest.write_char(' ')?;
299                    op.to_css(dest)?;
300                    dest.write_char(' ')?;
301                    val.to_css(dest, Some(self))?;
302                }
303            },
304        }
305        dest.write_char(')')
306    }
307}
308
309fn consume_operation_or_colon<'i>(
310    input: &mut Parser<'i, '_>,
311) -> Result<Option<Operator>, ParseError<'i>> {
312    if input.try_parse(|input| input.expect_colon()).is_ok() {
313        return Ok(None);
314    }
315    Operator::parse(input).map(|op| Some(op))
316}
317
318#[allow(unused_variables)]
319fn disabled_by_pref(feature: &Atom, context: &ParserContext) -> bool {
320    #[cfg(feature = "gecko")]
321    {
322        // prefers-reduced-transparency is always enabled in the ua and chrome. On
323        // the web it is hidden behind a preference (see Bug 1822176).
324        if *feature == atom!("prefers-reduced-transparency") {
325            return !context.chrome_rules_enabled()
326                && !static_prefs::pref!("layout.css.prefers-reduced-transparency.enabled");
327        }
328
329        // inverted-colors is always enabled in the ua and chrome. On
330        // the web it is hidden behind a preference.
331        if *feature == atom!("inverted-colors") {
332            return !context.chrome_rules_enabled()
333                && !static_prefs::pref!("layout.css.inverted-colors.enabled");
334        }
335    }
336    false
337}
338
339impl QueryFeatureExpression {
340    fn new(
341        feature_type: FeatureType,
342        feature_index: usize,
343        kind: QueryFeatureExpressionKind,
344    ) -> Self {
345        debug_assert!(feature_index < feature_type.features().len());
346        Self {
347            feature_type,
348            feature_index,
349            kind,
350        }
351    }
352
353    fn write_name<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
354    where
355        W: fmt::Write,
356    {
357        let feature = self.feature();
358        if feature.flags.contains(FeatureFlags::WEBKIT_PREFIX) {
359            dest.write_str("-webkit-")?;
360        }
361
362        if let QueryFeatureExpressionKind::LegacyRange(range, _) = self.kind {
363            match range {
364                LegacyRange::Min => dest.write_str("min-")?,
365                LegacyRange::Max => dest.write_str("max-")?,
366            }
367        }
368
369        // NB: CssStringWriter not needed, feature names are under control.
370        write!(dest, "{}", feature.name)?;
371
372        Ok(())
373    }
374
375    fn feature(&self) -> &'static QueryFeatureDescription {
376        &self.feature_type.features()[self.feature_index]
377    }
378
379    /// Returns the feature flags for our feature.
380    pub fn feature_flags(&self) -> FeatureFlags {
381        self.feature().flags
382    }
383
384    fn parse_feature_name<'i, 't>(
385        context: &ParserContext,
386        input: &mut Parser<'i, 't>,
387        feature_type: FeatureType,
388    ) -> Result<(usize, Option<LegacyRange>), ParseError<'i>> {
389        let mut flags = FeatureFlags::empty();
390        let location = input.current_source_location();
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(location.new_custom_error(
418                    StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()),
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(location.new_custom_error(
428                StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()),
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<'i, 't>(
440        context: &ParserContext,
441        input: &mut Parser<'i, 't>,
442        feature_type: FeatureType,
443    ) -> Result<Self, ParseError<'i>> {
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(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
455                }
456                break index;
457            }
458            if input.is_exhausted() {
459                return Err(start
460                    .source_location()
461                    .new_custom_error(StyleParseErrorKind::UnspecifiedError));
462            }
463        };
464
465        input.reset(&start);
466
467        let feature = &feature_type.features()[feature_index];
468        let left_val = QueryExpressionValue::parse(feature, context, input)?;
469        let left_op = Operator::parse(input)?;
470
471        {
472            let (parsed_index, _) = Self::parse_feature_name(context, input, feature_type)?;
473            debug_assert_eq!(
474                parsed_index, feature_index,
475                "How did we find a different feature?"
476            );
477        }
478
479        let right_op = input.try_parse(Operator::parse).ok();
480        let right = match right_op {
481            Some(op) => {
482                if !left_op.is_compatible_with(op) {
483                    return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
484                }
485                Some((op, QueryExpressionValue::parse(feature, context, input)?))
486            },
487            None => None,
488        };
489        Ok(Self::new(
490            feature_type,
491            feature_index,
492            QueryFeatureExpressionKind::Range {
493                left: Some((left_op, left_val)),
494                right,
495            },
496        ))
497    }
498
499    /// Parse a feature expression where we've already consumed the parenthesis.
500    pub fn parse_in_parenthesis_block<'i, 't>(
501        context: &ParserContext,
502        input: &mut Parser<'i, 't>,
503        feature_type: FeatureType,
504    ) -> Result<Self, ParseError<'i>> {
505        let (feature_index, range) =
506            match input.try_parse(|input| Self::parse_feature_name(context, input, feature_type)) {
507                Ok(v) => v,
508                Err(e) => {
509                    if let Ok(expr) = Self::parse_multi_range_syntax(context, input, feature_type) {
510                        return Ok(expr);
511                    }
512                    return Err(e);
513                },
514            };
515        let operator = input.try_parse(consume_operation_or_colon);
516        let operator = match operator {
517            Err(..) => {
518                // If there's no colon, this is a query of the form
519                // '(<feature>)', that is, there's no value specified.
520                //
521                // Gecko doesn't allow ranged expressions without a
522                // value, so just reject them here too.
523                if range.is_some() {
524                    return Err(
525                        input.new_custom_error(StyleParseErrorKind::RangedExpressionWithNoValue)
526                    );
527                }
528
529                return Ok(Self::new(
530                    feature_type,
531                    feature_index,
532                    QueryFeatureExpressionKind::Empty,
533                ));
534            },
535            Ok(operator) => operator,
536        };
537
538        let feature = &feature_type.features()[feature_index];
539
540        let value = QueryExpressionValue::parse(feature, context, input).map_err(|err| {
541            err.location
542                .new_custom_error(StyleParseErrorKind::MediaQueryExpectedFeatureValue)
543        })?;
544
545        let kind = match range {
546            Some(range) => {
547                if operator.is_some() {
548                    return Err(
549                        input.new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator)
550                    );
551                }
552                QueryFeatureExpressionKind::LegacyRange(range, value)
553            },
554            None => match operator {
555                Some(operator) => {
556                    if !feature.allows_ranges() {
557                        return Err(input
558                            .new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator));
559                    }
560                    QueryFeatureExpressionKind::Range {
561                        left: None,
562                        right: Some((operator, value)),
563                    }
564                },
565                None => QueryFeatureExpressionKind::Single(value),
566            },
567        };
568
569        Ok(Self::new(feature_type, feature_index, kind))
570    }
571
572    /// Returns whether this "plain" feature query evaluates to true for the given device.
573    pub fn matches(&self, context: &computed::Context) -> KleeneValue {
574        macro_rules! expect {
575            ($variant:ident, $v:expr) => {
576                match *$v {
577                    QueryExpressionValue::$variant(ref v) => v,
578                    _ => unreachable!("Unexpected QueryExpressionValue"),
579                }
580            };
581        }
582
583        KleeneValue::from(match self.feature().evaluator {
584            Evaluator::Length(eval) => {
585                let v = eval(context);
586                self.kind
587                    .evaluate(v, |v| expect!(Length, v).to_computed_value(context))
588            },
589            Evaluator::OptionalLength(eval) => {
590                let v = match eval(context) {
591                    Some(v) => v,
592                    None => return KleeneValue::Unknown,
593                };
594                self.kind
595                    .evaluate(v, |v| expect!(Length, v).to_computed_value(context))
596            },
597            Evaluator::Integer(eval) => {
598                let v = eval(context);
599                self.kind.evaluate(v, |v| *expect!(Integer, v))
600            },
601            Evaluator::Float(eval) => {
602                let v = eval(context);
603                self.kind.evaluate(v, |v| *expect!(Float, v))
604            },
605            Evaluator::NumberRatio(eval) => {
606                let ratio = eval(context);
607                // A ratio of 0/0 behaves as the ratio 1/0, so we need to call used_value()
608                // to convert it if necessary.
609                // FIXME: we may need to update here once
610                // https://github.com/w3c/csswg-drafts/issues/4954 got resolved.
611                self.kind
612                    .evaluate(ratio, |v| expect!(NumberRatio, v).used_value())
613            },
614            Evaluator::OptionalNumberRatio(eval) => {
615                let ratio = match eval(context) {
616                    Some(v) => v,
617                    None => return KleeneValue::Unknown,
618                };
619                // See above for subtleties here.
620                self.kind
621                    .evaluate(ratio, |v| expect!(NumberRatio, v).used_value())
622            },
623            Evaluator::Resolution(eval) => {
624                let v = eval(context).dppx();
625                self.kind.evaluate(v, |v| {
626                    expect!(Resolution, v).to_computed_value(context).dppx()
627                })
628            },
629            Evaluator::Enumerated { evaluator, .. } => {
630                let computed = self
631                    .kind
632                    .non_ranged_value()
633                    .map(|v| *expect!(Enumerated, v));
634                return evaluator(context, computed);
635            },
636            Evaluator::BoolInteger(eval) => {
637                let computed = self
638                    .kind
639                    .non_ranged_value()
640                    .map(|v| *expect!(BoolInteger, v));
641                let boolean = eval(context);
642                computed.map_or(boolean, |v| v == boolean)
643            },
644        })
645    }
646}
647
648/// A value found or expected in a expression.
649///
650/// FIXME(emilio): How should calc() serialize in the Number / Integer /
651/// BoolInteger / NumberRatio case, as computed or as specified value?
652///
653/// If the first, this would need to store the relevant values.
654///
655/// See: https://github.com/w3c/csswg-drafts/issues/1968
656#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
657pub enum QueryExpressionValue {
658    /// A length.
659    Length(Length),
660    /// An integer.
661    Integer(i32),
662    /// A floating point value.
663    Float(CSSFloat),
664    /// A boolean value, specified as an integer (i.e., either 0 or 1).
665    BoolInteger(bool),
666    /// A single non-negative number or two non-negative numbers separated by '/',
667    /// with optional whitespace on either side of the '/'.
668    NumberRatio(Ratio),
669    /// A resolution.
670    Resolution(Resolution),
671    /// An enumerated value, defined by the variant keyword table in the
672    /// feature's `mData` member.
673    Enumerated(KeywordDiscriminant),
674    /// Value types only used by style-range query expressions, not feature queries.
675    /// A CSS-wide keyword.
676    Keyword(CSSWideKeyword),
677    /// A percentage.
678    Percentage(Percentage),
679    /// An angle.
680    Angle(Angle),
681    /// A time value.
682    Time(Time),
683    /// A custom property name.
684    Custom(DashedIdent),
685    /// An arbitrary substitution function (var(), attr(), env()), stored as a string
686    /// for later evaluation. We store this as a custom-property value to make it easy
687    /// to resolve later.
688    Function(Box<CustomVariableValue>),
689}
690
691impl QueryExpressionValue {
692    fn to_css<W>(
693        &self,
694        dest: &mut CssWriter<W>,
695        for_expr: Option<&QueryFeatureExpression>,
696    ) -> fmt::Result
697    where
698        W: fmt::Write,
699    {
700        match *self {
701            QueryExpressionValue::Length(ref l) => l.to_css(dest),
702            QueryExpressionValue::Integer(v) => v.to_css(dest),
703            QueryExpressionValue::Float(v) => v.to_css(dest),
704            QueryExpressionValue::BoolInteger(v) => dest.write_str(if v { "1" } else { "0" }),
705            QueryExpressionValue::NumberRatio(ratio) => ratio.to_css(dest),
706            QueryExpressionValue::Resolution(ref r) => r.to_css(dest),
707            QueryExpressionValue::Keyword(k) => k.to_css(dest),
708            QueryExpressionValue::Percentage(v) => v.to_css(dest),
709            QueryExpressionValue::Angle(v) => v.to_css(dest),
710            QueryExpressionValue::Time(v) => v.to_css(dest),
711            QueryExpressionValue::Custom(ref v) => v.to_css(dest),
712            QueryExpressionValue::Function(ref f) => f.to_css(dest),
713            QueryExpressionValue::Enumerated(value) => match for_expr
714                .expect("caller should have passed for_expr")
715                .feature()
716                .evaluator
717            {
718                Evaluator::Enumerated { serializer, .. } => dest.write_str(&*serializer(value)),
719                _ => unreachable!(),
720            },
721        }
722    }
723
724    fn parse<'i, 't>(
725        for_feature: &QueryFeatureDescription,
726        context: &ParserContext,
727        input: &mut Parser<'i, 't>,
728    ) -> Result<QueryExpressionValue, ParseError<'i>> {
729        Ok(match for_feature.evaluator {
730            Evaluator::OptionalLength(..) | Evaluator::Length(..) => {
731                let length = Length::parse(context, input)?;
732                QueryExpressionValue::Length(length)
733            },
734            Evaluator::Integer(..) => {
735                let integer = Integer::parse(context, input)?;
736                QueryExpressionValue::Integer(integer.value())
737            },
738            Evaluator::BoolInteger(..) => {
739                let integer = Integer::parse_non_negative(context, input)?;
740                let value = integer.value();
741                if value > 1 {
742                    return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
743                }
744                QueryExpressionValue::BoolInteger(value == 1)
745            },
746            Evaluator::Float(..) => {
747                let number = Number::parse(context, input)?;
748                QueryExpressionValue::Float(number.get())
749            },
750            Evaluator::OptionalNumberRatio(..) | Evaluator::NumberRatio(..) => {
751                use crate::values::specified::Ratio as SpecifiedRatio;
752                let ratio = SpecifiedRatio::parse(context, input)?;
753                QueryExpressionValue::NumberRatio(Ratio::new(ratio.0.get(), ratio.1.get()))
754            },
755            Evaluator::Resolution(..) => {
756                QueryExpressionValue::Resolution(Resolution::parse(context, input)?)
757            },
758            Evaluator::Enumerated { parser, .. } => {
759                QueryExpressionValue::Enumerated(parser(context, input)?)
760            },
761        })
762    }
763
764    // Parse any of the types that can occur in a <style-range> query:
765    // <number>, <percentage>, <length>, <angle>, <time>, <frequency> or <resolution>,
766    // or a custom property name.
767    // NB: we don't currently implement the <frequency> type anywhere, so it is not
768    // parsed here.
769    fn parse_for_style_range<'i, 't>(
770        context: &ParserContext,
771        input: &mut Parser<'i, 't>,
772    ) -> Result<Self, ParseError<'i>> {
773        if let Ok(number) = input.try_parse(|i| Number::parse(context, i)) {
774            return Ok(Self::Float(number.get()));
775        }
776        if let Ok(percent) = input.try_parse(|i| Percentage::parse(context, i)) {
777            return Ok(Self::Percentage(percent));
778        }
779        if let Ok(length) = input.try_parse(|i| Length::parse(context, i)) {
780            return Ok(Self::Length(length));
781        }
782        if let Ok(angle) = input.try_parse(|i| Angle::parse(context, i)) {
783            return Ok(Self::Angle(angle));
784        }
785        if let Ok(time) = input.try_parse(|i| Time::parse(context, i)) {
786            return Ok(Self::Time(time));
787        }
788        if let Ok(resolution) = input.try_parse(|i| Resolution::parse(context, i)) {
789            return Ok(Self::Resolution(resolution));
790        }
791        if let Ok(ident) = input.try_parse(|i| DashedIdent::parse(context, i)) {
792            return Ok(Self::Custom(ident));
793        }
794        if let Ok(keyword) = input.try_parse(|i| CSSWideKeyword::parse(i)) {
795            return Ok(Self::Keyword(keyword));
796        }
797        input.skip_whitespace();
798        let start = input.position();
799        if let Ok(Token::Function(ref name)) = input.next() {
800            // Helper to parse the function arg and store the complete expression (function
801            // name and parenthesized argument) into a CustomVariableValue.
802            let parse_func =
803                |input: &mut Parser<'i, 't>| -> Result<CustomVariableValue, ParseError<'i>> {
804                    input.parse_nested_block(|i| i.expect_no_error_token().map_err(Into::into))?;
805                    let mut input = ParserInput::new(input.slice_from(start));
806                    CustomVariableValue::parse(
807                        &mut Parser::new(&mut input),
808                        Some(&context.namespaces.prefixes),
809                        context.url_data,
810                    )
811                };
812
813            if properties::enabled_arbitrary_substitution_functions()
814                .iter()
815                .any(|n| n.eq_ignore_ascii_case(name))
816            {
817                return Ok(Self::Function(Box::new(parse_func(input)?)));
818            }
819        }
820        Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
821    }
822}
823
824/// https://drafts.csswg.org/css-conditional-5/#typedef-style-range
825#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)]
826pub enum QueryStyleRange {
827    /// A style-range for style container queries with two values
828    /// (val1 OP val2).
829    #[allow(missing_docs)]
830    StyleRange2 {
831        value1: QueryExpressionValue,
832        op1: Operator,
833        value2: QueryExpressionValue,
834    },
835
836    /// A style-range for style container queries with three values
837    /// (val1 OP val2 OP val3).
838    #[allow(missing_docs)]
839    StyleRange3 {
840        value1: QueryExpressionValue,
841        op1: Operator,
842        value2: QueryExpressionValue,
843        op2: Operator,
844        value3: QueryExpressionValue,
845    },
846}
847
848impl ToCss for QueryStyleRange {
849    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
850    where
851        W: fmt::Write,
852    {
853        dest.write_char('(')?;
854        match self {
855            Self::StyleRange2 {
856                ref value1,
857                ref op1,
858                ref value2,
859            } => {
860                value1.to_css(dest, None)?;
861                dest.write_char(' ')?;
862                op1.to_css(dest)?;
863                dest.write_char(' ')?;
864                value2.to_css(dest, None)?;
865            },
866            Self::StyleRange3 {
867                ref value1,
868                ref op1,
869                ref value2,
870                ref op2,
871                ref value3,
872            } => {
873                value1.to_css(dest, None)?;
874                dest.write_char(' ')?;
875                op1.to_css(dest)?;
876                dest.write_char(' ')?;
877                value2.to_css(dest, None)?;
878                dest.write_char(' ')?;
879                op2.to_css(dest)?;
880                dest.write_char(' ')?;
881                value3.to_css(dest, None)?;
882            },
883        }
884        dest.write_char(')')
885    }
886}
887
888impl QueryStyleRange {
889    /// Parses the following range syntax:
890    ///
891    ///   value <operator> value
892    ///   value <operator> value <operator> value
893    ///
894    /// This is only used when parsing @container style() queries; the feature_type
895    /// and index is hardcoded (and ignored).
896    pub fn parse<'i, 't>(
897        context: &ParserContext,
898        input: &mut Parser<'i, 't>,
899    ) -> Result<Self, ParseError<'i>> {
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                ref value1,
933                ref op1,
934                ref 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                ref value1,
956                ref op1,
957                ref value2,
958                ref op2,
959                ref 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        let mut input = ParserInput::new(css_text);
1109        QueryExpressionValue::parse_for_style_range(&parser_context, &mut Parser::new(&mut input))
1110            .ok()
1111            .and_then(|parsed| {
1112                Self::resolve_value(&parsed, context, attribute_tracker, visited_set)
1113            })
1114    }
1115
1116    fn compare_values(value1: Option<&Component>, value2: Option<&Component>) -> Option<Ordering> {
1117        let value1 = value1?;
1118        let value2 = value2?;
1119        match (value1, value2) {
1120            (Component::Length(v1), Component::Length(v2)) => v1.partial_cmp(&v2),
1121            (Component::Number(v1), Component::Number(v2)) => v1.partial_cmp(&v2),
1122            (Component::Resolution(v1), Component::Resolution(v2)) => {
1123                v1.dppx().partial_cmp(&v2.dppx())
1124            },
1125            (Component::Percentage(v1), Component::Percentage(v2)) => v1.partial_cmp(&v2),
1126            (Component::Angle(v1), Component::Angle(v2)) => v1.partial_cmp(&v2),
1127            (Component::Time(v1), Component::Time(v2)) => v1.partial_cmp(&v2),
1128            (Component::Length(v1), Component::Number(v2)) => {
1129                if v2.is_zero() {
1130                    v1.partial_cmp(&CSSPixelLength::zero())
1131                } else {
1132                    None
1133                }
1134            },
1135            (Component::Number(v1), Component::Length(v2)) => {
1136                if v1.is_zero() {
1137                    CSSPixelLength::zero().partial_cmp(&v2)
1138                } else {
1139                    None
1140                }
1141            },
1142            _ => None,
1143        }
1144    }
1145}