/* 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, QueryStyleRange}; use crate::computed_value_flags::ComputedValueFlags; use crate::context::QuirksMode; use crate::custom_properties; use crate::derives::*; use crate::dom::AttributeTracker; use crate::properties::CSSWideKeyword; use crate::properties_and_values::rule::Descriptors as PropertyDescriptors; use crate::properties_and_values::value::{ AllowComputationallyDependent, ComputedValue as ComputedRegisteredValue, SpecifiedValue as SpecifiedRegisteredValue, }; use crate::stylesheets::{CssRuleType, CustomMediaEvaluator, Origin, UrlExtraData}; use crate::stylist::Stylist; use crate::values::{computed, AtomString, DashedIdent}; use crate::{error_reporting::ContextualParseError, parser::Parse, parser::ParserContext}; use cssparser::{ match_ignore_ascii_case, parse_important, Parser, ParserInput, SourcePosition, Token, }; use selectors::kleene_value::KleeneValue; use servo_arc::Arc; use std::fmt::{self, Write}; use style_traits::{CssWriter, ParseError, ParsingMode, 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, } #[derive(Clone, Debug, PartialEq, ToShmem)] enum StyleFeatureValue { Value(Option>), Keyword(CSSWideKeyword), } /// Trait for query elements that parse a series of conditions separated by /// AND or OR operators, or prefixed with NOT. /// /// This is used by both QueryCondition and StyleQuery as they support similar /// syntax for combining multiple conditions with a boolean operator. trait OperationParser: Sized { /// 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> { 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(Self::new_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(Self::new_operation(conditions.into_boxed_slice(), operator)); } conditions.push(Self::parse_in_parens(context, input, feature_type)?); } } // Parse a condition in parentheses, or ``. fn parse_in_parens<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, feature_type: FeatureType, ) -> Result>; // Helpers to create the appropriate enum variant of the implementing type: // Create a Not result that encapsulates the `inner` condition. fn new_not(inner: Box) -> Self; // Create an Operation result with the given list of `conditions` using `operator`. fn new_operation(conditions: Box<[Self]>, operator: Operator) -> Self; } /// https://drafts.csswg.org/css-conditional-5/#typedef-style-query #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] pub enum StyleQuery { /// A negation of a condition. Not(Box), /// A set of joint operations. Operation(Box<[StyleQuery]>, Operator), /// A condition wrapped in parenthesis. InParens(Box), /// A feature query (`--foo: bar` or just `--foo`). Feature(StyleFeature), /// An unknown "general-enclosed" term. GeneralEnclosed(String), } impl ToCss for StyleQuery { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: fmt::Write, { match *self { StyleQuery::Not(ref c) => { dest.write_str("not ")?; c.maybe_parenthesized(dest) }, StyleQuery::Operation(ref list, op) => { let mut iter = list.iter(); let item = iter.next().unwrap(); item.maybe_parenthesized(dest)?; for item in iter { dest.write_char(' ')?; op.to_css(dest)?; dest.write_char(' ')?; item.maybe_parenthesized(dest)?; } Ok(()) }, StyleQuery::InParens(ref c) => match &**c { StyleQuery::Feature(_) | StyleQuery::InParens(_) => { dest.write_char('(')?; c.to_css(dest)?; dest.write_char(')') }, _ => c.to_css(dest), }, StyleQuery::Feature(ref f) => f.to_css(dest), StyleQuery::GeneralEnclosed(ref s) => dest.write_str(&s), } } } impl StyleQuery { // Helper for to_css when handling values within boolean operators: // GeneralEnclosed includes its parens in the string, so we don't need to // wrap the value with an additional set here. fn maybe_parenthesized(&self, dest: &mut CssWriter) -> fmt::Result where W: fmt::Write, { if let StyleQuery::GeneralEnclosed(ref s) = self { dest.write_str(&s) } else { dest.write_char('(')?; self.to_css(dest)?; dest.write_char(')') } } fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, feature_type: FeatureType, ) -> Result> { if !static_prefs::pref!("layout.css.style-queries.enabled") || feature_type != FeatureType::Container { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } if let Ok(feature) = input.try_parse(|input| StyleFeature::parse(context, input)) { return Ok(Self::Feature(feature)); } let inner = Self::parse_internal(context, input, feature_type, AllowOr::Yes)?; Ok(Self::InParens(Box::new(inner))) } fn parse_in_parenthesis_block<'i>( context: &ParserContext, input: &mut Parser<'i, '_>, ) -> Result> { // Base case. Make sure to preserve this error as it's more generally // relevant. let feature_error = match input.try_parse(|input| StyleFeature::parse(context, input)) { Ok(feature) => return Ok(Self::Feature(feature)), Err(e) => e, }; if let Ok(inner) = Self::parse(context, input, FeatureType::Container) { return Ok(inner); } Err(feature_error) } fn try_parse_block<'i, T, F>( context: &ParserContext, input: &mut Parser<'i, '_>, start: SourcePosition, parse: F, ) -> Option where F: for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result>, { let nested = input.try_parse(|input| input.parse_nested_block(parse)); match nested { Ok(nested) => Some(nested), Err(e) => { // We're about to swallow the error in a `` // 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); None }, } } fn matches( &self, ctx: &computed::Context, attribute_tracker: &mut AttributeTracker, ) -> KleeneValue { ctx.builder .add_flags(ComputedValueFlags::DEPENDS_ON_CONTAINER_STYLE_QUERY); match *self { StyleQuery::Feature(ref f) => f.matches(ctx, attribute_tracker), StyleQuery::Not(ref c) => !c.matches(ctx, attribute_tracker), StyleQuery::InParens(ref c) => c.matches(ctx, attribute_tracker), StyleQuery::Operation(ref conditions, op) => { debug_assert!(!conditions.is_empty(), "We never create an empty op"); match op { Operator::And => KleeneValue::any_false(conditions.iter(), |c| { c.matches(ctx, attribute_tracker) }), Operator::Or => { KleeneValue::any(conditions.iter(), |c| c.matches(ctx, attribute_tracker)) }, } }, StyleQuery::GeneralEnclosed(_) => KleeneValue::Unknown, } } } impl OperationParser for StyleQuery { fn parse_in_parens<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, feature_type: FeatureType, ) -> Result> { assert!(feature_type == FeatureType::Container); input.skip_whitespace(); let start = input.position(); let start_location = input.current_source_location(); match *input.next()? { Token::ParenthesisBlock => { if let Some(nested) = Self::try_parse_block(context, input, start, |i| { Self::parse_in_parenthesis_block(context, i) }) { return Ok(nested); } // Accept : as a GeneralEnclosed (which evaluates // to false, but does not invalidate the query as a whole). input.parse_nested_block(|i| { i.expect_ident()?; i.expect_colon()?; consume_any_value(i) })?; Ok(Self::GeneralEnclosed(input.slice_from(start).to_owned())) }, ref t => return Err(start_location.new_unexpected_token_error(t.clone())), } } fn new_not(inner: Box) -> Self { Self::Not(inner) } fn new_operation(conditions: Box<[Self]>, operator: Operator) -> Self { Self::Operation(conditions, operator) } } /// A style query feature: /// https://drafts.csswg.org/css-conditional-5/#typedef-style-feature #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] pub enum StyleFeature { /// A property name and optional value to match. Plain(StyleFeaturePlain), /// A style query range expression. Range(QueryStyleRange), } impl StyleFeature { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { if let Ok(range) = input.try_parse(|i| QueryStyleRange::parse(context, i)) { return Ok(Self::Range(range)); } Ok(Self::Plain(StyleFeaturePlain::parse(context, input)?)) } fn matches( &self, ctx: &computed::Context, attribute_tracker: &mut AttributeTracker, ) -> KleeneValue { match self { Self::Plain(plain) => plain.matches(ctx, attribute_tracker), Self::Range(range) => range.evaluate(ctx, attribute_tracker), } } } /// A style feature consisting of a custom property name and (optionally) value. #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] pub struct StyleFeaturePlain { name: custom_properties::Name, #[ignore_malloc_size_of = "StyleFeatureValue has an Arc variant"] value: StyleFeatureValue, } impl ToCss for StyleFeaturePlain { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: fmt::Write, { dest.write_str("--")?; crate::values::serialize_atom_identifier(&self.name, dest)?; match self.value { StyleFeatureValue::Keyword(k) => { dest.write_str(": ")?; k.to_css(dest)?; }, StyleFeatureValue::Value(Some(ref v)) => { dest.write_str(": ")?; v.to_css(dest)?; }, StyleFeatureValue::Value(None) => (), } Ok(()) } } impl StyleFeaturePlain { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let ident = input.expect_ident()?; // TODO(emilio): Maybe support non-custom properties? let name = match custom_properties::parse_name(ident.as_ref()) { Ok(name) => custom_properties::Name::from(name), Err(()) => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), }; let value = if input.try_parse(|i| i.expect_colon()).is_ok() { input.skip_whitespace(); if let Ok(keyword) = input.try_parse(|i| CSSWideKeyword::parse(i)) { StyleFeatureValue::Keyword(keyword) } else { let value = custom_properties::SpecifiedValue::parse( input, Some(&context.namespaces.prefixes), &context.url_data, )?; // `!important` is allowed (but ignored) after the value. let _ = input.try_parse(parse_important); StyleFeatureValue::Value(Some(Arc::new(value))) } } else { StyleFeatureValue::Value(None) }; Ok(Self { name, value }) } // Substitute custom-property references in `value`, then re-parse and compute it, // and compare against `current_value`. fn substitute_and_compare( value: &Arc, registration: &PropertyDescriptors, stylist: &Stylist, ctx: &computed::Context, attribute_tracker: &mut AttributeTracker, current_value: Option<&ComputedRegisteredValue>, ) -> bool { let substitution_functions = custom_properties::ComputedSubstitutionFunctions::new( Some(ctx.inherited_custom_properties().clone()), None, ); // FIXME(bug2026241): `attribute_tainted` should be used. let custom_properties::SubstitutionResult { css, .. } = match custom_properties::substitute( &value, &substitution_functions, stylist, ctx, attribute_tracker, ) { Ok(sub) => sub, Err(_) => return current_value.is_none(), }; if registration.is_universal() { return match current_value { Some(v) => v.as_universal().is_some_and(|v| v.css == css), None => css.is_empty(), }; } let mut input = cssparser::ParserInput::new(&css); let mut parser = Parser::new(&mut input); let computed = SpecifiedRegisteredValue::compute( &mut parser, registration, None, &value.url_data, ctx, AllowComputationallyDependent::Yes, ) .ok(); computed.as_ref() == current_value } fn matches( &self, ctx: &computed::Context, attribute_tracker: &mut AttributeTracker, ) -> KleeneValue { // FIXME(emilio): Confirm this is the right style to query. let stylist = ctx .builder .stylist .expect("container queries should have a stylist around"); let registration = stylist.get_custom_property_registration(&self.name); let current_value = ctx .inherited_custom_properties() .get(registration, &self.name); KleeneValue::from(match self.value { StyleFeatureValue::Value(Some(ref v)) => { if ctx.container_info.is_none() { // If no container, custom props are guaranteed-unknown. false } else if v.has_references() { // If there are --var() references in the query value, // try to substitute them before comparing to current. Self::substitute_and_compare( v, registration, stylist, ctx, attribute_tracker, current_value, ) } else { custom_properties::compute_variable_value(&v, registration, ctx).as_ref() == current_value } }, StyleFeatureValue::Value(None) => current_value.is_some(), StyleFeatureValue::Keyword(kw) => { match kw { CSSWideKeyword::Unset => current_value.is_none(), CSSWideKeyword::Initial => { if let Some(initial) = ®istration.initial_value { let v = custom_properties::compute_variable_value( &initial, registration, ctx, ); v.as_ref() == current_value } else { current_value.is_none() } }, CSSWideKeyword::Inherit => { if let Some(inherited) = ctx .container_info .as_ref() .expect("queries should provide container info") .inherited_style() { inherited.custom_properties().get(registration, &self.name) == current_value } else { false } }, // Cascade-dependent keywords, such as revert and revert-layer, // are invalid as values in a style feature, and cause the // container style query to be false. // https://drafts.csswg.org/css-conditional-5/#evaluate-a-style-range CSSWideKeyword::Revert | CSSWideKeyword::RevertLayer | CSSWideKeyword::RevertRule => false, } }, }) } } /// A boolean value for a pref query. #[derive( Clone, Debug, MallocSizeOf, PartialEq, Eq, Parse, SpecifiedValueInfo, ToComputedValue, ToCss, ToShmem, )] #[repr(u8)] #[allow(missing_docs)] pub enum BoolValue { False, True, } /// Simple values we support for -moz-pref(). We don't want to deal with calc() and other /// shenanigans for now. #[derive( Clone, Debug, Eq, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToShmem, )] #[repr(u8)] pub enum MozPrefFeatureValue { /// No pref value, implicitly bool, but also used to represent missing prefs. #[css(skip)] None, /// A bool value. Boolean(BoolValue), /// An integer value, useful for int prefs. Integer(I), /// A string pref value. String(crate::values::AtomString), } type SpecifiedMozPrefFeatureValue = MozPrefFeatureValue; /// The computed -moz-pref() value. pub type ComputedMozPrefFeatureValue = MozPrefFeatureValue; /// A custom -moz-pref(, ) query feature. #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] pub struct MozPrefFeature { name: crate::values::AtomString, value: SpecifiedMozPrefFeatureValue, } impl MozPrefFeature { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, feature_type: FeatureType, ) -> Result> { use crate::parser::Parse; if !context.chrome_rules_enabled() || feature_type != FeatureType::Media { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } let name = AtomString::parse(context, input)?; let value = if input.try_parse(|i| i.expect_comma()).is_ok() { SpecifiedMozPrefFeatureValue::parse(context, input)? } else { SpecifiedMozPrefFeatureValue::None }; Ok(Self { name, value }) } #[cfg(feature = "gecko")] fn matches(&self, ctx: &computed::Context) -> KleeneValue { use crate::values::computed::ToComputedValue; let value = self.value.to_computed_value(ctx); KleeneValue::from(unsafe { crate::gecko_bindings::bindings::Gecko_EvalMozPrefFeature(self.name.as_ptr(), &value) }) } #[cfg(feature = "servo")] fn matches(&self, _: &computed::Context) -> KleeneValue { KleeneValue::Unknown } } impl ToCss for MozPrefFeature { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: fmt::Write, { self.name.to_css(dest)?; if !matches!(self.value, MozPrefFeatureValue::None) { dest.write_str(", ")?; self.value.to_css(dest)?; } Ok(()) } } /// Represents a condition. #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] pub enum QueryCondition { /// A simple feature expression, implicitly parenthesized. Feature(QueryFeatureExpression), /// A custom media query reference in a boolean context, implicitly parenthesized. Custom(DashedIdent), /// A negation of a condition. Not(Box), /// A set of joint operations. Operation(Box<[QueryCondition]>, Operator), /// A condition wrapped in parenthesis. InParens(Box), /// A