diff options
Diffstat (limited to 'servo/components/style/queries')
-rw-r--r-- | servo/components/style/queries/condition.rs | 366 | ||||
-rw-r--r-- | servo/components/style/queries/feature.rs | 198 | ||||
-rw-r--r-- | servo/components/style/queries/feature_expression.rs | 764 | ||||
-rw-r--r-- | servo/components/style/queries/mod.rs | 19 | ||||
-rw-r--r-- | servo/components/style/queries/values.rs | 36 |
5 files changed, 1383 insertions, 0 deletions
diff --git a/servo/components/style/queries/condition.rs b/servo/components/style/queries/condition.rs new file mode 100644 index 0000000000..e17e6abd2e --- /dev/null +++ b/servo/components/style/queries/condition.rs @@ -0,0 +1,366 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A query condition: +//! +//! https://drafts.csswg.org/mediaqueries-4/#typedef-media-condition +//! https://drafts.csswg.org/css-contain-3/#typedef-container-condition + +use super::{FeatureFlags, FeatureType, QueryFeatureExpression}; +use crate::values::computed; +use crate::{error_reporting::ContextualParseError, parser::ParserContext}; +use cssparser::{Parser, Token}; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// A binary `and` or `or` operator. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] +#[allow(missing_docs)] +pub enum Operator { + And, + Or, +} + +/// Whether to allow an `or` condition or not during parsing. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss)] +enum AllowOr { + Yes, + No, +} + +/// https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss)] +pub enum KleeneValue { + /// False + False = 0, + /// True + True = 1, + /// Either true or false, but we’re not sure which yet. + Unknown, +} + +impl From<bool> for KleeneValue { + fn from(b: bool) -> Self { + if b { + Self::True + } else { + Self::False + } + } +} + +impl KleeneValue { + /// Turns this Kleene value to a bool, taking the unknown value as an + /// argument. + pub fn to_bool(self, unknown: bool) -> bool { + match self { + Self::True => true, + Self::False => false, + Self::Unknown => unknown, + } + } +} + +impl std::ops::Not for KleeneValue { + type Output = Self; + + fn not(self) -> Self { + match self { + Self::True => Self::False, + Self::False => Self::True, + Self::Unknown => Self::Unknown, + } + } +} + +// Implements the logical and operation. +impl std::ops::BitAnd for KleeneValue { + type Output = Self; + + fn bitand(self, other: Self) -> Self { + if self == Self::False || other == Self::False { + return Self::False; + } + if self == Self::Unknown || other == Self::Unknown { + return Self::Unknown; + } + Self::True + } +} + +// Implements the logical or operation. +impl std::ops::BitOr for KleeneValue { + type Output = Self; + + fn bitor(self, other: Self) -> Self { + if self == Self::True || other == Self::True { + return Self::True; + } + if self == Self::Unknown || other == Self::Unknown { + return Self::Unknown; + } + Self::False + } +} + +impl std::ops::BitOrAssign for KleeneValue { + fn bitor_assign(&mut self, other: Self) { + *self = *self | other; + } +} + +impl std::ops::BitAndAssign for KleeneValue { + fn bitand_assign(&mut self, other: Self) { + *self = *self & other; + } +} + +/// Represents a condition. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum QueryCondition { + /// A simple feature expression, implicitly parenthesized. + Feature(QueryFeatureExpression), + /// A negation of a condition. + Not(Box<QueryCondition>), + /// A set of joint operations. + Operation(Box<[QueryCondition]>, Operator), + /// A condition wrapped in parenthesis. + InParens(Box<QueryCondition>), + /// [ <function-token> <any-value>? ) ] | [ ( <any-value>? ) ] + GeneralEnclosed(String), +} + +impl ToCss for QueryCondition { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + match *self { + // NOTE(emilio): QueryFeatureExpression already includes the + // parenthesis. + QueryCondition::Feature(ref f) => f.to_css(dest), + QueryCondition::Not(ref c) => { + dest.write_str("not ")?; + c.to_css(dest) + }, + QueryCondition::InParens(ref c) => { + dest.write_char('(')?; + c.to_css(dest)?; + dest.write_char(')') + }, + QueryCondition::Operation(ref list, op) => { + let mut iter = list.iter(); + iter.next().unwrap().to_css(dest)?; + for item in iter { + dest.write_char(' ')?; + op.to_css(dest)?; + dest.write_char(' ')?; + item.to_css(dest)?; + } + Ok(()) + }, + QueryCondition::GeneralEnclosed(ref s) => dest.write_str(&s), + } + } +} + +/// <https://drafts.csswg.org/css-syntax-3/#typedef-any-value> +fn consume_any_value<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> { + input.expect_no_error_token().map_err(Into::into) +} + +impl QueryCondition { + /// Parse a single condition. + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + feature_type: FeatureType, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, feature_type, AllowOr::Yes) + } + + fn visit<F>(&self, visitor: &mut F) + where + F: FnMut(&Self), + { + visitor(self); + match *self { + Self::Feature(..) => {}, + Self::GeneralEnclosed(..) => {}, + Self::Not(ref cond) => cond.visit(visitor), + Self::Operation(ref conds, _op) => { + for cond in conds.iter() { + cond.visit(visitor); + } + }, + Self::InParens(ref cond) => cond.visit(visitor), + } + } + + /// Returns the union of all flags in the expression. This is useful for + /// container queries. + pub fn cumulative_flags(&self) -> FeatureFlags { + let mut result = FeatureFlags::empty(); + self.visit(&mut |condition| { + if let Self::Feature(ref f) = condition { + result.insert(f.feature_flags()) + } + }); + result + } + + /// Parse a single condition, disallowing `or` expressions. + /// + /// To be used from the legacy query syntax. + pub fn parse_disallow_or<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + feature_type: FeatureType, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, feature_type, AllowOr::No) + } + + /// https://drafts.csswg.org/mediaqueries-5/#typedef-media-condition or + /// https://drafts.csswg.org/mediaqueries-5/#typedef-media-condition-without-or + /// (depending on `allow_or`). + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + feature_type: FeatureType, + allow_or: AllowOr, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + if input.try_parse(|i| i.expect_ident_matching("not")).is_ok() { + let inner_condition = Self::parse_in_parens(context, input, feature_type)?; + return Ok(QueryCondition::Not(Box::new(inner_condition))); + } + + let first_condition = Self::parse_in_parens(context, input, feature_type)?; + let operator = match input.try_parse(Operator::parse) { + Ok(op) => op, + Err(..) => return Ok(first_condition), + }; + + if allow_or == AllowOr::No && operator == Operator::Or { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + let mut conditions = vec![]; + conditions.push(first_condition); + conditions.push(Self::parse_in_parens(context, input, feature_type)?); + + let delim = match operator { + Operator::And => "and", + Operator::Or => "or", + }; + + loop { + if input.try_parse(|i| i.expect_ident_matching(delim)).is_err() { + return Ok(QueryCondition::Operation( + conditions.into_boxed_slice(), + operator, + )); + } + + conditions.push(Self::parse_in_parens(context, input, feature_type)?); + } + } + + fn parse_in_parenthesis_block<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + feature_type: FeatureType, + ) -> Result<Self, ParseError<'i>> { + // Base case. Make sure to preserve this error as it's more generally + // relevant. + let feature_error = match input.try_parse(|input| { + QueryFeatureExpression::parse_in_parenthesis_block(context, input, feature_type) + }) { + Ok(expr) => return Ok(Self::Feature(expr)), + Err(e) => e, + }; + if let Ok(inner) = Self::parse(context, input, feature_type) { + return Ok(Self::InParens(Box::new(inner))); + } + Err(feature_error) + } + + /// Parse a condition in parentheses, or `<general-enclosed>`. + /// + /// https://drafts.csswg.org/mediaqueries/#typedef-media-in-parens + pub fn parse_in_parens<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + feature_type: FeatureType, + ) -> Result<Self, ParseError<'i>> { + input.skip_whitespace(); + let start = input.position(); + let start_location = input.current_source_location(); + match *input.next()? { + Token::ParenthesisBlock => { + let nested = input.try_parse(|input| { + input.parse_nested_block(|input| { + Self::parse_in_parenthesis_block(context, input, feature_type) + }) + }); + match nested { + Ok(nested) => return Ok(nested), + Err(e) => { + // We're about to swallow the error in a `<general-enclosed>` + // condition, so report it while we can. + let loc = e.location; + let error = + ContextualParseError::InvalidMediaRule(input.slice_from(start), e); + context.log_css_error(loc, error); + }, + } + }, + Token::Function(..) => { + // TODO: handle `style()` queries, etc. + }, + ref t => return Err(start_location.new_unexpected_token_error(t.clone())), + } + input.parse_nested_block(consume_any_value)?; + Ok(Self::GeneralEnclosed(input.slice_from(start).to_owned())) + } + + /// Whether this condition matches the device and quirks mode. + /// https://drafts.csswg.org/mediaqueries/#evaluating + /// https://drafts.csswg.org/mediaqueries/#typedef-general-enclosed + /// Kleene 3-valued logic is adopted here due to the introduction of + /// <general-enclosed>. + pub fn matches(&self, context: &computed::Context) -> KleeneValue { + match *self { + QueryCondition::Feature(ref f) => f.matches(context), + QueryCondition::GeneralEnclosed(_) => KleeneValue::Unknown, + QueryCondition::InParens(ref c) => c.matches(context), + QueryCondition::Not(ref c) => !c.matches(context), + QueryCondition::Operation(ref conditions, op) => { + debug_assert!(!conditions.is_empty(), "We never create an empty op"); + match op { + Operator::And => { + let mut result = KleeneValue::True; + for c in conditions.iter() { + result &= c.matches(context); + if result == KleeneValue::False { + break; + } + } + result + }, + Operator::Or => { + let mut result = KleeneValue::False; + for c in conditions.iter() { + result |= c.matches(context); + if result == KleeneValue::True { + break; + } + } + result + }, + } + }, + } + } +} diff --git a/servo/components/style/queries/feature.rs b/servo/components/style/queries/feature.rs new file mode 100644 index 0000000000..83ff7e7522 --- /dev/null +++ b/servo/components/style/queries/feature.rs @@ -0,0 +1,198 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Query features. + +use super::condition::KleeneValue; +use crate::parser::ParserContext; +use crate::values::computed::{self, CSSPixelLength, Ratio, Resolution}; +use crate::values::AtomString; +use crate::Atom; +use cssparser::Parser; +use std::fmt; +use style_traits::ParseError; + +/// A generic discriminant for an enum value. +pub type KeywordDiscriminant = u8; + +type QueryFeatureGetter<T> = fn(device: &computed::Context) -> T; + +/// Serializes a given discriminant. +/// +/// FIXME(emilio): we could prevent this allocation if the ToCss code would +/// generate a method for keywords to get the static string or something. +pub type KeywordSerializer = fn(KeywordDiscriminant) -> String; + +/// Parses a given identifier. +pub type KeywordParser = for<'a, 'i, 't> fn( + context: &'a ParserContext, + input: &'a mut Parser<'i, 't>, +) -> Result<KeywordDiscriminant, ParseError<'i>>; + +/// An evaluator for a given feature. +/// +/// This determines the kind of values that get parsed, too. +#[allow(missing_docs)] +pub enum Evaluator { + Length(QueryFeatureGetter<CSSPixelLength>), + OptionalLength(QueryFeatureGetter<Option<CSSPixelLength>>), + Integer(QueryFeatureGetter<i32>), + Float(QueryFeatureGetter<f32>), + BoolInteger(QueryFeatureGetter<bool>), + /// A non-negative number ratio, such as the one from device-pixel-ratio. + NumberRatio(QueryFeatureGetter<Ratio>), + OptionalNumberRatio(QueryFeatureGetter<Option<Ratio>>), + /// A resolution. + Resolution(QueryFeatureGetter<Resolution>), + String(fn(&computed::Context, Option<&AtomString>) -> KleeneValue), + /// A keyword value. + Enumerated { + /// The parser to get a discriminant given a string. + parser: KeywordParser, + /// The serializer to get a string from a discriminant. + /// + /// This is guaranteed to be called with a keyword that `parser` has + /// produced. + serializer: KeywordSerializer, + /// The evaluator itself. This is guaranteed to be called with a + /// keyword that `parser` has produced. + evaluator: fn(&computed::Context, Option<KeywordDiscriminant>) -> KleeneValue, + }, +} + +/// A simple helper macro to create a keyword evaluator. +/// +/// This assumes that keyword feature expressions don't accept ranges, and +/// asserts if that's not true. As of today there's nothing like that (does that +/// even make sense?). +macro_rules! keyword_evaluator { + ($actual_evaluator:ident, $keyword_type:ty) => {{ + fn __parse<'i, 't>( + context: &$crate::parser::ParserContext, + input: &mut $crate::cssparser::Parser<'i, 't>, + ) -> Result<$crate::queries::feature::KeywordDiscriminant, ::style_traits::ParseError<'i>> + { + let kw = <$keyword_type as $crate::parser::Parse>::parse(context, input)?; + Ok(kw as $crate::queries::feature::KeywordDiscriminant) + } + + fn __serialize(kw: $crate::queries::feature::KeywordDiscriminant) -> String { + // This unwrap is ok because the only discriminants that get + // back to us is the ones that `parse` produces. + let value: $keyword_type = ::num_traits::cast::FromPrimitive::from_u8(kw).unwrap(); + <$keyword_type as ::style_traits::ToCss>::to_css_string(&value) + } + + fn __evaluate( + context: &$crate::values::computed::Context, + value: Option<$crate::queries::feature::KeywordDiscriminant>, + ) -> $crate::queries::condition::KleeneValue { + // This unwrap is ok because the only discriminants that get + // back to us is the ones that `parse` produces. + let value: Option<$keyword_type> = + value.map(|kw| ::num_traits::cast::FromPrimitive::from_u8(kw).unwrap()); + $crate::queries::condition::KleeneValue::from($actual_evaluator(context, value)) + } + + $crate::queries::feature::Evaluator::Enumerated { + parser: __parse, + serializer: __serialize, + evaluator: __evaluate, + } + }}; +} + +/// Different flags or toggles that change how a expression is parsed or +/// evaluated. +#[derive(Clone, Copy, Debug, ToShmem)] +pub struct FeatureFlags(u8); +bitflags! { + impl FeatureFlags : u8 { + /// The feature should only be parsed in chrome and ua sheets. + const CHROME_AND_UA_ONLY = 1 << 0; + /// The feature requires a -webkit- prefix. + const WEBKIT_PREFIX = 1 << 1; + /// The feature requires the inline-axis containment. + const CONTAINER_REQUIRES_INLINE_AXIS = 1 << 2; + /// The feature requires the block-axis containment. + const CONTAINER_REQUIRES_BLOCK_AXIS = 1 << 3; + /// The feature requires containment in the physical width axis. + const CONTAINER_REQUIRES_WIDTH_AXIS = 1 << 4; + /// The feature requires containment in the physical height axis. + const CONTAINER_REQUIRES_HEIGHT_AXIS = 1 << 5; + /// The feature evaluation depends on the viewport size. + const VIEWPORT_DEPENDENT = 1 << 6; + } +} + +impl FeatureFlags { + /// Returns parsing requirement flags. + pub fn parsing_requirements(self) -> Self { + self.intersection(Self::CHROME_AND_UA_ONLY | Self::WEBKIT_PREFIX) + } + + /// Returns all the container axis flags. + pub fn all_container_axes() -> Self { + Self::CONTAINER_REQUIRES_INLINE_AXIS | + Self::CONTAINER_REQUIRES_BLOCK_AXIS | + Self::CONTAINER_REQUIRES_WIDTH_AXIS | + Self::CONTAINER_REQUIRES_HEIGHT_AXIS + } + + /// Returns our subset of container axis flags. + pub fn container_axes(self) -> Self { + self.intersection(Self::all_container_axes()) + } +} + +/// Whether a feature allows ranges or not. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[allow(missing_docs)] +pub enum AllowsRanges { + Yes, + No, +} + +/// A description of a feature. +pub struct QueryFeatureDescription { + /// The feature name, in ascii lowercase. + pub name: Atom, + /// Whether min- / max- prefixes are allowed or not. + pub allows_ranges: AllowsRanges, + /// The evaluator, which we also use to determine which kind of value to + /// parse. + pub evaluator: Evaluator, + /// Different feature-specific flags. + pub flags: FeatureFlags, +} + +impl QueryFeatureDescription { + /// Whether this feature allows ranges. + #[inline] + pub fn allows_ranges(&self) -> bool { + self.allows_ranges == AllowsRanges::Yes + } +} + +/// A simple helper to construct a `QueryFeatureDescription`. +macro_rules! feature { + ($name:expr, $allows_ranges:expr, $evaluator:expr, $flags:expr,) => { + $crate::queries::feature::QueryFeatureDescription { + name: $name, + allows_ranges: $allows_ranges, + evaluator: $evaluator, + flags: $flags, + } + }; +} + +impl fmt::Debug for QueryFeatureDescription { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("QueryFeatureDescription") + .field("name", &self.name) + .field("allows_ranges", &self.allows_ranges) + .field("flags", &self.flags) + .finish() + } +} diff --git a/servo/components/style/queries/feature_expression.rs b/servo/components/style/queries/feature_expression.rs new file mode 100644 index 0000000000..c0171c2058 --- /dev/null +++ b/servo/components/style/queries/feature_expression.rs @@ -0,0 +1,764 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Parsing for query feature expressions, like `(foo: bar)` or +//! `(width >= 400px)`. + +use super::feature::{Evaluator, QueryFeatureDescription}; +use super::feature::{FeatureFlags, KeywordDiscriminant}; +use crate::parser::{Parse, ParserContext}; +use crate::queries::condition::KleeneValue; +use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase}; +use crate::values::computed::{self, Ratio, ToComputedValue}; +use crate::values::specified::{Integer, Length, Number, Resolution}; +use crate::values::{AtomString, CSSFloat}; +use crate::{Atom, Zero}; +use cssparser::{Parser, Token}; +use std::cmp::{Ordering, PartialOrd}; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// Whether we're parsing a media or container query feature. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +pub enum FeatureType { + /// We're parsing a media feature. + Media, + /// We're parsing a container feature. + Container, +} + +impl FeatureType { + fn features(&self) -> &'static [QueryFeatureDescription] { + #[cfg(feature = "gecko")] + use crate::gecko::media_features::MEDIA_FEATURES; + #[cfg(feature = "servo")] + use crate::servo::media_queries::MEDIA_FEATURES; + + use crate::stylesheets::container_rule::CONTAINER_FEATURES; + + match *self { + FeatureType::Media => &MEDIA_FEATURES, + FeatureType::Container => &CONTAINER_FEATURES, + } + } + + fn find_feature(&self, name: &Atom) -> Option<(usize, &'static QueryFeatureDescription)> { + self.features() + .iter() + .enumerate() + .find(|(_, f)| f.name == *name) + } +} + +/// The kind of matching that should be performed on a feature value. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +enum LegacyRange { + /// At least the specified value. + Min, + /// At most the specified value. + Max, +} + +/// The operator that was specified in this feature. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +enum Operator { + /// = + Equal, + /// > + GreaterThan, + /// >= + GreaterThanEqual, + /// < + LessThan, + /// <= + LessThanEqual, +} + +impl ToCss for Operator { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str(match *self { + Self::Equal => "=", + Self::LessThan => "<", + Self::LessThanEqual => "<=", + Self::GreaterThan => ">", + Self::GreaterThanEqual => ">=", + }) + } +} + +impl Operator { + fn is_compatible_with(self, right_op: Self) -> bool { + // Some operators are not compatible with each other in multi-range + // context. + match self { + Self::Equal => false, + Self::GreaterThan | Self::GreaterThanEqual => { + matches!(right_op, Self::GreaterThan | Self::GreaterThanEqual) + }, + Self::LessThan | Self::LessThanEqual => { + matches!(right_op, Self::LessThan | Self::LessThanEqual) + }, + } + } + + fn evaluate(&self, cmp: Ordering) -> bool { + match *self { + Self::Equal => cmp == Ordering::Equal, + Self::GreaterThan => cmp == Ordering::Greater, + Self::GreaterThanEqual => cmp == Ordering::Equal || cmp == Ordering::Greater, + Self::LessThan => cmp == Ordering::Less, + Self::LessThanEqual => cmp == Ordering::Equal || cmp == Ordering::Less, + } + } + + fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let operator = match *input.next()? { + Token::Delim('=') => return Ok(Operator::Equal), + Token::Delim('>') => Operator::GreaterThan, + Token::Delim('<') => Operator::LessThan, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }; + + // https://drafts.csswg.org/mediaqueries-4/#mq-syntax: + // + // No whitespace is allowed between the “<” or “>” + // <delim-token>s and the following “=” <delim-token>, if it’s + // present. + // + // TODO(emilio): Maybe we should ignore comments as well? + // https://github.com/w3c/csswg-drafts/issues/6248 + let parsed_equal = input + .try_parse(|i| { + let t = i.next_including_whitespace().map_err(|_| ())?; + if !matches!(t, Token::Delim('=')) { + return Err(()); + } + Ok(()) + }) + .is_ok(); + + if !parsed_equal { + return Ok(operator); + } + + Ok(match operator { + Operator::GreaterThan => Operator::GreaterThanEqual, + Operator::LessThan => Operator::LessThanEqual, + _ => unreachable!(), + }) + } +} + +#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)] +enum QueryFeatureExpressionKind { + /// Just the media feature name. + Empty, + + /// A single value. + Single(QueryExpressionValue), + + /// Legacy range syntax (min-*: value) or so. + LegacyRange(LegacyRange, QueryExpressionValue), + + /// Modern range context syntax: + /// https://drafts.csswg.org/mediaqueries-5/#mq-range-context + Range { + left: Option<(Operator, QueryExpressionValue)>, + right: Option<(Operator, QueryExpressionValue)>, + }, +} + +impl QueryFeatureExpressionKind { + /// Evaluate a given range given an optional query value and a value from + /// the browser. + fn evaluate<T>( + &self, + context_value: T, + mut compute: impl FnMut(&QueryExpressionValue) -> T, + ) -> bool + where + T: PartialOrd + Zero, + { + match *self { + Self::Empty => return !context_value.is_zero(), + Self::Single(ref value) => { + let value = compute(value); + let cmp = match context_value.partial_cmp(&value) { + Some(c) => c, + None => return false, + }; + cmp == Ordering::Equal + }, + Self::LegacyRange(ref range, ref value) => { + let value = compute(value); + let cmp = match context_value.partial_cmp(&value) { + Some(c) => c, + None => return false, + }; + cmp == Ordering::Equal || + match range { + LegacyRange::Min => cmp == Ordering::Greater, + LegacyRange::Max => cmp == Ordering::Less, + } + }, + Self::Range { + ref left, + ref right, + } => { + debug_assert!(left.is_some() || right.is_some()); + if let Some((ref op, ref value)) = left { + let value = compute(value); + let cmp = match value.partial_cmp(&context_value) { + Some(c) => c, + None => return false, + }; + if !op.evaluate(cmp) { + return false; + } + } + if let Some((ref op, ref value)) = right { + let value = compute(value); + let cmp = match context_value.partial_cmp(&value) { + Some(c) => c, + None => return false, + }; + if !op.evaluate(cmp) { + return false; + } + } + true + }, + } + } + + /// Non-ranged features only need to compare to one value at most. + fn non_ranged_value(&self) -> Option<&QueryExpressionValue> { + match *self { + Self::Empty => None, + Self::Single(ref v) => Some(v), + Self::LegacyRange(..) | Self::Range { .. } => { + debug_assert!(false, "Unexpected ranged value in non-ranged feature!"); + None + }, + } + } +} + +/// A feature expression contains a reference to the feature, the value the +/// query contained, and the range to evaluate. +#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)] +pub struct QueryFeatureExpression { + feature_type: FeatureType, + feature_index: usize, + kind: QueryFeatureExpressionKind, +} + +impl ToCss for QueryFeatureExpression { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + dest.write_char('(')?; + + match self.kind { + QueryFeatureExpressionKind::Empty => self.write_name(dest)?, + QueryFeatureExpressionKind::Single(ref v) | + QueryFeatureExpressionKind::LegacyRange(_, ref v) => { + self.write_name(dest)?; + dest.write_str(": ")?; + v.to_css(dest, self)?; + }, + QueryFeatureExpressionKind::Range { + ref left, + ref right, + } => { + if let Some((ref op, ref val)) = left { + val.to_css(dest, self)?; + dest.write_char(' ')?; + op.to_css(dest)?; + dest.write_char(' ')?; + } + self.write_name(dest)?; + if let Some((ref op, ref val)) = right { + dest.write_char(' ')?; + op.to_css(dest)?; + dest.write_char(' ')?; + val.to_css(dest, self)?; + } + }, + } + dest.write_char(')') + } +} + +fn consume_operation_or_colon<'i>( + input: &mut Parser<'i, '_>, +) -> Result<Option<Operator>, ParseError<'i>> { + if input.try_parse(|input| input.expect_colon()).is_ok() { + return Ok(None); + } + Operator::parse(input).map(|op| Some(op)) +} + +#[allow(unused_variables)] +fn disabled_by_pref(feature: &Atom, context: &ParserContext) -> bool { + #[cfg(feature = "gecko")] + { + if *feature == atom!("forced-colors") { + // forced-colors is always enabled in the ua and chrome. On + // the web it is hidden behind a preference, which is defaulted + // to 'true' as of bug 1659511. + return !context.chrome_rules_enabled() && + !static_prefs::pref!("layout.css.forced-colors.enabled"); + } + // prefers-contrast is always enabled in the ua and chrome. On + // the web it is hidden behind a preference. + if *feature == atom!("prefers-contrast") { + return !context.chrome_rules_enabled() && + !static_prefs::pref!("layout.css.prefers-contrast.enabled"); + } + + // prefers-reduced-transparency is always enabled in the ua and chrome. On + // the web it is hidden behind a preference (see Bug 1822176). + if *feature == atom!("prefers-reduced-transparency") { + return !context.chrome_rules_enabled() && + !static_prefs::pref!("layout.css.prefers-reduced-transparency.enabled"); + } + + // inverted-colors is always enabled in the ua and chrome. On + // the web it is hidden behind a preferenc. + if *feature == atom!("inverted-colors") { + return !context.chrome_rules_enabled() && + !static_prefs::pref!("layout.css.inverted-colors.enabled"); + } + } + false +} + +impl QueryFeatureExpression { + fn new( + feature_type: FeatureType, + feature_index: usize, + kind: QueryFeatureExpressionKind, + ) -> Self { + debug_assert!(feature_index < feature_type.features().len()); + Self { + feature_type, + feature_index, + kind, + } + } + + fn write_name<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + let feature = self.feature(); + if feature.flags.contains(FeatureFlags::WEBKIT_PREFIX) { + dest.write_str("-webkit-")?; + } + + if let QueryFeatureExpressionKind::LegacyRange(range, _) = self.kind { + match range { + LegacyRange::Min => dest.write_str("min-")?, + LegacyRange::Max => dest.write_str("max-")?, + } + } + + // NB: CssStringWriter not needed, feature names are under control. + write!(dest, "{}", feature.name)?; + + Ok(()) + } + + fn feature(&self) -> &'static QueryFeatureDescription { + &self.feature_type.features()[self.feature_index] + } + + /// Returns the feature flags for our feature. + pub fn feature_flags(&self) -> FeatureFlags { + self.feature().flags + } + + /// Parse a feature expression of the form: + /// + /// ``` + /// (media-feature: media-value) + /// ``` + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + feature_type: FeatureType, + ) -> Result<Self, ParseError<'i>> { + input.expect_parenthesis_block()?; + input.parse_nested_block(|input| { + Self::parse_in_parenthesis_block(context, input, feature_type) + }) + } + + fn parse_feature_name<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + feature_type: FeatureType, + ) -> Result<(usize, Option<LegacyRange>), ParseError<'i>> { + let mut flags = FeatureFlags::empty(); + let location = input.current_source_location(); + let ident = input.expect_ident()?; + + if context.chrome_rules_enabled() { + flags.insert(FeatureFlags::CHROME_AND_UA_ONLY); + } + + let mut feature_name = &**ident; + if starts_with_ignore_ascii_case(feature_name, "-webkit-") { + feature_name = &feature_name[8..]; + flags.insert(FeatureFlags::WEBKIT_PREFIX); + } + + let range = if starts_with_ignore_ascii_case(feature_name, "min-") { + feature_name = &feature_name[4..]; + Some(LegacyRange::Min) + } else if starts_with_ignore_ascii_case(feature_name, "max-") { + feature_name = &feature_name[4..]; + Some(LegacyRange::Max) + } else { + None + }; + + let atom = Atom::from(string_as_ascii_lowercase(feature_name)); + let (feature_index, feature) = match feature_type.find_feature(&atom) { + Some((i, f)) => (i, f), + None => { + return Err(location.new_custom_error( + StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), + )) + }, + }; + + if disabled_by_pref(&feature.name, context) || + !flags.contains(feature.flags.parsing_requirements()) || + (range.is_some() && !feature.allows_ranges()) + { + return Err(location.new_custom_error( + StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), + )); + } + + Ok((feature_index, range)) + } + + /// Parses the following range syntax: + /// + /// (feature-value <operator> feature-name) + /// (feature-value <operator> feature-name <operator> feature-value) + fn parse_multi_range_syntax<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + feature_type: FeatureType, + ) -> Result<Self, ParseError<'i>> { + let start = input.state(); + + // To parse the values, we first need to find the feature name. We rely + // on feature values for ranged features not being able to be top-level + // <ident>s, which holds. + let feature_index = loop { + // NOTE: parse_feature_name advances the input. + if let Ok((index, range)) = Self::parse_feature_name(context, input, feature_type) { + if range.is_some() { + // Ranged names are not allowed here. + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + break index; + } + if input.is_exhausted() { + return Err(start + .source_location() + .new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + }; + + input.reset(&start); + + let feature = &feature_type.features()[feature_index]; + let left_val = QueryExpressionValue::parse(feature, context, input)?; + let left_op = Operator::parse(input)?; + + { + let (parsed_index, _) = Self::parse_feature_name(context, input, feature_type)?; + debug_assert_eq!( + parsed_index, feature_index, + "How did we find a different feature?" + ); + } + + let right_op = input.try_parse(Operator::parse).ok(); + let right = match right_op { + Some(op) => { + if !left_op.is_compatible_with(op) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Some((op, QueryExpressionValue::parse(feature, context, input)?)) + }, + None => None, + }; + Ok(Self::new( + feature_type, + feature_index, + QueryFeatureExpressionKind::Range { + left: Some((left_op, left_val)), + right, + }, + )) + } + + /// Parse a feature expression where we've already consumed the parenthesis. + pub fn parse_in_parenthesis_block<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + feature_type: FeatureType, + ) -> Result<Self, ParseError<'i>> { + let (feature_index, range) = + match input.try_parse(|input| Self::parse_feature_name(context, input, feature_type)) { + Ok(v) => v, + Err(e) => { + if let Ok(expr) = Self::parse_multi_range_syntax(context, input, feature_type) { + return Ok(expr); + } + return Err(e); + }, + }; + let operator = input.try_parse(consume_operation_or_colon); + let operator = match operator { + Err(..) => { + // If there's no colon, this is a query of the form + // '(<feature>)', that is, there's no value specified. + // + // Gecko doesn't allow ranged expressions without a + // value, so just reject them here too. + if range.is_some() { + return Err( + input.new_custom_error(StyleParseErrorKind::RangedExpressionWithNoValue) + ); + } + + return Ok(Self::new( + feature_type, + feature_index, + QueryFeatureExpressionKind::Empty, + )); + }, + Ok(operator) => operator, + }; + + let feature = &feature_type.features()[feature_index]; + + let value = QueryExpressionValue::parse(feature, context, input).map_err(|err| { + err.location + .new_custom_error(StyleParseErrorKind::MediaQueryExpectedFeatureValue) + })?; + + let kind = match range { + Some(range) => { + if operator.is_some() { + return Err( + input.new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator) + ); + } + QueryFeatureExpressionKind::LegacyRange(range, value) + }, + None => match operator { + Some(operator) => { + if !feature.allows_ranges() { + return Err(input + .new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator)); + } + QueryFeatureExpressionKind::Range { + left: None, + right: Some((operator, value)), + } + }, + None => QueryFeatureExpressionKind::Single(value), + }, + }; + + Ok(Self::new(feature_type, feature_index, kind)) + } + + /// Returns whether this query evaluates to true for the given device. + pub fn matches(&self, context: &computed::Context) -> KleeneValue { + macro_rules! expect { + ($variant:ident, $v:expr) => { + match *$v { + QueryExpressionValue::$variant(ref v) => v, + _ => unreachable!("Unexpected QueryExpressionValue"), + } + }; + } + + KleeneValue::from(match self.feature().evaluator { + Evaluator::Length(eval) => { + let v = eval(context); + self.kind + .evaluate(v, |v| expect!(Length, v).to_computed_value(context)) + }, + Evaluator::OptionalLength(eval) => { + let v = match eval(context) { + Some(v) => v, + None => return KleeneValue::Unknown, + }; + self.kind + .evaluate(v, |v| expect!(Length, v).to_computed_value(context)) + }, + Evaluator::Integer(eval) => { + let v = eval(context); + self.kind.evaluate(v, |v| *expect!(Integer, v)) + }, + Evaluator::Float(eval) => { + let v = eval(context); + self.kind.evaluate(v, |v| *expect!(Float, v)) + }, + Evaluator::NumberRatio(eval) => { + let ratio = eval(context); + // A ratio of 0/0 behaves as the ratio 1/0, so we need to call used_value() + // to convert it if necessary. + // FIXME: we may need to update here once + // https://github.com/w3c/csswg-drafts/issues/4954 got resolved. + self.kind + .evaluate(ratio, |v| expect!(NumberRatio, v).used_value()) + }, + Evaluator::OptionalNumberRatio(eval) => { + let ratio = match eval(context) { + Some(v) => v, + None => return KleeneValue::Unknown, + }; + // See above for subtleties here. + self.kind + .evaluate(ratio, |v| expect!(NumberRatio, v).used_value()) + }, + Evaluator::Resolution(eval) => { + let v = eval(context).dppx(); + self.kind.evaluate(v, |v| { + expect!(Resolution, v).to_computed_value(context).dppx() + }) + }, + Evaluator::Enumerated { evaluator, .. } => { + let computed = self + .kind + .non_ranged_value() + .map(|v| *expect!(Enumerated, v)); + return evaluator(context, computed); + }, + Evaluator::String(evaluator) => { + let string = self.kind.non_ranged_value().map(|v| expect!(String, v)); + return evaluator(context, string); + }, + Evaluator::BoolInteger(eval) => { + let computed = self + .kind + .non_ranged_value() + .map(|v| *expect!(BoolInteger, v)); + let boolean = eval(context); + computed.map_or(boolean, |v| v == boolean) + }, + }) + } +} + +/// A value found or expected in a expression. +/// +/// FIXME(emilio): How should calc() serialize in the Number / Integer / +/// BoolInteger / NumberRatio case, as computed or as specified value? +/// +/// If the first, this would need to store the relevant values. +/// +/// See: https://github.com/w3c/csswg-drafts/issues/1968 +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum QueryExpressionValue { + /// A length. + Length(Length), + /// An integer. + Integer(i32), + /// A floating point value. + Float(CSSFloat), + /// A boolean value, specified as an integer (i.e., either 0 or 1). + BoolInteger(bool), + /// A single non-negative number or two non-negative numbers separated by '/', + /// with optional whitespace on either side of the '/'. + NumberRatio(Ratio), + /// A resolution. + Resolution(Resolution), + /// An enumerated value, defined by the variant keyword table in the + /// feature's `mData` member. + Enumerated(KeywordDiscriminant), + /// An arbitrary ident value. + String(AtomString), +} + +impl QueryExpressionValue { + fn to_css<W>(&self, dest: &mut CssWriter<W>, for_expr: &QueryFeatureExpression) -> fmt::Result + where + W: fmt::Write, + { + match *self { + QueryExpressionValue::Length(ref l) => l.to_css(dest), + QueryExpressionValue::Integer(v) => v.to_css(dest), + QueryExpressionValue::Float(v) => v.to_css(dest), + QueryExpressionValue::BoolInteger(v) => dest.write_str(if v { "1" } else { "0" }), + QueryExpressionValue::NumberRatio(ratio) => ratio.to_css(dest), + QueryExpressionValue::Resolution(ref r) => r.to_css(dest), + QueryExpressionValue::Enumerated(value) => match for_expr.feature().evaluator { + Evaluator::Enumerated { serializer, .. } => dest.write_str(&*serializer(value)), + _ => unreachable!(), + }, + QueryExpressionValue::String(ref s) => s.to_css(dest), + } + } + + fn parse<'i, 't>( + for_feature: &QueryFeatureDescription, + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<QueryExpressionValue, ParseError<'i>> { + Ok(match for_feature.evaluator { + Evaluator::OptionalLength(..) | Evaluator::Length(..) => { + let length = Length::parse(context, input)?; + QueryExpressionValue::Length(length) + }, + Evaluator::Integer(..) => { + let integer = Integer::parse(context, input)?; + QueryExpressionValue::Integer(integer.value()) + }, + Evaluator::BoolInteger(..) => { + let integer = Integer::parse_non_negative(context, input)?; + let value = integer.value(); + if value > 1 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + QueryExpressionValue::BoolInteger(value == 1) + }, + Evaluator::Float(..) => { + let number = Number::parse(context, input)?; + QueryExpressionValue::Float(number.get()) + }, + Evaluator::OptionalNumberRatio(..) | Evaluator::NumberRatio(..) => { + use crate::values::specified::Ratio as SpecifiedRatio; + let ratio = SpecifiedRatio::parse(context, input)?; + QueryExpressionValue::NumberRatio(Ratio::new(ratio.0.get(), ratio.1.get())) + }, + Evaluator::Resolution(..) => { + QueryExpressionValue::Resolution(Resolution::parse(context, input)?) + }, + Evaluator::String(..) => { + QueryExpressionValue::String(input.expect_string()?.as_ref().into()) + }, + Evaluator::Enumerated { parser, .. } => { + QueryExpressionValue::Enumerated(parser(context, input)?) + }, + }) + } +} diff --git a/servo/components/style/queries/mod.rs b/servo/components/style/queries/mod.rs new file mode 100644 index 0000000000..ec11ab3721 --- /dev/null +++ b/servo/components/style/queries/mod.rs @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Code shared between [media queries][mq] and [container queries][cq]. +//! +//! [mq]: https://drafts.csswg.org/mediaqueries/ +//! [cq]: https://drafts.csswg.org/css-contain-3/#container-rule + +pub mod condition; + +#[macro_use] +pub mod feature; +pub mod feature_expression; +pub mod values; + +pub use self::condition::QueryCondition; +pub use self::feature::FeatureFlags; +pub use self::feature_expression::{FeatureType, QueryFeatureExpression}; diff --git a/servo/components/style/queries/values.rs b/servo/components/style/queries/values.rs new file mode 100644 index 0000000000..f4934408c4 --- /dev/null +++ b/servo/components/style/queries/values.rs @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Common feature values between media and container features. + +use app_units::Au; +use euclid::default::Size2D; + +/// The orientation media / container feature. +/// https://drafts.csswg.org/mediaqueries-5/#orientation +/// https://drafts.csswg.org/css-contain-3/#orientation +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum Orientation { + Portrait, + Landscape, +} + +impl Orientation { + /// A helper to evaluate a orientation query given a generic size getter. + pub fn eval(size: Size2D<Au>, value: Option<Self>) -> bool { + let query_orientation = match value { + Some(v) => v, + None => return true, + }; + + // Per spec, square viewports should be 'portrait' + let is_landscape = size.width > size.height; + match query_orientation { + Self::Landscape => is_landscape, + Self::Portrait => !is_landscape, + } + } +} |