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