Skip to main content

script/dom/svg/
svgelement.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 dom_struct::dom_struct;
6use html5ever::{LocalName, Prefix, local_name, ns};
7use js::context::JSContext;
8use js::rust::HandleObject;
9use script_bindings::codegen::GenericBindings::ElementBinding::ScrollLogicalPosition;
10use script_bindings::codegen::GenericBindings::WindowBinding::ScrollBehavior;
11use script_bindings::str::DOMString;
12use style::attr::AttrValue;
13use style::parser::ParserContext;
14use style::properties::{PropertyDeclaration, longhands};
15use style::stylesheets::{CssRuleType, Origin, UrlExtraData};
16use style::values::generics::NonNegative;
17use style::values::specified;
18use style_traits::ParsingMode;
19use stylo_dom::ElementState;
20
21use crate::dom::bindings::codegen::Bindings::HTMLOrSVGElementBinding::FocusOptions;
22use crate::dom::bindings::codegen::Bindings::SVGElementBinding::SVGElementMethods;
23use crate::dom::bindings::inheritance::Castable;
24use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, MutNullableDom};
25use crate::dom::css::cssstyledeclaration::{
26    CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner,
27};
28use crate::dom::document::Document;
29use crate::dom::document::focus::FocusableArea;
30use crate::dom::element::attributes::storage::AttrRef;
31use crate::dom::element::{AttributeMutation, Element};
32use crate::dom::node::virtualmethods::VirtualMethods;
33use crate::dom::node::{Node, NodeTraits};
34use crate::dom::scrolling_box::{ScrollAxisState, ScrollRequirement};
35use crate::dom::svg::svgcircleelement::SVGCircleElement;
36use crate::dom::svg::svgellipseelement::SVGEllipseElement;
37use crate::dom::svg::svgimageelement::SVGImageElement;
38use crate::dom::svg::svgpathelement::SVGPathElement;
39use crate::dom::svg::svgrectelement::SVGRectElement;
40use crate::dom::svg::svgsvgelement::SVGSVGElement;
41
42#[dom_struct]
43pub(crate) struct SVGElement {
44    element: Element,
45    style_decl: MutNullableDom<CSSStyleDeclaration>,
46}
47
48impl SVGElement {
49    fn new_inherited(
50        tag_name: LocalName,
51        prefix: Option<Prefix>,
52        document: &Document,
53    ) -> SVGElement {
54        SVGElement::new_inherited_with_state(ElementState::empty(), tag_name, prefix, document)
55    }
56
57    pub(crate) fn new_inherited_with_state(
58        state: ElementState,
59        tag_name: LocalName,
60        prefix: Option<Prefix>,
61        document: &Document,
62    ) -> SVGElement {
63        SVGElement {
64            element: Element::new_inherited_with_state(state, tag_name, ns!(svg), prefix, document),
65            style_decl: Default::default(),
66        }
67    }
68
69    pub(crate) fn new(
70        cx: &mut js::context::JSContext,
71        tag_name: LocalName,
72        prefix: Option<Prefix>,
73        document: &Document,
74        proto: Option<HandleObject>,
75    ) -> DomRoot<SVGElement> {
76        Node::reflect_node_with_proto(
77            cx,
78            Box::new(SVGElement::new_inherited(tag_name, prefix, document)),
79            document,
80            proto,
81        )
82    }
83
84    fn as_element(&self) -> &Element {
85        self.upcast::<Element>()
86    }
87}
88
89impl VirtualMethods for SVGElement {
90    fn super_type(&self) -> Option<&dyn VirtualMethods> {
91        Some(self.as_element() as &dyn VirtualMethods)
92    }
93
94    fn attribute_mutated(
95        &self,
96        cx: &mut js::context::JSContext,
97        attr: AttrRef<'_>,
98        mutation: AttributeMutation,
99    ) {
100        self.super_type()
101            .unwrap()
102            .attribute_mutated(cx, attr, mutation);
103        let element = self.as_element();
104        if let (&local_name!("nonce"), mutation) = (attr.local_name(), mutation) {
105            match mutation {
106                AttributeMutation::Set(..) => {
107                    let nonce = &**attr.value();
108                    element.update_nonce_internal_slot(nonce.to_owned());
109                },
110                AttributeMutation::Removed => {
111                    element.update_nonce_internal_slot(String::new());
112                },
113            }
114        }
115    }
116
117    fn attribute_affects_presentational_hints(&self, attr: AttrRef<'_>) -> bool {
118        matches!(
119            attr.local_name(),
120            &local_name!("fill") |
121                &local_name!("fill-opacity") |
122                &local_name!("fill-rule") |
123                &local_name!("stroke") |
124                &local_name!("stroke-width") |
125                &local_name!("stroke-linecap") |
126                &local_name!("stroke-linejoin") |
127                &local_name!("stroke-dasharray") |
128                &local_name!("stroke-dashoffset") |
129                &local_name!("stroke-miterlimit") |
130                &local_name!("stroke-opacity") |
131                &local_name!("display") |
132                &local_name!("visibility") |
133                &local_name!("opacity") |
134                &local_name!("cx") |
135                &local_name!("cy") |
136                &local_name!("r") |
137                &local_name!("rx") |
138                &local_name!("ry") |
139                &local_name!("x") |
140                &local_name!("y") |
141                &local_name!("width") |
142                &local_name!("height") |
143                &local_name!("d")
144        ) || self
145            .super_type()
146            .unwrap()
147            .attribute_affects_presentational_hints(attr)
148    }
149}
150
151impl SVGElementMethods<crate::DomTypeHolder> for SVGElement {
152    /// <https://html.spec.whatwg.org/multipage/#the-style-attribute>
153    fn Style(&self, cx: &mut JSContext) -> DomRoot<CSSStyleDeclaration> {
154        self.style_decl.or_init(|| {
155            let global = self.owner_window();
156            CSSStyleDeclaration::new(
157                cx,
158                &global,
159                CSSStyleOwner::Element(Dom::from_ref(self.upcast())),
160                None,
161                CSSModificationAccess::ReadWrite,
162            )
163        })
164    }
165
166    // https://html.spec.whatwg.org/multipage/#globaleventhandlers
167    global_event_handlers!();
168
169    /// <https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce>
170    fn Nonce(&self) -> DOMString {
171        self.as_element().nonce_value().into()
172    }
173
174    /// <https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce>
175    fn SetNonce(&self, _cx: &mut JSContext, value: DOMString) {
176        self.as_element()
177            .update_nonce_internal_slot(String::from(value))
178    }
179
180    /// <https://html.spec.whatwg.org/multipage/#dom-fe-autofocus>
181    fn Autofocus(&self) -> bool {
182        self.element.has_attribute(&local_name!("autofocus"))
183    }
184
185    /// <https://html.spec.whatwg.org/multipage/#dom-fe-autofocus>
186    fn SetAutofocus(&self, cx: &mut JSContext, autofocus: bool) {
187        self.element
188            .set_bool_attribute(cx, &local_name!("autofocus"), autofocus);
189    }
190
191    /// <https://html.spec.whatwg.org/multipage/#dom-focus>
192    fn Focus(&self, cx: &mut js::context::JSContext, options: &FocusOptions) {
193        // 1. If the allow focus steps given this's node document return false, then return.
194        // TODO: Implement this.
195
196        // 2. Run the focusing steps for this.
197        if !self.upcast::<Node>().run_the_focusing_steps(cx, None) {
198            // The specification seems to imply we should scroll into view even if this element
199            // is not a focusable area. No browser does this, so we return early in that case.
200            // See https://github.com/whatwg/html/issues/12231.
201            return;
202        }
203
204        // > 3. If options["focusVisible"] is true, or does not exist but in an
205        // >    implementation-defined  way the user agent determines it would be best to do so,
206        // >    then indicate focus. TODO: Implement this.
207        // TODO: Implement this.
208
209        // > 4. If options["preventScroll"] is false, then scroll a target into view given this,
210        // >    "auto", "center", and "center".
211        if !options.preventScroll {
212            let scroll_axis = ScrollAxisState {
213                position: ScrollLogicalPosition::Center,
214                requirement: ScrollRequirement::IfNotVisible,
215            };
216            self.upcast::<Element>().scroll_into_view_with_options(
217                cx,
218                ScrollBehavior::Smooth,
219                scroll_axis,
220                scroll_axis,
221                None,
222                None,
223            );
224        }
225    }
226
227    /// <https://html.spec.whatwg.org/multipage/#dom-blur>
228    fn Blur(&self, cx: &mut js::context::JSContext) {
229        // TODO: Run the unfocusing steps. Focus the top-level document, not
230        //       the current document.
231        if !self.as_element().focus_state() {
232            return;
233        }
234        // <https://html.spec.whatwg.org/multipage/#unfocusing-steps>
235        self.owner_document()
236            .focus_handler()
237            .focus(cx, FocusableArea::Viewport);
238    }
239
240    /// <https://html.spec.whatwg.org/multipage/#dom-tabindex>
241    fn TabIndex(&self) -> i32 {
242        self.element.tab_index()
243    }
244
245    /// <https://html.spec.whatwg.org/multipage/#dom-tabindex>
246    fn SetTabIndex(&self, cx: &mut JSContext, tab_index: i32) {
247        self.element
248            .set_attribute(cx, &local_name!("tabindex"), tab_index.into());
249    }
250}
251
252impl<'dom> LayoutDom<'dom, SVGElement> {
253    pub(crate) fn synthesize_presentational_hints(
254        self,
255        document: LayoutDom<'dom, Document>,
256        push: &mut impl FnMut(PropertyDeclaration),
257    ) {
258        let element = self.upcast::<Element>();
259
260        if element.is::<SVGSVGElement>() {
261            if let Some(width) = element
262                .get_attr_for_layout(&ns!(), &local_name!("width"))
263                .and_then(AttrValue::as_length_percentage)
264            {
265                push(PropertyDeclaration::Width(
266                    specified::Size::LengthPercentage(NonNegative(width.clone())),
267                ));
268            }
269            if let Some(height) = element
270                .get_attr_for_layout(&ns!(), &local_name!("height"))
271                .and_then(AttrValue::as_length_percentage)
272            {
273                push(PropertyDeclaration::Height(
274                    specified::Size::LengthPercentage(NonNegative(height.clone())),
275                ));
276            }
277        }
278        let url_data = UrlExtraData(document.url_for_layout().get_arc());
279        let parsing_mode =
280            ParsingMode::ALLOW_UNITLESS_LENGTH | ParsingMode::ALLOW_ALL_NUMERIC_VALUES;
281        let parser_context = ParserContext::new(
282            Origin::Author,
283            &url_data,
284            Some(CssRuleType::Style),
285            parsing_mode,
286            document.quirks_mode(),
287            Default::default(),
288            None,
289            None,
290            Default::default(),
291        );
292
293        self.parse_svg_attribute(
294            &parser_context,
295            "fill",
296            longhands::fill::parse_declared,
297            push,
298        );
299        self.parse_svg_attribute(
300            &parser_context,
301            "fill-opacity",
302            longhands::fill_opacity::parse_declared,
303            push,
304        );
305        self.parse_svg_attribute(
306            &parser_context,
307            "fill-rule",
308            longhands::fill_rule::parse_declared,
309            push,
310        );
311
312        self.parse_svg_attribute(
313            &parser_context,
314            "stroke",
315            longhands::stroke::parse_declared,
316            push,
317        );
318        self.parse_svg_attribute(
319            &parser_context,
320            "stroke-width",
321            longhands::stroke_width::parse_declared,
322            push,
323        );
324        self.parse_svg_attribute(
325            &parser_context,
326            "stroke-linecap",
327            longhands::stroke_linecap::parse_declared,
328            push,
329        );
330        self.parse_svg_attribute(
331            &parser_context,
332            "stroke-linejoin",
333            longhands::stroke_linejoin::parse_declared,
334            push,
335        );
336        self.parse_svg_attribute(
337            &parser_context,
338            "stroke-dasharray",
339            longhands::stroke_dasharray::parse_declared,
340            push,
341        );
342        self.parse_svg_attribute(
343            &parser_context,
344            "stroke-dashoffset",
345            longhands::stroke_dashoffset::parse_declared,
346            push,
347        );
348        self.parse_svg_attribute(
349            &parser_context,
350            "stroke-miterlimit",
351            longhands::stroke_miterlimit::parse_declared,
352            push,
353        );
354        self.parse_svg_attribute(
355            &parser_context,
356            "stroke-opacity",
357            longhands::stroke_opacity::parse_declared,
358            push,
359        );
360        self.parse_svg_attribute(
361            &parser_context,
362            "display",
363            longhands::display::parse_declared,
364            push,
365        );
366        self.parse_svg_attribute(
367            &parser_context,
368            "visibility",
369            longhands::visibility::parse_declared,
370            push,
371        );
372        self.parse_svg_attribute(
373            &parser_context,
374            "opacity",
375            longhands::opacity::parse_declared,
376            push,
377        );
378
379        // Parse geometry attributes based on element type
380        // <circle>: https://svgwg.org/svg2-draft/shapes.html#CircleElement
381        if element.downcast::<SVGCircleElement>().is_some() {
382            self.parse_svg_attribute(&parser_context, "cx", longhands::cx::parse_declared, push);
383            self.parse_svg_attribute(&parser_context, "cy", longhands::cy::parse_declared, push);
384            self.parse_svg_attribute(&parser_context, "r", longhands::r::parse_declared, push);
385        }
386        // <ellipse>: https://svgwg.org/svg2-draft/shapes.html#EllipseElement
387        if element.downcast::<SVGEllipseElement>().is_some() {
388            self.parse_svg_attribute(&parser_context, "cx", longhands::cx::parse_declared, push);
389            self.parse_svg_attribute(&parser_context, "cy", longhands::cy::parse_declared, push);
390            self.parse_svg_attribute(&parser_context, "rx", longhands::rx::parse_declared, push);
391            self.parse_svg_attribute(&parser_context, "ry", longhands::ry::parse_declared, push);
392        }
393        // <rect>: https://svgwg.org/svg2-draft/shapes.html#RectElement
394        if element.downcast::<SVGRectElement>().is_some() {
395            self.parse_svg_attribute(&parser_context, "x", longhands::x::parse_declared, push);
396            self.parse_svg_attribute(&parser_context, "y", longhands::y::parse_declared, push);
397            self.parse_svg_attribute(
398                &parser_context,
399                "width",
400                longhands::width::parse_declared,
401                push,
402            );
403            self.parse_svg_attribute(
404                &parser_context,
405                "height",
406                longhands::height::parse_declared,
407                push,
408            );
409            self.parse_svg_attribute(&parser_context, "rx", longhands::rx::parse_declared, push);
410            self.parse_svg_attribute(&parser_context, "ry", longhands::ry::parse_declared, push);
411        }
412        // <image>: https://svgwg.org/svg2-draft/embedded.html#ImageElement
413        if element.downcast::<SVGImageElement>().is_some() {
414            self.parse_svg_attribute(&parser_context, "x", longhands::x::parse_declared, push);
415            self.parse_svg_attribute(&parser_context, "y", longhands::y::parse_declared, push);
416            self.parse_svg_attribute(
417                &parser_context,
418                "width",
419                longhands::width::parse_declared,
420                push,
421            );
422            self.parse_svg_attribute(
423                &parser_context,
424                "height",
425                longhands::height::parse_declared,
426                push,
427            );
428        }
429        // <path>: https://svgwg.org/svg2-draft/paths.html#PathElement
430        if element.downcast::<SVGPathElement>().is_some() {
431            // The d CSS property only accepts `none` or `path(<string>)`,
432            // but the SVG presentation attribute uses raw path data (e.g. "M0,0 L1,1").
433            // Wrap the raw path data in `path("...")` so the CSS parser can handle it.
434            if let Some(value) = element.get_attr_val_for_layout(&ns!(), &local_name!("d")) {
435                if value.eq_ignore_ascii_case("none") {
436                    let mut input = cssparser::ParserInput::new(value);
437                    let mut parser = cssparser::Parser::new(&mut input);
438                    if let Ok(property) =
439                        parser.parse_entirely(|i| longhands::d::parse_declared(&parser_context, i))
440                    {
441                        push(property);
442                    }
443                } else {
444                    let wrapped = format!("path(\"{}\")", value);
445                    let mut input = cssparser::ParserInput::new(&wrapped);
446                    let mut parser = cssparser::Parser::new(&mut input);
447                    if let Ok(property) = parser.parse_entirely(|parse_input| {
448                        longhands::d::parse_declared(&parser_context, parse_input)
449                    }) {
450                        push(property);
451                    }
452                }
453            }
454        }
455    }
456
457    fn parse_svg_attribute<F>(
458        self,
459        parser_context: &ParserContext,
460        attr_name: &str,
461        parse: F,
462        push: &mut impl FnMut(PropertyDeclaration),
463    ) where
464        F: for<'i, 't> FnOnce(
465            &ParserContext,
466            &mut cssparser::Parser<'i, 't>,
467        ) -> Result<PropertyDeclaration, style_traits::ParseError<'i>>,
468    {
469        let element = self.upcast::<Element>();
470        if let Some(value) = element.get_attr_val_for_layout(&ns!(), &LocalName::from(attr_name)) {
471            let mut input = cssparser::ParserInput::new(value);
472            let mut parser = cssparser::Parser::new(&mut input);
473            if let Ok(property) =
474                parser.parse_entirely(|parse_input| parse(parser_context, parse_input))
475            {
476                push(property);
477            }
478        }
479    }
480}