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::FxHashMap;
10use crate::custom_properties_map::{CustomPropertiesMap, OwnMap};
11use crate::device::Device;
12use crate::dom::AttributeTracker;
13use crate::properties::{CSSWideKeyword, PrioritaryPropertyId, PropertyIdRef};
14use crate::properties_and_values::{
15    rule::Descriptors as PropertyDescriptors,
16    syntax::Descriptor as SyntaxDescriptor,
17    value::{
18        AllowComputationallyDependent, ComputedValue as ComputedRegisteredValue,
19        SpecifiedValue as SpecifiedRegisteredValue,
20    },
21};
22use crate::stylesheets::UrlExtraData;
23use crate::stylesheets::container_rule::AttrReferenceSet;
24use crate::stylist::Stylist;
25use crate::typed_om::{
26    ToTyped, TypedValue, UnparsedSegment, UnparsedValue, VariableReferenceValue,
27};
28use crate::values::computed;
29use crate::values::generics::calc::SortKey as AttrUnit;
30use crate::values::specified::{NoCalcLength, ParsedNamespace};
31use crate::{Atom, LocalName, Namespace, Prefix, derives::*};
32use cssparser::{CowRcStr, Delimiter, Parser, SourcePosition, Token, TokenSerializationType};
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    property_id: Option<PropertyIdRef>,
306) -> Option<ComputedRegisteredValue> {
307    if registration.is_universal() {
308        return Some(ComputedRegisteredValue::universal(Arc::clone(value)));
309    }
310    compute_value(
311        &value.css,
312        &value.url_data,
313        registration,
314        computed_context,
315        AttrTaint::default(),
316        property_id,
317    )
318    .ok()
319}
320
321// For all purposes, we want values to be considered equal if their css text is equal.
322impl PartialEq for VariableValue {
323    fn eq(&self, other: &Self) -> bool {
324        self.css == other.css
325    }
326}
327
328impl Eq for VariableValue {}
329
330impl ToCss for SpecifiedValue {
331    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
332    where
333        W: Write,
334    {
335        dest.write_str(&self.css)
336    }
337}
338
339impl ToTyped for SpecifiedValue {
340    fn to_typed(&self, dest: &mut ThinVec<TypedValue>) -> Result<(), ()> {
341        let unparsed_value = reify_variable_value(self)?;
342        dest.push(TypedValue::Unparsed(unparsed_value));
343        Ok(())
344    }
345}
346
347fn reify_variable_value(value: &VariableValue) -> Result<UnparsedValue, ()> {
348    let mut reference_index = 0;
349    reify_variable_value_range(
350        &value.css,
351        &value.references.refs,
352        &mut reference_index,
353        0,
354        value.css.len(),
355    )
356}
357
358/// Reify a slice of the CSS string into UnparsedSegment entries.
359///
360/// References are stored in source order, with outer substitution functions
361/// inserted before references in their fallback. The shared `reference_index`
362/// relies on this ordering to recurse into fallbacks without reprocessing
363/// nested referecences.
364fn reify_variable_value_range(
365    css: &str,
366    references: &[SubstitutionFunctionReference],
367    reference_index: &mut usize,
368    start: usize,
369    end: usize,
370) -> Result<UnparsedValue, ()> {
371    debug_assert!(start <= end);
372    debug_assert!(end <= css.len());
373
374    let mut values = ThinVec::new();
375    let mut cur_pos = start;
376
377    while *reference_index < references.len() {
378        let reference = &references[*reference_index];
379
380        if reference.start >= end {
381            break;
382        }
383
384        debug_assert!(reference.start >= cur_pos);
385        debug_assert!(reference.start <= reference.end);
386        debug_assert!(reference.end <= css.len());
387
388        if cur_pos < reference.start {
389            values.push(UnparsedSegment::String(CssString::from(
390                &css[cur_pos..reference.start],
391            )));
392        }
393
394        *reference_index += 1;
395
396        if reference.substitution_kind != SubstitutionFunctionKind::Var {
397            return Err(());
398        }
399
400        let (fallback, has_fallback) = if let Some(fallback) = &reference.fallback {
401            debug_assert!(fallback.start.get() < reference.end);
402
403            (
404                reify_variable_value_range(
405                    css,
406                    references,
407                    reference_index,
408                    fallback.start.get(),
409                    reference.end - 1, // Skip the closing ')'.
410                )?,
411                true,
412            )
413        } else {
414            (ThinVec::new(), false)
415        };
416
417        values.push(UnparsedSegment::VariableReference(VariableReferenceValue {
418            variable: CssString::from(format!("--{}", reference.name)),
419            fallback,
420            has_fallback,
421        }));
422
423        cur_pos = reference.end;
424    }
425
426    if cur_pos < end {
427        values.push(UnparsedSegment::String(CssString::from(&css[cur_pos..end])));
428    }
429
430    Ok(values)
431}
432
433/// A pair of separate CustomPropertiesMaps, split between custom properties
434/// that have the inherit flag set and those with the flag unset.
435#[repr(C)]
436#[derive(Clone, Debug, Default, PartialEq)]
437pub struct ComputedCustomProperties {
438    /// Map for custom properties with inherit flag set, including non-registered
439    /// ones.
440    pub inherited: CustomPropertiesMap,
441    /// Map for custom properties with inherit flag unset.
442    pub non_inherited: CustomPropertiesMap,
443}
444
445impl ComputedCustomProperties {
446    /// Return whether the inherited and non_inherited maps are none.
447    pub fn is_empty(&self) -> bool {
448        self.inherited.is_empty() && self.non_inherited.is_empty()
449    }
450
451    /// Return the name and value of the property at specified index, if any.
452    pub fn property_at(&self, index: usize) -> Option<(&Name, &Option<ComputedRegisteredValue>)> {
453        // Just expose the custom property items from custom_properties.inherited, followed
454        // by custom property items from custom_properties.non_inherited.
455        self.inherited
456            .get_index(index)
457            .or_else(|| self.non_inherited.get_index(index - self.inherited.len()))
458    }
459
460    /// Insert a custom property in the corresponding inherited/non_inherited
461    /// map, depending on whether the inherit flag is set or unset.
462    pub fn insert(
463        &mut self,
464        registration: &PropertyDescriptors,
465        name: &Name,
466        value: ComputedRegisteredValue,
467    ) {
468        self.map_mut(registration).insert(name, value)
469    }
470
471    /// Remove a custom property from the corresponding inherited/non_inherited
472    /// map, depending on whether the inherit flag is set or unset.
473    pub fn remove(&mut self, registration: &PropertyDescriptors, name: &Name) {
474        self.map_mut(registration).remove(name);
475    }
476
477    /// Shrink the capacity of the inherited maps as much as possible.
478    pub fn shrink_to_fit(&mut self) {
479        self.inherited.shrink_to_fit();
480        self.non_inherited.shrink_to_fit();
481    }
482
483    fn map_mut(&mut self, registration: &PropertyDescriptors) -> &mut CustomPropertiesMap {
484        if registration.inherits() {
485            &mut self.inherited
486        } else {
487            &mut self.non_inherited
488        }
489    }
490
491    /// Returns the relevant custom property value given a registration.
492    pub fn get(
493        &self,
494        registration: &PropertyDescriptors,
495        name: &Name,
496    ) -> Option<&ComputedRegisteredValue> {
497        if registration.inherits() {
498            self.inherited.get(name)
499        } else {
500            self.non_inherited.get(name)
501        }
502    }
503
504    /// Returns a property just by the name. Slightly less efficient than get(), if you already have
505    /// a custom registration handy, thus the longer name.
506    pub fn get_for_cssom(&self, name: &Name) -> Option<&ComputedRegisteredValue> {
507        self.inherited
508            .get(name)
509            .or_else(|| self.non_inherited.get(name))
510    }
511}
512
513/// Both specified and computed values are VariableValues, the difference is
514/// whether var() functions are expanded.
515pub type SpecifiedValue = VariableValue;
516/// Both specified and computed values are VariableValues, the difference is
517/// whether var() functions are expanded.
518pub type ComputedValue = VariableValue;
519
520/// Set of flags to references this custom property makes.
521#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, MallocSizeOf, ToShmem)]
522pub struct ReferenceFlags(u8);
523
524bitflags! {
525    impl ReferenceFlags : u8 {
526        /// At least one custom property depends on font-relative units.
527        const FONT_UNITS = 1 << 0;
528        /// At least one custom property depends on root element's font-relative units.
529        const ROOT_FONT_UNITS = 1 << 1;
530        /// At least one custom property depends on line height units.
531        const LH_UNITS = 1 << 2;
532        /// At least one custom property depends on root element's line height units.
533        const ROOT_LH_UNITS = 1 << 3;
534        /// The value depends on the used color-scheme (e.g. a registered `<color>` property).
535        const COLOR_SCHEME = 1 << 4;
536        /// All dependencies not depending on the root element.
537        const NON_ROOT_DEPENDENCIES = Self::FONT_UNITS.0 | Self::LH_UNITS.0;
538        /// All dependencies depending on the root element.
539        const ROOT_DEPENDENCIES = Self::ROOT_FONT_UNITS.0 | Self::ROOT_LH_UNITS.0;
540        /// All non-custom dependencies
541        const NON_CUSTOM = Self::NON_ROOT_DEPENDENCIES.0 | Self::ROOT_DEPENDENCIES.0;
542        /// At least one attr() reference.
543        const ATTR = 1 << 5;
544        /// At least one env() reference.
545        const ENV = 1 << 6;
546        /// At least one var() reference.
547        const VAR = 1 << 7;
548    }
549}
550
551impl ReferenceFlags {
552    /// Iterate for each non custom
553    pub fn for_each_non_custom<F>(mut self, is_root_element: bool, mut f: F)
554    where
555        F: FnMut(SingleNonCustomReference),
556    {
557        // On the root element, rem etc refer to the root's own dependencies.
558        if is_root_element {
559            if self.intersects(Self::ROOT_FONT_UNITS) {
560                self.remove(Self::ROOT_FONT_UNITS);
561                self |= Self::FONT_UNITS;
562            }
563            if self.intersects(Self::ROOT_LH_UNITS) {
564                self.remove(Self::ROOT_FONT_UNITS);
565                self |= Self::LH_UNITS;
566            }
567        }
568
569        for (_, r) in self.iter_names() {
570            let single = match r {
571                Self::FONT_UNITS => SingleNonCustomReference::FontUnits,
572                Self::LH_UNITS => SingleNonCustomReference::LhUnits,
573                Self::COLOR_SCHEME => SingleNonCustomReference::ColorScheme,
574                Self::ROOT_FONT_UNITS
575                | Self::ROOT_LH_UNITS
576                | Self::VAR
577                | Self::ENV
578                | Self::ATTR => continue,
579                _ => unreachable!("Unexpected single bit value"),
580            };
581            f(single);
582        }
583    }
584
585    fn from_unit(value: &CowRcStr) -> Self {
586        // For registered properties, any reference to font-relative dimensions
587        // make it dependent on font-related properties.
588        // TODO(dshin): When we unit algebra gets implemented and handled -
589        // Is it valid to say that `calc(1em / 2em * 3px)` triggers this?
590        if value.eq_ignore_ascii_case(NoCalcLength::LH) {
591            return Self::FONT_UNITS | Self::LH_UNITS;
592        }
593        if value.eq_ignore_ascii_case(NoCalcLength::EM)
594            || value.eq_ignore_ascii_case(NoCalcLength::EX)
595            || value.eq_ignore_ascii_case(NoCalcLength::CAP)
596            || value.eq_ignore_ascii_case(NoCalcLength::CH)
597            || value.eq_ignore_ascii_case(NoCalcLength::IC)
598        {
599            return Self::FONT_UNITS;
600        }
601        if value.eq_ignore_ascii_case(NoCalcLength::RLH) {
602            return Self::ROOT_FONT_UNITS | Self::ROOT_LH_UNITS;
603        }
604        if value.eq_ignore_ascii_case(NoCalcLength::REM)
605            || value.eq_ignore_ascii_case(NoCalcLength::REX)
606            || value.eq_ignore_ascii_case(NoCalcLength::RCH)
607            || value.eq_ignore_ascii_case(NoCalcLength::RCAP)
608            || value.eq_ignore_ascii_case(NoCalcLength::RIC)
609        {
610            return Self::ROOT_FONT_UNITS;
611        }
612        Self::empty()
613    }
614}
615
616/// A non-custom reference that participates in cycle resolution.
617/// TODO(emilio): This should probably eventually become just PrioritaryPropertyId.
618#[derive(Clone, Copy, Debug, Eq, PartialEq)]
619#[allow(missing_docs)]
620pub enum SingleNonCustomReference {
621    FontUnits = 0,
622    LhUnits,
623    ColorScheme,
624}
625
626impl SingleNonCustomReference {
627    /// Returns a prioritary id for this reference.
628    pub fn to_prioritary_id(self) -> PrioritaryPropertyId {
629        match self {
630            Self::FontUnits => PrioritaryPropertyId::FontSize,
631            Self::LhUnits => PrioritaryPropertyId::LineHeight,
632            Self::ColorScheme => PrioritaryPropertyId::ColorScheme,
633        }
634    }
635}
636
637/// A map from NonCustomReferenceMap to a T.
638pub struct NonCustomReferenceMap<T>([Option<T>; 3]);
639
640impl<T> Default for NonCustomReferenceMap<T> {
641    fn default() -> Self {
642        NonCustomReferenceMap(Default::default())
643    }
644}
645
646impl<T> Index<SingleNonCustomReference> for NonCustomReferenceMap<T> {
647    type Output = Option<T>;
648
649    fn index(&self, reference: SingleNonCustomReference) -> &Self::Output {
650        &self.0[reference as usize]
651    }
652}
653
654impl<T> IndexMut<SingleNonCustomReference> for NonCustomReferenceMap<T> {
655    fn index_mut(&mut self, reference: SingleNonCustomReference) -> &mut Self::Output {
656        &mut self.0[reference as usize]
657    }
658}
659
660/// Substitution function source: var, env, attr.
661#[derive(Copy, Clone, Debug, MallocSizeOf, Hash, Eq, PartialEq, ToShmem, Parse)]
662pub enum SubstitutionFunctionKind {
663    /// CSS variable / custom property
664    Var,
665    /// Environment variable
666    Env,
667    /// DOM attribute
668    Attr,
669}
670
671/// A wrapper map that encapsulates both the custom properties and attributes
672/// for a given element.
673#[repr(C)]
674#[derive(Clone, Debug, Default, PartialEq)]
675pub struct ComputedSubstitutionFunctions {
676    /// The applicable custom properties (includes inherited and non-inherited).
677    pub custom_properties: ComputedCustomProperties,
678    /// The applicable DOM attributes.
679    pub attributes: OwnMap,
680}
681
682impl ComputedSubstitutionFunctions {
683    /// Creates a substitution function map from optional custom properties
684    /// and DOM attributes.
685    #[inline(always)]
686    pub fn new(
687        custom_properties: Option<ComputedCustomProperties>,
688        attributes: Option<OwnMap>,
689    ) -> Self {
690        Self {
691            custom_properties: custom_properties.unwrap_or_default(),
692            attributes: attributes.unwrap_or_default(),
693        }
694    }
695
696    #[inline(always)]
697    pub(crate) fn insert_var(
698        &mut self,
699        registration: &PropertyDescriptors,
700        name: &Name,
701        value: ComputedRegisteredValue,
702    ) {
703        self.custom_properties.insert(registration, name, value);
704    }
705
706    #[inline(always)]
707    pub(crate) fn insert_attr(&mut self, name: &Name, value: ComputedRegisteredValue) {
708        self.attributes.insert(name.clone(), Some(value));
709    }
710
711    #[inline(always)]
712    pub(crate) fn remove_var(&mut self, registration: &PropertyDescriptors, name: &Name) {
713        self.custom_properties.remove(registration, name);
714    }
715
716    #[inline(always)]
717    pub(crate) fn remove_attr(&mut self, name: &Name) {
718        self.attributes.insert(name.clone(), None);
719    }
720
721    #[inline(always)]
722    pub(crate) fn get_var(
723        &self,
724        registration: &PropertyDescriptors,
725        name: &Name,
726    ) -> Option<&ComputedRegisteredValue> {
727        self.custom_properties.get(registration, name)
728    }
729
730    #[inline(always)]
731    pub(crate) fn get_attr(&self, name: &Name) -> Option<&ComputedRegisteredValue> {
732        self.attributes.get(name).and_then(|p| p.as_ref())
733    }
734}
735
736#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem, Parse)]
737enum AttributeType {
738    Invalid,
739    None,
740    RawString,
741    Type(SyntaxDescriptor),
742    Unit(AttrUnit),
743}
744
745/// Data specific to an attr() call like type and namespace.
746#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
747pub struct AttributeData {
748    kind: AttributeType,
749    namespace: ParsedNamespace,
750}
751
752/// For a CSS string, the range, counted in bytes, that is attr()-tainted.
753#[derive(Clone, Debug, Default, MallocSizeOf, PartialEq, ToShmem, ToComputedValue)]
754pub struct AttrTaintedRange {
755    /// Start of the range, counted in bytes. Inclusive.
756    start: usize,
757    /// End of the range, counted in bytes. Exclusive.
758    end: usize,
759}
760
761impl AttrTaintedRange {
762    /// Creates a range within a CSS string that is tainted by attr().
763    #[inline(always)]
764    pub fn new(start: usize, end: usize) -> Self {
765        debug_assert!(start <= end);
766        Self { start, end }
767    }
768}
769
770/// In CSS Values and Units, values produced by `attr()` are considered attr()-tainted, as are
771/// functions that contain an attr()-tainted value. Using an attr()-tainted value as or in a <url>
772/// makes a declaration invalid at computed-value time.
773/// https://drafts.csswg.org/css-values-5/#attr-security
774#[derive(Clone, Debug, Default, MallocSizeOf, PartialEq, ToShmem)]
775pub struct AttrTaint(SmallVec<[AttrTaintedRange; 1]>);
776
777impl AttrTaint {
778    /// For a CSS string, determine whether any `<url>` overlapping this `range`
779    /// is disallowed due to attr()-tainting.
780    #[inline(always)]
781    pub fn should_disallow_urls_in_range(&self, range: &AttrTaintedRange) -> bool {
782        self.0
783            .iter()
784            .any(|r| r.start <= range.end && r.end >= range.start)
785    }
786
787    /// Returns true if the attr()-tainted range contains no elements.
788    #[inline(always)]
789    pub fn is_empty(&self) -> bool {
790        self.0.is_empty()
791    }
792
793    #[inline(always)]
794    fn new_fully_tainted(end: usize) -> Self {
795        let mut taint = Self::default();
796        taint.push(0, end);
797        taint
798    }
799
800    #[inline(always)]
801    fn push(&mut self, start: usize, end: usize) {
802        self.0.push(AttrTaintedRange::new(start, end));
803    }
804}
805
806/// The fallback of a particular value.
807#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
808pub struct VariableFallback {
809    // NOTE(emilio): We don't track fallback end, because we rely on the missing closing
810    // parenthesis, if any, to be inserted, which means that we can rely on our end being
811    // reference.end - 1.
812    start: num::NonZeroUsize,
813    first_token_type: TokenSerializationType,
814    last_token_type: TokenSerializationType,
815    /// References from this fallback value.
816    pub references: References,
817}
818
819/// A reference to a substitution function like env() / var() / attr().
820#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
821pub struct SubstitutionFunctionReference {
822    /// The identifier for this substitution function (the first argument).
823    pub name: Name,
824    start: usize,
825    end: usize,
826    /// The fallback for this function, if any.
827    pub fallback: Option<VariableFallback>,
828    /// attr() specific data, only relevant if substitution_kind is `Attr`..
829    pub attribute_data: AttributeData,
830    prev_token_type: TokenSerializationType,
831    next_token_type: TokenSerializationType,
832    /// The kind of substitution function we are.
833    pub substitution_kind: SubstitutionFunctionKind,
834}
835
836impl SubstitutionFunctionReference {
837    /// Whether we're an attr(... type(...)) reference.
838    pub fn is_attr_with_type(&self) -> bool {
839        self.substitution_kind == SubstitutionFunctionKind::Attr
840            && matches!(self.attribute_data.kind, AttributeType::Type(..))
841    }
842}
843
844/// A struct holding information about the external references to that a custom property value may
845/// have.
846#[derive(Clone, Debug, Default, MallocSizeOf, PartialEq, ToShmem)]
847pub struct References {
848    /// The actual list of references that we end up with.
849    pub refs: Vec<SubstitutionFunctionReference>,
850    /// Various flags about the kind of references we hold. Note that these include data about our
851    /// nested references.
852    ///
853    /// TODO(emilio): Do we need to distinguish between own and nested flags?
854    pub flags: ReferenceFlags,
855}
856
857impl References {
858    fn has_references(&self) -> bool {
859        !self.refs.is_empty()
860    }
861
862    pub(crate) fn non_custom_references(&self, is_root_element: bool) -> ReferenceFlags {
863        let mut mask = ReferenceFlags::NON_ROOT_DEPENDENCIES;
864        if is_root_element {
865            mask |= ReferenceFlags::ROOT_DEPENDENCIES
866        }
867        self.flags & mask
868    }
869}
870
871impl VariableValue {
872    fn empty(url_data: &UrlExtraData) -> Self {
873        Self {
874            css: String::new(),
875            last_token_type: Default::default(),
876            first_token_type: Default::default(),
877            url_data: url_data.clone(),
878            references: Default::default(),
879            explicitly_attr_tainted: false,
880        }
881    }
882
883    /// Create a new custom property without parsing if the CSS is known to be valid and contain no
884    /// references.
885    pub fn new(
886        css: String,
887        url_data: &UrlExtraData,
888        first_token_type: TokenSerializationType,
889        last_token_type: TokenSerializationType,
890    ) -> Self {
891        Self {
892            css,
893            url_data: url_data.clone(),
894            first_token_type,
895            last_token_type,
896            references: References::default(),
897            explicitly_attr_tainted: false,
898        }
899    }
900
901    fn push(
902        &mut self,
903        css: &str,
904        css_first_token_type: TokenSerializationType,
905        css_last_token_type: TokenSerializationType,
906        attr_taint: Option<&mut AttrTaint>,
907    ) -> Result<(), ()> {
908        /// Prevent values from getting terribly big since you can use custom
909        /// properties exponentially.
910        ///
911        /// This number (2MB) is somewhat arbitrary, but silly enough that no
912        /// reasonable page should hit it. We could limit by number of total
913        /// substitutions, but that was very easy to work around in practice
914        /// (just choose a larger initial value and boom).
915        const MAX_VALUE_LENGTH_IN_BYTES: usize = 2 * 1024 * 1024;
916
917        if self.css.len() + css.len() > MAX_VALUE_LENGTH_IN_BYTES {
918            return Err(());
919        }
920
921        // This happens e.g. between two subsequent var() functions:
922        // `var(--a)var(--b)`.
923        //
924        // In that case, css_*_token_type is nonsensical.
925        if css.is_empty() {
926            return Ok(());
927        }
928
929        self.first_token_type.set_if_nothing(css_first_token_type);
930        // If self.first_token_type was nothing,
931        // self.last_token_type is also nothing and this will be false:
932        if self
933            .last_token_type
934            .needs_separator_when_before(css_first_token_type)
935        {
936            self.css.push_str("/**/")
937        }
938        let start = self.css.len();
939        self.css.push_str(css);
940        let end = self.css.len();
941        if let Some(taint) = attr_taint {
942            taint.push(start, end);
943        }
944        self.last_token_type = css_last_token_type;
945        Ok(())
946    }
947
948    /// Parse a custom property value.
949    pub fn parse(
950        input: &mut Parser,
951        namespaces: Option<&FxHashMap<Prefix, Namespace>>,
952        url_data: &UrlExtraData,
953    ) -> Result<Self, ParseError> {
954        let mut references = References::default();
955        let mut missing_closing_characters = String::new();
956        let start_position = input.position();
957        let (first_token_type, last_token_type) = parse_declaration_value(
958            input,
959            start_position,
960            namespaces,
961            &mut references,
962            &mut missing_closing_characters,
963        )?;
964        let mut css = input
965            .slice_from(start_position)
966            .trim_ascii_start()
967            .to_owned();
968        if !missing_closing_characters.is_empty() {
969            // Unescaped backslash at EOF in a quoted string is ignored.
970            if css.ends_with("\\")
971                && matches!(missing_closing_characters.as_bytes()[0], b'"' | b'\'')
972            {
973                css.pop();
974            }
975            css.push_str(&missing_closing_characters);
976        }
977
978        css.truncate(css.trim_ascii_end().len());
979        css.shrink_to_fit();
980        references.refs.shrink_to_fit();
981
982        Ok(Self {
983            css,
984            url_data: url_data.clone(),
985            first_token_type,
986            last_token_type,
987            references,
988            explicitly_attr_tainted: false,
989        })
990    }
991
992    /// Returns whether this value is tainted by `attr()`.
993    pub fn is_attr_tainted(&self) -> bool {
994        self.references.flags.intersects(ReferenceFlags::ATTR) | self.explicitly_attr_tainted
995    }
996
997    /// Create VariableValue from an int.
998    fn integer(number: i32, url_data: &UrlExtraData) -> Self {
999        Self::from_token(
1000            Token::Number {
1001                has_sign: false,
1002                value: number as f32,
1003                int_value: Some(number),
1004            },
1005            url_data,
1006        )
1007    }
1008
1009    /// Create VariableValue from an int.
1010    fn ident(ident: &'static str, url_data: &UrlExtraData) -> Self {
1011        Self::from_token(Token::Ident(ident.into()), url_data)
1012    }
1013
1014    /// Create VariableValue from a float amount of CSS pixels.
1015    fn pixels(number: f32, url_data: &UrlExtraData) -> Self {
1016        // FIXME (https://github.com/servo/rust-cssparser/issues/266):
1017        // No way to get TokenSerializationType::Dimension without creating
1018        // Token object.
1019        Self::from_token(
1020            Token::Dimension {
1021                has_sign: false,
1022                value: number,
1023                int_value: None,
1024                unit: CowRcStr::from("px"),
1025            },
1026            url_data,
1027        )
1028    }
1029
1030    /// Create VariableValue from an integer amount of milliseconds.
1031    fn int_ms(number: i32, url_data: &UrlExtraData) -> Self {
1032        Self::from_token(
1033            Token::Dimension {
1034                has_sign: false,
1035                value: number as f32,
1036                int_value: Some(number),
1037                unit: CowRcStr::from("ms"),
1038            },
1039            url_data,
1040        )
1041    }
1042
1043    /// Create VariableValue from an integer amount of CSS pixels.
1044    fn int_pixels(number: i32, url_data: &UrlExtraData) -> Self {
1045        Self::from_token(
1046            Token::Dimension {
1047                has_sign: false,
1048                value: number as f32,
1049                int_value: Some(number),
1050                unit: CowRcStr::from("px"),
1051            },
1052            url_data,
1053        )
1054    }
1055
1056    fn from_token(token: Token, url_data: &UrlExtraData) -> Self {
1057        let token_type = token.serialization_type();
1058        let mut css = token.to_css_string();
1059        css.shrink_to_fit();
1060
1061        VariableValue {
1062            css,
1063            url_data: url_data.clone(),
1064            first_token_type: token_type,
1065            last_token_type: token_type,
1066            references: Default::default(),
1067            explicitly_attr_tainted: false,
1068        }
1069    }
1070
1071    /// Returns the raw CSS text from this VariableValue
1072    pub fn css_text(&self) -> &str {
1073        &self.css
1074    }
1075
1076    /// Returns whether this variable value has any reference to the environment or other
1077    /// variables.
1078    pub fn has_references(&self) -> bool {
1079        self.references.has_references()
1080    }
1081
1082    /// Returns the attribute references in this variable value, if any.
1083    pub fn collect_attribute_references(&self, references: &mut AttrReferenceSet) {
1084        self.references.refs.iter().for_each(|r| {
1085            if r.substitution_kind == SubstitutionFunctionKind::Attr {
1086                // For a non-ascii-lowercase attribute, whether we match
1087                // case-sensitively depends on whether the element we're
1088                // matching against is an HTML element in an HTML document.
1089                // So to simplify invalidation we collect both potential
1090                // references here.
1091                references.insert(r.name.clone().into());
1092                let lowercase = r.name.to_ascii_lowercase();
1093                if *lowercase != *r.name {
1094                    references.insert(Atom::from(lowercase).into());
1095                }
1096            }
1097        })
1098    }
1099}
1100
1101/// <https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value>
1102fn parse_declaration_value(
1103    input: &mut Parser,
1104    input_start: SourcePosition,
1105    namespaces: Option<&FxHashMap<Prefix, Namespace>>,
1106    references: &mut References,
1107    missing_closing_characters: &mut String,
1108) -> Result<(TokenSerializationType, TokenSerializationType), ParseError> {
1109    input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| {
1110        parse_declaration_value_block(
1111            input,
1112            input_start,
1113            namespaces,
1114            references,
1115            missing_closing_characters,
1116        )
1117    })
1118}
1119
1120/// Like parse_declaration_value, but accept `!` and `;` since they are only invalid at the top level.
1121fn parse_declaration_value_block(
1122    input: &mut Parser,
1123    input_start: SourcePosition,
1124    namespaces: Option<&FxHashMap<Prefix, Namespace>>,
1125    references: &mut References,
1126    missing_closing_characters: &mut String,
1127) -> Result<(TokenSerializationType, TokenSerializationType), ParseError> {
1128    let mut is_first = true;
1129    let mut first_token_type = TokenSerializationType::Nothing;
1130    let mut last_token_type = TokenSerializationType::Nothing;
1131    let mut prev_reference_index: Option<usize> = None;
1132    loop {
1133        let token_start = input.position();
1134        let Ok(token) = input.next_including_whitespace_and_comments() else {
1135            break;
1136        };
1137
1138        let prev_token_type = last_token_type;
1139        let serialization_type = token.serialization_type();
1140        last_token_type = serialization_type;
1141        if is_first {
1142            first_token_type = last_token_type;
1143            is_first = false;
1144        }
1145
1146        macro_rules! nested {
1147            ($closing:expr) => {{
1148                let mut inner_end_position = None;
1149                let result = input.parse_nested_block(|input| {
1150                    let result = parse_declaration_value_block(
1151                        input,
1152                        input_start,
1153                        namespaces,
1154                        references,
1155                        missing_closing_characters,
1156                    )?;
1157                    inner_end_position = Some(input.position());
1158                    Ok(result)
1159                })?;
1160                if inner_end_position.unwrap() == input.position() {
1161                    missing_closing_characters.push_str($closing);
1162                }
1163                result
1164            }};
1165        }
1166        if let Some(index) = prev_reference_index.take() {
1167            references.refs[index].next_token_type = serialization_type;
1168        }
1169        match *token {
1170            Token::Comment(_) => {
1171                let token_slice = input.slice_from(token_start);
1172                if !token_slice.ends_with("*/") {
1173                    missing_closing_characters.push_str(if token_slice.ends_with('*') {
1174                        "/"
1175                    } else {
1176                        "*/"
1177                    })
1178                }
1179            },
1180            Token::BadUrl(..) => {
1181                let e = StyleParseErrorKind::BadUrlInDeclarationValueBlock;
1182                return Err(ParseError::custom(e));
1183            },
1184            Token::BadString(..) => {
1185                let e = StyleParseErrorKind::BadStringInDeclarationValueBlock;
1186                return Err(ParseError::custom(e));
1187            },
1188            Token::CloseParenthesis => {
1189                let e = StyleParseErrorKind::UnbalancedCloseParenthesisInDeclarationValueBlock;
1190                return Err(ParseError::custom(e));
1191            },
1192            Token::CloseSquareBracket => {
1193                let e = StyleParseErrorKind::UnbalancedCloseSquareBracketInDeclarationValueBlock;
1194                return Err(ParseError::custom(e));
1195            },
1196            Token::CloseCurlyBracket => {
1197                let e = StyleParseErrorKind::UnbalancedCloseCurlyBracketInDeclarationValueBlock;
1198                return Err(ParseError::custom(e));
1199            },
1200            Token::Function(ref name) => {
1201                let substitution_kind = SubstitutionFunctionKind::from_ident(name).ok();
1202                if let Some(substitution_kind) = substitution_kind {
1203                    let our_ref_index = references.refs.len();
1204                    let mut input_end_position = None;
1205                    let fallback = input.parse_nested_block(|input| {
1206                        let mut namespace = ParsedNamespace::Known(Namespace::default());
1207                        if substitution_kind == SubstitutionFunctionKind::Attr
1208                            && let Some(namespaces) = namespaces
1209                            && let Ok(ns) =
1210                                input.try_parse(|input| ParsedNamespace::parse(namespaces, input))
1211                        {
1212                            namespace = ns;
1213                            let prev = input.state();
1214                            let next = match *input.next_including_whitespace()? {
1215                                Token::Ident(_) => Ok(()),
1216                                _ => Err(ParseError::unexpected_token()),
1217                            };
1218                            input.reset(&prev);
1219                            next?;
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            && let Ok(initial_value) = compute_value(
1439                &initial_value.css,
1440                &initial_value.url_data,
1441                registration,
1442                context,
1443                AttrTaint::default(),
1444                Some(PropertyIdRef::from(name)),
1445            )
1446        {
1447            context
1448                .builder
1449                .substitution_functions
1450                .insert_var(registration, name, initial_value);
1451            return;
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        Some(PropertyIdRef::from(name)),
1486        &context.builder.substitution_functions,
1487        stylist,
1488        context,
1489        attribute_tracker,
1490        &mut SmallVec::new(),
1491        None,
1492    );
1493
1494    let Ok(substitution) = substitution else {
1495        if is_var {
1496            handle_invalid_at_computed_value_time(name, registration, context);
1497        } else {
1498            context.builder.substitution_functions.remove_attr(name);
1499        }
1500        return;
1501    };
1502
1503    // If variable fallback results in a wide keyword, deal with it now.
1504    let inherited = context.builder.inherited_style.custom_properties();
1505    if is_var {
1506        let css = &substitution.css;
1507        let css_wide_kw = {
1508            let mut input = Parser::new(css);
1509            input.try_parse(CSSWideKeyword::parse)
1510        };
1511
1512        if let Ok(kw) = css_wide_kw {
1513            // TODO: It's unclear what this should do for revert / revert-layer, see
1514            // https://github.com/w3c/csswg-drafts/issues/9131. For now treating as unset
1515            // seems fine?
1516            match (kw, registration.inherits(), context.is_root_element()) {
1517                (CSSWideKeyword::Initial, _, _)
1518                | (CSSWideKeyword::Revert, false, _)
1519                | (CSSWideKeyword::RevertLayer, false, _)
1520                | (CSSWideKeyword::RevertRule, false, _)
1521                | (CSSWideKeyword::Unset, false, _)
1522                | (CSSWideKeyword::Revert, true, true)
1523                | (CSSWideKeyword::RevertLayer, true, true)
1524                | (CSSWideKeyword::RevertRule, true, true)
1525                | (CSSWideKeyword::Unset, true, true)
1526                | (CSSWideKeyword::Inherit, _, true) => {
1527                    remove_and_insert_initial_value(
1528                        name,
1529                        registration,
1530                        &mut context.builder.substitution_functions,
1531                    );
1532                },
1533                (CSSWideKeyword::Revert, true, false)
1534                | (CSSWideKeyword::RevertLayer, true, false)
1535                | (CSSWideKeyword::RevertRule, true, false)
1536                | (CSSWideKeyword::Inherit, _, false)
1537                | (CSSWideKeyword::Unset, true, false) => {
1538                    match inherited.get(registration, name) {
1539                        Some(value) => {
1540                            context.builder.substitution_functions.insert_var(
1541                                registration,
1542                                name,
1543                                value.clone(),
1544                            );
1545                        },
1546                        None => {
1547                            context
1548                                .builder
1549                                .substitution_functions
1550                                .remove_var(registration, name);
1551                        },
1552                    };
1553                },
1554            }
1555            return;
1556        }
1557    }
1558
1559    match kind {
1560        SubstitutionFunctionKind::Var => {
1561            let value = match substitution.into_value(
1562                url_data,
1563                registration,
1564                context,
1565                Some(PropertyIdRef::from(name)),
1566            ) {
1567                Ok(v) => v,
1568                Err(()) => {
1569                    handle_invalid_at_computed_value_time(name, registration, context);
1570                    return;
1571                },
1572            };
1573            context
1574                .builder
1575                .substitution_functions
1576                .insert_var(registration, name, value);
1577        },
1578        SubstitutionFunctionKind::Attr => {
1579            let mut value = ComputedRegisteredValue::universal(Arc::new(VariableValue::new(
1580                substitution.css.into_owned(),
1581                url_data,
1582                substitution.first_token_type,
1583                substitution.last_token_type,
1584            )));
1585            value.attr_tainted |= substitution.attr_tainted;
1586            context
1587                .builder
1588                .substitution_functions
1589                .insert_attr(name, value);
1590        },
1591        SubstitutionFunctionKind::Env => unreachable!("Kind cannot be env."),
1592    }
1593}
1594
1595#[derive(Default, Debug)]
1596struct Substitution<'a> {
1597    css: Cow<'a, str>,
1598    first_token_type: TokenSerializationType,
1599    last_token_type: TokenSerializationType,
1600    attr_tainted: bool,
1601}
1602
1603impl<'a> Substitution<'a> {
1604    fn from_value(v: VariableValue, attr_tainted: bool) -> Self {
1605        Substitution {
1606            css: v.css.into(),
1607            first_token_type: v.first_token_type,
1608            last_token_type: v.last_token_type,
1609            attr_tainted,
1610        }
1611    }
1612
1613    fn into_value(
1614        self,
1615        url_data: &UrlExtraData,
1616        registration: &PropertyDescriptors,
1617        computed_context: &computed::Context,
1618        property_id: Option<PropertyIdRef>,
1619    ) -> Result<ComputedRegisteredValue, ()> {
1620        if registration.is_universal() {
1621            let mut value = ComputedRegisteredValue::universal(Arc::new(VariableValue::new(
1622                self.css.into_owned(),
1623                url_data,
1624                self.first_token_type,
1625                self.last_token_type,
1626            )));
1627            value.attr_tainted |= self.attr_tainted;
1628            return Ok(value);
1629        }
1630        let taint = if self.attr_tainted {
1631            // Per spec: substitution value of an arbitrary substitution function is
1632            // attr()-tainted as a whole if any attr()-tainted values were involved
1633            // in creating that substitution value.
1634            // https://drafts.csswg.org/css-values-5/#attr-security
1635            AttrTaint::new_fully_tainted(self.css.len())
1636        } else {
1637            AttrTaint::default()
1638        };
1639        let mut v = compute_value(
1640            &self.css,
1641            url_data,
1642            registration,
1643            computed_context,
1644            taint,
1645            property_id,
1646        )?;
1647        v.attr_tainted |= self.attr_tainted;
1648        Ok(v)
1649    }
1650
1651    fn new(
1652        css: Cow<'a, str>,
1653        first_token_type: TokenSerializationType,
1654        last_token_type: TokenSerializationType,
1655        attr_tainted: bool,
1656    ) -> Self {
1657        Self {
1658            css,
1659            first_token_type,
1660            last_token_type,
1661            attr_tainted,
1662        }
1663    }
1664}
1665
1666/// Result of var(), env(), and attr() substitution.
1667#[derive(Debug)]
1668pub struct SubstitutionResult<'a> {
1669    /// The resolved CSS string after substitution.
1670    pub css: Cow<'a, str>,
1671    /// Regions in the `css` string that are attr()-tainted, if any.
1672    pub attr_taint: AttrTaint,
1673}
1674
1675fn compute_value(
1676    css: &str,
1677    url_data: &UrlExtraData,
1678    registration: &PropertyDescriptors,
1679    computed_context: &computed::Context,
1680    attr_taint: AttrTaint,
1681    property_id: Option<PropertyIdRef>,
1682) -> Result<ComputedRegisteredValue, ()> {
1683    debug_assert!(!registration.is_universal());
1684
1685    let mut input = Parser::new(css);
1686    SpecifiedRegisteredValue::compute(
1687        &mut input,
1688        registration,
1689        None,
1690        url_data,
1691        computed_context,
1692        AllowComputationallyDependent::Yes,
1693        attr_taint,
1694        property_id,
1695    )
1696}
1697
1698/// Removes the named registered custom property and inserts its uncomputed initial value.
1699pub(crate) fn remove_and_insert_initial_value(
1700    name: &Name,
1701    registration: &PropertyDescriptors,
1702    substitution_functions: &mut ComputedSubstitutionFunctions,
1703) {
1704    substitution_functions.remove_var(registration, name);
1705    if let Some(ref initial_value) = registration.initial_value {
1706        let value = ComputedRegisteredValue::universal(Arc::clone(initial_value));
1707        substitution_functions.insert_var(registration, name, value);
1708    }
1709}
1710
1711fn do_substitute_chunk<'a>(
1712    css: &'a str,
1713    start: usize,
1714    end: usize,
1715    first_token_type: TokenSerializationType,
1716    last_token_type: TokenSerializationType,
1717    url_data: &UrlExtraData,
1718    property_id: Option<PropertyIdRef>,
1719    substitution_functions: &'a ComputedSubstitutionFunctions,
1720    stylist: &Stylist,
1721    computed_context: &computed::Context,
1722    references: &'a [SubstitutionFunctionReference],
1723    attribute_tracker: &mut AttributeTracker,
1724    seen: &mut SmallVec<[&'a Name; 8]>,
1725    mut attr_taint: Option<&mut AttrTaint>,
1726) -> Result<Substitution<'a>, ()> {
1727    if start == end {
1728        // Empty string. Easy.
1729        return Ok(Substitution::default());
1730    }
1731    // Easy case: no references involved.
1732    if references.is_empty() {
1733        let result = &css[start..end];
1734        return Ok(Substitution::new(
1735            Cow::Borrowed(result),
1736            first_token_type,
1737            last_token_type,
1738            Default::default(),
1739        ));
1740    }
1741
1742    let mut substituted = ComputedValue::empty(url_data);
1743    let mut next_token_type = first_token_type;
1744    let mut cur_pos = start;
1745    let mut attr_tainted = false;
1746    let references = references.iter();
1747    for reference in references {
1748        if reference.start != cur_pos {
1749            substituted.push(
1750                &css[cur_pos..reference.start],
1751                next_token_type,
1752                reference.prev_token_type,
1753                /* attr_taint */ None,
1754            )?;
1755        }
1756
1757        let substitution = substitute_one_reference(
1758            css,
1759            url_data,
1760            property_id,
1761            substitution_functions,
1762            reference,
1763            stylist,
1764            computed_context,
1765            attribute_tracker,
1766            seen,
1767        )?;
1768
1769        // Optimize the property: var(--...) case to avoid allocating at all.
1770        if reference.start == start && reference.end == end {
1771            if let Some(taint) = attr_taint.filter(|_| substitution.attr_tainted) {
1772                taint.push(start, substitution.css.len());
1773            }
1774            return Ok(substitution);
1775        }
1776
1777        substituted.push(
1778            &substitution.css,
1779            substitution.first_token_type,
1780            substitution.last_token_type,
1781            attr_taint
1782                .as_deref_mut()
1783                .filter(|_| substitution.attr_tainted),
1784        )?;
1785        attr_tainted |= substitution.attr_tainted;
1786        next_token_type = reference.next_token_type;
1787        cur_pos = reference.end;
1788    }
1789    // Push the rest of the value if needed.
1790    if cur_pos != end {
1791        substituted.push(
1792            &css[cur_pos..end],
1793            next_token_type,
1794            last_token_type,
1795            /* attr_taint */ None,
1796        )?;
1797    }
1798    Ok(Substitution::from_value(substituted, attr_tainted))
1799}
1800
1801fn quoted_css_string(src: &str) -> String {
1802    let mut dest = String::with_capacity(src.len() + 2);
1803    cssparser::serialize_string(src, &mut dest).unwrap();
1804    dest
1805}
1806
1807fn substitute_one_reference<'a>(
1808    css: &'a str,
1809    url_data: &UrlExtraData,
1810    property_id: Option<PropertyIdRef>,
1811    substitution_functions: &'a ComputedSubstitutionFunctions,
1812    reference: &'a SubstitutionFunctionReference,
1813    stylist: &Stylist,
1814    computed_context: &computed::Context,
1815    attribute_tracker: &mut AttributeTracker,
1816    seen: &mut SmallVec<[&'a Name; 8]>,
1817) -> Result<Substitution<'a>, ()> {
1818    let simple_attr_subst = |s: &str| {
1819        Some(Substitution::new(
1820            Cow::Owned(quoted_css_string(s)),
1821            TokenSerializationType::Nothing,
1822            TokenSerializationType::Nothing,
1823            /* attr_tainted */ true,
1824        ))
1825    };
1826    let substitution: Option<_> = match reference.substitution_kind {
1827        SubstitutionFunctionKind::Var => {
1828            let registration = stylist.get_custom_property_registration(&reference.name);
1829            match substitution_functions.get_var(registration, &reference.name) {
1830                None => None,
1831                // If the referenced value is itself still unresolved (i.e. it has references), we
1832                // are substituting against a partially-resolved map -- this happens while applying
1833                // a prioritary property in the middle of custom-property resolution. Resolve it
1834                // recursively, guarding against cycles (which the resolver will also remove, but it
1835                // may not have gotten to this variable yet).
1836                Some(v) => match v.as_universal() {
1837                    Some(u) if u.has_references() => {
1838                        if seen.contains(&&reference.name) {
1839                            // Cycle: the primary is guaranteed-invalid, so fall through to the
1840                            // fallback (if any), like any other invalid primary.
1841                            None
1842                        } else {
1843                            seen.push(&reference.name);
1844                            let result = substitute_internal(
1845                                u,
1846                                property_id,
1847                                substitution_functions,
1848                                stylist,
1849                                computed_context,
1850                                attribute_tracker,
1851                                seen,
1852                                /* attr_taint */ None,
1853                            );
1854                            seen.pop();
1855                            match result {
1856                                Ok(mut substitution) => {
1857                                    substitution.attr_tainted |= v.attr_tainted;
1858                                    Some(substitution)
1859                                },
1860                                // The primary couldn't be resolved (invalid); use the fallback.
1861                                Err(()) => None,
1862                            }
1863                        }
1864                    },
1865                    _ => Some(Substitution::from_value(
1866                        v.to_variable_value(),
1867                        v.attr_tainted,
1868                    )),
1869                },
1870            }
1871        },
1872        SubstitutionFunctionKind::Env => {
1873            let device = stylist.device();
1874            device
1875                .environment()
1876                .get(&reference.name, device, url_data)
1877                .map(|v| Substitution::from_value(v, /* attr_tainted */ false))
1878        },
1879        // https://drafts.csswg.org/css-values-5/#attr-substitution
1880        SubstitutionFunctionKind::Attr => {
1881            let namespace = match reference.attribute_data.namespace {
1882                ParsedNamespace::Known(ref ns) => Some(ns),
1883                ParsedNamespace::Unknown => None,
1884            };
1885            namespace
1886                .and_then(|namespace| {
1887                    LocalName::with_name(&reference.name, |local_name| {
1888                        attribute_tracker.query(local_name, namespace)
1889                    })
1890                })
1891                .map_or_else(
1892                    || {
1893                        // Special case when fallback and <attr-type> are omitted.
1894                        // See FAILURE: https://drafts.csswg.org/css-values-5/#attr-substitution
1895                        if reference.fallback.is_none()
1896                            && reference.attribute_data.kind == AttributeType::None
1897                        {
1898                            simple_attr_subst("")
1899                        } else {
1900                            None
1901                        }
1902                    },
1903                    |attr| {
1904                        let attr = if let AttributeType::Type(_) = &reference.attribute_data.kind {
1905                            // If we're evaluating a container query, we haven't run the cascade
1906                            // and populated substitution_functions.attributes, so we can't do the
1907                            // get_attr() lookup here.
1908                            // TODO: This means chained attr() references will not work reliably in
1909                            // container style queries:
1910                            // https://bugzilla.mozilla.org/show_bug.cgi?id=2028861
1911                            if computed_context.in_container_query {
1912                                attr
1913                            } else {
1914                                substitution_functions
1915                                    .get_attr(&reference.name)
1916                                    .map(|v| v.to_variable_value())?
1917                                    .css
1918                            }
1919                        } else {
1920                            attr
1921                        };
1922                        let mut parser = Parser::new(&attr);
1923                        match &reference.attribute_data.kind {
1924                            AttributeType::Unit(unit) => {
1925                                let css = {
1926                                    // Verify that attribute data is a <number-token>.
1927                                    parser.expect_number().ok()?;
1928                                    let mut s = attr.clone();
1929                                    s.push_str(unit.as_ref());
1930                                    s
1931                                };
1932                                let serialization = match unit {
1933                                    AttrUnit::Number => TokenSerializationType::Number,
1934                                    AttrUnit::Percentage => TokenSerializationType::Percentage,
1935                                    _ => TokenSerializationType::Dimension,
1936                                };
1937                                let value =
1938                                    ComputedValue::new(css, url_data, serialization, serialization);
1939                                Some(Substitution::from_value(
1940                                    value, /* attr_tainted */ true,
1941                                ))
1942                            },
1943                            AttributeType::Type(syntax) => {
1944                                let value = SpecifiedRegisteredValue::parse(
1945                                    &mut parser,
1946                                    syntax,
1947                                    url_data,
1948                                    None,
1949                                    AllowComputationallyDependent::Yes,
1950                                    AttrTaint::default(),
1951                                    property_id,
1952                                )
1953                                .ok()?;
1954                                let value = value.to_variable_value();
1955                                Some(Substitution::from_value(
1956                                    value, /* attr_tainted */ true,
1957                                ))
1958                            },
1959                            AttributeType::RawString | AttributeType::None => {
1960                                simple_attr_subst(&attr)
1961                            },
1962                            AttributeType::Invalid => None,
1963                        }
1964                    },
1965                )
1966        },
1967    };
1968
1969    if let Some(s) = substitution {
1970        return Ok(s);
1971    }
1972
1973    let Some(ref fallback) = reference.fallback else {
1974        return Err(());
1975    };
1976
1977    do_substitute_chunk(
1978        css,
1979        fallback.start.get(),
1980        reference.end - 1, // Skip the closing parenthesis of the reference value.
1981        fallback.first_token_type,
1982        fallback.last_token_type,
1983        url_data,
1984        property_id,
1985        substitution_functions,
1986        stylist,
1987        computed_context,
1988        &fallback.references.refs,
1989        attribute_tracker,
1990        seen,
1991        /* attr_taint */ None,
1992    )
1993}
1994
1995/// Replace `var()`, `env()`, and `attr()` functions. Return `Err(..)` for invalid at computed time.
1996fn substitute_internal<'a>(
1997    variable_value: &'a VariableValue,
1998    property_id: Option<PropertyIdRef>,
1999    substitution_functions: &'a ComputedSubstitutionFunctions,
2000    stylist: &Stylist,
2001    computed_context: &computed::Context,
2002    attribute_tracker: &mut AttributeTracker,
2003    seen: &mut SmallVec<[&'a Name; 8]>,
2004    attr_taint: Option<&mut AttrTaint>,
2005) -> Result<Substitution<'a>, ()> {
2006    do_substitute_chunk(
2007        &variable_value.css,
2008        /* start = */ 0,
2009        /* end = */ variable_value.css.len(),
2010        variable_value.first_token_type,
2011        variable_value.last_token_type,
2012        &variable_value.url_data,
2013        property_id,
2014        substitution_functions,
2015        stylist,
2016        computed_context,
2017        &variable_value.references.refs,
2018        attribute_tracker,
2019        seen,
2020        attr_taint,
2021    )
2022}
2023
2024/// Replace var(), env(), and attr() functions, returning the resulting CSS string.
2025pub fn substitute<'a>(
2026    variable_value: &'a VariableValue,
2027    property_id: Option<PropertyIdRef>,
2028    substitution_functions: &'a ComputedSubstitutionFunctions,
2029    stylist: &Stylist,
2030    computed_context: &computed::Context,
2031    attribute_tracker: &mut AttributeTracker,
2032) -> Result<SubstitutionResult<'a>, ()> {
2033    debug_assert!(variable_value.has_references());
2034    let mut attr_taint = AttrTaint::default();
2035    let v = substitute_internal(
2036        variable_value,
2037        property_id,
2038        substitution_functions,
2039        stylist,
2040        computed_context,
2041        attribute_tracker,
2042        &mut SmallVec::new(),
2043        Some(&mut attr_taint),
2044    )?;
2045    Ok(SubstitutionResult {
2046        css: v.css,
2047        attr_taint,
2048    })
2049}