style/properties_and_values/
rule.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//! The [`@property`] at-rule.
6//!
7//! https://drafts.css-houdini.org/css-properties-values-api-1/#at-property-rule
8
9use super::{
10    registry::{PropertyRegistration, PropertyRegistrationData},
11    syntax::Descriptor,
12    value::{
13        AllowComputationallyDependent, ComputedValue as ComputedRegisteredValue,
14        SpecifiedValue as SpecifiedRegisteredValue,
15    },
16};
17use crate::custom_properties::{Name as CustomPropertyName, SpecifiedValue};
18use crate::error_reporting::ContextualParseError;
19use crate::parser::{Parse, ParserContext};
20use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard};
21use crate::str::CssStringWriter;
22use crate::values::{computed, serialize_atom_name};
23use cssparser::{
24    AtRuleParser, BasicParseErrorKind, CowRcStr, DeclarationParser, ParseErrorKind, Parser,
25    ParserInput, ParserState, QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, SourceLocation,
26};
27#[cfg(feature = "gecko")] use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
28use selectors::parser::SelectorParseErrorKind;
29use servo_arc::Arc;
30use std::fmt::{self, Write};
31use style_traits::{
32    CssWriter, ParseError, PropertyInheritsParseError, PropertySyntaxParseError,
33    StyleParseErrorKind, ToCss,
34};
35use to_shmem::{SharedMemoryBuilder, ToShmem};
36
37/// Parse the block inside a `@property` rule.
38///
39/// Valid `@property` rules result in a registered custom property, as if `registerProperty()` had
40/// been called with equivalent parameters.
41pub fn parse_property_block<'i, 't>(
42    context: &ParserContext,
43    input: &mut Parser<'i, 't>,
44    name: PropertyRuleName,
45    source_location: SourceLocation,
46) -> Result<PropertyRegistration, ParseError<'i>> {
47    let mut descriptors = PropertyDescriptors::default();
48    let mut parser = PropertyRuleParser {
49        context,
50        descriptors: &mut descriptors,
51    };
52    let mut iter = RuleBodyParser::new(input, &mut parser);
53    let mut syntax_err = None;
54    let mut inherits_err = None;
55    while let Some(declaration) = iter.next() {
56        if !context.error_reporting_enabled() {
57            continue;
58        }
59        if let Err((error, slice)) = declaration {
60            let location = error.location;
61            let error = match error.kind {
62                // If the provided string is not a valid syntax string (if it
63                // returns failure when consume a syntax definition is called on
64                // it), the descriptor is invalid and must be ignored.
65                ParseErrorKind::Custom(StyleParseErrorKind::PropertySyntaxField(_)) => {
66                    syntax_err = Some(error.clone());
67                    ContextualParseError::UnsupportedValue(slice, error)
68                },
69
70                // If the provided string is not a valid inherits string,
71                // the descriptor is invalid and must be ignored.
72                ParseErrorKind::Custom(StyleParseErrorKind::PropertyInheritsField(_)) => {
73                    inherits_err = Some(error.clone());
74                    ContextualParseError::UnsupportedValue(slice, error)
75                },
76
77                // Unknown descriptors are invalid and ignored, but do not
78                // invalidate the @property rule.
79                _ => ContextualParseError::UnsupportedPropertyDescriptor(slice, error),
80            };
81            context.log_css_error(location, error);
82        }
83    }
84
85    // https://drafts.css-houdini.org/css-properties-values-api-1/#the-syntax-descriptor:
86    //
87    //     The syntax descriptor is required for the @property rule to be valid; if it’s
88    //     missing, the @property rule is invalid.
89    let Some(syntax) = descriptors.syntax else {
90        return Err(if let Some(err) = syntax_err {
91            err
92        } else {
93            let err = input.new_custom_error(StyleParseErrorKind::PropertySyntaxField(
94                PropertySyntaxParseError::NoSyntax,
95            ));
96            context.log_css_error(
97                source_location,
98                ContextualParseError::UnsupportedValue("", err.clone()),
99            );
100            err
101        });
102    };
103
104    // https://drafts.css-houdini.org/css-properties-values-api-1/#inherits-descriptor:
105    //
106    //     The inherits descriptor is required for the @property rule to be valid; if it’s
107    //     missing, the @property rule is invalid.
108    let Some(inherits) = descriptors.inherits else {
109        return Err(if let Some(err) = inherits_err {
110            err
111        } else {
112            let err = input.new_custom_error(StyleParseErrorKind::PropertyInheritsField(
113                PropertyInheritsParseError::NoInherits,
114            ));
115            context.log_css_error(
116                source_location,
117                ContextualParseError::UnsupportedValue("", err.clone()),
118            );
119            err
120        });
121    };
122
123    if PropertyRegistration::validate_initial_value(&syntax, descriptors.initial_value.as_deref())
124        .is_err()
125    {
126        return Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid));
127    }
128
129    Ok(PropertyRegistration {
130        name,
131        data: PropertyRegistrationData {
132            syntax,
133            inherits,
134            initial_value: descriptors.initial_value,
135        },
136        url_data: context.url_data.clone(),
137        source_location,
138    })
139}
140
141struct PropertyRuleParser<'a, 'b: 'a> {
142    context: &'a ParserContext<'b>,
143    descriptors: &'a mut PropertyDescriptors,
144}
145
146/// Default methods reject all at rules.
147impl<'a, 'b, 'i> AtRuleParser<'i> for PropertyRuleParser<'a, 'b> {
148    type Prelude = ();
149    type AtRule = ();
150    type Error = StyleParseErrorKind<'i>;
151}
152
153impl<'a, 'b, 'i> QualifiedRuleParser<'i> for PropertyRuleParser<'a, 'b> {
154    type Prelude = ();
155    type QualifiedRule = ();
156    type Error = StyleParseErrorKind<'i>;
157}
158
159impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>>
160    for PropertyRuleParser<'a, 'b>
161{
162    fn parse_qualified(&self) -> bool {
163        false
164    }
165    fn parse_declarations(&self) -> bool {
166        true
167    }
168}
169
170macro_rules! property_descriptors {
171    (
172        $( #[$doc: meta] $name: tt $ident: ident: $ty: ty, )*
173    ) => {
174        /// Data inside a `@property` rule.
175        ///
176        /// <https://drafts.css-houdini.org/css-properties-values-api-1/#at-property-rule>
177        #[derive(Clone, Debug, Default, PartialEq)]
178        struct PropertyDescriptors {
179            $(
180                #[$doc]
181                $ident: Option<$ty>,
182            )*
183        }
184
185        impl PropertyRegistration {
186            fn decl_to_css(&self, dest: &mut CssStringWriter) -> fmt::Result {
187                $(
188                    let $ident = Option::<&$ty>::from(&self.data.$ident);
189                    if let Some(ref value) = $ident {
190                        dest.write_str(concat!($name, ": "))?;
191                        value.to_css(&mut CssWriter::new(dest))?;
192                        dest.write_str("; ")?;
193                    }
194                )*
195                Ok(())
196            }
197        }
198
199        impl<'a, 'b, 'i> DeclarationParser<'i> for PropertyRuleParser<'a, 'b> {
200            type Declaration = ();
201            type Error = StyleParseErrorKind<'i>;
202
203            fn parse_value<'t>(
204                &mut self,
205                name: CowRcStr<'i>,
206                input: &mut Parser<'i, 't>,
207                _declaration_start: &ParserState,
208            ) -> Result<(), ParseError<'i>> {
209                match_ignore_ascii_case! { &*name,
210                    $(
211                        $name => {
212                            // DeclarationParser also calls parse_entirely so we’d normally not need
213                            // to, but in this case we do because we set the value as a side effect
214                            // rather than returning it.
215                            let value = input.parse_entirely(|i| Parse::parse(self.context, i))?;
216                            self.descriptors.$ident = Some(value)
217                        },
218                    )*
219                    _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))),
220                }
221                Ok(())
222            }
223        }
224    }
225}
226
227property_descriptors! {
228    /// <https://drafts.css-houdini.org/css-properties-values-api-1/#the-syntax-descriptor>
229    "syntax" syntax: Descriptor,
230
231    /// <https://drafts.css-houdini.org/css-properties-values-api-1/#inherits-descriptor>
232    "inherits" inherits: Inherits,
233
234    /// <https://drafts.css-houdini.org/css-properties-values-api-1/#initial-value-descriptor>
235    "initial-value" initial_value: InitialValue,
236}
237
238/// Errors that can happen when registering a property.
239#[allow(missing_docs)]
240pub enum PropertyRegistrationError {
241    NoInitialValue,
242    InvalidInitialValue,
243    InitialValueNotComputationallyIndependent,
244}
245
246impl PropertyRegistration {
247    /// Measure heap usage.
248    #[cfg(feature = "gecko")]
249    pub fn size_of(&self, _: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
250        MallocSizeOf::size_of(self, ops)
251    }
252
253    /// Computes the value of the computationally independent initial value.
254    pub fn compute_initial_value(
255        &self,
256        computed_context: &computed::Context,
257    ) -> Result<ComputedRegisteredValue, ()> {
258        let Some(ref initial) = self.data.initial_value else {
259            return Err(());
260        };
261
262        if self.data.syntax.is_universal() {
263            return Ok(ComputedRegisteredValue::universal(Arc::clone(initial)));
264        }
265
266        let mut input = ParserInput::new(initial.css_text());
267        let mut input = Parser::new(&mut input);
268        input.skip_whitespace();
269
270        match SpecifiedRegisteredValue::compute(
271            &mut input,
272            &self.data,
273            &self.url_data,
274            computed_context,
275            AllowComputationallyDependent::No,
276        ) {
277            Ok(computed) => Ok(computed),
278            Err(_) => Err(()),
279        }
280    }
281
282    /// Performs syntax validation as per the initial value descriptor.
283    /// https://drafts.css-houdini.org/css-properties-values-api-1/#initial-value-descriptor
284    pub fn validate_initial_value(
285        syntax: &Descriptor,
286        initial_value: Option<&SpecifiedValue>,
287    ) -> Result<(), PropertyRegistrationError> {
288        use crate::properties::CSSWideKeyword;
289        // If the value of the syntax descriptor is the universal syntax definition, then the
290        // initial-value descriptor is optional. If omitted, the initial value of the property is
291        // the guaranteed-invalid value.
292        if syntax.is_universal() && initial_value.is_none() {
293            return Ok(());
294        }
295
296        // Otherwise, if the value of the syntax descriptor is not the universal syntax definition,
297        // the following conditions must be met for the @property rule to be valid:
298
299        // The initial-value descriptor must be present.
300        let Some(initial) = initial_value else {
301            return Err(PropertyRegistrationError::NoInitialValue);
302        };
303
304        // A value that references the environment or other variables is not computationally
305        // independent.
306        if initial.has_references() {
307            return Err(PropertyRegistrationError::InitialValueNotComputationallyIndependent);
308        }
309
310        let mut input = ParserInput::new(initial.css_text());
311        let mut input = Parser::new(&mut input);
312        input.skip_whitespace();
313
314        // The initial-value cannot include CSS-wide keywords.
315        if input.try_parse(CSSWideKeyword::parse).is_ok() {
316            return Err(PropertyRegistrationError::InitialValueNotComputationallyIndependent);
317        }
318
319        match SpecifiedRegisteredValue::parse(
320            &mut input,
321            syntax,
322            &initial.url_data,
323            AllowComputationallyDependent::No,
324        ) {
325            Ok(_) => {},
326            Err(_) => return Err(PropertyRegistrationError::InvalidInitialValue),
327        }
328
329        Ok(())
330    }
331}
332
333impl ToCssWithGuard for PropertyRegistration {
334    /// <https://drafts.css-houdini.org/css-properties-values-api-1/#serialize-a-csspropertyrule>
335    fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
336        dest.write_str("@property ")?;
337        self.name.to_css(&mut CssWriter::new(dest))?;
338        dest.write_str(" { ")?;
339        self.decl_to_css(dest)?;
340        dest.write_char('}')
341    }
342}
343
344impl ToShmem for PropertyRegistration {
345    fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> {
346        Err(String::from(
347            "ToShmem failed for PropertyRule: cannot handle @property rules",
348        ))
349    }
350}
351
352/// A custom property name wrapper that includes the `--` prefix in its serialization
353#[derive(Clone, Debug, PartialEq, MallocSizeOf)]
354pub struct PropertyRuleName(pub CustomPropertyName);
355
356impl ToCss for PropertyRuleName {
357    fn to_css<W: Write>(&self, dest: &mut CssWriter<W>) -> fmt::Result {
358        dest.write_str("--")?;
359        serialize_atom_name(&self.0, dest)
360    }
361}
362
363/// <https://drafts.css-houdini.org/css-properties-values-api-1/#inherits-descriptor>
364#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss)]
365pub enum Inherits {
366    /// `true` value for the `inherits` descriptor
367    True,
368    /// `false` value for the `inherits` descriptor
369    False,
370}
371
372impl Parse for Inherits {
373    fn parse<'i, 't>(
374        _context: &ParserContext,
375        input: &mut Parser<'i, 't>,
376    ) -> Result<Self, ParseError<'i>> {
377        // FIXME(bug 1927012): Remove `return` from try_match_ident_ignore_ascii_case so the closure
378        // can be removed.
379        let result: Result<Inherits, ParseError> = (|| {
380            try_match_ident_ignore_ascii_case! { input,
381                "true" => Ok(Inherits::True),
382                "false" => Ok(Inherits::False),
383            }
384        })();
385        if let Err(err) = result {
386            Err(ParseError {
387                kind: ParseErrorKind::Custom(StyleParseErrorKind::PropertyInheritsField(
388                    PropertyInheritsParseError::InvalidInherits,
389                )),
390                location: err.location,
391            })
392        } else {
393            result
394        }
395    }
396}
397
398/// Specifies the initial value of the custom property registration represented by the @property
399/// rule, controlling the property’s initial value.
400///
401/// The SpecifiedValue is wrapped in an Arc to avoid copying when using it.
402pub type InitialValue = Arc<SpecifiedValue>;
403
404impl Parse for InitialValue {
405    fn parse<'i, 't>(
406        context: &ParserContext,
407        input: &mut Parser<'i, 't>,
408    ) -> Result<Self, ParseError<'i>> {
409        input.skip_whitespace();
410        Ok(Arc::new(SpecifiedValue::parse(input, &context.url_data)?))
411    }
412}