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