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