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