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