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