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::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
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!(cx, 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, cx: &mut JSContext, rel: DOMString) {
382        self.upcast::<Element>().set_tokenlist_attribute(
383            &local_name!("rel"),
384            rel,
385            CanGc::from_cx(cx),
386        );
387    }
388
389    /// <https://html.spec.whatwg.org/multipage/#dom-area-rellist>
390    fn RelList(&self, cx: &mut JSContext) -> 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                CanGc::from_cx(cx),
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!(cx, 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, cx: &mut JSContext, value: USVString) {
420        self.set_href(cx, value);
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, cx: &mut JSContext, value: USVString) {
435        self.set_protocol(cx, value);
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, cx: &mut JSContext, value: USVString) {
445        self.set_password(cx, value);
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, cx: &mut JSContext, value: USVString) {
455        self.set_hash(cx, value);
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, cx: &mut JSContext, value: USVString) {
465        self.set_host(cx, value);
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, cx: &mut JSContext, value: USVString) {
475        self.set_hostname(cx, value);
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, cx: &mut JSContext, value: USVString) {
485        self.set_port(cx, value);
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, cx: &mut JSContext, value: USVString) {
495        self.set_pathname(cx, value);
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, cx: &mut JSContext, value: USVString) {
505        self.set_search(cx, value);
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, cx: &mut JSContext, value: USVString) {
515        self.set_username(cx, value);
516    }
517
518    // https://html.spec.whatwg.org/multipage/#dom-area-nohref
519    make_bool_getter!(NoHref, "nohref");
520
521    // https://html.spec.whatwg.org/multipage/#dom-area-nohref
522    make_bool_setter!(cx, SetNoHref, "nohref");
523}
524
525impl Activatable for HTMLAreaElement {
526    /// <https://html.spec.whatwg.org/multipage/#the-area-element:activation-behaviour>
527    fn as_element(&self) -> &Element {
528        self.upcast::<Element>()
529    }
530
531    fn is_instance_activatable(&self) -> bool {
532        self.as_element().has_attribute(&local_name!("href"))
533    }
534
535    fn activation_behavior(
536        &self,
537        _cx: &mut js::context::JSContext,
538        _event: &Event,
539        _target: &EventTarget,
540    ) {
541        follow_hyperlink(self.as_element(), self.relations.get(), None);
542    }
543}