script/dom/html/
htmlareaelement.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
5use std::cell::Cell;
6use std::default::Default;
7use std::{f32, str};
8
9use cssparser::match_ignore_ascii_case;
10use dom_struct::dom_struct;
11use euclid::default::Point2D;
12use html5ever::{LocalName, Prefix, local_name};
13use js::rust::HandleObject;
14use servo_url::ServoUrl;
15use style::attr::AttrValue;
16use stylo_atoms::Atom;
17use stylo_dom::ElementState;
18
19use crate::dom::activation::Activatable;
20use crate::dom::attr::Attr;
21use crate::dom::bindings::cell::DomRefCell;
22use crate::dom::bindings::codegen::Bindings::HTMLAreaElementBinding::HTMLAreaElementMethods;
23use crate::dom::bindings::inheritance::Castable;
24use crate::dom::bindings::root::{DomRoot, MutNullableDom};
25use crate::dom::bindings::str::{DOMString, USVString};
26use crate::dom::document::Document;
27use crate::dom::domtokenlist::DOMTokenList;
28use crate::dom::element::{AttributeMutation, Element, reflect_referrer_policy_attribute};
29use crate::dom::event::Event;
30use crate::dom::eventtarget::EventTarget;
31use crate::dom::html::htmlelement::HTMLElement;
32use crate::dom::html::htmlhyperlinkelementutils::{HyperlinkElement, HyperlinkElementTraits};
33use crate::dom::node::{BindContext, Node};
34use crate::dom::virtualmethods::VirtualMethods;
35use crate::links::{LinkRelations, follow_hyperlink};
36use crate::script_runtime::CanGc;
37
38#[derive(Debug, PartialEq)]
39pub enum Area {
40    Circle {
41        left: f32,
42        top: f32,
43        radius: f32,
44    },
45    Rectangle {
46        top_left: (f32, f32),
47        bottom_right: (f32, f32),
48    },
49    Polygon {
50        /// Stored as a flat array of coordinates
51        /// e.g. [x1, y1, x2, y2, x3, y3] for a triangle
52        points: Vec<f32>,
53    },
54}
55
56pub enum Shape {
57    Circle,
58    Rectangle,
59    Polygon,
60}
61
62// https://html.spec.whatwg.org/multipage/#rules-for-parsing-a-list-of-floating-point-numbers
63// https://html.spec.whatwg.org/multipage/#image-map-processing-model
64impl Area {
65    pub fn parse(coord: &str, target: Shape) -> Option<Area> {
66        let points_count = match target {
67            Shape::Circle => 3,
68            Shape::Rectangle => 4,
69            Shape::Polygon => 0,
70        };
71
72        let size = coord.len();
73        let num = coord.as_bytes();
74        let mut index = 0;
75
76        // Step 4: Walk till char is not a delimiter
77        while index < size {
78            let val = num[index];
79            match val {
80                b',' | b';' | b' ' | b'\t' | b'\n' | 0x0C | b'\r' => {},
81                _ => break,
82            }
83
84            index += 1;
85        }
86
87        // This vector will hold all parsed coordinates
88        let mut number_list = Vec::new();
89        let mut array = Vec::new();
90
91        // Step 5: walk till end of string
92        while index < size {
93            // Step 5.1: walk till we hit a valid char i.e., 0 to 9, dot or dash, e, E
94            while index < size {
95                let val = num[index];
96                match val {
97                    b'0'..=b'9' | b'.' | b'-' | b'E' | b'e' => break,
98                    _ => {},
99                }
100
101                index += 1;
102            }
103
104            // Step 5.2: collect valid symbols till we hit another delimiter
105            while index < size {
106                let val = num[index];
107
108                match val {
109                    b',' | b';' | b' ' | b'\t' | b'\n' | 0x0C | b'\r' => break,
110                    _ => array.push(val),
111                }
112
113                index += 1;
114            }
115
116            // The input does not consist any valid characters
117            if array.is_empty() {
118                break;
119            }
120
121            // Convert String to float
122            match str::from_utf8(&array)
123                .ok()
124                .and_then(|s| s.parse::<f32>().ok())
125            {
126                Some(v) => number_list.push(v),
127                None => number_list.push(0.0),
128            };
129
130            array.clear();
131
132            // For rectangle and circle, stop parsing once we have three
133            // and four coordinates respectively
134            if points_count > 0 && number_list.len() == points_count {
135                break;
136            }
137        }
138
139        let final_size = number_list.len();
140
141        match target {
142            Shape::Circle => {
143                if final_size == 3 {
144                    if number_list[2] <= 0.0 {
145                        None
146                    } else {
147                        Some(Area::Circle {
148                            left: number_list[0],
149                            top: number_list[1],
150                            radius: number_list[2],
151                        })
152                    }
153                } else {
154                    None
155                }
156            },
157
158            Shape::Rectangle => {
159                if final_size == 4 {
160                    if number_list[0] > number_list[2] {
161                        number_list.swap(0, 2);
162                    }
163
164                    if number_list[1] > number_list[3] {
165                        number_list.swap(1, 3);
166                    }
167
168                    Some(Area::Rectangle {
169                        top_left: (number_list[0], number_list[1]),
170                        bottom_right: (number_list[2], number_list[3]),
171                    })
172                } else {
173                    None
174                }
175            },
176
177            Shape::Polygon => {
178                if final_size >= 6 {
179                    if final_size % 2 != 0 {
180                        // Drop last element if there are odd number of coordinates
181                        number_list.remove(final_size - 1);
182                    }
183                    Some(Area::Polygon {
184                        points: number_list,
185                    })
186                } else {
187                    None
188                }
189            },
190        }
191    }
192
193    pub fn hit_test(&self, p: &Point2D<f32>) -> bool {
194        match *self {
195            Area::Circle { left, top, radius } => {
196                (p.x - left) * (p.x - left) + (p.y - top) * (p.y - top) - radius * radius <= 0.0
197            },
198
199            Area::Rectangle {
200                top_left,
201                bottom_right,
202            } => {
203                p.x <= bottom_right.0 &&
204                    p.x >= top_left.0 &&
205                    p.y <= bottom_right.1 &&
206                    p.y >= top_left.1
207            },
208
209            Area::Polygon { ref points } => {
210                // Ray-casting algorithm to determine if point is inside polygon
211                // https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm
212                let mut inside = false;
213
214                debug_assert!(points.len() % 2 == 0);
215                let vertices = points.len() / 2;
216
217                for i in 0..vertices {
218                    let next_i = if i + 1 == vertices { 0 } else { i + 1 };
219
220                    let xi = points[2 * i];
221                    let yi = points[2 * i + 1];
222                    let xj = points[2 * next_i];
223                    let yj = points[2 * next_i + 1];
224
225                    if (yi > p.y) != (yj > p.y) && p.x < (xj - xi) * (p.y - yi) / (yj - yi) + xi {
226                        inside = !inside;
227                    }
228                }
229                inside
230            },
231        }
232    }
233
234    pub(crate) fn absolute_coords(&self, p: Point2D<f32>) -> Area {
235        match *self {
236            Area::Rectangle {
237                top_left,
238                bottom_right,
239            } => Area::Rectangle {
240                top_left: (top_left.0 + p.x, top_left.1 + p.y),
241                bottom_right: (bottom_right.0 + p.x, bottom_right.1 + p.y),
242            },
243            Area::Circle { left, top, radius } => Area::Circle {
244                left: (left + p.x),
245                top: (top + p.y),
246                radius,
247            },
248            Area::Polygon { ref points } => {
249                //                let new_points = Vec::new();
250                let iter = points
251                    .iter()
252                    .enumerate()
253                    .map(|(index, point)| match index % 2 {
254                        0 => point + p.x,
255                        _ => point + p.y,
256                    });
257                Area::Polygon {
258                    points: iter.collect::<Vec<_>>(),
259                }
260            },
261        }
262    }
263}
264
265#[dom_struct]
266pub(crate) struct HTMLAreaElement {
267    htmlelement: HTMLElement,
268    rel_list: MutNullableDom<DOMTokenList>,
269    #[no_trace]
270    relations: Cell<LinkRelations>,
271    #[no_trace]
272    url: DomRefCell<Option<ServoUrl>>,
273}
274
275impl HTMLAreaElement {
276    fn new_inherited(
277        local_name: LocalName,
278        prefix: Option<Prefix>,
279        document: &Document,
280    ) -> HTMLAreaElement {
281        HTMLAreaElement {
282            htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
283            rel_list: Default::default(),
284            relations: Cell::new(LinkRelations::empty()),
285            url: DomRefCell::new(None),
286        }
287    }
288
289    pub(crate) fn new(
290        local_name: LocalName,
291        prefix: Option<Prefix>,
292        document: &Document,
293        proto: Option<HandleObject>,
294        can_gc: CanGc,
295    ) -> DomRoot<HTMLAreaElement> {
296        Node::reflect_node_with_proto(
297            Box::new(HTMLAreaElement::new_inherited(local_name, prefix, document)),
298            document,
299            proto,
300            can_gc,
301        )
302    }
303
304    pub(crate) fn get_shape_from_coords(&self) -> Option<Area> {
305        let elem = self.upcast::<Element>();
306        let shape = elem.get_string_attribute(&"shape".into());
307        let shp: Shape = match_ignore_ascii_case! { &*shape.str(),
308           "circle" => Shape::Circle,
309           "circ" => Shape::Circle,
310           "rectangle" => Shape::Rectangle,
311           "rect" => Shape::Rectangle,
312           "polygon" => Shape::Rectangle,
313           "poly" => Shape::Polygon,
314           _ => return None,
315        };
316        if elem.has_attribute(&"coords".into()) {
317            let attribute = elem.get_string_attribute(&"coords".into());
318            Area::parse(&attribute.str(), shp)
319        } else {
320            None
321        }
322    }
323}
324
325impl HyperlinkElement for HTMLAreaElement {
326    fn get_url(&self) -> &DomRefCell<Option<ServoUrl>> {
327        &self.url
328    }
329}
330
331impl VirtualMethods for HTMLAreaElement {
332    fn super_type(&self) -> Option<&dyn VirtualMethods> {
333        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
334    }
335
336    fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue {
337        match name {
338            &local_name!("rel") => AttrValue::from_serialized_tokenlist(value.into()),
339            _ => self
340                .super_type()
341                .unwrap()
342                .parse_plain_attribute(name, value),
343        }
344    }
345
346    fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) {
347        self.super_type()
348            .unwrap()
349            .attribute_mutated(attr, mutation, can_gc);
350
351        match *attr.local_name() {
352            local_name!("href") => self
353                .upcast::<Element>()
354                .set_state(ElementState::UNVISITED, !mutation.is_removal()),
355            local_name!("rel") | local_name!("rev") => {
356                self.relations
357                    .set(LinkRelations::for_element(self.upcast()));
358            },
359            _ => {},
360        }
361    }
362
363    fn bind_to_tree(&self, context: &BindContext, can_gc: CanGc) {
364        if let Some(s) = self.super_type() {
365            s.bind_to_tree(context, can_gc);
366        }
367
368        self.relations
369            .set(LinkRelations::for_element(self.upcast()));
370    }
371}
372
373impl HTMLAreaElementMethods<crate::DomTypeHolder> for HTMLAreaElement {
374    // https://html.spec.whatwg.org/multipage/#attr-hyperlink-target
375    make_getter!(Target, "target");
376
377    // https://html.spec.whatwg.org/multipage/#attr-hyperlink-target
378    make_setter!(SetTarget, "target");
379
380    // https://html.spec.whatwg.org/multipage/#dom-a-rel
381    make_getter!(Rel, "rel");
382
383    /// <https://html.spec.whatwg.org/multipage/#dom-a-rel>
384    fn SetRel(&self, rel: DOMString, can_gc: CanGc) {
385        self.upcast::<Element>()
386            .set_tokenlist_attribute(&local_name!("rel"), rel, can_gc);
387    }
388
389    /// <https://html.spec.whatwg.org/multipage/#dom-area-rellist>
390    fn RelList(&self, can_gc: CanGc) -> DomRoot<DOMTokenList> {
391        self.rel_list.or_init(|| {
392            DOMTokenList::new(
393                self.upcast(),
394                &local_name!("rel"),
395                Some(vec![
396                    Atom::from("noopener"),
397                    Atom::from("noreferrer"),
398                    Atom::from("opener"),
399                ]),
400                can_gc,
401            )
402        })
403    }
404
405    /// <https://html.spec.whatwg.org/multipage/#attr-iframe-referrerpolicy>
406    fn ReferrerPolicy(&self) -> DOMString {
407        reflect_referrer_policy_attribute(self.upcast::<Element>())
408    }
409
410    // https://html.spec.whatwg.org/multipage/#attr-iframe-referrerpolicy
411    make_setter!(SetReferrerPolicy, "referrerpolicy");
412
413    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-href>
414    fn Href(&self) -> USVString {
415        self.get_href()
416    }
417
418    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-href>
419    fn SetHref(&self, value: USVString, can_gc: CanGc) {
420        self.set_href(value, can_gc);
421    }
422
423    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-origin>
424    fn Origin(&self) -> USVString {
425        self.get_origin()
426    }
427
428    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-protocol>
429    fn Protocol(&self) -> USVString {
430        self.get_protocol()
431    }
432
433    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-protocol>
434    fn SetProtocol(&self, value: USVString, can_gc: CanGc) {
435        self.set_protocol(value, can_gc);
436    }
437
438    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-password>
439    fn Password(&self) -> USVString {
440        self.get_password()
441    }
442
443    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-password>
444    fn SetPassword(&self, value: USVString, can_gc: CanGc) {
445        self.set_password(value, can_gc);
446    }
447
448    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-hash>
449    fn Hash(&self) -> USVString {
450        self.get_hash()
451    }
452
453    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-hash>
454    fn SetHash(&self, value: USVString, can_gc: CanGc) {
455        self.set_hash(value, can_gc);
456    }
457
458    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-host>
459    fn Host(&self) -> USVString {
460        self.get_host()
461    }
462
463    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-host>
464    fn SetHost(&self, value: USVString, can_gc: CanGc) {
465        self.set_host(value, can_gc);
466    }
467
468    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-hostname>
469    fn Hostname(&self) -> USVString {
470        self.get_hostname()
471    }
472
473    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-hostname>
474    fn SetHostname(&self, value: USVString, can_gc: CanGc) {
475        self.set_hostname(value, can_gc);
476    }
477
478    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-port>
479    fn Port(&self) -> USVString {
480        self.get_port()
481    }
482
483    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-port>
484    fn SetPort(&self, value: USVString, can_gc: CanGc) {
485        self.set_port(value, can_gc);
486    }
487
488    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-pathname>
489    fn Pathname(&self) -> USVString {
490        self.get_pathname()
491    }
492
493    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-pathname>
494    fn SetPathname(&self, value: USVString, can_gc: CanGc) {
495        self.set_pathname(value, can_gc);
496    }
497
498    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-search>
499    fn Search(&self) -> USVString {
500        self.get_search()
501    }
502
503    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-search>
504    fn SetSearch(&self, value: USVString, can_gc: CanGc) {
505        self.set_search(value, can_gc);
506    }
507
508    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-username>
509    fn Username(&self) -> USVString {
510        self.get_username()
511    }
512
513    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-username>
514    fn SetUsername(&self, value: USVString, can_gc: CanGc) {
515        self.set_username(value, can_gc);
516    }
517}
518
519impl Activatable for HTMLAreaElement {
520    /// <https://html.spec.whatwg.org/multipage/#the-area-element:activation-behaviour>
521    fn as_element(&self) -> &Element {
522        self.upcast::<Element>()
523    }
524
525    fn is_instance_activatable(&self) -> bool {
526        self.as_element().has_attribute(&local_name!("href"))
527    }
528
529    fn activation_behavior(&self, _event: &Event, _target: &EventTarget, _can_gc: CanGc) {
530        follow_hyperlink(self.as_element(), self.relations.get(), None);
531    }
532}