Skip to main content

style/values/specified/
random.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//! Specified types for the `<random-key>` of the CSS `random()` function.
6//! https://drafts.csswg.org/css-values-5/#randomness
7
8use crate::Atom;
9use crate::derives::*;
10use crate::parser::{Parse, ParserContext};
11use crate::properties::PropertyIdRef;
12use crate::values::computed::{Context, ToComputedValue};
13use crate::values::specified::calc::PercentageContext;
14use crate::values::specified::number::{Number, parse_number_with_clamping_mode};
15use crate::values::{CSSFloat, CustomIdent, DashedIdent};
16use cssparser::{Parser, match_ignore_ascii_case};
17use selectors::parser::SelectorParseErrorKind;
18use std::fmt::{self, Write};
19use style_traits::values::SequenceWriter;
20use style_traits::values::specified::AllowedNumericType;
21use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
22
23/// The <random-ua-ident> part of a <random-cache-key>. This is a <custom-ident>
24/// starting with `ua-` that is typically automatically constructed by the user agent.
25///
26/// https://drafts.csswg.org/css-values-5/#valdef-random-ua-ident
27#[repr(C)]
28#[derive(Clone, Debug, Hash, MallocSizeOf, PartialEq, ToCss, ToShmem)]
29pub struct RandomUaIdent(CustomIdent);
30
31impl RandomUaIdent {
32    fn ua_prefixed(property_id: PropertyIdRef) -> String {
33        let mut ua_ident = "ua-".to_string();
34        match property_id {
35            PropertyIdRef::NonCustom(non_custom) => {
36                ua_ident += non_custom.name();
37            },
38            PropertyIdRef::Custom(custom) => {
39                ua_ident += "--";
40                #[cfg(feature = "gecko")]
41                {
42                    custom.with_str(|name| ua_ident += name);
43                }
44                #[cfg(feature = "servo")]
45                {
46                    ua_ident += custom;
47                }
48            },
49        };
50        ua_ident
51    }
52
53    /// Builds the `ua-PROPERTY` ident used to replace `property-scoped`.
54    pub fn from_property(property_id: PropertyIdRef) -> Self {
55        Self(CustomIdent(Atom::from(Self::ua_prefixed(property_id))))
56    }
57
58    /// Builds the `ua-PROPERTY-INDEX` ident used to replace `property-index-scoped`,
59    /// where `index` is the 1-based index of this random function among all of the
60    /// random functions in the declaration.
61    pub fn from_property_and_index(property_id: PropertyIdRef, index: i32) -> Self {
62        let mut ua_ident = Self::ua_prefixed(property_id);
63        write!(ua_ident, "-{index}").unwrap();
64        Self(CustomIdent(Atom::from(ua_ident)))
65    }
66
67    /// Special value for internal use. Useful where we can't use Option<>.
68    pub fn empty() -> Self {
69        Self(CustomIdent(atom!("")))
70    }
71
72    /// Check for special internal value.
73    pub fn is_empty(&self) -> bool {
74        self.0.0 == atom!("")
75    }
76}
77
78impl Parse for RandomUaIdent {
79    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
80        let ident = input.expect_ident()?;
81
82        if ident.eq_ignore_ascii_case("property-scoped") {
83            let Some(property_id) = context.property_declaration_context.property_id() else {
84                return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
85            };
86            return Ok(RandomUaIdent::from_property(property_id));
87        }
88
89        if ident.eq_ignore_ascii_case("property-index-scoped") {
90            let Some(property_id) = context.property_declaration_context.property_id() else {
91                return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
92            };
93            let index = context.property_declaration_context.current_random_index();
94            return Ok(RandomUaIdent::from_property_and_index(property_id, index));
95        }
96
97        if !ident.starts_with("ua-") {
98            return Err(ParseError::custom(SelectorParseErrorKind::UnexpectedIdent));
99        }
100        CustomIdent::from_ident(ident, &[]).map(RandomUaIdent)
101    }
102}
103
104/// A <random-cache-key> that specifies parts of the random cache name.
105///
106/// https://drafts.csswg.org/css-values-5/#valdef-random-random-cache-key
107#[repr(C)]
108#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
109pub struct RandomCacheKey {
110    /// The author-specified <dashed-ident>, or `DashedIdent::empty()` if absent.
111    pub name: DashedIdent,
112    /// The <random-ua-ident>, either as author-specified or as constructed to replace
113    /// `property-scoped`/`property-index-scoped`, or `RandomUaIdent::empty()` if absent.
114    pub ua_ident: RandomUaIdent,
115    /// Whether `element-scoped` was specified.
116    pub is_element_scoped: bool,
117}
118
119impl Parse for RandomCacheKey {
120    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
121        let mut key = RandomCacheKey {
122            name: DashedIdent::empty(),
123            ua_ident: RandomUaIdent::empty(),
124            is_element_scoped: false,
125        };
126
127        loop {
128            if key.name.is_empty()
129                && let Ok(name) = input.try_parse(|input| DashedIdent::parse(context, input))
130            {
131                key.name = name;
132                continue;
133            }
134
135            if !key.is_element_scoped
136                && input
137                    .try_parse(|input| input.expect_ident_matching("element-scoped"))
138                    .is_ok()
139            {
140                if !context.has_element_context() {
141                    return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
142                }
143
144                key.is_element_scoped = true;
145                continue;
146            }
147
148            if key.ua_ident.is_empty()
149                && let Ok(ua_ident) = input.try_parse(|input| RandomUaIdent::parse(context, input))
150            {
151                key.ua_ident = ua_ident;
152                continue;
153            }
154
155            break;
156        }
157
158        if key.name.is_empty() && !key.is_element_scoped && key.ua_ident.is_empty() {
159            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
160        }
161
162        Ok(key)
163    }
164}
165
166impl ToCss for RandomCacheKey {
167    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
168    where
169        W: Write,
170    {
171        let mut writer = SequenceWriter::new(dest, " ");
172        if !self.name.is_empty() {
173            writer.item(&self.name)?;
174        }
175        if self.is_element_scoped {
176            writer.raw_item("element-scoped")?;
177        }
178        if !self.ua_ident.is_empty() {
179            writer.item(&self.ua_ident)?;
180        }
181        Ok(())
182    }
183}
184
185/// A <random-key> that specifies how to compute the random base value for
186/// a random function. Note that the `auto` value is not modeled as it is
187/// immediately converted to the `element-scoped property-index-scoped`
188/// cache key format during parsing.
189///
190/// https://drafts.csswg.org/css-values-5/#typedef-random-key
191#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
192#[repr(u8)]
193pub enum RandomKey {
194    /// A <number> used directly as the random base value.
195    /// TODO(bug 2071971): This is broken if it manages to get into a computed node. Refactor this
196    /// away altogether.
197    Fixed(Number),
198    /// A <random-cache-key> used to generate the random base value.
199    CacheKey(RandomCacheKey),
200}
201
202impl RandomKey {
203    /// The result of the `auto` value (the default value of an omitted `<random-key>`),
204    /// which is equivalent to specifying `element-scoped property-index-scoped`.
205    pub fn auto(context: &ParserContext) -> Result<Self, ParseError> {
206        if !context.has_element_context() {
207            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
208        }
209
210        let Some(property_id) = context.property_declaration_context.property_id() else {
211            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
212        };
213        let index = context.property_declaration_context.current_random_index();
214
215        Ok(Self::CacheKey(RandomCacheKey {
216            name: DashedIdent::empty(),
217            ua_ident: RandomUaIdent::from_property_and_index(property_id, index),
218            is_element_scoped: true,
219        }))
220    }
221}
222
223impl Parse for RandomKey {
224    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
225        if let Ok(key) = input.try_parse(|input| RandomCacheKey::parse(context, input)) {
226            return Ok(Self::CacheKey(key));
227        }
228
229        let ident = input.expect_ident()?;
230        match_ignore_ascii_case! { &ident,
231            "auto" => Self::auto(context),
232            "fixed" => {
233                let fixed = parse_number_with_clamping_mode(
234                    context,
235                    input,
236                    AllowedNumericType::ZeroToOne,
237                    PercentageContext::not_allowed(),
238                )?;
239                Ok(Self::Fixed(fixed))
240            },
241            _ => Err(ParseError::custom(SelectorParseErrorKind::UnexpectedIdent)),
242        }
243    }
244}
245
246impl ToComputedValue for RandomKey {
247    type ComputedValue = CSSFloat;
248
249    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
250        match self {
251            Self::Fixed(number) => number.to_computed_value(context),
252            Self::CacheKey(cache_key) => context.random_base_value(cache_key),
253        }
254    }
255
256    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
257        Self::Fixed(Number::from_computed_value(computed))
258    }
259}
260
261impl ToCss for RandomKey {
262    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
263    where
264        W: Write,
265    {
266        match self {
267            RandomKey::Fixed(number) => {
268                dest.write_str("fixed ")?;
269                number.to_css(dest)
270            },
271            RandomKey::CacheKey(cache_key) => cache_key.to_css(dest),
272        }
273    }
274}