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