Skip to main content

style/
custom_properties.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//! Support for [custom properties for cascading variables][custom].
6//!
7//! [custom]: https://drafts.csswg.org/css-variables/
8
9use crate::custom_properties_map::{CustomPropertiesMap, OwnMap};
10use crate::device::Device;
11use crate::dom::AttributeTracker;
12use crate::properties::{CSSWideKeyword, PrioritaryPropertyId};
13use crate::properties_and_values::{
14    rule::Descriptors as PropertyDescriptors,
15    syntax::Descriptor as SyntaxDescriptor,
16    value::{
17        AllowComputationallyDependent, ComputedValue as ComputedRegisteredValue,
18        SpecifiedValue as SpecifiedRegisteredValue,
19    },
20};
21use crate::stylesheets::UrlExtraData;
22use crate::stylesheets::container_rule::AttrReferenceSet;
23use crate::stylist::Stylist;
24use crate::typed_om::{
25    ToTyped, TypedValue, UnparsedSegment, UnparsedValue, VariableReferenceValue,
26};
27use crate::values::computed;
28use crate::values::generics::calc::SortKey as AttrUnit;
29use crate::values::specified::{NoCalcLength, ParsedNamespace};
30use crate::{Atom, LocalName, Namespace, Prefix, derives::*};
31use cssparser::{CowRcStr, Delimiter, Parser, SourcePosition, Token, TokenSerializationType};
32use rustc_hash::FxHashMap;
33use selectors::parser::SelectorParseErrorKind;
34use servo_arc::Arc;
35use smallvec::SmallVec;
36use std::borrow::Cow;
37use std::fmt::{self, Write};
38use std::num;
39use std::ops::{Index, IndexMut};
40use style_traits::{CssString, CssWriter, ParseError, StyleParseErrorKind, ToCss};
41use thin_vec::ThinVec;
42
43/// The environment from which to get `env` function values.
44///
45/// TODO(emilio): If this becomes a bit more complex we should probably move it
46/// to the `media_queries` module, or something.
47#[derive(Debug, MallocSizeOf)]
48pub struct CssEnvironment;
49
50type EnvironmentEvaluator = fn(device: &Device, url_data: &UrlExtraData) -> VariableValue;
51
52struct EnvironmentVariable {
53    name: Atom,
54    evaluator: EnvironmentEvaluator,
55}
56
57macro_rules! make_variable {
58    ($name:expr, $evaluator:expr) => {{
59        EnvironmentVariable {
60            name: $name,
61            evaluator: $evaluator,
62        }
63    }};
64}
65
66fn get_safearea_inset_top(device: &Device, url_data: &UrlExtraData) -> VariableValue {
67    VariableValue::pixels(device.safe_area_insets().top, url_data)
68}
69
70fn get_safearea_inset_bottom(device: &Device, url_data: &UrlExtraData) -> VariableValue {
71    VariableValue::pixels(device.safe_area_insets().bottom, url_data)
72}
73
74fn get_safearea_inset_left(device: &Device, url_data: &UrlExtraData) -> VariableValue {
75    VariableValue::pixels(device.safe_area_insets().left, url_data)
76}
77
78fn get_safearea_inset_right(device: &Device, url_data: &UrlExtraData) -> VariableValue {
79    VariableValue::pixels(device.safe_area_insets().right, url_data)
80}
81
82#[cfg(feature = "gecko")]
83fn get_content_preferred_color_scheme(device: &Device, url_data: &UrlExtraData) -> VariableValue {
84    use crate::queries::values::PrefersColorScheme;
85    let prefers_color_scheme = unsafe {
86        crate::gecko_bindings::bindings::Gecko_MediaFeatures_PrefersColorScheme(
87            device.document(),
88            /* use_content = */ true,
89        )
90    };
91    VariableValue::ident(
92        match prefers_color_scheme {
93            PrefersColorScheme::Light => "light",
94            PrefersColorScheme::Dark => "dark",
95        },
96        url_data,
97    )
98}
99
100#[cfg(feature = "servo")]
101fn get_content_preferred_color_scheme(_device: &Device, url_data: &UrlExtraData) -> VariableValue {
102    // TODO: Add an implementation for Servo.
103    VariableValue::ident("light", url_data)
104}
105
106fn get_scrollbar_inline_size(device: &Device, url_data: &UrlExtraData) -> VariableValue {
107    VariableValue::pixels(device.scrollbar_inline_size().px(), url_data)
108}
109
110fn get_hairline(device: &Device, url_data: &UrlExtraData) -> VariableValue {
111    VariableValue::pixels(
112        app_units::Au(device.app_units_per_device_pixel()).to_f32_px(),
113        url_data,
114    )
115}
116
117static ENVIRONMENT_VARIABLES: [EnvironmentVariable; 4] = [
118    make_variable!(atom!("safe-area-inset-top"), get_safearea_inset_top),
119    make_variable!(atom!("safe-area-inset-bottom"), get_safearea_inset_bottom),
120    make_variable!(atom!("safe-area-inset-left"), get_safearea_inset_left),
121    make_variable!(atom!("safe-area-inset-right"), get_safearea_inset_right),
122];
123
124#[cfg(feature = "gecko")]
125macro_rules! lnf_int {
126    ($id:ident) => {
127        unsafe {
128            crate::gecko_bindings::bindings::Gecko_GetLookAndFeelInt(
129                crate::gecko_bindings::bindings::LookAndFeel_IntID::$id as i32,
130            )
131        }
132    };
133}
134
135#[cfg(feature = "servo")]
136macro_rules! lnf_int {
137    ($id:ident) => {
138        // TODO: Add an implementation for Servo.
139        0
140    };
141}
142
143macro_rules! lnf_int_variable {
144    ($atom:expr, $id:ident, $ctor:ident) => {{
145        fn __eval(_: &Device, url_data: &UrlExtraData) -> VariableValue {
146            VariableValue::$ctor(lnf_int!($id), url_data)
147        }
148        make_variable!($atom, __eval)
149    }};
150}
151
152fn eval_gtk_csd_titlebar_radius(device: &Device, url_data: &UrlExtraData) -> VariableValue {
153    let int_pixels = lnf_int!(TitlebarRadius);
154    let unzoomed_scale =
155        device.device_pixel_ratio_ignoring_full_zoom().get() / device.device_pixel_ratio().get();
156    VariableValue::pixels(int_pixels as f32 * unzoomed_scale, url_data)
157}
158
159static CHROME_ENVIRONMENT_VARIABLES: [EnvironmentVariable; 9] = [
160    make_variable!(
161        atom!("-moz-gtk-csd-titlebar-radius"),
162        eval_gtk_csd_titlebar_radius
163    ),
164    lnf_int_variable!(
165        atom!("-moz-gtk-csd-tooltip-radius"),
166        TooltipRadius,
167        int_pixels
168    ),
169    lnf_int_variable!(
170        atom!("-moz-gtk-csd-close-button-position"),
171        GTKCSDCloseButtonPosition,
172        integer
173    ),
174    lnf_int_variable!(
175        atom!("-moz-gtk-csd-minimize-button-position"),
176        GTKCSDMinimizeButtonPosition,
177        integer
178    ),
179    lnf_int_variable!(
180        atom!("-moz-gtk-csd-maximize-button-position"),
181        GTKCSDMaximizeButtonPosition,
182        integer
183    ),
184    lnf_int_variable!(
185        atom!("-moz-overlay-scrollbar-fade-duration"),
186        ScrollbarFadeDuration,
187        int_ms
188    ),
189    make_variable!(
190        atom!("-moz-content-preferred-color-scheme"),
191        get_content_preferred_color_scheme
192    ),
193    make_variable!(atom!("scrollbar-inline-size"), get_scrollbar_inline_size),
194    make_variable!(atom!("hairline"), get_hairline),
195];
196
197impl CssEnvironment {
198    /// Get an env() variable, either custom or not.
199    #[inline]
200    pub fn get(
201        &self,
202        name: &Atom,
203        device: &Device,
204        url_data: &UrlExtraData,
205    ) -> Option<VariableValue> {
206        #[cfg(feature = "gecko")]
207        let is_link_parameter = name.as_slice().starts_with(&[b'-' as u16, b'-' as u16]);
208        #[cfg(feature = "servo")]
209        let is_link_parameter = name.starts_with("--");
210        if is_link_parameter {
211            let param = device
212                .link_parameters()?
213                .0
214                .iter()
215                .find(|p| p.name.0 == *name)?;
216            let mut parser = cssparser::Parser::new(param.value.0.as_ref());
217            // need to carry around full variable value https://bugzilla.mozilla.org/show_bug.cgi?id=2028998
218            return VariableValue::parse(&mut parser, None, url_data).ok();
219        }
220
221        if let Some(var) = ENVIRONMENT_VARIABLES.iter().find(|var| var.name == *name) {
222            return Some((var.evaluator)(device, url_data));
223        }
224        if !url_data.chrome_rules_enabled() {
225            return None;
226        }
227        let var = CHROME_ENVIRONMENT_VARIABLES
228            .iter()
229            .find(|var| var.name == *name)?;
230        Some((var.evaluator)(device, url_data))
231    }
232}
233
234/// A custom property name is just an `Atom`.
235///
236/// Note that this does not include the `--` prefix
237pub type Name = Atom;
238
239impl LocalName {
240    #[cfg(feature = "gecko")]
241    fn with_name<R>(name: &Name, callback: impl FnOnce(&Self) -> R) -> R {
242        callback(Self::cast(name))
243    }
244
245    #[cfg(feature = "servo")]
246    fn with_name<'a, R>(name: &'a Name, callback: impl FnOnce(&Self) -> R) -> R {
247        callback(&name.as_str().into())
248    }
249}
250
251impl From<Name> for LocalName {
252    #[cfg(feature = "gecko")]
253    fn from(name: Name) -> Self {
254        Self::new(name)
255    }
256
257    #[cfg(feature = "servo")]
258    fn from(name: Name) -> Self {
259        name.as_str().into()
260    }
261}
262
263/// Parse a custom property name.
264///
265/// <https://drafts.csswg.org/css-variables/#typedef-custom-property-name>
266pub fn parse_name(s: &str) -> Result<&str, ()> {
267    if s.starts_with("--") && s.len() > 2 {
268        Ok(&s[2..])
269    } else {
270        Err(())
271    }
272}
273
274/// A value for a custom property is just a set of tokens.
275///
276/// We preserve the original CSS for serialization, and also the variable
277/// references to other custom property names.
278#[derive(Clone, Debug, MallocSizeOf, ToShmem)]
279pub struct VariableValue {
280    /// The raw CSS string.
281    pub css: String,
282
283    /// The url data of the stylesheet where this value came from.
284    pub url_data: UrlExtraData,
285
286    first_token_type: TokenSerializationType,
287    last_token_type: TokenSerializationType,
288
289    /// var(), env(), attr() or non-custom property (e.g. through `em`) references.
290    pub references: References,
291
292    /// Was this variable value attr tainted before. Used to keep attr tainting
293    /// information when uncomputing a style that needs to be put back into the
294    /// cascade.
295    pub explicitly_attr_tainted: bool,
296}
297
298trivial_to_computed_value!(VariableValue);
299
300/// Given a potentially registered variable value turn it into a computed custom property value.
301pub(crate) fn compute_variable_value(
302    value: &Arc<VariableValue>,
303    registration: &PropertyDescriptors,
304    computed_context: &computed::Context,
305) -> Option<ComputedRegisteredValue> {
306    if registration.is_universal() {
307        return Some(ComputedRegisteredValue::universal(Arc::clone(value)));
308    }
309    compute_value(
310        &value.css,
311        &value.url_data,
312        registration,
313        computed_context,
314        AttrTaint::default(),
315    )
316    .ok()
317}
318
319// For all purposes, we want values to be considered equal if their css text is equal.
320impl PartialEq for VariableValue {
321    fn eq(&self, other: &Self) -> bool {
322        self.css == other.css
323    }
324}
325
326impl Eq for VariableValue {}
327
328impl ToCss for SpecifiedValue {
329    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
330    where
331        W: Write,
332    {
333        dest.write_str(&self.css)
334    }
335}
336
337impl ToTyped for SpecifiedValue {
338    fn to_typed(&self, dest: &mut ThinVec<TypedValue>) -> Result<(), ()> {
339        let unparsed_value = reify_variable_value(self)?;
340        dest.push(TypedValue::Unparsed(unparsed_value));
341        Ok(())
342    }
343}
344
345fn reify_variable_value(value: &VariableValue) -> Result<UnparsedValue, ()> {
346    let mut reference_index = 0;
347    reify_variable_value_range(
348        &value.css,
349        &value.references.refs,
350        &mut reference_index,
351        0,
352        value.css.len(),
353    )
354}
355
356/// Reify a slice of the CSS string into UnparsedSegment entries.
357///
358/// References are stored in source order, with outer substitution functions
359/// inserted before references in their fallback. The shared `reference_index`
360/// relies on this ordering to recurse into fallbacks without reprocessing
361/// nested referecences.
362fn reify_variable_value_range(
363    css: &str,
364    references: &[SubstitutionFunctionReference],
365    reference_index: &mut usize,
366    start: usize,
367    end: usize,
368) -> Result<UnparsedValue, ()> {
369    debug_assert!(start <= end);
370    debug_assert!(end <= css.len());
371
372    let mut values = ThinVec::new();
373    let mut cur_pos = start;
374
375    while *reference_index < references.len() {
376        let reference = &references[*reference_index];
377
378        if reference.start >= end {
379            break;
380        }
381
382        debug_assert!(reference.start >= cur_pos);
383        debug_assert!(reference.start <= reference.end);
384        debug_assert!(reference.end <= css.len());
385
386        if cur_pos < reference.start {
387            values.push(UnparsedSegment::String(CssString::from(
388                &css[cur_pos..reference.start],
389            )));
390        }
391
392        *reference_index += 1;
393
394        if reference.substitution_kind != SubstitutionFunctionKind::Var {
395            return Err(());
396        }
397
398        let (fallback, has_fallback) = if let Some(fallback) = &reference.fallback {
399            debug_assert!(fallback.start.get() < reference.end);
400
401            (
402                reify_variable_value_range(
403                    css,
404                    references,
405                    reference_index,
406                    fallback.start.get(),
407                    reference.end - 1, // Skip the closing ')'.
408                )?,
409                true,
410            )
411        } else {
412            (ThinVec::new(), false)
413        };
414
415        values.push(UnparsedSegment::VariableReference(VariableReferenceValue {
416            variable: CssString::from(format!("--{}", reference.name)),
417            fallback,
418            has_fallback,
419        }));
420
421        cur_pos = reference.end;
422    }
423
424    if cur_pos < end {
425        values.push(UnparsedSegment::String(CssString::from(&css[cur_pos..end])));
426    }
427
428    Ok(values)
429}
430
431/// A pair of separate CustomPropertiesMaps, split between custom properties
432/// that have the inherit flag set and those with the flag unset.
433#[repr(C)]
434#[derive(Clone, Debug, Default, PartialEq)]
435pub struct ComputedCustomProperties {
436    /// Map for custom properties with inherit flag set, including non-registered
437    /// ones.
438    pub inherited: CustomPropertiesMap,
439    /// Map for custom properties with inherit flag unset.
440    pub non_inherited: CustomPropertiesMap,
441}
442
443impl ComputedCustomProperties {
444    /// Return whether the inherited and non_inherited maps are none.
445    pub fn is_empty(&self) -> bool {
446        self.inherited.is_empty() && self.non_inherited.is_empty()
447    }
448
449    /// Return the name and value of the property at specified index, if any.
450    pub fn property_at(&self, index: usize) -> Option<(&Name, &Option<ComputedRegisteredValue>)> {
451        // Just expose the custom property items from custom_properties.inherited, followed
452        // by custom property items from custom_properties.non_inherited.
453        self.inherited
454            .get_index(index)
455            .or_else(|| self.non_inherited.get_index(index - self.inherited.len()))
456    }
457
458    /// Insert a custom property in the corresponding inherited/non_inherited
459    /// map, depending on whether the inherit flag is set or unset.
460    pub fn insert(
461        &mut self,
462        registration: &PropertyDescriptors,
463        name: &Name,
464        value: ComputedRegisteredValue,
465    ) {
466        self.map_mut(registration).insert(name, value)
467    }
468
469    /// Remove a custom property from the corresponding inherited/non_inherited
470    /// map, depending on whether the inherit flag is set or unset.
471    pub fn remove(&mut self, registration: &PropertyDescriptors, name: &Name) {
472        self.map_mut(registration).remove(name);
473    }
474
475    /// Shrink the capacity of the inherited maps as much as possible.
476    pub fn shrink_to_fit(&mut self) {
477        self.inherited.shrink_to_fit();
478        self.non_inherited.shrink_to_fit();
479    }
480
481    fn map_mut(&mut self, registration: &PropertyDescriptors) -> &mut CustomPropertiesMap {
482        if registration.inherits() {
483            &mut self.inherited
484        } else {
485            &mut self.non_inherited
486        }
487    }
488
489    /// Returns the relevant custom property value given a registration.
490    pub fn get(
491        &self,
492        registration: &PropertyDescriptors,
493        name: &Name,
494    ) -> Option<&ComputedRegisteredValue> {
495        if registration.inherits() {
496            self.inherited.get(name)
497        } else {
498            self.non_inherited.get(name)
499        }
500    }
501
502    /// Returns a property just by the name. Slightly less efficient than get(), if you already have
503    /// a custom registration handy, thus the longer name.
504    pub fn get_for_cssom(&self, name: &Name) -> Option<&ComputedRegisteredValue> {
505        self.inherited
506            .get(name)
507            .or_else(|| self.non_inherited.get(name))
508    }
509}
510
511/// Both specified and computed values are VariableValues, the difference is
512/// whether var() functions are expanded.
513pub type SpecifiedValue = VariableValue;
514/// Both specified and computed values are VariableValues, the difference is
515/// whether var() functions are expanded.
516pub type ComputedValue = VariableValue;
517
518/// Set of flags to references this custom property makes.
519#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, MallocSizeOf, ToShmem)]
520pub struct ReferenceFlags(u8);
521
522bitflags! {
523    impl ReferenceFlags : u8 {
524        /// At least one custom property depends on font-relative units.
525        const FONT_UNITS = 1 << 0;
526        /// At least one custom property depends on root element's font-relative units.
527        const ROOT_FONT_UNITS = 1 << 1;
528        /// At least one custom property depends on line height units.
529        const LH_UNITS = 1 << 2;
530        /// At least one custom property depends on root element's line height units.
531        const ROOT_LH_UNITS = 1 << 3;
532        /// The value depends on the used color-scheme (e.g. a registered `<color>` property).
533        const COLOR_SCHEME = 1 << 4;
534        /// All dependencies not depending on the root element.
535        const NON_ROOT_DEPENDENCIES = Self::FONT_UNITS.0 | Self::LH_UNITS.0;
536        /// All dependencies depending on the root element.
537        const ROOT_DEPENDENCIES = Self::ROOT_FONT_UNITS.0 | Self::ROOT_LH_UNITS.0;
538        /// All non-custom dependencies
539        const NON_CUSTOM = Self::NON_ROOT_DEPENDENCIES.0 | Self::ROOT_DEPENDENCIES.0;
540        /// At least one attr() reference.
541        const ATTR = 1 << 5;
542        /// At least one env() reference.
543        const ENV = 1 << 6;
544        /// At least one var() reference.
545        const VAR = 1 << 7;
546    }
547}
548
549impl ReferenceFlags {
550    /// Iterate for each non custom
551    pub fn for_each_non_custom<F>(mut self, is_root_element: bool, mut f: F)
552    where
553        F: FnMut(SingleNonCustomReference),
554    {
555        // On the root element, rem etc refer to the root's own dependencies.
556        if is_root_element {
557            if self.intersects(Self::ROOT_FONT_UNITS) {
558                self.remove(Self::ROOT_FONT_UNITS);
559                self |= Self::FONT_UNITS;
560            }
561            if self.intersects(Self::ROOT_LH_UNITS) {
562                self.remove(Self::ROOT_FONT_UNITS);
563                self |= Self::LH_UNITS;
564            }
565        }
566
567        for (_, r) in self.iter_names() {
568            let single = match r {
569                Self::FONT_UNITS => SingleNonCustomReference::FontUnits,
570                Self::LH_UNITS => SingleNonCustomReference::LhUnits,
571                Self::COLOR_SCHEME => SingleNonCustomReference::ColorScheme,
572                Self::ROOT_FONT_UNITS
573                | Self::ROOT_LH_UNITS
574                | Self::VAR
575                | Self::ENV
576                | Self::ATTR => continue,
577                _ => unreachable!("Unexpected single bit value"),
578            };
579            f(single);
580        }
581    }
582
583    fn from_unit(value: &CowRcStr) -> Self {
584        // For registered properties, any reference to font-relative dimensions
585        // make it dependent on font-related properties.
586        // TODO(dshin): When we unit algebra gets implemented and handled -
587        // Is it valid to say that `calc(1em / 2em * 3px)` triggers this?
588        if value.eq_ignore_ascii_case(NoCalcLength::LH) {
589            return Self::FONT_UNITS | Self::LH_UNITS;
590        }
591        if value.eq_ignore_ascii_case(NoCalcLength::EM)
592            || value.eq_ignore_ascii_case(NoCalcLength::EX)
593            || value.eq_ignore_ascii_case(NoCalcLength::CAP)
594            || value.eq_ignore_ascii_case(NoCalcLength::CH)
595            || value.eq_ignore_ascii_case(NoCalcLength::IC)
596        {
597            return Self::FONT_UNITS;
598        }
599        if value.eq_ignore_ascii_case(NoCalcLength::RLH) {
600            return Self::ROOT_FONT_UNITS | Self::ROOT_LH_UNITS;
601        }
602        if value.eq_ignore_ascii_case(NoCalcLength::REM)
603            || value.eq_ignore_ascii_case(NoCalcLength::REX)
604            || value.eq_ignore_ascii_case(NoCalcLength::RCH)
605            || value.eq_ignore_ascii_case(NoCalcLength::RCAP)
606            || value.eq_ignore_ascii_case(NoCalcLength::RIC)
607        {
608            return Self::ROOT_FONT_UNITS;
609        }
610        Self::empty()
611    }
612}
613
614/// A non-custom reference that participates in cycle resolution.
615/// TODO(emilio): This should probably eventually become just PrioritaryPropertyId.
616#[derive(Clone, Copy, Debug, Eq, PartialEq)]
617#[allow(missing_docs)]
618pub enum SingleNonCustomReference {
619    FontUnits = 0,
620    LhUnits,
621    ColorScheme,
622}
623
624impl SingleNonCustomReference {
625    /// Returns a prioritary id for this reference.
626    pub fn to_prioritary_id(self) -> PrioritaryPropertyId {
627        match self {
628            Self::FontUnits => PrioritaryPropertyId::FontSize,
629            Self::LhUnits => PrioritaryPropertyId::LineHeight,
630            Self::ColorScheme => PrioritaryPropertyId::ColorScheme,
631        }
632    }
633}
634
635/// A map from NonCustomReferenceMap to a T.
636pub struct NonCustomReferenceMap<T>([Option<T>; 3]);
637
638impl<T> Default for NonCustomReferenceMap<T> {
639    fn default() -> Self {
640        NonCustomReferenceMap(Default::default())
641    }
642}
643
644impl<T> Index<SingleNonCustomReference> for NonCustomReferenceMap<T> {
645    type Output = Option<T>;
646
647    fn index(&self, reference: SingleNonCustomReference) -> &Self::Output {
648        &self.0[reference as usize]
649    }
650}
651
652impl<T> IndexMut<SingleNonCustomReference> for NonCustomReferenceMap<T> {
653    fn index_mut(&mut self, reference: SingleNonCustomReference) -> &mut Self::Output {
654        &mut self.0[reference as usize]
655    }
656}
657
658/// Substitution function source: var, env, attr.
659#[derive(Copy, Clone, Debug, MallocSizeOf, Hash, Eq, PartialEq, ToShmem, Parse)]
660pub enum SubstitutionFunctionKind {
661    /// CSS variable / custom property
662    Var,
663    /// Environment variable
664    Env,
665    /// DOM attribute
666    Attr,
667}
668
669/// A wrapper map that encapsulates both the custom properties and attributes
670/// for a given element.
671#[repr(C)]
672#[derive(Clone, Debug, Default, PartialEq)]
673pub struct ComputedSubstitutionFunctions {
674    /// The applicable custom properties (includes inherited and non-inherited).
675    pub custom_properties: ComputedCustomProperties,
676    /// The applicable DOM attributes.
677    pub attributes: OwnMap,
678}
679
680impl ComputedSubstitutionFunctions {
681    /// Creates a substitution function map from optional custom properties
682    /// and DOM attributes.
683    #[inline(always)]
684    pub fn new(
685        custom_properties: Option<ComputedCustomProperties>,
686        attributes: Option<OwnMap>,
687    ) -> Self {
688        Self {
689            custom_properties: custom_properties.unwrap_or_default(),
690            attributes: attributes.unwrap_or_default(),
691        }
692    }
693
694    #[inline(always)]
695    pub(crate) fn insert_var(
696        &mut self,
697        registration: &PropertyDescriptors,
698        name: &Name,
699        value: ComputedRegisteredValue,
700    ) {
701        self.custom_properties.insert(registration, name, value);
702    }
703
704    #[inline(always)]
705    pub(crate) fn insert_attr(&mut self, name: &Name, value: ComputedRegisteredValue) {
706        self.attributes.insert(name.clone(), Some(value));
707    }
708
709    #[inline(always)]
710    pub(crate) fn remove_var(&mut self, registration: &PropertyDescriptors, name: &Name) {
711        self.custom_properties.remove(registration, name);
712    }
713
714    #[inline(always)]
715    pub(crate) fn remove_attr(&mut self, name: &Name) {
716        self.attributes.insert(name.clone(), None);
717    }
718
719    #[inline(always)]
720    pub(crate) fn get_var(
721        &self,
722        registration: &PropertyDescriptors,
723        name: &Name,
724    ) -> Option<&ComputedRegisteredValue> {
725        self.custom_properties.get(registration, name)
726    }
727
728    #[inline(always)]
729    pub(crate) fn get_attr(&self, name: &Name) -> Option<&ComputedRegisteredValue> {
730        self.attributes.get(name).and_then(|p| p.as_ref())
731    }
732}
733
734#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem, Parse)]
735enum AttributeType {
736    Invalid,
737    None,
738    RawString,
739    Type(SyntaxDescriptor),
740    Unit(AttrUnit),
741}
742
743/// Data specific to an attr() call like type and namespace.
744#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
745pub struct AttributeData {
746    kind: AttributeType,
747    namespace: ParsedNamespace,
748}
749
750/// For a CSS string, the range, counted in bytes, that is attr()-tainted.
751#[derive(Clone, Debug, Default, MallocSizeOf, PartialEq, ToShmem, ToComputedValue)]
752pub struct AttrTaintedRange {
753    /// Start of the range, counted in bytes. Inclusive.
754    start: usize,
755    /// End of the range, counted in bytes. Exclusive.
756    end: usize,
757}
758
759impl AttrTaintedRange {
760    /// Creates a range within a CSS string that is tainted by attr().
761    #[inline(always)]
762    pub fn new(start: usize, end: usize) -> Self {
763        debug_assert!(start <= end);
764        Self { start, end }
765    }
766}
767
768/// In CSS Values and Units, values produced by `attr()` are considered attr()-tainted, as are
769/// functions that contain an attr()-tainted value. Using an attr()-tainted value as or in a <url>
770/// makes a declaration invalid at computed-value time.
771/// https://drafts.csswg.org/css-values-5/#attr-security
772#[derive(Clone, Debug, Default, MallocSizeOf, PartialEq, ToShmem)]
773pub struct AttrTaint(SmallVec<[AttrTaintedRange; 1]>);
774
775impl AttrTaint {
776    /// For a CSS string, determine whether any `<url>` overlapping this `range`
777    /// is disallowed due to attr()-tainting.
778    #[inline(always)]
779    pub fn should_disallow_urls_in_range(&self, range: &AttrTaintedRange) -> bool {
780        self.0
781            .iter()
782            .any(|r| r.start <= range.end && r.end >= range.start)
783    }
784
785    /// Returns true if the attr()-tainted range contains no elements.
786    #[inline(always)]
787    pub fn is_empty(&self) -> bool {
788        self.0.is_empty()
789    }
790
791    #[inline(always)]
792    fn new_fully_tainted(end: usize) -> Self {
793        let mut taint = Self::default();
794        taint.push(0, end);
795        taint
796    }
797
798    #[inline(always)]
799    fn push(&mut self, start: usize, end: usize) {
800        self.0.push(AttrTaintedRange::new(start, end));
801    }
802}
803
804/// The fallback of a particular value.
805#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
806pub struct VariableFallback {
807    // NOTE(emilio): We don't track fallback end, because we rely on the missing closing
808    // parenthesis, if any, to be inserted, which means that we can rely on our end being
809    // reference.end - 1.
810    start: num::NonZeroUsize,
811    first_token_type: TokenSerializationType,
812    last_token_type: TokenSerializationType,
813    /// References from this fallback value.
814    pub references: References,
815}
816
817/// A reference to a substitution function like env() / var() / attr().
818#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
819pub struct SubstitutionFunctionReference {
820    /// The identifier for this substitution function (the first argument).
821    pub name: Name,
822    start: usize,
823    end: usize,
824    /// The fallback for this function, if any.
825    pub fallback: Option<VariableFallback>,
826    /// attr() specific data, only relevant if substitution_kind is `Attr`..
827    pub attribute_data: AttributeData,
828    prev_token_type: TokenSerializationType,
829    next_token_type: TokenSerializationType,
830    /// The kind of substitution function we are.
831    pub substitution_kind: SubstitutionFunctionKind,
832}
833
834impl SubstitutionFunctionReference {
835    /// Whether we're an attr(... type(...)) reference.
836    pub fn is_attr_with_type(&self) -> bool {
837        self.substitution_kind == SubstitutionFunctionKind::Attr
838            && matches!(self.attribute_data.kind, AttributeType::Type(..))
839    }
840}
841
842/// A struct holding information about the external references to that a custom property value may
843/// have.
844#[derive(Clone, Debug, Default, MallocSizeOf, PartialEq, ToShmem)]
845pub struct References {
846    /// The actual list of references that we end up with.
847    pub refs: Vec<SubstitutionFunctionReference>,
848    /// Various flags about the kind of references we hold. Note that these include data about our
849    /// nested references.
850    ///
851    /// TODO(emilio): Do we need to distinguish between own and nested flags?
852    pub flags: ReferenceFlags,
853}
854
855impl References {
856    fn has_references(&self) -> bool {
857        !self.refs.is_empty()
858    }
859
860    pub(crate) fn non_custom_references(&self, is_root_element: bool) -> ReferenceFlags {
861        let mut mask = ReferenceFlags::NON_ROOT_DEPENDENCIES;
862        if is_root_element {
863            mask |= ReferenceFlags::ROOT_DEPENDENCIES
864        }
865        self.flags & mask
866    }
867}
868
869impl VariableValue {
870    fn empty(url_data: &UrlExtraData) -> Self {
871        Self {
872            css: String::new(),
873            last_token_type: Default::default(),
874            first_token_type: Default::default(),
875            url_data: url_data.clone(),
876            references: Default::default(),
877            explicitly_attr_tainted: false,
878        }
879    }
880
881    /// Create a new custom property without parsing if the CSS is known to be valid and contain no
882    /// references.
883    pub fn new(
884        css: String,
885        url_data: &UrlExtraData,
886        first_token_type: TokenSerializationType,
887        last_token_type: TokenSerializationType,
888    ) -> Self {
889        Self {
890            css,
891            url_data: url_data.clone(),
892            first_token_type,
893            last_token_type,
894            references: References::default(),
895            explicitly_attr_tainted: false,
896        }
897    }
898
899    fn push(
900        &mut self,
901        css: &str,
902        css_first_token_type: TokenSerializationType,
903        css_last_token_type: TokenSerializationType,
904        attr_taint: Option<&mut AttrTaint>,
905    ) -> Result<(), ()> {
906        /// Prevent values from getting terribly big since you can use custom
907        /// properties exponentially.
908        ///
909        /// This number (2MB) is somewhat arbitrary, but silly enough that no
910        /// reasonable page should hit it. We could limit by number of total
911        /// substitutions, but that was very easy to work around in practice
912        /// (just choose a larger initial value and boom).
913        const MAX_VALUE_LENGTH_IN_BYTES: usize = 2 * 1024 * 1024;
914
915        if self.css.len() + css.len() > MAX_VALUE_LENGTH_IN_BYTES {
916            return Err(());
917        }
918
919        // This happens e.g. between two subsequent var() functions:
920        // `var(--a)var(--b)`.
921        //
922        // In that case, css_*_token_type is nonsensical.
923        if css.is_empty() {
924            return Ok(());
925        }
926
927        self.first_token_type.set_if_nothing(css_first_token_type);
928        // If self.first_token_type was nothing,
929        // self.last_token_type is also nothing and this will be false:
930        if self
931            .last_token_type
932            .needs_separator_when_before(css_first_token_type)
933        {
934            self.css.push_str("/**/")
935        }
936        let start = self.css.len();
937        self.css.push_str(css);
938        let end = self.css.len();
939        if let Some(taint) = attr_taint {
940            taint.push(start, end);
941        }
942        self.last_token_type = css_last_token_type;
943        Ok(())
944    }
945
946    /// Parse a custom property value.
947    pub fn parse(
948        input: &mut Parser,
949        namespaces: Option<&FxHashMap<Prefix, Namespace>>,
950        url_data: &UrlExtraData,
951    ) -> Result<Self, ParseError> {
952        let mut references = References::default();
953        let mut missing_closing_characters = String::new();
954        let start_position = input.position();
955        let (first_token_type, last_token_type) = parse_declaration_value(
956            input,
957            start_position,
958            namespaces,
959            &mut references,
960            &mut missing_closing_characters,
961        )?;
962        let mut css = input
963            .slice_from(start_position)
964            .trim_ascii_start()
965            .to_owned();
966        if !missing_closing_characters.is_empty() {
967            // Unescaped backslash at EOF in a quoted string is ignored.
968            if css.ends_with("\\")
969                && matches!(missing_closing_characters.as_bytes()[0], b'"' | b'\'')
970            {
971                css.pop();
972            }
973            css.push_str(&missing_closing_characters);
974        }
975
976        css.truncate(css.trim_ascii_end().len());
977        css.shrink_to_fit();
978        references.refs.shrink_to_fit();
979
980        Ok(Self {
981            css,
982            url_data: url_data.clone(),
983            first_token_type,
984            last_token_type,
985            references,
986            explicitly_attr_tainted: false,
987        })
988    }
989
990    /// Returns whether this value is tainted by `attr()`.
991    pub fn is_attr_tainted(&self) -> bool {
992        self.references.flags.intersects(ReferenceFlags::ATTR) | self.explicitly_attr_tainted
993    }
994
995    /// Create VariableValue from an int.
996    fn integer(number: i32, url_data: &UrlExtraData) -> Self {
997        Self::from_token(
998            Token::Number {
999                has_sign: false,
1000                value: number as f32,
1001                int_value: Some(number),
1002            },
1003            url_data,
1004        )
1005    }
1006
1007    /// Create VariableValue from an int.
1008    fn ident(ident: &'static str, url_data: &UrlExtraData) -> Self {
1009        Self::from_token(Token::Ident(ident.into()), url_data)
1010    }
1011
1012    /// Create VariableValue from a float amount of CSS pixels.
1013    fn pixels(number: f32, url_data: &UrlExtraData) -> Self {
1014        // FIXME (https://github.com/servo/rust-cssparser/issues/266):
1015        // No way to get TokenSerializationType::Dimension without creating
1016        // Token object.
1017        Self::from_token(
1018            Token::Dimension {
1019                has_sign: false,
1020                value: number,
1021                int_value: None,
1022                unit: CowRcStr::from("px"),
1023            },
1024            url_data,
1025        )
1026    }
1027
1028    /// Create VariableValue from an integer amount of milliseconds.
1029    fn int_ms(number: i32, url_data: &UrlExtraData) -> Self {
1030        Self::from_token(
1031            Token::Dimension {
1032                has_sign: false,
1033                value: number as f32,
1034                int_value: Some(number),
1035                unit: CowRcStr::from("ms"),
1036            },
1037            url_data,
1038        )
1039    }
1040
1041    /// Create VariableValue from an integer amount of CSS pixels.
1042    fn int_pixels(number: i32, url_data: &UrlExtraData) -> Self {
1043        Self::from_token(
1044            Token::Dimension {
1045                has_sign: false,
1046                value: number as f32,
1047                int_value: Some(number),
1048                unit: CowRcStr::from("px"),
1049            },
1050            url_data,
1051        )
1052    }
1053
1054    fn from_token(token: Token, url_data: &UrlExtraData) -> Self {
1055        let token_type = token.serialization_type();
1056        let mut css = token.to_css_string();
1057        css.shrink_to_fit();
1058
1059        VariableValue {
1060            css,
1061            url_data: url_data.clone(),
1062            first_token_type: token_type,
1063            last_token_type: token_type,
1064            references: Default::default(),
1065            explicitly_attr_tainted: false,
1066        }
1067    }
1068
1069    /// Returns the raw CSS text from this VariableValue
1070    pub fn css_text(&self) -> &str {
1071        &self.css
1072    }
1073
1074    /// Returns whether this variable value has any reference to the environment or other
1075    /// variables.
1076    pub fn has_references(&self) -> bool {
1077        self.references.has_references()
1078    }
1079
1080    /// Returns the attribute references in this variable value, if any.
1081    pub fn collect_attribute_references(&self, references: &mut AttrReferenceSet) {
1082        self.references.refs.iter().for_each(|r| {
1083            if r.substitution_kind == SubstitutionFunctionKind::Attr {
1084                // For a non-ascii-lowercase attribute, whether we match
1085                // case-sensitively depends on whether the element we're
1086                // matching against is an HTML element in an HTML document.
1087                // So to simplify invalidation we collect both potential
1088                // references here.
1089                references.insert(r.name.clone().into());
1090                let lowercase = r.name.to_ascii_lowercase();
1091                if r.name != lowercase {
1092                    references.insert(lowercase.into());
1093                }
1094            }
1095        })
1096    }
1097}
1098
1099/// <https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value>
1100fn parse_declaration_value(
1101    input: &mut Parser,
1102    input_start: SourcePosition,
1103    namespaces: Option<&FxHashMap<Prefix, Namespace>>,
1104    references: &mut References,
1105    missing_closing_characters: &mut String,
1106) -> Result<(TokenSerializationType, TokenSerializationType), ParseError> {
1107    input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| {
1108        parse_declaration_value_block(
1109            input,
1110            input_start,
1111            namespaces,
1112            references,
1113            missing_closing_characters,
1114        )
1115    })
1116}
1117
1118/// Like parse_declaration_value, but accept `!` and `;` since they are only invalid at the top level.
1119fn parse_declaration_value_block(
1120    input: &mut Parser,
1121    input_start: SourcePosition,
1122    namespaces: Option<&FxHashMap<Prefix, Namespace>>,
1123    references: &mut References,
1124    missing_closing_characters: &mut String,
1125) -> Result<(TokenSerializationType, TokenSerializationType), ParseError> {
1126    let mut is_first = true;
1127    let mut first_token_type = TokenSerializationType::Nothing;
1128    let mut last_token_type = TokenSerializationType::Nothing;
1129    let mut prev_reference_index: Option<usize> = None;
1130    loop {
1131        let token_start = input.position();
1132        let Ok(token) = input.next_including_whitespace_and_comments() else {
1133            break;
1134        };
1135
1136        let prev_token_type = last_token_type;
1137        let serialization_type = token.serialization_type();
1138        last_token_type = serialization_type;
1139        if is_first {
1140            first_token_type = last_token_type;
1141            is_first = false;
1142        }
1143
1144        macro_rules! nested {
1145            ($closing:expr) => {{
1146                let mut inner_end_position = None;
1147                let result = input.parse_nested_block(|input| {
1148                    let result = parse_declaration_value_block(
1149                        input,
1150                        input_start,
1151                        namespaces,
1152                        references,
1153                        missing_closing_characters,
1154                    )?;
1155                    inner_end_position = Some(input.position());
1156                    Ok(result)
1157                })?;
1158                if inner_end_position.unwrap() == input.position() {
1159                    missing_closing_characters.push_str($closing);
1160                }
1161                result
1162            }};
1163        }
1164        if let Some(index) = prev_reference_index.take() {
1165            references.refs[index].next_token_type = serialization_type;
1166        }
1167        match *token {
1168            Token::Comment(_) => {
1169                let token_slice = input.slice_from(token_start);
1170                if !token_slice.ends_with("*/") {
1171                    missing_closing_characters.push_str(if token_slice.ends_with('*') {
1172                        "/"
1173                    } else {
1174                        "*/"
1175                    })
1176                }
1177            },
1178            Token::BadUrl(..) => {
1179                let e = StyleParseErrorKind::BadUrlInDeclarationValueBlock;
1180                return Err(ParseError::custom(e));
1181            },
1182            Token::BadString(..) => {
1183                let e = StyleParseErrorKind::BadStringInDeclarationValueBlock;
1184                return Err(ParseError::custom(e));
1185            },
1186            Token::CloseParenthesis => {
1187                let e = StyleParseErrorKind::UnbalancedCloseParenthesisInDeclarationValueBlock;
1188                return Err(ParseError::custom(e));
1189            },
1190            Token::CloseSquareBracket => {
1191                let e = StyleParseErrorKind::UnbalancedCloseSquareBracketInDeclarationValueBlock;
1192                return Err(ParseError::custom(e));
1193            },
1194            Token::CloseCurlyBracket => {
1195                let e = StyleParseErrorKind::UnbalancedCloseCurlyBracketInDeclarationValueBlock;
1196                return Err(ParseError::custom(e));
1197            },
1198            Token::Function(ref name) => {
1199                let substitution_kind = SubstitutionFunctionKind::from_ident(name).ok();
1200                if let Some(substitution_kind) = substitution_kind {
1201                    let our_ref_index = references.refs.len();
1202                    let mut input_end_position = None;
1203                    let fallback = input.parse_nested_block(|input| {
1204                        let mut namespace = ParsedNamespace::Known(Namespace::default());
1205                        if substitution_kind == SubstitutionFunctionKind::Attr {
1206                            if let Some(namespaces) = namespaces {
1207                                if let Ok(ns) = input
1208                                    .try_parse(|input| ParsedNamespace::parse(namespaces, input))
1209                                {
1210                                    namespace = ns;
1211                                    let prev = input.state();
1212                                    let next = match *input.next_including_whitespace()? {
1213                                        Token::Ident(_) => Ok(()),
1214                                        _ => Err(ParseError::unexpected_token()),
1215                                    };
1216                                    input.reset(&prev);
1217                                    next?;
1218                                }
1219                            }
1220                        }
1221                        // TODO(emilio): For env() this should be <custom-ident> per spec, but no other browser does
1222                        // that, see https://github.com/w3c/csswg-drafts/issues/3262.
1223                        let name = input.expect_ident()?;
1224                        let name =
1225                            Atom::from(if substitution_kind == SubstitutionFunctionKind::Var {
1226                                match parse_name(name.as_ref()) {
1227                                    Ok(name) => name,
1228                                    Err(()) => {
1229                                        return Err(ParseError::custom(
1230                                            SelectorParseErrorKind::UnexpectedIdent,
1231                                        ));
1232                                    },
1233                                }
1234                            } else {
1235                                name.as_ref()
1236                            });
1237
1238                        let attribute_kind = if substitution_kind == SubstitutionFunctionKind::Attr
1239                        {
1240                            parse_attr_type(input)
1241                        } else {
1242                            AttributeType::None
1243                        };
1244
1245                        // We want the order of the references to match source order. So we need to reserve our slot
1246                        // now, _before_ parsing our fallback. Note that we don't care if parsing fails after all, since
1247                        // if this fails we discard the whole result anyways.
1248                        let start = token_start.byte_index() - input_start.byte_index();
1249                        references.refs.push(SubstitutionFunctionReference {
1250                            name,
1251                            start,
1252                            // To be fixed up after parsing fallback and auto-closing via our_ref_index.
1253                            end: start,
1254                            prev_token_type,
1255                            // To be fixed up (if needed) on the next loop iteration via prev_reference_index.
1256                            next_token_type: TokenSerializationType::Nothing,
1257                            // To be fixed up after parsing fallback.
1258                            fallback: None,
1259                            attribute_data: AttributeData {
1260                                kind: attribute_kind,
1261                                namespace,
1262                            },
1263                            substitution_kind,
1264                        });
1265
1266                        let mut fallback = None;
1267                        if input.try_parse(|input| input.expect_comma()).is_ok() {
1268                            input.skip_whitespace();
1269                            let fallback_start = num::NonZeroUsize::new(
1270                                input.position().byte_index() - input_start.byte_index(),
1271                            )
1272                            .unwrap();
1273                            let mut references = References::default();
1274                            // NOTE(emilio): Intentionally using parse_declaration_value rather than
1275                            // parse_declaration_value_block, since that's what parse_fallback used to do.
1276                            let (first, last) = parse_declaration_value(
1277                                input,
1278                                input_start,
1279                                namespaces,
1280                                &mut references,
1281                                missing_closing_characters,
1282                            )?;
1283                            fallback = Some(VariableFallback {
1284                                start: fallback_start,
1285                                first_token_type: first,
1286                                last_token_type: last,
1287                                references,
1288                            });
1289                            input_end_position = Some(input.position());
1290                        } else {
1291                            let state = input.state();
1292                            // We still need to consume the rest of the potentially-unclosed
1293                            // tokens, but make sure to not consume tokens that would otherwise be
1294                            // invalid, by calling reset().
1295                            parse_declaration_value_block(
1296                                input,
1297                                input_start,
1298                                namespaces,
1299                                references,
1300                                missing_closing_characters,
1301                            )?;
1302                            input_end_position = Some(input.position());
1303                            input.reset(&state);
1304                        }
1305                        Ok(fallback)
1306                    })?;
1307                    if input_end_position.unwrap() == input.position() {
1308                        missing_closing_characters.push(')');
1309                    }
1310                    prev_reference_index = Some(our_ref_index);
1311                    let reference = &mut references.refs[our_ref_index];
1312                    reference.end = input.position().byte_index() - input_start.byte_index()
1313                        + missing_closing_characters.len();
1314                    reference.fallback = fallback;
1315                    references.flags |= match substitution_kind {
1316                        SubstitutionFunctionKind::Var => ReferenceFlags::VAR,
1317                        SubstitutionFunctionKind::Env => ReferenceFlags::ENV,
1318                        SubstitutionFunctionKind::Attr => ReferenceFlags::ATTR,
1319                    };
1320                    // Bubble up flags from our fallback, so we know what we might reference from the outer scope.
1321                    if let Some(ref fb) = reference.fallback {
1322                        references.flags |= fb.references.flags;
1323                    }
1324                } else {
1325                    nested!(")");
1326                }
1327            },
1328            Token::ParenthesisBlock => {
1329                nested!(")");
1330            },
1331            Token::CurlyBracketBlock => {
1332                nested!("}");
1333            },
1334            Token::SquareBracketBlock => {
1335                nested!("]");
1336            },
1337            Token::QuotedString(_) => {
1338                let token_slice = input.slice_from(token_start);
1339                let quote = &token_slice[..1];
1340                debug_assert!(matches!(quote, "\"" | "'"));
1341                if !(token_slice.ends_with(quote) && token_slice.len() > 1) {
1342                    missing_closing_characters.push_str(quote)
1343                }
1344            },
1345            Token::Ident(ref value)
1346            | Token::AtKeyword(ref value)
1347            | Token::Hash(ref value)
1348            | Token::IDHash(ref value)
1349            | Token::UnquotedUrl(ref value)
1350            | Token::Dimension {
1351                unit: ref value, ..
1352            } => {
1353                references.flags.insert(ReferenceFlags::from_unit(value));
1354                let is_unquoted_url = matches!(token, Token::UnquotedUrl(_));
1355                if value.ends_with("�") && input.slice_from(token_start).ends_with("\\") {
1356                    // Unescaped backslash at EOF in these contexts is interpreted as U+FFFD
1357                    // Check the value in case the final backslash was itself escaped.
1358                    // Serialize as escaped U+FFFD, which is also interpreted as U+FFFD.
1359                    // (Unescaped U+FFFD would also work, but removing the backslash is annoying.)
1360                    missing_closing_characters.push('�')
1361                }
1362                if is_unquoted_url && !input.slice_from(token_start).ends_with(")") {
1363                    missing_closing_characters.push(')');
1364                }
1365            },
1366            _ => {},
1367        };
1368    }
1369    Ok((first_token_type, last_token_type))
1370}
1371
1372/// Parse <attr-type> = type( <syntax> ) | raw-string | number | <attr-unit>.
1373/// https://drafts.csswg.org/css-values-5/#attr-notation
1374fn parse_attr_type(input: &mut Parser) -> AttributeType {
1375    input
1376        .try_parse(|input| {
1377            Ok(match input.next()? {
1378                Token::Function(name) if name.eq_ignore_ascii_case("type") => AttributeType::Type(
1379                    input.parse_nested_block(SyntaxDescriptor::from_css_parser)?,
1380                ),
1381                Token::Ident(ident) => {
1382                    if ident.eq_ignore_ascii_case("raw-string") {
1383                        AttributeType::RawString
1384                    } else if let Ok(unit) = AttrUnit::from_ident(ident) {
1385                        AttributeType::Unit(unit)
1386                    } else {
1387                        AttributeType::Invalid
1388                    }
1389                },
1390                Token::Delim('%') => AttributeType::Unit(AttrUnit::Percentage),
1391                _ => return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError)),
1392            })
1393        })
1394        .unwrap_or(AttributeType::None)
1395}
1396
1397/// Attribute values may reference other substitution functions we may need to process.
1398/// See step 6: https://drafts.csswg.org/css-values-5/#attr-substitution
1399pub fn get_attr_value_for_cycle_resolution(
1400    name: &Name,
1401    attribute_data: &AttributeData,
1402    url_data: &UrlExtraData,
1403    attribute_tracker: &mut AttributeTracker,
1404) -> Result<ComputedRegisteredValue, ()> {
1405    let namespace = match attribute_data.namespace {
1406        ParsedNamespace::Known(ref ns) => ns,
1407        ParsedNamespace::Unknown => return Err(()),
1408    };
1409    let attr = LocalName::with_name(name, |local_name| {
1410        attribute_tracker.query(local_name, namespace).ok_or(())
1411    })?;
1412    let mut parser = Parser::new(&attr);
1413    // TODO(Bug 2021110): Support namespaced attributes in chained references.
1414    let value = VariableValue::parse(&mut parser, None, url_data).map_err(|_| ())?;
1415    Ok(ComputedRegisteredValue::universal(Arc::new(value)))
1416}
1417
1418/// See https://drafts.csswg.org/css-variables-2/#invalid-at-computed-value-time
1419pub fn handle_invalid_at_computed_value_time(
1420    name: &Name,
1421    registration: &PropertyDescriptors,
1422    context: &mut computed::Context,
1423) {
1424    if !registration.is_universal() {
1425        // For the root element, inherited maps are empty. We should just
1426        // use the initial value if any, rather than removing the name.
1427        if registration.inherits() && !context.builder.is_root_element {
1428            let inherited = context.builder.inherited_style.custom_properties();
1429            if let Some(value) = inherited.get(registration, name) {
1430                context.builder.substitution_functions.insert_var(
1431                    registration,
1432                    name,
1433                    value.clone(),
1434                );
1435                return;
1436            }
1437        } else if let Some(ref initial_value) = registration.initial_value {
1438            if let Ok(initial_value) = compute_value(
1439                &initial_value.css,
1440                &initial_value.url_data,
1441                registration,
1442                context,
1443                AttrTaint::default(),
1444            ) {
1445                context.builder.substitution_functions.insert_var(
1446                    registration,
1447                    name,
1448                    initial_value,
1449                );
1450                return;
1451            }
1452        }
1453    }
1454    context
1455        .builder
1456        .substitution_functions
1457        .remove_var(registration, name);
1458}
1459
1460/// Replace `var()`, `env()`, and `attr()` functions in a pre-existing variable value.
1461pub fn substitute_references_if_needed_and_apply(
1462    name: &Name,
1463    kind: SubstitutionFunctionKind,
1464    value: &Arc<VariableValue>,
1465    stylist: &Stylist,
1466    context: &mut computed::Context,
1467    attribute_tracker: &mut AttributeTracker,
1468) {
1469    debug_assert_ne!(kind, SubstitutionFunctionKind::Env);
1470    let is_var = matches!(kind, SubstitutionFunctionKind::Var);
1471    let registration = stylist.get_custom_property_registration(name);
1472    if is_var && !value.has_references() && registration.is_universal() {
1473        // Trivial path: no references and no need to compute the value, just apply it directly.
1474        let computed_value = ComputedRegisteredValue::universal(Arc::clone(value));
1475        context
1476            .builder
1477            .substitution_functions
1478            .insert_var(registration, name, computed_value);
1479        return;
1480    }
1481
1482    let url_data = &value.url_data;
1483    let substitution = substitute_internal(
1484        value,
1485        &context.builder.substitution_functions,
1486        stylist,
1487        context,
1488        attribute_tracker,
1489        &mut SmallVec::new(),
1490        None,
1491    );
1492
1493    let Ok(substitution) = substitution else {
1494        if is_var {
1495            handle_invalid_at_computed_value_time(name, registration, context);
1496        } else {
1497            context.builder.substitution_functions.remove_attr(name);
1498        }
1499        return;
1500    };
1501
1502    // If variable fallback results in a wide keyword, deal with it now.
1503    let inherited = context.builder.inherited_style.custom_properties();
1504    if is_var {
1505        let css = &substitution.css;
1506        let css_wide_kw = {
1507            let mut input = Parser::new(css);
1508            input.try_parse(CSSWideKeyword::parse)
1509        };
1510
1511        if let Ok(kw) = css_wide_kw {
1512            // TODO: It's unclear what this should do for revert / revert-layer, see
1513            // https://github.com/w3c/csswg-drafts/issues/9131. For now treating as unset
1514            // seems fine?
1515            match (kw, registration.inherits(), context.is_root_element()) {
1516                (CSSWideKeyword::Initial, _, _)
1517                | (CSSWideKeyword::Revert, false, _)
1518                | (CSSWideKeyword::RevertLayer, false, _)
1519                | (CSSWideKeyword::RevertRule, false, _)
1520                | (CSSWideKeyword::Unset, false, _)
1521                | (CSSWideKeyword::Revert, true, true)
1522                | (CSSWideKeyword::RevertLayer, true, true)
1523                | (CSSWideKeyword::RevertRule, true, true)
1524                | (CSSWideKeyword::Unset, true, true)
1525                | (CSSWideKeyword::Inherit, _, true) => {
1526                    remove_and_insert_initial_value(
1527                        name,
1528                        registration,
1529                        &mut context.builder.substitution_functions,
1530                    );
1531                },
1532                (CSSWideKeyword::Revert, true, false)
1533                | (CSSWideKeyword::RevertLayer, true, false)
1534                | (CSSWideKeyword::RevertRule, true, false)
1535                | (CSSWideKeyword::Inherit, _, false)
1536                | (CSSWideKeyword::Unset, true, false) => {
1537                    match inherited.get(registration, name) {
1538                        Some(value) => {
1539                            context.builder.substitution_functions.insert_var(
1540                                registration,
1541                                name,
1542                                value.clone(),
1543                            );
1544                        },
1545                        None => {
1546                            context
1547                                .builder
1548                                .substitution_functions
1549                                .remove_var(registration, name);
1550                        },
1551                    };
1552                },
1553            }
1554            return;
1555        }
1556    }
1557
1558    match kind {
1559        SubstitutionFunctionKind::Var => {
1560            let value = match substitution.into_value(url_data, registration, context) {
1561                Ok(v) => v,
1562                Err(()) => {
1563                    handle_invalid_at_computed_value_time(name, registration, context);
1564                    return;
1565                },
1566            };
1567            context
1568                .builder
1569                .substitution_functions
1570                .insert_var(registration, name, value);
1571        },
1572        SubstitutionFunctionKind::Attr => {
1573            let mut value = ComputedRegisteredValue::universal(Arc::new(VariableValue::new(
1574                substitution.css.into_owned(),
1575                url_data,
1576                substitution.first_token_type,
1577                substitution.last_token_type,
1578            )));
1579            value.attr_tainted |= substitution.attr_tainted;
1580            context
1581                .builder
1582                .substitution_functions
1583                .insert_attr(name, value);
1584        },
1585        SubstitutionFunctionKind::Env => unreachable!("Kind cannot be env."),
1586    }
1587}
1588
1589#[derive(Default, Debug)]
1590struct Substitution<'a> {
1591    css: Cow<'a, str>,
1592    first_token_type: TokenSerializationType,
1593    last_token_type: TokenSerializationType,
1594    attr_tainted: bool,
1595}
1596
1597impl<'a> Substitution<'a> {
1598    fn from_value(v: VariableValue, attr_tainted: bool) -> Self {
1599        Substitution {
1600            css: v.css.into(),
1601            first_token_type: v.first_token_type,
1602            last_token_type: v.last_token_type,
1603            attr_tainted,
1604        }
1605    }
1606
1607    fn into_value(
1608        self,
1609        url_data: &UrlExtraData,
1610        registration: &PropertyDescriptors,
1611        computed_context: &computed::Context,
1612    ) -> Result<ComputedRegisteredValue, ()> {
1613        if registration.is_universal() {
1614            let mut value = ComputedRegisteredValue::universal(Arc::new(VariableValue::new(
1615                self.css.into_owned(),
1616                url_data,
1617                self.first_token_type,
1618                self.last_token_type,
1619            )));
1620            value.attr_tainted |= self.attr_tainted;
1621            return Ok(value);
1622        }
1623        let taint = if self.attr_tainted {
1624            // Per spec: substitution value of an arbitrary substitution function is
1625            // attr()-tainted as a whole if any attr()-tainted values were involved
1626            // in creating that substitution value.
1627            // https://drafts.csswg.org/css-values-5/#attr-security
1628            AttrTaint::new_fully_tainted(self.css.len())
1629        } else {
1630            AttrTaint::default()
1631        };
1632        let mut v = compute_value(&self.css, url_data, registration, computed_context, taint)?;
1633        v.attr_tainted |= self.attr_tainted;
1634        Ok(v)
1635    }
1636
1637    fn new(
1638        css: Cow<'a, str>,
1639        first_token_type: TokenSerializationType,
1640        last_token_type: TokenSerializationType,
1641        attr_tainted: bool,
1642    ) -> Self {
1643        Self {
1644            css,
1645            first_token_type,
1646            last_token_type,
1647            attr_tainted,
1648        }
1649    }
1650}
1651
1652/// Result of var(), env(), and attr() substitution.
1653#[derive(Debug)]
1654pub struct SubstitutionResult<'a> {
1655    /// The resolved CSS string after substitution.
1656    pub css: Cow<'a, str>,
1657    /// Regions in the `css` string that are attr()-tainted, if any.
1658    pub attr_taint: AttrTaint,
1659}
1660
1661fn compute_value(
1662    css: &str,
1663    url_data: &UrlExtraData,
1664    registration: &PropertyDescriptors,
1665    computed_context: &computed::Context,
1666    attr_taint: AttrTaint,
1667) -> Result<ComputedRegisteredValue, ()> {
1668    debug_assert!(!registration.is_universal());
1669
1670    let mut input = Parser::new(css);
1671    SpecifiedRegisteredValue::compute(
1672        &mut input,
1673        registration,
1674        None,
1675        url_data,
1676        computed_context,
1677        AllowComputationallyDependent::Yes,
1678        attr_taint,
1679    )
1680}
1681
1682/// Removes the named registered custom property and inserts its uncomputed initial value.
1683pub(crate) fn remove_and_insert_initial_value(
1684    name: &Name,
1685    registration: &PropertyDescriptors,
1686    substitution_functions: &mut ComputedSubstitutionFunctions,
1687) {
1688    substitution_functions.remove_var(registration, name);
1689    if let Some(ref initial_value) = registration.initial_value {
1690        let value = ComputedRegisteredValue::universal(Arc::clone(initial_value));
1691        substitution_functions.insert_var(registration, name, value);
1692    }
1693}
1694
1695fn do_substitute_chunk<'a>(
1696    css: &'a str,
1697    start: usize,
1698    end: usize,
1699    first_token_type: TokenSerializationType,
1700    last_token_type: TokenSerializationType,
1701    url_data: &UrlExtraData,
1702    substitution_functions: &'a ComputedSubstitutionFunctions,
1703    stylist: &Stylist,
1704    computed_context: &computed::Context,
1705    references: &'a [SubstitutionFunctionReference],
1706    attribute_tracker: &mut AttributeTracker,
1707    seen: &mut SmallVec<[&'a Name; 8]>,
1708    mut attr_taint: Option<&mut AttrTaint>,
1709) -> Result<Substitution<'a>, ()> {
1710    if start == end {
1711        // Empty string. Easy.
1712        return Ok(Substitution::default());
1713    }
1714    // Easy case: no references involved.
1715    if references.is_empty() {
1716        let result = &css[start..end];
1717        return Ok(Substitution::new(
1718            Cow::Borrowed(result),
1719            first_token_type,
1720            last_token_type,
1721            Default::default(),
1722        ));
1723    }
1724
1725    let mut substituted = ComputedValue::empty(url_data);
1726    let mut next_token_type = first_token_type;
1727    let mut cur_pos = start;
1728    let mut attr_tainted = false;
1729    let references = references.iter();
1730    for reference in references {
1731        if reference.start != cur_pos {
1732            substituted.push(
1733                &css[cur_pos..reference.start],
1734                next_token_type,
1735                reference.prev_token_type,
1736                /* attr_taint */ None,
1737            )?;
1738        }
1739
1740        let substitution = substitute_one_reference(
1741            css,
1742            url_data,
1743            substitution_functions,
1744            reference,
1745            stylist,
1746            computed_context,
1747            attribute_tracker,
1748            seen,
1749        )?;
1750
1751        // Optimize the property: var(--...) case to avoid allocating at all.
1752        if reference.start == start && reference.end == end {
1753            if let Some(taint) = attr_taint.filter(|_| substitution.attr_tainted) {
1754                taint.push(start, substitution.css.len());
1755            }
1756            return Ok(substitution);
1757        }
1758
1759        substituted.push(
1760            &substitution.css,
1761            substitution.first_token_type,
1762            substitution.last_token_type,
1763            attr_taint
1764                .as_deref_mut()
1765                .filter(|_| substitution.attr_tainted),
1766        )?;
1767        attr_tainted |= substitution.attr_tainted;
1768        next_token_type = reference.next_token_type;
1769        cur_pos = reference.end;
1770    }
1771    // Push the rest of the value if needed.
1772    if cur_pos != end {
1773        substituted.push(
1774            &css[cur_pos..end],
1775            next_token_type,
1776            last_token_type,
1777            /* attr_taint */ None,
1778        )?;
1779    }
1780    Ok(Substitution::from_value(substituted, attr_tainted))
1781}
1782
1783fn quoted_css_string(src: &str) -> String {
1784    let mut dest = String::with_capacity(src.len() + 2);
1785    cssparser::serialize_string(src, &mut dest).unwrap();
1786    dest
1787}
1788
1789fn substitute_one_reference<'a>(
1790    css: &'a str,
1791    url_data: &UrlExtraData,
1792    substitution_functions: &'a ComputedSubstitutionFunctions,
1793    reference: &'a SubstitutionFunctionReference,
1794    stylist: &Stylist,
1795    computed_context: &computed::Context,
1796    attribute_tracker: &mut AttributeTracker,
1797    seen: &mut SmallVec<[&'a Name; 8]>,
1798) -> Result<Substitution<'a>, ()> {
1799    let simple_attr_subst = |s: &str| {
1800        Some(Substitution::new(
1801            Cow::Owned(quoted_css_string(s)),
1802            TokenSerializationType::Nothing,
1803            TokenSerializationType::Nothing,
1804            /* attr_tainted */ true,
1805        ))
1806    };
1807    let substitution: Option<_> = match reference.substitution_kind {
1808        SubstitutionFunctionKind::Var => {
1809            let registration = stylist.get_custom_property_registration(&reference.name);
1810            match substitution_functions.get_var(registration, &reference.name) {
1811                None => None,
1812                // If the referenced value is itself still unresolved (i.e. it has references), we
1813                // are substituting against a partially-resolved map -- this happens while applying
1814                // a prioritary property in the middle of custom-property resolution. Resolve it
1815                // recursively, guarding against cycles (which the resolver will also remove, but it
1816                // may not have gotten to this variable yet).
1817                Some(v) => match v.as_universal() {
1818                    Some(u) if u.has_references() => {
1819                        if seen.contains(&&reference.name) {
1820                            // Cycle: the primary is guaranteed-invalid, so fall through to the
1821                            // fallback (if any), like any other invalid primary.
1822                            None
1823                        } else {
1824                            seen.push(&reference.name);
1825                            let result = substitute_internal(
1826                                u,
1827                                substitution_functions,
1828                                stylist,
1829                                computed_context,
1830                                attribute_tracker,
1831                                seen,
1832                                /* attr_taint */ None,
1833                            );
1834                            seen.pop();
1835                            match result {
1836                                Ok(mut substitution) => {
1837                                    substitution.attr_tainted |= v.attr_tainted;
1838                                    Some(substitution)
1839                                },
1840                                // The primary couldn't be resolved (invalid); use the fallback.
1841                                Err(()) => None,
1842                            }
1843                        }
1844                    },
1845                    _ => Some(Substitution::from_value(
1846                        v.to_variable_value(),
1847                        v.attr_tainted,
1848                    )),
1849                },
1850            }
1851        },
1852        SubstitutionFunctionKind::Env => {
1853            let device = stylist.device();
1854            device
1855                .environment()
1856                .get(&reference.name, device, url_data)
1857                .map(|v| Substitution::from_value(v, /* attr_tainted */ false))
1858        },
1859        // https://drafts.csswg.org/css-values-5/#attr-substitution
1860        SubstitutionFunctionKind::Attr => {
1861            let namespace = match reference.attribute_data.namespace {
1862                ParsedNamespace::Known(ref ns) => Some(ns),
1863                ParsedNamespace::Unknown => None,
1864            };
1865            namespace
1866                .and_then(|namespace| {
1867                    LocalName::with_name(&reference.name, |local_name| {
1868                        attribute_tracker.query(local_name, namespace)
1869                    })
1870                })
1871                .map_or_else(
1872                    || {
1873                        // Special case when fallback and <attr-type> are omitted.
1874                        // See FAILURE: https://drafts.csswg.org/css-values-5/#attr-substitution
1875                        if reference.fallback.is_none()
1876                            && reference.attribute_data.kind == AttributeType::None
1877                        {
1878                            simple_attr_subst("")
1879                        } else {
1880                            None
1881                        }
1882                    },
1883                    |attr| {
1884                        let attr = if let AttributeType::Type(_) = &reference.attribute_data.kind {
1885                            // If we're evaluating a container query, we haven't run the cascade
1886                            // and populated substitution_functions.attributes, so we can't do the
1887                            // get_attr() lookup here.
1888                            // TODO: This means chained attr() references will not work reliably in
1889                            // container style queries:
1890                            // https://bugzilla.mozilla.org/show_bug.cgi?id=2028861
1891                            if computed_context.in_container_query {
1892                                attr
1893                            } else {
1894                                substitution_functions
1895                                    .get_attr(&reference.name)
1896                                    .map(|v| v.to_variable_value())?
1897                                    .css
1898                            }
1899                        } else {
1900                            attr
1901                        };
1902                        let mut parser = Parser::new(&attr);
1903                        match &reference.attribute_data.kind {
1904                            AttributeType::Unit(unit) => {
1905                                let css = {
1906                                    // Verify that attribute data is a <number-token>.
1907                                    parser.expect_number().ok()?;
1908                                    let mut s = attr.clone();
1909                                    s.push_str(unit.as_ref());
1910                                    s
1911                                };
1912                                let serialization = match unit {
1913                                    AttrUnit::Number => TokenSerializationType::Number,
1914                                    AttrUnit::Percentage => TokenSerializationType::Percentage,
1915                                    _ => TokenSerializationType::Dimension,
1916                                };
1917                                let value =
1918                                    ComputedValue::new(css, url_data, serialization, serialization);
1919                                Some(Substitution::from_value(
1920                                    value, /* attr_tainted */ true,
1921                                ))
1922                            },
1923                            AttributeType::Type(syntax) => {
1924                                let value = SpecifiedRegisteredValue::parse(
1925                                    &mut parser,
1926                                    syntax,
1927                                    url_data,
1928                                    None,
1929                                    AllowComputationallyDependent::Yes,
1930                                    AttrTaint::default(),
1931                                )
1932                                .ok()?;
1933                                let value = value.to_variable_value();
1934                                Some(Substitution::from_value(
1935                                    value, /* attr_tainted */ true,
1936                                ))
1937                            },
1938                            AttributeType::RawString | AttributeType::None => {
1939                                simple_attr_subst(&attr)
1940                            },
1941                            AttributeType::Invalid => None,
1942                        }
1943                    },
1944                )
1945        },
1946    };
1947
1948    if let Some(s) = substitution {
1949        return Ok(s);
1950    }
1951
1952    let Some(ref fallback) = reference.fallback else {
1953        return Err(());
1954    };
1955
1956    do_substitute_chunk(
1957        css,
1958        fallback.start.get(),
1959        reference.end - 1, // Skip the closing parenthesis of the reference value.
1960        fallback.first_token_type,
1961        fallback.last_token_type,
1962        url_data,
1963        substitution_functions,
1964        stylist,
1965        computed_context,
1966        &fallback.references.refs,
1967        attribute_tracker,
1968        seen,
1969        /* attr_taint */ None,
1970    )
1971}
1972
1973/// Replace `var()`, `env()`, and `attr()` functions. Return `Err(..)` for invalid at computed time.
1974fn substitute_internal<'a>(
1975    variable_value: &'a VariableValue,
1976    substitution_functions: &'a ComputedSubstitutionFunctions,
1977    stylist: &Stylist,
1978    computed_context: &computed::Context,
1979    attribute_tracker: &mut AttributeTracker,
1980    seen: &mut SmallVec<[&'a Name; 8]>,
1981    attr_taint: Option<&mut AttrTaint>,
1982) -> Result<Substitution<'a>, ()> {
1983    do_substitute_chunk(
1984        &variable_value.css,
1985        /* start = */ 0,
1986        /* end = */ variable_value.css.len(),
1987        variable_value.first_token_type,
1988        variable_value.last_token_type,
1989        &variable_value.url_data,
1990        substitution_functions,
1991        stylist,
1992        computed_context,
1993        &variable_value.references.refs,
1994        attribute_tracker,
1995        seen,
1996        attr_taint,
1997    )
1998}
1999
2000/// Replace var(), env(), and attr() functions, returning the resulting CSS string.
2001pub fn substitute<'a>(
2002    variable_value: &'a VariableValue,
2003    substitution_functions: &'a ComputedSubstitutionFunctions,
2004    stylist: &Stylist,
2005    computed_context: &computed::Context,
2006    attribute_tracker: &mut AttributeTracker,
2007) -> Result<SubstitutionResult<'a>, ()> {
2008    debug_assert!(variable_value.has_references());
2009    let mut attr_taint = AttrTaint::default();
2010    let v = substitute_internal(
2011        variable_value,
2012        substitution_functions,
2013        stylist,
2014        computed_context,
2015        attribute_tracker,
2016        &mut SmallVec::new(),
2017        Some(&mut attr_taint),
2018    )?;
2019    Ok(SubstitutionResult {
2020        css: v.css,
2021        attr_taint,
2022    })
2023}