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.upcast::<Element>().set_state(
366                cx.no_gc(),
367                ElementState::UNVISITED,
368                !mutation.is_removal(),
369            ),
370            local_name!("rel") | local_name!("rev") => {
371                self.relations
372                    .set(LinkRelations::for_element(self.upcast()));
373            },
374            _ => {},
375        }
376    }
377}
378
379impl HTMLAreaElementMethods<crate::DomTypeHolder> for HTMLAreaElement {
380    // https://html.spec.whatwg.org/multipage/#attr-hyperlink-target
381    make_getter!(Target, "target");
382
383    // https://html.spec.whatwg.org/multipage/#attr-hyperlink-target
384    make_setter!(SetTarget, "target");
385
386    // https://html.spec.whatwg.org/multipage/#dom-a-rel
387    make_getter!(Rel, "rel");
388
389    /// <https://html.spec.whatwg.org/multipage/#dom-a-rel>
390    fn SetRel(&self, cx: &mut JSContext, rel: DOMString) {
391        self.upcast::<Element>()
392            .set_tokenlist_attribute(cx, &local_name!("rel"), rel);
393    }
394
395    /// <https://html.spec.whatwg.org/multipage/#dom-area-rellist>
396    fn RelList(&self, cx: &mut JSContext) -> DomRoot<DOMTokenList> {
397        self.rel_list.or_init(|| {
398            DOMTokenList::new(
399                cx,
400                self.upcast(),
401                &local_name!("rel"),
402                Some(vec![
403                    Atom::from("noopener"),
404                    Atom::from("noreferrer"),
405                    Atom::from("opener"),
406                ]),
407            )
408        })
409    }
410
411    /// <https://html.spec.whatwg.org/multipage/#attr-iframe-referrerpolicy>
412    fn ReferrerPolicy(&self) -> DOMString {
413        reflect_referrer_policy_attribute(self.upcast::<Element>())
414    }
415
416    // https://html.spec.whatwg.org/multipage/#attr-iframe-referrerpolicy
417    make_setter!(SetReferrerPolicy, "referrerpolicy");
418
419    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-href>
420    fn Href(&self, no_gc: &NoGC) -> USVString {
421        self.get_href(no_gc)
422    }
423
424    // https://html.spec.whatwg.org/multipage/#dom-hyperlink-href
425    make_url_setter!(SetHref, "href");
426
427    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-origin>
428    fn Origin(&self, no_gc: &NoGC) -> USVString {
429        self.get_origin(no_gc)
430    }
431
432    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-protocol>
433    fn Protocol(&self, no_gc: &NoGC) -> USVString {
434        self.get_protocol(no_gc)
435    }
436
437    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-protocol>
438    fn SetProtocol(&self, cx: &mut JSContext, value: USVString) {
439        self.set_protocol(cx, value);
440    }
441
442    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-password>
443    fn Password(&self, no_gc: &NoGC) -> USVString {
444        self.get_password(no_gc)
445    }
446
447    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-password>
448    fn SetPassword(&self, cx: &mut JSContext, value: USVString) {
449        self.set_password(cx, value);
450    }
451
452    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-hash>
453    fn Hash(&self, no_gc: &NoGC) -> USVString {
454        self.get_hash(no_gc)
455    }
456
457    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-hash>
458    fn SetHash(&self, cx: &mut JSContext, value: USVString) {
459        self.set_hash(cx, value);
460    }
461
462    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-host>
463    fn Host(&self, no_gc: &NoGC) -> USVString {
464        self.get_host(no_gc)
465    }
466
467    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-host>
468    fn SetHost(&self, cx: &mut JSContext, value: USVString) {
469        self.set_host(cx, value);
470    }
471
472    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-hostname>
473    fn Hostname(&self, no_gc: &NoGC) -> USVString {
474        self.get_hostname(no_gc)
475    }
476
477    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-hostname>
478    fn SetHostname(&self, cx: &mut JSContext, value: USVString) {
479        self.set_hostname(cx, value);
480    }
481
482    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-port>
483    fn Port(&self, no_gc: &NoGC) -> USVString {
484        self.get_port(no_gc)
485    }
486
487    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-port>
488    fn SetPort(&self, cx: &mut JSContext, value: USVString) {
489        self.set_port(cx, value);
490    }
491
492    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-pathname>
493    fn Pathname(&self, no_gc: &NoGC) -> USVString {
494        self.get_pathname(no_gc)
495    }
496
497    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-pathname>
498    fn SetPathname(&self, cx: &mut JSContext, value: USVString) {
499        self.set_pathname(cx, value);
500    }
501
502    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-search>
503    fn Search(&self, no_gc: &NoGC) -> USVString {
504        self.get_search(no_gc)
505    }
506
507    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-search>
508    fn SetSearch(&self, cx: &mut JSContext, value: USVString) {
509        self.set_search(cx, value);
510    }
511
512    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-username>
513    fn Username(&self, no_gc: &NoGC) -> USVString {
514        self.get_username(no_gc)
515    }
516
517    /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-username>
518    fn SetUsername(&self, cx: &mut JSContext, value: USVString) {
519        self.set_username(cx, value);
520    }
521
522    // https://html.spec.whatwg.org/multipage/#dom-area-nohref
523    make_bool_getter!(NoHref, "nohref");
524
525    // https://html.spec.whatwg.org/multipage/#dom-area-nohref
526    make_bool_setter!(SetNoHref, "nohref");
527}
528
529impl Activatable for HTMLAreaElement {
530    /// <https://html.spec.whatwg.org/multipage/#the-area-element:activation-behaviour>
531    fn as_element(&self) -> &Element {
532        self.upcast::<Element>()
533    }
534
535    fn is_instance_activatable(&self) -> bool {
536        self.as_element().has_attribute(&local_name!("href"))
537    }
538
539    fn activation_behavior(
540        &self,
541        cx: &mut js::context::JSContext,
542        _event: &Event,
543        _target: &EventTarget,
544    ) {
545        follow_hyperlink(cx, self.as_element(), self.relations.get(), None);
546    }
547}