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