feature_expression.rs (26130B)
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 8 use super::feature::{Evaluator, QueryFeatureDescription}; 9 use super::feature::{FeatureFlags, KeywordDiscriminant}; 10 use crate::derives::*; 11 use crate::parser::{Parse, ParserContext}; 12 use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase}; 13 use crate::values::computed::{self, Ratio, ToComputedValue}; 14 use crate::values::specified::{Integer, Length, Number, Resolution}; 15 use crate::values::CSSFloat; 16 use crate::{Atom, Zero}; 17 use cssparser::{Parser, Token}; 18 use selectors::kleene_value::KleeneValue; 19 use std::cmp::Ordering; 20 use std::fmt::{self, Write}; 21 use 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)] 25 pub enum FeatureType { 26 /// We're parsing a media feature. 27 Media, 28 /// We're parsing a container feature. 29 Container, 30 } 31 32 impl 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)] 57 enum 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)] 66 enum Operator { 67 /// = 68 Equal, 69 /// > 70 GreaterThan, 71 /// >= 72 GreaterThanEqual, 73 /// < 74 LessThan, 75 /// <= 76 LessThanEqual, 77 } 78 79 impl 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 94 impl 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)] 159 enum 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 177 impl 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)] 256 pub struct QueryFeatureExpression { 257 feature_type: FeatureType, 258 feature_index: usize, 259 kind: QueryFeatureExpressionKind, 260 } 261 262 impl 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 300 fn 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)] 310 fn 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 330 impl 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)] 648 pub 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 667 impl 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 }