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