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