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