Skip to main content

style/substitution/
attr.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 `attr()` arbitrary substitution function.
6//! https://drafts.csswg.org/css-values-5/#funcdef-attr
7
8use super::{
9    ArbitraryDeclarationValue, AttrTaint, ParsedArgument, Substitution, SubstitutionArgument,
10    SubstitutionParserContext, SubstitutionReplacementContext, parse_optional_fallback,
11};
12use crate::custom_properties::{ComputedValue, Name, VariableValue};
13use crate::parser::{Parse, ParserContext};
14use crate::properties_and_values::{
15    syntax::Descriptor as SyntaxDescriptor,
16    value::{
17        AllowComputationallyDependent, ComputedValue as ComputedRegisteredValue,
18        SpecifiedValue as SpecifiedRegisteredValue,
19    },
20};
21use crate::stylesheets::UrlExtraData;
22use crate::values::generics::calc::SortKey as AttrUnit;
23use crate::values::specified::ParsedNamespace;
24use crate::{Atom, FxHashMap, LocalName, Namespace, Prefix, derives::*};
25use cssparser::{Delimiter, Parser, Token, TokenSerializationType};
26use servo_arc::Arc;
27use std::borrow::Cow;
28use style_traits::{ParseError, StyleParseErrorKind};
29
30impl LocalName {
31    #[cfg(feature = "gecko")]
32    fn with_name<R>(name: &Name, callback: impl FnOnce(&Self) -> R) -> R {
33        callback(Self::cast(name))
34    }
35
36    #[cfg(feature = "servo")]
37    fn with_name<'a, R>(name: &'a Name, callback: impl FnOnce(&Self) -> R) -> R {
38        callback(&name.as_str().into())
39    }
40}
41
42impl From<Name> for LocalName {
43    #[cfg(feature = "gecko")]
44    fn from(name: Name) -> Self {
45        Self::new(name)
46    }
47
48    #[cfg(feature = "servo")]
49    fn from(name: Name) -> Self {
50        name.as_str().into()
51    }
52}
53
54/// The `<attr-type>` of an `attr()` function.
55#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
56enum AttrType {
57    /// No type was provided.
58    None,
59    /// Treat the attribute value as a <string>.
60    RawString,
61    /// Parse the attribute value as the given <syntax>.
62    Type(SyntaxDescriptor),
63    /// Parse the attribute value as a dimension with the given unit,
64    /// or as a <number-token> if the unit is `number`.
65    Unit(AttrUnit),
66}
67
68impl Parse for AttrType {
69    /// Parse <attr-type> = type( <syntax> ) | raw-string | number | <attr-unit>.
70    /// https://drafts.csswg.org/css-values-5/#attr-notation
71    fn parse(_: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
72        Ok(input
73            .try_parse(|input| {
74                Ok(match input.next()? {
75                    Token::Function(name) if name.eq_ignore_ascii_case("type") => {
76                        Self::Type(input.parse_nested_block(SyntaxDescriptor::from_css_parser)?)
77                    },
78                    Token::Ident(ident) => {
79                        if ident.eq_ignore_ascii_case("raw-string") {
80                            Self::RawString
81                        } else if let Ok(unit) = AttrUnit::from_ident(ident) {
82                            Self::Unit(unit)
83                        } else {
84                            return Err(ParseError::custom(StyleParseErrorKind::UnexpectedIdent));
85                        }
86                    },
87                    Token::Delim('%') => Self::Unit(AttrUnit::Percentage),
88                    _ => return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError)),
89                })
90            })
91            .unwrap_or(Self::None))
92    }
93}
94
95/// The `<attr-name>` of an `attr()` function.
96#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
97pub struct AttrName {
98    /// The referenced attribute name.
99    pub name: Name,
100    namespace: ParsedNamespace,
101}
102
103impl AttrName {
104    /// Attribute values may reference other substitution functions we may need to process.
105    /// See step 6: https://drafts.csswg.org/css-values-5/#attr-substitution
106    pub fn value_for_cycle_resolution(
107        &self,
108        context: &mut SubstitutionReplacementContext,
109        url_data: &UrlExtraData,
110    ) -> Result<ComputedRegisteredValue, ()> {
111        let ParsedNamespace::Known(ref namespace) = self.namespace else {
112            return Err(());
113        };
114        let attr = LocalName::with_name(&self.name, |local_name| {
115            context
116                .attribute_tracker
117                .query(local_name, namespace)
118                .ok_or(())
119        })?;
120        let parser_context = context.parser_context(url_data);
121        let mut parser = Parser::new(&attr);
122        // TODO(Bug 2021110): Support namespaced attributes in chained references.
123        let value = VariableValue::parse(&parser_context, &mut parser).map_err(|_| ())?;
124        Ok(ComputedRegisteredValue::universal(Arc::new(value)))
125    }
126}
127
128impl Parse for AttrName {
129    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
130        let mut namespace = ParsedNamespace::Known(Namespace::default());
131        if let Ok(ns) =
132            input.try_parse(|input| ParsedNamespace::parse(&context.namespaces.prefixes, input))
133        {
134            namespace = ns;
135            let prev = input.state();
136            let is_ident = matches!(input.next_including_whitespace(), Ok(Token::Ident(_)));
137            input.reset(&prev);
138            if !is_ident {
139                return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
140            }
141        }
142        let name = Atom::from(input.expect_ident()?.as_ref());
143        Ok(AttrName { name, namespace })
144    }
145}
146
147/// The `<attr-name> <attr-type>?` argument of an `attr()` function.
148#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
149pub struct AttrArgument {
150    /// The referenced attribute name.
151    pub name: AttrName,
152    /// The type the attribute value is parsed as.
153    ty: AttrType,
154}
155
156impl AttrArgument {
157    /// Whether the attribute has a `type()`.
158    pub fn has_type(&self) -> bool {
159        matches!(self.ty, AttrType::Type(..))
160    }
161}
162
163impl Parse for AttrArgument {
164    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
165        let name = AttrName::parse(context, input)?;
166        let ty = AttrType::parse(context, input)?;
167        Ok(AttrArgument { name, ty })
168    }
169}
170
171/// Data specific to an `attr()` function.
172#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
173pub struct AttrFunction {
174    /// The referenced attribute name and optional type.
175    pub arg: SubstitutionArgument<AttrArgument>,
176    /// The fallback for this function, if any.
177    pub fallback: Option<ArbitraryDeclarationValue>,
178    /// The namespace prefixes in scope where the function was parsed.
179    namespaces: Option<FxHashMap<Prefix, Namespace>>,
180}
181
182impl AttrFunction {
183    /// Parses the `attr()` function arguments according to its argument grammar and syntax:
184    /// - <attr-args> = attr(<declaration-value>, <declaration-value>?)
185    /// - attr() = attr(<attr-name> <attr-type>? , <declaration-value>?)
186    pub fn parse(
187        input: &mut Parser,
188        context: &mut SubstitutionParserContext,
189    ) -> Result<Self, ParseError> {
190        let arg = SubstitutionArgument::try_parse(input, context, Delimiter::Comma)?;
191        let namespaces = match arg {
192            SubstitutionArgument::Unparsed(..) => {
193                Some(context.parser_context.namespaces.prefixes.clone())
194            },
195            SubstitutionArgument::Parsed(..) | SubstitutionArgument::Invalid => None,
196        };
197        let fallback = parse_optional_fallback(input, context)?;
198        Ok(Self {
199            arg,
200            namespaces,
201            fallback,
202        })
203    }
204
205    /// Replaces the `attr()` function with its substitution value.
206    /// https://drafts.csswg.org/css-values-5/#replace-an-attr-function
207    pub fn replace<'a>(
208        &'a self,
209        source: &'a VariableValue,
210        context: &mut SubstitutionReplacementContext<'a, '_, '_, '_>,
211    ) -> Result<Substitution<'a>, ()> {
212        let arg = self.replace_arg(source, context);
213        let is_type_omitted = arg
214            .value
215            .as_ref()
216            .is_some_and(|arg| arg.ty == AttrType::None);
217        let mut substitution = arg
218            .value
219            .and_then(|arg| {
220                let ParsedNamespace::Known(ref namespace) = arg.name.namespace else {
221                    return None;
222                };
223                let attr = LocalName::with_name(&arg.name.name, |local_name| {
224                    context.attribute_tracker.query(local_name, namespace)
225                })?;
226                Some((arg, attr))
227            })
228            .map_or_else(
229                || {
230                    // Special case when fallback and <attr-type> are omitted.
231                    // See FAILURE: https://drafts.csswg.org/css-values-5/#attr-substitution
232                    if self.fallback.is_none() && is_type_omitted {
233                        Some(simple_attr_substitution(""))
234                    } else {
235                        None
236                    }
237                },
238                |(arg, attr)| {
239                    let attr = if let AttrType::Type(_) = &arg.ty {
240                        // If we're evaluating a container query, we haven't run the cascade
241                        // and populated substitution_functions.attributes, so we can't do the
242                        // get_attr() lookup here.
243                        // TODO: This means chained attr() references will not work reliably in
244                        // container style queries:
245                        // https://bugzilla.mozilla.org/show_bug.cgi?id=2028861
246                        if context.computed_context.in_container_query {
247                            attr
248                        } else {
249                            context
250                                .substitution_functions
251                                .get_attr(&arg.name.name)
252                                .map(|v| v.to_variable_value())?
253                                .css
254                        }
255                    } else {
256                        attr
257                    };
258                    let mut parser = Parser::new(&attr);
259                    match &arg.ty {
260                        AttrType::Unit(unit) => {
261                            let css = {
262                                // Verify that attribute data is a <number-token>.
263                                parser.expect_number().ok()?;
264                                let mut s = attr.clone();
265                                s.push_str(unit.as_ref());
266                                s
267                            };
268                            let serialization = match unit {
269                                AttrUnit::Number => TokenSerializationType::Number,
270                                AttrUnit::Percentage => TokenSerializationType::Percentage,
271                                _ => TokenSerializationType::Dimension,
272                            };
273                            let value = ComputedValue::new(
274                                css,
275                                &source.url_data,
276                                serialization,
277                                serialization,
278                            );
279                            Some(Substitution::from_value(
280                                value, /* attr_tainted */ true,
281                            ))
282                        },
283                        AttrType::Type(syntax) => {
284                            let value = SpecifiedRegisteredValue::parse(
285                                &mut parser,
286                                syntax,
287                                &source.url_data,
288                                AllowComputationallyDependent::Yes,
289                                AttrTaint::default(),
290                                context.property_id,
291                            )
292                            .ok()?;
293                            let value = value.to_variable_value();
294                            Some(Substitution::from_value(
295                                value, /* attr_tainted */ true,
296                            ))
297                        },
298                        AttrType::RawString | AttrType::None => {
299                            Some(simple_attr_substitution(&attr))
300                        },
301                    }
302                },
303            )
304            .ok_or(())
305            .or_else(|_| {
306                context.substitute_declaration_value(self.fallback.as_ref().ok_or(())?, source)
307            })?;
308        substitution.attr_tainted |= arg.attr_tainted;
309        Ok(substitution)
310    }
311
312    /// Returns the argument, substituting and parsing it first if it has references.
313    pub fn replace_arg<'a>(
314        &'a self,
315        source: &'a VariableValue,
316        context: &mut SubstitutionReplacementContext<'a, '_, '_, '_>,
317    ) -> ParsedArgument<'a, AttrArgument> {
318        self.arg.substitute_and_parse(source, context)
319    }
320
321    /// Calls `f` for each arbitrary declaration value nested in the function.
322    pub fn for_each_unparsed_value<F>(&self, mut f: F)
323    where
324        F: FnMut(&ArbitraryDeclarationValue),
325    {
326        if let SubstitutionArgument::Unparsed(ref arg) = self.arg {
327            f(arg);
328        }
329        if let Some(ref fallback) = self.fallback {
330            f(fallback);
331        }
332    }
333}
334
335fn simple_attr_substitution<'a>(s: &str) -> Substitution<'a> {
336    let mut css = String::with_capacity(s.len() + 2);
337    cssparser::serialize_string(s, &mut css).unwrap();
338    Substitution::new(
339        Cow::Owned(css),
340        TokenSerializationType::Nothing,
341        TokenSerializationType::Nothing,
342        /* attr_tainted */ true,
343    )
344}