tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

condition.rs (18242B)


      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 //! A query condition:
      6 //!
      7 //! https://drafts.csswg.org/mediaqueries-4/#typedef-media-condition
      8 //! https://drafts.csswg.org/css-contain-3/#typedef-container-condition
      9 
     10 use super::{FeatureFlags, FeatureType, QueryFeatureExpression};
     11 use crate::custom_properties;
     12 use crate::derives::*;
     13 use crate::stylesheets::CustomMediaEvaluator;
     14 use crate::values::{computed, AtomString, DashedIdent};
     15 use crate::{error_reporting::ContextualParseError, parser::Parse, parser::ParserContext};
     16 use cssparser::{match_ignore_ascii_case, Parser, SourcePosition, Token};
     17 use selectors::kleene_value::KleeneValue;
     18 use servo_arc::Arc;
     19 use std::fmt::{self, Write};
     20 use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
     21 
     22 /// A binary `and` or `or` operator.
     23 #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)]
     24 #[allow(missing_docs)]
     25 pub enum Operator {
     26    And,
     27    Or,
     28 }
     29 
     30 /// Whether to allow an `or` condition or not during parsing.
     31 #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss)]
     32 enum AllowOr {
     33    Yes,
     34    No,
     35 }
     36 
     37 /// A style query feature:
     38 /// https://drafts.csswg.org/css-conditional-5/#typedef-style-feature
     39 #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
     40 pub struct StyleFeature {
     41    name: custom_properties::Name,
     42    // TODO: This is a "primary" reference, probably should be unconditionally measured.
     43    #[ignore_malloc_size_of = "Arc"]
     44    value: Option<Arc<custom_properties::SpecifiedValue>>,
     45 }
     46 
     47 impl ToCss for StyleFeature {
     48    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
     49    where
     50        W: fmt::Write,
     51    {
     52        dest.write_str("--")?;
     53        crate::values::serialize_atom_identifier(&self.name, dest)?;
     54        if let Some(ref v) = self.value {
     55            dest.write_str(": ")?;
     56            v.to_css(dest)?;
     57        }
     58        Ok(())
     59    }
     60 }
     61 
     62 impl StyleFeature {
     63    fn parse<'i, 't>(
     64        context: &ParserContext,
     65        input: &mut Parser<'i, 't>,
     66        feature_type: FeatureType,
     67    ) -> Result<Self, ParseError<'i>> {
     68        if !static_prefs::pref!("layout.css.style-queries.enabled")
     69            || feature_type != FeatureType::Container
     70        {
     71            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
     72        }
     73        // TODO: Allow parsing nested style feature queries.
     74        let ident = input.expect_ident()?;
     75        // TODO(emilio): Maybe support non-custom properties?
     76        let name = match custom_properties::parse_name(ident.as_ref()) {
     77            Ok(name) => custom_properties::Name::from(name),
     78            Err(()) => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
     79        };
     80        let value = if input.try_parse(|i| i.expect_colon()).is_ok() {
     81            input.skip_whitespace();
     82            Some(Arc::new(custom_properties::SpecifiedValue::parse(
     83                input,
     84                &context.url_data,
     85            )?))
     86        } else {
     87            None
     88        };
     89        Ok(Self { name, value })
     90    }
     91 
     92    fn matches(&self, ctx: &computed::Context) -> KleeneValue {
     93        // FIXME(emilio): Confirm this is the right style to query.
     94        let registration = ctx
     95            .builder
     96            .stylist
     97            .expect("container queries should have a stylist around")
     98            .get_custom_property_registration(&self.name);
     99        let current_value = ctx
    100            .inherited_custom_properties()
    101            .get(registration, &self.name);
    102        KleeneValue::from(match self.value {
    103            Some(ref v) => current_value.is_some_and(|cur| {
    104                custom_properties::compute_variable_value(v, registration, ctx)
    105                    .is_some_and(|v| v == *cur)
    106            }),
    107            None => current_value.is_some(),
    108        })
    109    }
    110 }
    111 
    112 /// A boolean value for a pref query.
    113 #[derive(
    114    Clone,
    115    Debug,
    116    MallocSizeOf,
    117    PartialEq,
    118    Eq,
    119    Parse,
    120    SpecifiedValueInfo,
    121    ToComputedValue,
    122    ToCss,
    123    ToShmem,
    124 )]
    125 #[repr(u8)]
    126 #[allow(missing_docs)]
    127 pub enum BoolValue {
    128    False,
    129    True,
    130 }
    131 
    132 /// Simple values we support for -moz-pref(). We don't want to deal with calc() and other
    133 /// shenanigans for now.
    134 #[derive(
    135    Clone,
    136    Debug,
    137    Eq,
    138    MallocSizeOf,
    139    Parse,
    140    PartialEq,
    141    SpecifiedValueInfo,
    142    ToComputedValue,
    143    ToCss,
    144    ToShmem,
    145 )]
    146 #[repr(u8)]
    147 pub enum MozPrefFeatureValue<I> {
    148    /// No pref value, implicitly bool, but also used to represent missing prefs.
    149    #[css(skip)]
    150    None,
    151    /// A bool value.
    152    Boolean(BoolValue),
    153    /// An integer value, useful for int prefs.
    154    Integer(I),
    155    /// A string pref value.
    156    String(crate::values::AtomString),
    157 }
    158 
    159 type SpecifiedMozPrefFeatureValue = MozPrefFeatureValue<crate::values::specified::Integer>;
    160 /// The computed -moz-pref() value.
    161 pub type ComputedMozPrefFeatureValue = MozPrefFeatureValue<crate::values::computed::Integer>;
    162 
    163 /// A custom -moz-pref(<name>, <value>) query feature.
    164 #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
    165 pub struct MozPrefFeature {
    166    name: crate::values::AtomString,
    167    value: SpecifiedMozPrefFeatureValue,
    168 }
    169 
    170 impl MozPrefFeature {
    171    fn parse<'i, 't>(
    172        context: &ParserContext,
    173        input: &mut Parser<'i, 't>,
    174        feature_type: FeatureType,
    175    ) -> Result<Self, ParseError<'i>> {
    176        use crate::parser::Parse;
    177        if !context.chrome_rules_enabled() || feature_type != FeatureType::Media {
    178            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
    179        }
    180        let name = AtomString::parse(context, input)?;
    181        let value = if input.try_parse(|i| i.expect_comma()).is_ok() {
    182            SpecifiedMozPrefFeatureValue::parse(context, input)?
    183        } else {
    184            SpecifiedMozPrefFeatureValue::None
    185        };
    186        Ok(Self { name, value })
    187    }
    188 
    189    #[cfg(feature = "gecko")]
    190    fn matches(&self, ctx: &computed::Context) -> KleeneValue {
    191        use crate::values::computed::ToComputedValue;
    192        let value = self.value.to_computed_value(ctx);
    193        KleeneValue::from(unsafe {
    194            crate::gecko_bindings::bindings::Gecko_EvalMozPrefFeature(self.name.as_ptr(), &value)
    195        })
    196    }
    197 
    198    #[cfg(feature = "servo")]
    199    fn matches(&self, _: &computed::Context) -> KleeneValue {
    200        KleeneValue::Unknown
    201    }
    202 }
    203 
    204 impl ToCss for MozPrefFeature {
    205    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
    206    where
    207        W: fmt::Write,
    208    {
    209        self.name.to_css(dest)?;
    210        if !matches!(self.value, MozPrefFeatureValue::None) {
    211            dest.write_str(", ")?;
    212            self.value.to_css(dest)?;
    213        }
    214        Ok(())
    215    }
    216 }
    217 
    218 /// Represents a condition.
    219 #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
    220 pub enum QueryCondition {
    221    /// A simple feature expression, implicitly parenthesized.
    222    Feature(QueryFeatureExpression),
    223    /// A custom media query reference in a boolean context, implicitly parenthesized.
    224    Custom(DashedIdent),
    225    /// A negation of a condition.
    226    Not(Box<QueryCondition>),
    227    /// A set of joint operations.
    228    Operation(Box<[QueryCondition]>, Operator),
    229    /// A condition wrapped in parenthesis.
    230    InParens(Box<QueryCondition>),
    231    /// A <style> query.
    232    Style(StyleFeature),
    233    /// A -moz-pref() query.
    234    MozPref(MozPrefFeature),
    235    /// [ <function-token> <any-value>? ) ] | [ ( <any-value>? ) ]
    236    GeneralEnclosed(String),
    237 }
    238 
    239 impl ToCss for QueryCondition {
    240    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
    241    where
    242        W: fmt::Write,
    243    {
    244        match *self {
    245            // NOTE(emilio): QueryFeatureExpression already includes the
    246            // parenthesis.
    247            QueryCondition::Feature(ref f) => f.to_css(dest),
    248            QueryCondition::Custom(ref name) => {
    249                dest.write_char('(')?;
    250                name.to_css(dest)?;
    251                dest.write_char(')')
    252            },
    253            QueryCondition::Not(ref c) => {
    254                dest.write_str("not ")?;
    255                c.to_css(dest)
    256            },
    257            QueryCondition::InParens(ref c) => {
    258                dest.write_char('(')?;
    259                c.to_css(dest)?;
    260                dest.write_char(')')
    261            },
    262            QueryCondition::Style(ref c) => {
    263                dest.write_str("style(")?;
    264                c.to_css(dest)?;
    265                dest.write_char(')')
    266            },
    267            QueryCondition::MozPref(ref c) => {
    268                dest.write_str("-moz-pref(")?;
    269                c.to_css(dest)?;
    270                dest.write_char(')')
    271            },
    272            QueryCondition::Operation(ref list, op) => {
    273                let mut iter = list.iter();
    274                iter.next().unwrap().to_css(dest)?;
    275                for item in iter {
    276                    dest.write_char(' ')?;
    277                    op.to_css(dest)?;
    278                    dest.write_char(' ')?;
    279                    item.to_css(dest)?;
    280                }
    281                Ok(())
    282            },
    283            QueryCondition::GeneralEnclosed(ref s) => dest.write_str(&s),
    284        }
    285    }
    286 }
    287 
    288 /// <https://drafts.csswg.org/css-syntax-3/#typedef-any-value>
    289 fn consume_any_value<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> {
    290    input.expect_no_error_token().map_err(Into::into)
    291 }
    292 
    293 impl QueryCondition {
    294    /// Parse a single condition.
    295    pub fn parse<'i, 't>(
    296        context: &ParserContext,
    297        input: &mut Parser<'i, 't>,
    298        feature_type: FeatureType,
    299    ) -> Result<Self, ParseError<'i>> {
    300        Self::parse_internal(context, input, feature_type, AllowOr::Yes)
    301    }
    302 
    303    fn visit<F>(&self, visitor: &mut F)
    304    where
    305        F: FnMut(&Self),
    306    {
    307        visitor(self);
    308        match *self {
    309            Self::Custom(..)
    310            | Self::Feature(..)
    311            | Self::GeneralEnclosed(..)
    312            | Self::Style(..)
    313            | Self::MozPref(..) => {},
    314            Self::Not(ref cond) => cond.visit(visitor),
    315            Self::Operation(ref conds, _op) => {
    316                for cond in conds.iter() {
    317                    cond.visit(visitor);
    318                }
    319            },
    320            Self::InParens(ref cond) => cond.visit(visitor),
    321        }
    322    }
    323 
    324    /// Returns the union of all flags in the expression. This is useful for
    325    /// container queries.
    326    pub fn cumulative_flags(&self) -> FeatureFlags {
    327        let mut result = FeatureFlags::empty();
    328        self.visit(&mut |condition| {
    329            if let Self::Style(..) = condition {
    330                result.insert(FeatureFlags::STYLE);
    331            }
    332            if let Self::Feature(ref f) = condition {
    333                result.insert(f.feature_flags())
    334            }
    335        });
    336        result
    337    }
    338 
    339    /// Parse a single condition, disallowing `or` expressions.
    340    ///
    341    /// To be used from the legacy query syntax.
    342    pub fn parse_disallow_or<'i, 't>(
    343        context: &ParserContext,
    344        input: &mut Parser<'i, 't>,
    345        feature_type: FeatureType,
    346    ) -> Result<Self, ParseError<'i>> {
    347        Self::parse_internal(context, input, feature_type, AllowOr::No)
    348    }
    349 
    350    /// https://drafts.csswg.org/mediaqueries-5/#typedef-media-condition or
    351    /// https://drafts.csswg.org/mediaqueries-5/#typedef-media-condition-without-or
    352    /// (depending on `allow_or`).
    353    fn parse_internal<'i, 't>(
    354        context: &ParserContext,
    355        input: &mut Parser<'i, 't>,
    356        feature_type: FeatureType,
    357        allow_or: AllowOr,
    358    ) -> Result<Self, ParseError<'i>> {
    359        let location = input.current_source_location();
    360        if input.try_parse(|i| i.expect_ident_matching("not")).is_ok() {
    361            let inner_condition = Self::parse_in_parens(context, input, feature_type)?;
    362            return Ok(QueryCondition::Not(Box::new(inner_condition)));
    363        }
    364 
    365        let first_condition = Self::parse_in_parens(context, input, feature_type)?;
    366        let operator = match input.try_parse(Operator::parse) {
    367            Ok(op) => op,
    368            Err(..) => return Ok(first_condition),
    369        };
    370 
    371        if allow_or == AllowOr::No && operator == Operator::Or {
    372            return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
    373        }
    374 
    375        let mut conditions = vec![];
    376        conditions.push(first_condition);
    377        conditions.push(Self::parse_in_parens(context, input, feature_type)?);
    378 
    379        let delim = match operator {
    380            Operator::And => "and",
    381            Operator::Or => "or",
    382        };
    383 
    384        loop {
    385            if input.try_parse(|i| i.expect_ident_matching(delim)).is_err() {
    386                return Ok(QueryCondition::Operation(
    387                    conditions.into_boxed_slice(),
    388                    operator,
    389                ));
    390            }
    391 
    392            conditions.push(Self::parse_in_parens(context, input, feature_type)?);
    393        }
    394    }
    395 
    396    fn parse_in_parenthesis_block<'i>(
    397        context: &ParserContext,
    398        input: &mut Parser<'i, '_>,
    399        feature_type: FeatureType,
    400    ) -> Result<Self, ParseError<'i>> {
    401        // Base case. Make sure to preserve this error as it's more generally
    402        // relevant.
    403        let feature_error = match input.try_parse(|input| {
    404            QueryFeatureExpression::parse_in_parenthesis_block(context, input, feature_type)
    405        }) {
    406            Ok(expr) => return Ok(Self::Feature(expr)),
    407            Err(e) => e,
    408        };
    409        if static_prefs::pref!("layout.css.custom-media.enabled") {
    410            if let Ok(custom) = input.try_parse(|input| DashedIdent::parse(context, input)) {
    411                return Ok(Self::Custom(custom));
    412            }
    413        }
    414        if let Ok(inner) = Self::parse(context, input, feature_type) {
    415            return Ok(Self::InParens(Box::new(inner)));
    416        }
    417        Err(feature_error)
    418    }
    419 
    420    fn try_parse_block<'i, T, F>(
    421        context: &ParserContext,
    422        input: &mut Parser<'i, '_>,
    423        start: SourcePosition,
    424        parse: F,
    425    ) -> Option<T>
    426    where
    427        F: for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i>>,
    428    {
    429        let nested = input.try_parse(|input| input.parse_nested_block(parse));
    430        match nested {
    431            Ok(nested) => Some(nested),
    432            Err(e) => {
    433                // We're about to swallow the error in a `<general-enclosed>`
    434                // condition, so report it while we can.
    435                let loc = e.location;
    436                let error = ContextualParseError::InvalidMediaRule(input.slice_from(start), e);
    437                context.log_css_error(loc, error);
    438                None
    439            },
    440        }
    441    }
    442 
    443    /// Parse a condition in parentheses, or `<general-enclosed>`.
    444    ///
    445    /// https://drafts.csswg.org/mediaqueries/#typedef-media-in-parens
    446    pub fn parse_in_parens<'i, 't>(
    447        context: &ParserContext,
    448        input: &mut Parser<'i, 't>,
    449        feature_type: FeatureType,
    450    ) -> Result<Self, ParseError<'i>> {
    451        input.skip_whitespace();
    452        let start = input.position();
    453        let start_location = input.current_source_location();
    454        match *input.next()? {
    455            Token::ParenthesisBlock => {
    456                let nested = Self::try_parse_block(context, input, start, |input| {
    457                    Self::parse_in_parenthesis_block(context, input, feature_type)
    458                });
    459                if let Some(nested) = nested {
    460                    return Ok(nested);
    461                }
    462            },
    463            Token::Function(ref name) => {
    464                match_ignore_ascii_case! { name,
    465                    "style" => {
    466                        let feature = Self::try_parse_block(context, input, start, |input| {
    467                            StyleFeature::parse(context, input, feature_type)
    468                        });
    469                        if let Some(feature) = feature {
    470                            return Ok(Self::Style(feature));
    471                        }
    472                    },
    473                    "-moz-pref" => {
    474                        let feature = Self::try_parse_block(context, input, start, |input| {
    475                            MozPrefFeature::parse(context, input, feature_type)
    476                        });
    477                        if let Some(feature) = feature {
    478                            return Ok(Self::MozPref(feature));
    479                        }
    480                    },
    481                    _ => {},
    482                }
    483            },
    484            ref t => return Err(start_location.new_unexpected_token_error(t.clone())),
    485        }
    486        input.parse_nested_block(consume_any_value)?;
    487        Ok(Self::GeneralEnclosed(input.slice_from(start).to_owned()))
    488    }
    489 
    490    /// Whether this condition matches the device and quirks mode.
    491    /// https://drafts.csswg.org/mediaqueries/#evaluating
    492    /// https://drafts.csswg.org/mediaqueries/#typedef-general-enclosed
    493    /// Kleene 3-valued logic is adopted here due to the introduction of
    494    /// <general-enclosed>.
    495    pub fn matches(
    496        &self,
    497        context: &computed::Context,
    498        custom: &mut CustomMediaEvaluator,
    499    ) -> KleeneValue {
    500        match *self {
    501            QueryCondition::Custom(ref f) => custom.matches(f, context),
    502            QueryCondition::Feature(ref f) => f.matches(context),
    503            QueryCondition::GeneralEnclosed(_) => KleeneValue::Unknown,
    504            QueryCondition::InParens(ref c) => c.matches(context, custom),
    505            QueryCondition::Not(ref c) => !c.matches(context, custom),
    506            QueryCondition::Style(ref c) => c.matches(context),
    507            QueryCondition::MozPref(ref c) => c.matches(context),
    508            QueryCondition::Operation(ref conditions, op) => {
    509                debug_assert!(!conditions.is_empty(), "We never create an empty op");
    510                match op {
    511                    Operator::And => {
    512                        KleeneValue::any_false(conditions.iter(), |c| c.matches(context, custom))
    513                    },
    514                    Operator::Or => {
    515                        KleeneValue::any(conditions.iter(), |c| c.matches(context, custom))
    516                    },
    517                }
    518            },
    519        }
    520    }
    521 }