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