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::parser::{Parse, ParserContext};
11use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase};
12use crate::values::computed::{self, Ratio, ToComputedValue};
13use crate::values::specified::{Integer, Length, Number, Resolution};
14use crate::values::CSSFloat;
15use crate::{Atom, Zero};
16use cssparser::{Parser, Token};
17use selectors::kleene_value::KleeneValue;
18use std::cmp::Ordering;
19use std::fmt::{self, Write};
20use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
21
22/// Whether we're parsing a media or container query feature.
23#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
24pub enum FeatureType {
25    /// We're parsing a media feature.
26    Media,
27    /// We're parsing a container feature.
28    Container,
29}
30
31impl FeatureType {
32    fn features(&self) -> &'static [QueryFeatureDescription] {
33        #[cfg(feature = "gecko")]
34        use crate::gecko::media_features::MEDIA_FEATURES;
35        #[cfg(feature = "servo")]
36        use crate::servo::media_queries::MEDIA_FEATURES;
37
38        use crate::stylesheets::container_rule::CONTAINER_FEATURES;
39
40        match *self {
41            FeatureType::Media => &MEDIA_FEATURES,
42            FeatureType::Container => &CONTAINER_FEATURES,
43        }
44    }
45
46    fn find_feature(&self, name: &Atom) -> Option<(usize, &'static QueryFeatureDescription)> {
47        self.features()
48            .iter()
49            .enumerate()
50            .find(|(_, f)| f.name == *name)
51    }
52}
53
54/// The kind of matching that should be performed on a feature value.
55#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
56enum LegacyRange {
57    /// At least the specified value.
58    Min,
59    /// At most the specified value.
60    Max,
61}
62
63/// The operator that was specified in this feature.
64#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
65enum Operator {
66    /// =
67    Equal,
68    /// >
69    GreaterThan,
70    /// >=
71    GreaterThanEqual,
72    /// <
73    LessThan,
74    /// <=
75    LessThanEqual,
76}
77
78impl ToCss for Operator {
79    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
80    where
81        W: fmt::Write,
82    {
83        dest.write_str(match *self {
84            Self::Equal => "=",
85            Self::LessThan => "<",
86            Self::LessThanEqual => "<=",
87            Self::GreaterThan => ">",
88            Self::GreaterThanEqual => ">=",
89        })
90    }
91}
92
93impl Operator {
94    fn is_compatible_with(self, right_op: Self) -> bool {
95        // Some operators are not compatible with each other in multi-range
96        // context.
97        match self {
98            Self::Equal => false,
99            Self::GreaterThan | Self::GreaterThanEqual => {
100                matches!(right_op, Self::GreaterThan | Self::GreaterThanEqual)
101            },
102            Self::LessThan | Self::LessThanEqual => {
103                matches!(right_op, Self::LessThan | Self::LessThanEqual)
104            },
105        }
106    }
107
108    fn evaluate(&self, cmp: Ordering) -> bool {
109        match *self {
110            Self::Equal => cmp == Ordering::Equal,
111            Self::GreaterThan => cmp == Ordering::Greater,
112            Self::GreaterThanEqual => cmp == Ordering::Equal || cmp == Ordering::Greater,
113            Self::LessThan => cmp == Ordering::Less,
114            Self::LessThanEqual => cmp == Ordering::Equal || cmp == Ordering::Less,
115        }
116    }
117
118    fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
119        let location = input.current_source_location();
120        let operator = match *input.next()? {
121            Token::Delim('=') => return Ok(Operator::Equal),
122            Token::Delim('>') => Operator::GreaterThan,
123            Token::Delim('<') => Operator::LessThan,
124            ref t => return Err(location.new_unexpected_token_error(t.clone())),
125        };
126
127        // https://drafts.csswg.org/mediaqueries-4/#mq-syntax:
128        //
129        //     No whitespace is allowed between the “<” or “>”
130        //     <delim-token>s and the following “=” <delim-token>, if it’s
131        //     present.
132        //
133        // TODO(emilio): Maybe we should ignore comments as well?
134        // https://github.com/w3c/csswg-drafts/issues/6248
135        let parsed_equal = input
136            .try_parse(|i| {
137                let t = i.next_including_whitespace().map_err(|_| ())?;
138                if !matches!(t, Token::Delim('=')) {
139                    return Err(());
140                }
141                Ok(())
142            })
143            .is_ok();
144
145        if !parsed_equal {
146            return Ok(operator);
147        }
148
149        Ok(match operator {
150            Operator::GreaterThan => Operator::GreaterThanEqual,
151            Operator::LessThan => Operator::LessThanEqual,
152            _ => unreachable!(),
153        })
154    }
155}
156
157#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)]
158enum QueryFeatureExpressionKind {
159    /// Just the media feature name.
160    Empty,
161
162    /// A single value.
163    Single(QueryExpressionValue),
164
165    /// Legacy range syntax (min-*: value) or so.
166    LegacyRange(LegacyRange, QueryExpressionValue),
167
168    /// Modern range context syntax:
169    /// https://drafts.csswg.org/mediaqueries-5/#mq-range-context
170    Range {
171        left: Option<(Operator, QueryExpressionValue)>,
172        right: Option<(Operator, QueryExpressionValue)>,
173    },
174}
175
176impl QueryFeatureExpressionKind {
177    /// Evaluate a given range given an optional query value and a value from
178    /// the browser.
179    fn evaluate<T>(
180        &self,
181        context_value: T,
182        mut compute: impl FnMut(&QueryExpressionValue) -> T,
183    ) -> bool
184    where
185        T: PartialOrd + Zero,
186    {
187        match *self {
188            Self::Empty => return !context_value.is_zero(),
189            Self::Single(ref value) => {
190                let value = compute(value);
191                let cmp = match context_value.partial_cmp(&value) {
192                    Some(c) => c,
193                    None => return false,
194                };
195                cmp == Ordering::Equal
196            },
197            Self::LegacyRange(ref range, ref value) => {
198                let value = compute(value);
199                let cmp = match context_value.partial_cmp(&value) {
200                    Some(c) => c,
201                    None => return false,
202                };
203                cmp == Ordering::Equal
204                    || match range {
205                        LegacyRange::Min => cmp == Ordering::Greater,
206                        LegacyRange::Max => cmp == Ordering::Less,
207                    }
208            },
209            Self::Range {
210                ref left,
211                ref right,
212            } => {
213                debug_assert!(left.is_some() || right.is_some());
214                if let Some((ref op, ref value)) = left {
215                    let value = compute(value);
216                    let cmp = match value.partial_cmp(&context_value) {
217                        Some(c) => c,
218                        None => return false,
219                    };
220                    if !op.evaluate(cmp) {
221                        return false;
222                    }
223                }
224                if let Some((ref op, ref value)) = right {
225                    let value = compute(value);
226                    let cmp = match context_value.partial_cmp(&value) {
227                        Some(c) => c,
228                        None => return false,
229                    };
230                    if !op.evaluate(cmp) {
231                        return false;
232                    }
233                }
234                true
235            },
236        }
237    }
238
239    /// Non-ranged features only need to compare to one value at most.
240    fn non_ranged_value(&self) -> Option<&QueryExpressionValue> {
241        match *self {
242            Self::Empty => None,
243            Self::Single(ref v) => Some(v),
244            Self::LegacyRange(..) | Self::Range { .. } => {
245                debug_assert!(false, "Unexpected ranged value in non-ranged feature!");
246                None
247            },
248        }
249    }
250}
251
252/// A feature expression contains a reference to the feature, the value the
253/// query contained, and the range to evaluate.
254#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)]
255pub struct QueryFeatureExpression {
256    feature_type: FeatureType,
257    feature_index: usize,
258    kind: QueryFeatureExpressionKind,
259}
260
261impl ToCss for QueryFeatureExpression {
262    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
263    where
264        W: fmt::Write,
265    {
266        dest.write_char('(')?;
267
268        match self.kind {
269            QueryFeatureExpressionKind::Empty => self.write_name(dest)?,
270            QueryFeatureExpressionKind::Single(ref v)
271            | QueryFeatureExpressionKind::LegacyRange(_, ref v) => {
272                self.write_name(dest)?;
273                dest.write_str(": ")?;
274                v.to_css(dest, self)?;
275            },
276            QueryFeatureExpressionKind::Range {
277                ref left,
278                ref right,
279            } => {
280                if let Some((ref op, ref val)) = left {
281                    val.to_css(dest, self)?;
282                    dest.write_char(' ')?;
283                    op.to_css(dest)?;
284                    dest.write_char(' ')?;
285                }
286                self.write_name(dest)?;
287                if let Some((ref op, ref val)) = right {
288                    dest.write_char(' ')?;
289                    op.to_css(dest)?;
290                    dest.write_char(' ')?;
291                    val.to_css(dest, self)?;
292                }
293            },
294        }
295        dest.write_char(')')
296    }
297}
298
299fn consume_operation_or_colon<'i>(
300    input: &mut Parser<'i, '_>,
301) -> Result<Option<Operator>, ParseError<'i>> {
302    if input.try_parse(|input| input.expect_colon()).is_ok() {
303        return Ok(None);
304    }
305    Operator::parse(input).map(|op| Some(op))
306}
307
308#[allow(unused_variables)]
309fn disabled_by_pref(feature: &Atom, context: &ParserContext) -> bool {
310    #[cfg(feature = "gecko")]
311    {
312        // prefers-reduced-transparency is always enabled in the ua and chrome. On
313        // the web it is hidden behind a preference (see Bug 1822176).
314        if *feature == atom!("prefers-reduced-transparency") {
315            return !context.chrome_rules_enabled()
316                && !static_prefs::pref!("layout.css.prefers-reduced-transparency.enabled");
317        }
318
319        // inverted-colors is always enabled in the ua and chrome. On
320        // the web it is hidden behind a preference.
321        if *feature == atom!("inverted-colors") {
322            return !context.chrome_rules_enabled()
323                && !static_prefs::pref!("layout.css.inverted-colors.enabled");
324        }
325    }
326    false
327}
328
329impl QueryFeatureExpression {
330    fn new(
331        feature_type: FeatureType,
332        feature_index: usize,
333        kind: QueryFeatureExpressionKind,
334    ) -> Self {
335        debug_assert!(feature_index < feature_type.features().len());
336        Self {
337            feature_type,
338            feature_index,
339            kind,
340        }
341    }
342
343    fn write_name<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
344    where
345        W: fmt::Write,
346    {
347        let feature = self.feature();
348        if feature.flags.contains(FeatureFlags::WEBKIT_PREFIX) {
349            dest.write_str("-webkit-")?;
350        }
351
352        if let QueryFeatureExpressionKind::LegacyRange(range, _) = self.kind {
353            match range {
354                LegacyRange::Min => dest.write_str("min-")?,
355                LegacyRange::Max => dest.write_str("max-")?,
356            }
357        }
358
359        // NB: CssStringWriter not needed, feature names are under control.
360        write!(dest, "{}", feature.name)?;
361
362        Ok(())
363    }
364
365    fn feature(&self) -> &'static QueryFeatureDescription {
366        &self.feature_type.features()[self.feature_index]
367    }
368
369    /// Returns the feature flags for our feature.
370    pub fn feature_flags(&self) -> FeatureFlags {
371        self.feature().flags
372    }
373
374    fn parse_feature_name<'i, 't>(
375        context: &ParserContext,
376        input: &mut Parser<'i, 't>,
377        feature_type: FeatureType,
378    ) -> Result<(usize, Option<LegacyRange>), ParseError<'i>> {
379        let mut flags = FeatureFlags::empty();
380        let location = input.current_source_location();
381        let ident = input.expect_ident()?;
382
383        if context.chrome_rules_enabled() {
384            flags.insert(FeatureFlags::CHROME_AND_UA_ONLY);
385        }
386
387        let mut feature_name = &**ident;
388        if starts_with_ignore_ascii_case(feature_name, "-webkit-") {
389            feature_name = &feature_name[8..];
390            flags.insert(FeatureFlags::WEBKIT_PREFIX);
391        }
392
393        let range = if starts_with_ignore_ascii_case(feature_name, "min-") {
394            feature_name = &feature_name[4..];
395            Some(LegacyRange::Min)
396        } else if starts_with_ignore_ascii_case(feature_name, "max-") {
397            feature_name = &feature_name[4..];
398            Some(LegacyRange::Max)
399        } else {
400            None
401        };
402
403        let atom = Atom::from(string_as_ascii_lowercase(feature_name));
404        let (feature_index, feature) = match feature_type.find_feature(&atom) {
405            Some((i, f)) => (i, f),
406            None => {
407                return Err(location.new_custom_error(
408                    StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()),
409                ))
410            },
411        };
412
413        if disabled_by_pref(&feature.name, context)
414            || !flags.contains(feature.flags.parsing_requirements())
415            || (range.is_some() && !feature.allows_ranges())
416        {
417            return Err(location.new_custom_error(
418                StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()),
419            ));
420        }
421
422        Ok((feature_index, range))
423    }
424
425    /// Parses the following range syntax:
426    ///
427    ///   (feature-value <operator> feature-name)
428    ///   (feature-value <operator> feature-name <operator> feature-value)
429    fn parse_multi_range_syntax<'i, 't>(
430        context: &ParserContext,
431        input: &mut Parser<'i, 't>,
432        feature_type: FeatureType,
433    ) -> Result<Self, ParseError<'i>> {
434        let start = input.state();
435
436        // To parse the values, we first need to find the feature name. We rely
437        // on feature values for ranged features not being able to be top-level
438        // <ident>s, which holds.
439        let feature_index = loop {
440            // NOTE: parse_feature_name advances the input.
441            if let Ok((index, range)) = Self::parse_feature_name(context, input, feature_type) {
442                if range.is_some() {
443                    // Ranged names are not allowed here.
444                    return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
445                }
446                break index;
447            }
448            if input.is_exhausted() {
449                return Err(start
450                    .source_location()
451                    .new_custom_error(StyleParseErrorKind::UnspecifiedError));
452            }
453        };
454
455        input.reset(&start);
456
457        let feature = &feature_type.features()[feature_index];
458        let left_val = QueryExpressionValue::parse(feature, context, input)?;
459        let left_op = Operator::parse(input)?;
460
461        {
462            let (parsed_index, _) = Self::parse_feature_name(context, input, feature_type)?;
463            debug_assert_eq!(
464                parsed_index, feature_index,
465                "How did we find a different feature?"
466            );
467        }
468
469        let right_op = input.try_parse(Operator::parse).ok();
470        let right = match right_op {
471            Some(op) => {
472                if !left_op.is_compatible_with(op) {
473                    return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
474                }
475                Some((op, QueryExpressionValue::parse(feature, context, input)?))
476            },
477            None => None,
478        };
479        Ok(Self::new(
480            feature_type,
481            feature_index,
482            QueryFeatureExpressionKind::Range {
483                left: Some((left_op, left_val)),
484                right,
485            },
486        ))
487    }
488
489    /// Parse a feature expression where we've already consumed the parenthesis.
490    pub fn parse_in_parenthesis_block<'i, 't>(
491        context: &ParserContext,
492        input: &mut Parser<'i, 't>,
493        feature_type: FeatureType,
494    ) -> Result<Self, ParseError<'i>> {
495        let (feature_index, range) =
496            match input.try_parse(|input| Self::parse_feature_name(context, input, feature_type)) {
497                Ok(v) => v,
498                Err(e) => {
499                    if let Ok(expr) = Self::parse_multi_range_syntax(context, input, feature_type) {
500                        return Ok(expr);
501                    }
502                    return Err(e);
503                },
504            };
505        let operator = input.try_parse(consume_operation_or_colon);
506        let operator = match operator {
507            Err(..) => {
508                // If there's no colon, this is a query of the form
509                // '(<feature>)', that is, there's no value specified.
510                //
511                // Gecko doesn't allow ranged expressions without a
512                // value, so just reject them here too.
513                if range.is_some() {
514                    return Err(
515                        input.new_custom_error(StyleParseErrorKind::RangedExpressionWithNoValue)
516                    );
517                }
518
519                return Ok(Self::new(
520                    feature_type,
521                    feature_index,
522                    QueryFeatureExpressionKind::Empty,
523                ));
524            },
525            Ok(operator) => operator,
526        };
527
528        let feature = &feature_type.features()[feature_index];
529
530        let value = QueryExpressionValue::parse(feature, context, input).map_err(|err| {
531            err.location
532                .new_custom_error(StyleParseErrorKind::MediaQueryExpectedFeatureValue)
533        })?;
534
535        let kind = match range {
536            Some(range) => {
537                if operator.is_some() {
538                    return Err(
539                        input.new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator)
540                    );
541                }
542                QueryFeatureExpressionKind::LegacyRange(range, value)
543            },
544            None => match operator {
545                Some(operator) => {
546                    if !feature.allows_ranges() {
547                        return Err(input
548                            .new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator));
549                    }
550                    QueryFeatureExpressionKind::Range {
551                        left: None,
552                        right: Some((operator, value)),
553                    }
554                },
555                None => QueryFeatureExpressionKind::Single(value),
556            },
557        };
558
559        Ok(Self::new(feature_type, feature_index, kind))
560    }
561
562    /// Returns whether this query evaluates to true for the given device.
563    pub fn matches(&self, context: &computed::Context) -> KleeneValue {
564        macro_rules! expect {
565            ($variant:ident, $v:expr) => {
566                match *$v {
567                    QueryExpressionValue::$variant(ref v) => v,
568                    _ => unreachable!("Unexpected QueryExpressionValue"),
569                }
570            };
571        }
572
573        KleeneValue::from(match self.feature().evaluator {
574            Evaluator::Length(eval) => {
575                let v = eval(context);
576                self.kind
577                    .evaluate(v, |v| expect!(Length, v).to_computed_value(context))
578            },
579            Evaluator::OptionalLength(eval) => {
580                let v = match eval(context) {
581                    Some(v) => v,
582                    None => return KleeneValue::Unknown,
583                };
584                self.kind
585                    .evaluate(v, |v| expect!(Length, v).to_computed_value(context))
586            },
587            Evaluator::Integer(eval) => {
588                let v = eval(context);
589                self.kind.evaluate(v, |v| *expect!(Integer, v))
590            },
591            Evaluator::Float(eval) => {
592                let v = eval(context);
593                self.kind.evaluate(v, |v| *expect!(Float, v))
594            },
595            Evaluator::NumberRatio(eval) => {
596                let ratio = eval(context);
597                // A ratio of 0/0 behaves as the ratio 1/0, so we need to call used_value()
598                // to convert it if necessary.
599                // FIXME: we may need to update here once
600                // https://github.com/w3c/csswg-drafts/issues/4954 got resolved.
601                self.kind
602                    .evaluate(ratio, |v| expect!(NumberRatio, v).used_value())
603            },
604            Evaluator::OptionalNumberRatio(eval) => {
605                let ratio = match eval(context) {
606                    Some(v) => v,
607                    None => return KleeneValue::Unknown,
608                };
609                // See above for subtleties here.
610                self.kind
611                    .evaluate(ratio, |v| expect!(NumberRatio, v).used_value())
612            },
613            Evaluator::Resolution(eval) => {
614                let v = eval(context).dppx();
615                self.kind.evaluate(v, |v| {
616                    expect!(Resolution, v).to_computed_value(context).dppx()
617                })
618            },
619            Evaluator::Enumerated { evaluator, .. } => {
620                let computed = self
621                    .kind
622                    .non_ranged_value()
623                    .map(|v| *expect!(Enumerated, v));
624                return evaluator(context, computed);
625            },
626            Evaluator::BoolInteger(eval) => {
627                let computed = self
628                    .kind
629                    .non_ranged_value()
630                    .map(|v| *expect!(BoolInteger, v));
631                let boolean = eval(context);
632                computed.map_or(boolean, |v| v == boolean)
633            },
634        })
635    }
636}
637
638/// A value found or expected in a expression.
639///
640/// FIXME(emilio): How should calc() serialize in the Number / Integer /
641/// BoolInteger / NumberRatio case, as computed or as specified value?
642///
643/// If the first, this would need to store the relevant values.
644///
645/// See: https://github.com/w3c/csswg-drafts/issues/1968
646#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
647pub enum QueryExpressionValue {
648    /// A length.
649    Length(Length),
650    /// An integer.
651    Integer(i32),
652    /// A floating point value.
653    Float(CSSFloat),
654    /// A boolean value, specified as an integer (i.e., either 0 or 1).
655    BoolInteger(bool),
656    /// A single non-negative number or two non-negative numbers separated by '/',
657    /// with optional whitespace on either side of the '/'.
658    NumberRatio(Ratio),
659    /// A resolution.
660    Resolution(Resolution),
661    /// An enumerated value, defined by the variant keyword table in the
662    /// feature's `mData` member.
663    Enumerated(KeywordDiscriminant),
664}
665
666impl QueryExpressionValue {
667    fn to_css<W>(&self, dest: &mut CssWriter<W>, for_expr: &QueryFeatureExpression) -> fmt::Result
668    where
669        W: fmt::Write,
670    {
671        match *self {
672            QueryExpressionValue::Length(ref l) => l.to_css(dest),
673            QueryExpressionValue::Integer(v) => v.to_css(dest),
674            QueryExpressionValue::Float(v) => v.to_css(dest),
675            QueryExpressionValue::BoolInteger(v) => dest.write_str(if v { "1" } else { "0" }),
676            QueryExpressionValue::NumberRatio(ratio) => ratio.to_css(dest),
677            QueryExpressionValue::Resolution(ref r) => r.to_css(dest),
678            QueryExpressionValue::Enumerated(value) => match for_expr.feature().evaluator {
679                Evaluator::Enumerated { serializer, .. } => dest.write_str(&*serializer(value)),
680                _ => unreachable!(),
681            },
682        }
683    }
684
685    fn parse<'i, 't>(
686        for_feature: &QueryFeatureDescription,
687        context: &ParserContext,
688        input: &mut Parser<'i, 't>,
689    ) -> Result<QueryExpressionValue, ParseError<'i>> {
690        Ok(match for_feature.evaluator {
691            Evaluator::OptionalLength(..) | Evaluator::Length(..) => {
692                let length = Length::parse(context, input)?;
693                QueryExpressionValue::Length(length)
694            },
695            Evaluator::Integer(..) => {
696                let integer = Integer::parse(context, input)?;
697                QueryExpressionValue::Integer(integer.value())
698            },
699            Evaluator::BoolInteger(..) => {
700                let integer = Integer::parse_non_negative(context, input)?;
701                let value = integer.value();
702                if value > 1 {
703                    return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
704                }
705                QueryExpressionValue::BoolInteger(value == 1)
706            },
707            Evaluator::Float(..) => {
708                let number = Number::parse(context, input)?;
709                QueryExpressionValue::Float(number.get())
710            },
711            Evaluator::OptionalNumberRatio(..) | Evaluator::NumberRatio(..) => {
712                use crate::values::specified::Ratio as SpecifiedRatio;
713                let ratio = SpecifiedRatio::parse(context, input)?;
714                QueryExpressionValue::NumberRatio(Ratio::new(ratio.0.get(), ratio.1.get()))
715            },
716            Evaluator::Resolution(..) => {
717                QueryExpressionValue::Resolution(Resolution::parse(context, input)?)
718            },
719            Evaluator::Enumerated { parser, .. } => {
720                QueryExpressionValue::Enumerated(parser(context, input)?)
721            },
722        })
723    }
724}