Skip to main content

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