Skip to main content

style/servo/
attr.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Parsed representations of [DOM attributes][attr].
6//!
7//! [attr]: https://dom.spec.whatwg.org/#interface-attr
8
9use std::str::FromStr;
10use std::sync::OnceLock;
11
12use app_units::Au;
13use cssparser::color::{OPAQUE, parse_named_color};
14use euclid::num::Zero;
15use num_traits::ToPrimitive;
16use selectors::attr::AttrSelectorOperation;
17use servo_arc::Arc;
18
19use super::shadow_parts::ShadowParts;
20use crate::color::AbsoluteColor;
21use crate::derives::*;
22use crate::properties::PropertyDeclarationBlock;
23use crate::shared_lock::{Locked, SharedRwLock};
24use crate::str::{
25    HTML_SPACE_CHARACTERS, read_exponent, read_fraction, read_numbers, split_commas,
26    split_html_space_chars, str_join,
27};
28use crate::values::AtomString;
29use crate::values::specified::LengthPercentage;
30use crate::{Atom, LocalName, Namespace, Prefix};
31
32// Duplicated from script::dom::values.
33const UNSIGNED_LONG_MAX: u32 = 2147483647;
34
35#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq)]
36pub enum LengthOrPercentageOrAuto {
37    Auto,
38    Percentage(f32),
39    Length(Au),
40}
41
42#[derive(Clone, Debug, MallocSizeOf)]
43pub enum AttrValue {
44    //
45    // Variants that are stored in their serialized form.
46    //
47    String(String),
48    Atom(Atom),
49
50    //
51    // Variants that support lazy serialization.
52    //
53    TokenList(OnceLock<String>, Vec<Atom>),
54    UInt(OnceLock<String>, u32),
55    Int(OnceLock<String>, i32),
56    Double(OnceLock<String>, f64),
57    /// Note that this variant is only used transitively as a fast path to set
58    /// the property declaration block relevant to the style of an element when
59    /// set from the inline declaration of that element (that is,
60    /// `element.style`).
61    Declaration {
62        #[ignore_malloc_size_of = "Arc"]
63        block: Arc<Locked<PropertyDeclarationBlock>>,
64        lock: SharedRwLock,
65        serialization: OnceLock<String>,
66    },
67
68    //
69    // Variants without a serialization implementation which must be eagerly serialized.
70    //
71    LengthPercentage(String, Option<LengthPercentage>),
72    Color(String, Option<AbsoluteColor>),
73    Dimension(String, LengthOrPercentageOrAuto),
74    /// Stores a URL, computed from the input string and a document's base URL.
75    ///
76    /// The URL is resolved at setting-time, so this kind of attribute value is
77    /// not actually suitable for most URL-reflecting IDL attributes.
78    ResolvedUrl(
79        String,
80        #[ignore_malloc_size_of = "Arc"] Option<Arc<url::Url>>,
81    ),
82    /// The value of an `exportparts` attribute.
83    ShadowParts(String, ShadowParts),
84}
85
86impl From<String> for AttrValue {
87    fn from(value: String) -> Self {
88        Self::String(value)
89    }
90}
91
92impl From<u32> for AttrValue {
93    fn from(value: u32) -> Self {
94        Self::UInt(OnceLock::new(), value)
95    }
96}
97
98impl From<i32> for AttrValue {
99    fn from(value: i32) -> Self {
100        Self::Int(OnceLock::new(), value)
101    }
102}
103
104impl From<f64> for AttrValue {
105    fn from(value: f64) -> Self {
106        Self::Double(OnceLock::new(), value)
107    }
108}
109
110impl From<Vec<Atom>> for AttrValue {
111    fn from(value: Vec<Atom>) -> Self {
112        Self::TokenList(OnceLock::new(), value)
113    }
114}
115
116/// Shared implementation to parse an integer according to
117/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-integers> or
118/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-non-negative-integers>
119fn do_parse_integer<T: Iterator<Item = char>>(input: T) -> Result<i64, ()> {
120    let mut input = input
121        .skip_while(|c| HTML_SPACE_CHARACTERS.iter().any(|s| s == c))
122        .peekable();
123
124    let sign = match input.peek() {
125        None => return Err(()),
126        Some(&'-') => {
127            input.next();
128            -1
129        },
130        Some(&'+') => {
131            input.next();
132            1
133        },
134        Some(_) => 1,
135    };
136
137    let (value, _) = read_numbers(input);
138
139    value.and_then(|value| value.checked_mul(sign)).ok_or(())
140}
141
142/// Parse an integer according to
143/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-integers>.
144pub fn parse_integer<T: Iterator<Item = char>>(input: T) -> Result<i32, ()> {
145    do_parse_integer(input).and_then(|result| result.to_i32().ok_or(()))
146}
147
148/// Parse an integer according to
149/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-non-negative-integers>
150pub fn parse_unsigned_integer<T: Iterator<Item = char>>(input: T) -> Result<u32, ()> {
151    do_parse_integer(input).and_then(|result| result.to_u32().ok_or(()))
152}
153
154/// Parse a floating-point number according to
155/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-floating-point-number-values>
156pub fn parse_double(string: &str) -> Result<f64, ()> {
157    let trimmed = string.trim_matches(HTML_SPACE_CHARACTERS);
158    let mut input = trimmed.chars().peekable();
159
160    let (value, divisor, chars_skipped) = match input.peek() {
161        None => return Err(()),
162        Some(&'-') => {
163            input.next();
164            (-1f64, -1f64, 1)
165        },
166        Some(&'+') => {
167            input.next();
168            (1f64, 1f64, 1)
169        },
170        _ => (1f64, 1f64, 0),
171    };
172
173    let (value, value_digits) = if let Some(&'.') = input.peek() {
174        (0f64, 0)
175    } else {
176        let (read_val, read_digits) = read_numbers(input);
177        (
178            value * read_val.and_then(|result| result.to_f64()).unwrap_or(1f64),
179            read_digits,
180        )
181    };
182
183    let input = trimmed
184        .chars()
185        .skip(value_digits + chars_skipped)
186        .peekable();
187
188    let (mut value, fraction_digits) = read_fraction(input, divisor, value);
189
190    let input = trimmed
191        .chars()
192        .skip(value_digits + chars_skipped + fraction_digits)
193        .peekable();
194
195    if let Some(exp) = read_exponent(input) {
196        value *= 10f64.powi(exp)
197    };
198
199    Ok(value)
200}
201
202impl AttrValue {
203    pub fn from_serialized_tokenlist(tokens: String) -> AttrValue {
204        let atoms =
205            split_html_space_chars(&tokens)
206                .map(Atom::from)
207                .fold(vec![], |mut acc, atom| {
208                    if !acc.contains(&atom) {
209                        acc.push(atom)
210                    }
211                    acc
212                });
213        AttrValue::TokenList(tokens.into(), atoms)
214    }
215
216    pub fn from_comma_separated_tokenlist(tokens: String) -> AttrValue {
217        let atoms = split_commas(&tokens)
218            .map(Atom::from)
219            .fold(vec![], |mut acc, atom| {
220                if !acc.contains(&atom) {
221                    acc.push(atom)
222                }
223                acc
224            });
225        AttrValue::TokenList(tokens.into(), atoms)
226    }
227
228    // https://html.spec.whatwg.org/multipage/#reflecting-content-attributes-in-idl-attributes:idl-unsigned-long
229    pub fn from_u32(string: String, default: u32) -> AttrValue {
230        let result = parse_unsigned_integer(string.chars()).unwrap_or(default);
231        let result = if result > UNSIGNED_LONG_MAX {
232            default
233        } else {
234            result
235        };
236        AttrValue::UInt(string.into(), result)
237    }
238
239    pub fn from_i32(string: String, default: i32) -> AttrValue {
240        let result = parse_integer(string.chars()).unwrap_or(default);
241        AttrValue::Int(string.into(), result)
242    }
243
244    // https://html.spec.whatwg.org/multipage/#reflecting-content-attributes-in-idl-attributes:idl-double
245    pub fn from_double(string: String, default: f64) -> AttrValue {
246        let result = parse_double(&string).unwrap_or(default);
247
248        if result.is_normal() {
249            AttrValue::Double(string.into(), result)
250        } else {
251            AttrValue::Double(string.into(), default)
252        }
253    }
254
255    // https://html.spec.whatwg.org/multipage/#limited-to-only-non-negative-numbers
256    pub fn from_limited_i32(string: String, default: i32) -> AttrValue {
257        let result = parse_integer(string.chars()).unwrap_or(default);
258
259        if result < 0 {
260            AttrValue::Int(string.into(), default)
261        } else {
262            AttrValue::Int(string.into(), result)
263        }
264    }
265
266    // https://html.spec.whatwg.org/multipage/#limited-to-only-non-negative-numbers-greater-than-zero
267    pub fn from_limited_u32(string: String, default: u32) -> AttrValue {
268        let result = parse_unsigned_integer(string.chars()).unwrap_or(default);
269        let result = if result == 0 || result > UNSIGNED_LONG_MAX {
270            default
271        } else {
272            result
273        };
274        AttrValue::UInt(string.into(), result)
275    }
276
277    pub fn from_atomic(string: String) -> AttrValue {
278        AttrValue::Atom(string.into())
279    }
280
281    pub fn from_resolved_url(base: &Arc<::url::Url>, url: String) -> AttrValue {
282        let joined = base.join(&url).ok().map(Arc::new);
283        AttrValue::ResolvedUrl(url, joined)
284    }
285
286    pub fn from_legacy_color(string: String) -> AttrValue {
287        let parsed = parse_legacy_color(&string).ok();
288        AttrValue::Color(string, parsed)
289    }
290
291    pub fn from_dimension(string: String) -> AttrValue {
292        let parsed = parse_length(&string);
293        AttrValue::Dimension(string, parsed)
294    }
295
296    pub fn from_nonzero_dimension(string: String) -> AttrValue {
297        let parsed = parse_nonzero_length(&string);
298        AttrValue::Dimension(string, parsed)
299    }
300
301    pub fn from_shadow_parts(string: String) -> AttrValue {
302        let shadow_parts = ShadowParts::parse(&string);
303        AttrValue::ShadowParts(string, shadow_parts)
304    }
305
306    pub fn from_declaration(
307        block: Arc<Locked<PropertyDeclarationBlock>>,
308        lock: SharedRwLock,
309    ) -> AttrValue {
310        AttrValue::Declaration {
311            block,
312            lock,
313            serialization: OnceLock::new(),
314        }
315    }
316
317    /// Assumes the `AttrValue` is a `TokenList` and returns its tokens
318    ///
319    /// ## Panics
320    ///
321    /// Panics if the `AttrValue` is not a `TokenList`
322    pub fn as_tokens(&self) -> &[Atom] {
323        match *self {
324            AttrValue::TokenList(_, ref tokens) => tokens,
325            _ => panic!("Tokens not found"),
326        }
327    }
328
329    /// Assumes the `AttrValue` is an `Atom` and returns its value
330    ///
331    /// ## Panics
332    ///
333    /// Panics if the `AttrValue` is not an `Atom`
334    pub fn as_atom(&self) -> &Atom {
335        match *self {
336            AttrValue::Atom(ref value) => value,
337            _ => panic!("Atom not found"),
338        }
339    }
340
341    /// Assumes the `AttrValue` is a `LengthPercentage` and returns its value
342    ///
343    /// ## Panics
344    ///
345    /// Panics if the `AttrValue` is not a `LengthPercentage`
346    pub fn as_length_percentage(&self) -> Option<&LengthPercentage> {
347        match *self {
348            AttrValue::LengthPercentage(_, ref length_percentage) => length_percentage.as_ref(),
349            _ => panic!("LengthPercentage not found"),
350        }
351    }
352
353    /// Assumes the `AttrValue` is a `Color` and returns its value
354    ///
355    /// ## Panics
356    ///
357    /// Panics if the `AttrValue` is not a `Color`
358    pub fn as_color(&self) -> Option<&AbsoluteColor> {
359        match *self {
360            AttrValue::Color(_, ref color) => color.as_ref(),
361            _ => panic!("Color not found"),
362        }
363    }
364
365    /// Assumes the `AttrValue` is a `Dimension` and returns its value
366    ///
367    /// ## Panics
368    ///
369    /// Panics if the `AttrValue` is not a `Dimension`
370    pub fn as_dimension(&self) -> &LengthOrPercentageOrAuto {
371        match *self {
372            AttrValue::Dimension(_, ref l) => l,
373            _ => panic!("Dimension not found"),
374        }
375    }
376
377    /// Assumes the `AttrValue` is a `ResolvedUrl` and returns its value.
378    ///
379    /// ## Panics
380    ///
381    /// Panics if the `AttrValue` is not a `ResolvedUrl`
382    pub fn as_resolved_url(&self) -> Option<&Arc<::url::Url>> {
383        match *self {
384            AttrValue::ResolvedUrl(_, ref url) => url.as_ref(),
385            _ => panic!("Url not found"),
386        }
387    }
388
389    /// Return the AttrValue as its signed integer representation, if any.
390    /// This corresponds to attribute values returned as `AttrValue::Int(_)`
391    /// by `VirtualMethods::parse_plain_attribute()`.
392    ///
393    /// ## Panics
394    ///
395    /// Panics if the `AttrValue` is not a `Int`
396    pub fn as_int(&self) -> i32 {
397        if let AttrValue::Int(_, value) = *self {
398            value
399        } else {
400            panic!("Int not found");
401        }
402    }
403
404    /// Return the AttrValue as its unsigned integer representation, if any.
405    /// This corresponds to attribute values returned as `AttrValue::UInt(_)`
406    /// by `VirtualMethods::parse_plain_attribute()`.
407    ///
408    /// ## Panics
409    ///
410    /// Panics if the `AttrValue` is not a `UInt`
411    pub fn as_uint(&self) -> u32 {
412        if let AttrValue::UInt(_, value) = *self {
413            value
414        } else {
415            panic!("Uint not found");
416        }
417    }
418
419    /// Return the AttrValue as a dimension computed from its unsigned integer
420    /// representation, assuming that integer representation specifies pixels.
421    ///
422    /// This corresponds to attribute values returned as `AttrValue::UInt(_)`
423    /// by `VirtualMethods::parse_plain_attribute()`.
424    ///
425    /// ## Panics
426    ///
427    /// Panics if the `AttrValue` is not a `UInt`
428    pub fn as_uint_px_dimension(&self) -> LengthOrPercentageOrAuto {
429        if let AttrValue::UInt(_, value) = *self {
430            LengthOrPercentageOrAuto::Length(Au::from_px(value as i32))
431        } else {
432            panic!("Uint not found");
433        }
434    }
435
436    /// Return the AttrValue as it's shadow-part representation.
437    ///
438    /// This corresponds to attribute values returned as `AttrValue::ShadowParts(_)`
439    /// by `VirtualMethods::parse_plain_attribute()`.
440    ///
441    /// ## Panics
442    ///
443    /// Panics if the `AttrValue` is not a shadow-part.
444    pub fn as_shadow_parts(&self) -> &ShadowParts {
445        if let AttrValue::ShadowParts(_, value) = &self {
446            value
447        } else {
448            panic!("Not a shadowpart attribute");
449        }
450    }
451
452    pub fn eval_selector(&self, selector: &AttrSelectorOperation<&AtomString>) -> bool {
453        // FIXME(SimonSapin) this can be more efficient by matching on `(self, selector)` variants
454        // and doing Atom comparisons instead of string comparisons where possible,
455        // with SelectorImpl::AttrValue changed to Atom.
456        selector.eval_str(self)
457    }
458}
459
460impl ::std::ops::Deref for AttrValue {
461    type Target = str;
462
463    fn deref(&self) -> &str {
464        match self {
465            AttrValue::String(value) => &value,
466            AttrValue::Atom(atom) => &atom,
467            AttrValue::TokenList(serialization, tokens) => {
468                serialization.get_or_init(|| {
469                    // TODO(ajeffrey): Efficient conversion of Vec<Atom> to String
470                    str_join(tokens, "\x20")
471                })
472            },
473            AttrValue::UInt(serialization, value) => {
474                serialization.get_or_init(|| value.to_string())
475            },
476            AttrValue::Double(serialization, value) => {
477                serialization.get_or_init(|| value.to_string())
478            },
479            AttrValue::Int(serialization, value) => serialization.get_or_init(|| value.to_string()),
480            AttrValue::Declaration {
481                block,
482                lock,
483                serialization,
484            } => serialization.get_or_init(|| {
485                let mut serialization = String::new();
486                block
487                    .read_with(&lock.read())
488                    .to_css(&mut serialization)
489                    .expect("Should always be able to produce a valid serialization");
490                serialization
491            }),
492            AttrValue::LengthPercentage(serialization, _)
493            | AttrValue::Color(serialization, _)
494            | AttrValue::Dimension(serialization, _)
495            | AttrValue::ResolvedUrl(serialization, _)
496            | AttrValue::ShadowParts(serialization, _) => &serialization,
497        }
498    }
499}
500
501impl PartialEq<Atom> for AttrValue {
502    fn eq(&self, other: &Atom) -> bool {
503        match *self {
504            AttrValue::Atom(ref value) => value == other,
505            _ => other == &**self,
506        }
507    }
508}
509
510/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-non-zero-dimension-values>
511pub fn parse_nonzero_length(value: &str) -> LengthOrPercentageOrAuto {
512    match parse_length(value) {
513        LengthOrPercentageOrAuto::Length(x) if x == Au::zero() => LengthOrPercentageOrAuto::Auto,
514        LengthOrPercentageOrAuto::Percentage(x) if x == 0. => LengthOrPercentageOrAuto::Auto,
515        x => x,
516    }
517}
518
519/// Parses a [legacy color][color]. If unparseable, `Err` is returned.
520///
521/// [color]: https://html.spec.whatwg.org/multipage/#rules-for-parsing-a-legacy-colour-value
522pub fn parse_legacy_color(mut input: &str) -> Result<AbsoluteColor, ()> {
523    // Steps 1 and 2.
524    if input.is_empty() {
525        return Err(());
526    }
527
528    // Step 3.
529    input = input.trim_matches(HTML_SPACE_CHARACTERS);
530
531    // Step 4.
532    if input.eq_ignore_ascii_case("transparent") {
533        return Err(());
534    }
535
536    // Step 5.
537    if let Ok((r, g, b)) = parse_named_color(input) {
538        return Ok(AbsoluteColor::srgb_legacy(r, g, b, OPAQUE));
539    }
540
541    // Step 6.
542    if input.len() == 4 {
543        if let (b'#', Ok(r), Ok(g), Ok(b)) = (
544            input.as_bytes()[0],
545            hex(input.as_bytes()[1] as char),
546            hex(input.as_bytes()[2] as char),
547            hex(input.as_bytes()[3] as char),
548        ) {
549            return Ok(AbsoluteColor::srgb_legacy(r * 17, g * 17, b * 17, OPAQUE));
550        }
551    }
552
553    // Step 7.
554    let mut new_input = String::new();
555    for ch in input.chars() {
556        if ch as u32 > 0xffff {
557            new_input.push_str("00")
558        } else {
559            new_input.push(ch)
560        }
561    }
562    let mut input = &*new_input;
563
564    // Step 8.
565    for (char_count, (index, _)) in input.char_indices().enumerate() {
566        if char_count == 128 {
567            input = &input[..index];
568            break;
569        }
570    }
571
572    // Step 9.
573    if input.as_bytes()[0] == b'#' {
574        input = &input[1..]
575    }
576
577    // Step 10.
578    let mut new_input = Vec::new();
579    for ch in input.chars() {
580        if hex(ch).is_ok() {
581            new_input.push(ch as u8)
582        } else {
583            new_input.push(b'0')
584        }
585    }
586    let mut input = new_input;
587
588    // Step 11.
589    while input.is_empty() || (input.len() % 3) != 0 {
590        input.push(b'0')
591    }
592
593    // Step 12.
594    let mut length = input.len() / 3;
595    let (mut red, mut green, mut blue) = (
596        &input[..length],
597        &input[length..length * 2],
598        &input[length * 2..],
599    );
600
601    // Step 13.
602    if length > 8 {
603        red = &red[length - 8..];
604        green = &green[length - 8..];
605        blue = &blue[length - 8..];
606        length = 8
607    }
608
609    // Step 14.
610    while length > 2 && red[0] == b'0' && green[0] == b'0' && blue[0] == b'0' {
611        red = &red[1..];
612        green = &green[1..];
613        blue = &blue[1..];
614        length -= 1
615    }
616
617    // Steps 15-20.
618    return Ok(AbsoluteColor::srgb_legacy(
619        hex_string(red).unwrap(),
620        hex_string(green).unwrap(),
621        hex_string(blue).unwrap(),
622        OPAQUE,
623    ));
624
625    fn hex(ch: char) -> Result<u8, ()> {
626        match ch {
627            '0'..='9' => Ok((ch as u8) - b'0'),
628            'a'..='f' => Ok((ch as u8) - b'a' + 10),
629            'A'..='F' => Ok((ch as u8) - b'A' + 10),
630            _ => Err(()),
631        }
632    }
633
634    fn hex_string(string: &[u8]) -> Result<u8, ()> {
635        match string.len() {
636            0 => Err(()),
637            1 => hex(string[0] as char),
638            _ => {
639                let upper = hex(string[0] as char)?;
640                let lower = hex(string[1] as char)?;
641                Ok((upper << 4) | lower)
642            },
643        }
644    }
645}
646
647/// Parses a [dimension value][dim]. If unparseable, `Auto` is returned.
648///
649/// [dim]: https://html.spec.whatwg.org/multipage/#rules-for-parsing-dimension-values
650// TODO: this function can be rewritten to return Result<LengthPercentage, _>
651pub fn parse_length(mut value: &str) -> LengthOrPercentageOrAuto {
652    // Steps 1 & 2 are not relevant
653
654    // Step 3
655    value = value.trim_start_matches(HTML_SPACE_CHARACTERS);
656
657    // Step 4
658    match value.chars().nth(0) {
659        Some('0'..='9') => {},
660        _ => return LengthOrPercentageOrAuto::Auto,
661    }
662
663    // Steps 5 to 8
664    // We trim the string length to the minimum of:
665    // 1. the end of the string
666    // 2. the first occurence of a '%' (U+0025 PERCENT SIGN)
667    // 3. the second occurrence of a '.' (U+002E FULL STOP)
668    // 4. the occurrence of a character that is neither a digit nor '%' nor '.'
669    // Note: Step 7.4 is directly subsumed by FromStr::from_str
670    let mut end_index = value.len();
671    let (mut found_full_stop, mut found_percent) = (false, false);
672    for (i, ch) in value.chars().enumerate() {
673        match ch {
674            '0'..='9' => continue,
675            '%' => {
676                found_percent = true;
677                end_index = i;
678                break;
679            },
680            '.' if !found_full_stop => {
681                found_full_stop = true;
682                continue;
683            },
684            _ => {
685                end_index = i;
686                break;
687            },
688        }
689    }
690    value = &value[..end_index];
691
692    if found_percent {
693        let result: Result<f32, _> = FromStr::from_str(value);
694        match result {
695            Ok(number) => return LengthOrPercentageOrAuto::Percentage((number as f32) / 100.0),
696            Err(_) => return LengthOrPercentageOrAuto::Auto,
697        }
698    }
699
700    match FromStr::from_str(value) {
701        Ok(number) => LengthOrPercentageOrAuto::Length(Au::from_f64_px(number)),
702        Err(_) => LengthOrPercentageOrAuto::Auto,
703    }
704}
705
706/// A struct that uniquely identifies an element's attribute.
707#[derive(Clone, Debug, MallocSizeOf)]
708pub struct AttrIdentifier {
709    pub local_name: LocalName,
710    pub name: LocalName,
711    pub namespace: Namespace,
712    pub prefix: Option<Prefix>,
713}