style/queries/
condition.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! 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
10use super::{FeatureFlags, FeatureType, QueryFeatureExpression};
11use crate::custom_properties;
12use crate::derives::*;
13use crate::stylesheets::CustomMediaEvaluator;
14use crate::values::{computed, AtomString, DashedIdent};
15use crate::{error_reporting::ContextualParseError, parser::Parse, parser::ParserContext};
16use cssparser::{match_ignore_ascii_case, Parser, SourcePosition, Token};
17use selectors::kleene_value::KleeneValue;
18use servo_arc::Arc;
19use std::fmt::{self, Write};
20use 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)]
25pub 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)]
32enum 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)]
40pub 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
47impl 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
62impl 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)]
127pub 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)]
147pub 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
159type SpecifiedMozPrefFeatureValue = MozPrefFeatureValue<crate::values::specified::Integer>;
160/// The computed -moz-pref() value.
161pub type ComputedMozPrefFeatureValue = MozPrefFeatureValue<crate::values::computed::Integer>;
162
163/// A custom -moz-pref(<name>, <value>) query feature.
164#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
165pub struct MozPrefFeature {
166    name: crate::values::AtomString,
167    value: SpecifiedMozPrefFeatureValue,
168}
169
170impl 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
204impl 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)]
220pub 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
239impl 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>
289fn 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
293impl 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}