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