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