/src/serenity/Userland/Libraries/LibWeb/SVG/SVGUseElement.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2023, Preston Taylor <95388976+PrestonLTaylor@users.noreply.github.com> |
3 | | * Copyright (c) 2024, Shannon Booth <shannon@serenityos.org> |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #include <LibWeb/Bindings/Intrinsics.h> |
9 | | #include <LibWeb/Bindings/SVGUseElementPrototype.h> |
10 | | #include <LibWeb/DOM/Document.h> |
11 | | #include <LibWeb/DOM/DocumentLoadEventDelayer.h> |
12 | | #include <LibWeb/DOM/ElementFactory.h> |
13 | | #include <LibWeb/DOM/Event.h> |
14 | | #include <LibWeb/DOM/ShadowRoot.h> |
15 | | #include <LibWeb/HTML/PotentialCORSRequest.h> |
16 | | #include <LibWeb/Layout/Box.h> |
17 | | #include <LibWeb/Layout/SVGGraphicsBox.h> |
18 | | #include <LibWeb/Namespace.h> |
19 | | #include <LibWeb/SVG/AttributeNames.h> |
20 | | #include <LibWeb/SVG/SVGDecodedImageData.h> |
21 | | #include <LibWeb/SVG/SVGSVGElement.h> |
22 | | #include <LibWeb/SVG/SVGUseElement.h> |
23 | | |
24 | | namespace Web::SVG { |
25 | | |
26 | | JS_DEFINE_ALLOCATOR(SVGUseElement); |
27 | | |
28 | | SVGUseElement::SVGUseElement(DOM::Document& document, DOM::QualifiedName qualified_name) |
29 | 0 | : SVGGraphicsElement(document, qualified_name) |
30 | 0 | { |
31 | 0 | } |
32 | | |
33 | | void SVGUseElement::initialize(JS::Realm& realm) |
34 | 0 | { |
35 | 0 | Base::initialize(realm); |
36 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGUseElement); |
37 | | |
38 | | // The shadow tree is open (inspectable by script), but read-only. |
39 | 0 | auto shadow_root = heap().allocate<DOM::ShadowRoot>(realm, document(), *this, Bindings::ShadowRootMode::Open); |
40 | | |
41 | | // The user agent must create a use-element shadow tree whose host is the ‘use’ element itself |
42 | 0 | set_shadow_root(shadow_root); |
43 | |
|
44 | 0 | m_document_observer = realm.heap().allocate<DOM::DocumentObserver>(realm, realm, document()); |
45 | 0 | m_document_observer->set_document_completely_loaded([this]() { |
46 | 0 | clone_element_tree_as_our_shadow_tree(referenced_element()); |
47 | 0 | }); |
48 | 0 | } |
49 | | |
50 | | void SVGUseElement::visit_edges(Cell::Visitor& visitor) |
51 | 0 | { |
52 | 0 | Base::visit_edges(visitor); |
53 | 0 | SVGURIReferenceMixin::visit_edges(visitor); |
54 | 0 | visitor.visit(m_document_observer); |
55 | 0 | visitor.visit(m_resource_request); |
56 | 0 | } |
57 | | |
58 | | void SVGUseElement::attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value) |
59 | 0 | { |
60 | 0 | Base::attribute_changed(name, old_value, value); |
61 | | |
62 | | // https://svgwg.org/svg2-draft/struct.html#UseLayout |
63 | 0 | if (name == SVG::AttributeNames::x) { |
64 | 0 | m_x = AttributeParser::parse_coordinate(value.value_or(String {})); |
65 | 0 | } else if (name == SVG::AttributeNames::y) { |
66 | 0 | m_y = AttributeParser::parse_coordinate(value.value_or(String {})); |
67 | 0 | } else if (name == SVG::AttributeNames::href || name == "xlink:href"_fly_string) { |
68 | | // When the ‘href’ attribute is set (or, in the absence of an ‘href’ attribute, an ‘xlink:href’ attribute), the user agent must process the URL. |
69 | 0 | process_the_url(value); |
70 | 0 | } |
71 | 0 | } |
72 | | |
73 | | // https://www.w3.org/TR/SVG2/linking.html#processingURL |
74 | | void SVGUseElement::process_the_url(Optional<String> const& href) |
75 | 0 | { |
76 | | // In all other cases, the URL is for a resource to be used in this SVG document. The user agent |
77 | | // must parse the URL to separate out the target fragment from the rest of the URL, and compare |
78 | | // it with the document base URL. If all parts other than the target fragment are equal, this is |
79 | | // a same-document URL reference, and processing the URL must continue as indicated in Identifying |
80 | | // the target element with the current document as the referenced document. |
81 | 0 | m_href = document().url().complete_url(href.value_or(String {})); |
82 | 0 | if (!m_href.is_valid()) |
83 | 0 | return; |
84 | | |
85 | 0 | if (is_referrenced_element_same_document()) { |
86 | 0 | clone_element_tree_as_our_shadow_tree(referenced_element()); |
87 | 0 | } else { |
88 | 0 | fetch_the_document(m_href); |
89 | 0 | } |
90 | 0 | } |
91 | | |
92 | | bool SVGUseElement::is_referrenced_element_same_document() const |
93 | 0 | { |
94 | 0 | return m_href.equals(document().url(), URL::ExcludeFragment::Yes); |
95 | 0 | } |
96 | | |
97 | | Gfx::AffineTransform SVGUseElement::element_transform() const |
98 | 0 | { |
99 | | // The x and y properties define an additional transformation (translate(x,y), where x and y represent the computed value of the corresponding property) |
100 | | // to be applied to the ‘use’ element, after any transformations specified with other properties |
101 | 0 | return Base::element_transform().translate(m_x.value_or(0), m_y.value_or(0)); |
102 | 0 | } |
103 | | |
104 | | void SVGUseElement::inserted() |
105 | 0 | { |
106 | 0 | Base::inserted(); |
107 | 0 | } |
108 | | |
109 | | void SVGUseElement::svg_element_changed(SVGElement& svg_element) |
110 | 0 | { |
111 | 0 | auto to_clone = referenced_element(); |
112 | 0 | if (!to_clone) { |
113 | 0 | return; |
114 | 0 | } |
115 | | |
116 | | // NOTE: We need to check the ancestor because attribute_changed of a child doesn't call children_changed on the parent(s) |
117 | 0 | if (to_clone == &svg_element || to_clone->is_ancestor_of(svg_element)) { |
118 | 0 | clone_element_tree_as_our_shadow_tree(to_clone); |
119 | 0 | } |
120 | 0 | } |
121 | | |
122 | | void SVGUseElement::svg_element_removed(SVGElement& svg_element) |
123 | 0 | { |
124 | 0 | if (!m_href.fragment().has_value() || !is_referrenced_element_same_document()) { |
125 | 0 | return; |
126 | 0 | } |
127 | | |
128 | 0 | if (AK::StringUtils::matches(svg_element.get_attribute_value("id"_fly_string), m_href.fragment().value())) { |
129 | 0 | shadow_root()->remove_all_children(); |
130 | 0 | } |
131 | 0 | } |
132 | | |
133 | | // https://svgwg.org/svg2-draft/linking.html#processingURL-target |
134 | | JS::GCPtr<DOM::Element> SVGUseElement::referenced_element() |
135 | 0 | { |
136 | 0 | if (!m_href.is_valid()) |
137 | 0 | return nullptr; |
138 | | |
139 | 0 | if (!m_href.fragment().has_value()) |
140 | 0 | return nullptr; |
141 | | |
142 | 0 | if (is_referrenced_element_same_document()) |
143 | 0 | return document().get_element_by_id(*m_href.fragment()); |
144 | | |
145 | 0 | if (!m_resource_request) |
146 | 0 | return nullptr; |
147 | | |
148 | 0 | auto data = m_resource_request->image_data(); |
149 | 0 | if (!data || !is<SVG::SVGDecodedImageData>(*data)) |
150 | 0 | return nullptr; |
151 | | |
152 | 0 | return verify_cast<SVG::SVGDecodedImageData>(*data).svg_document().get_element_by_id(*m_href.fragment()); |
153 | 0 | } |
154 | | |
155 | | // https://svgwg.org/svg2-draft/linking.html#processingURL-fetch |
156 | | void SVGUseElement::fetch_the_document(URL::URL const& url) |
157 | 0 | { |
158 | 0 | m_load_event_delayer.emplace(document()); |
159 | 0 | m_resource_request = HTML::SharedResourceRequest::get_or_create(realm(), document().page(), url); |
160 | 0 | m_resource_request->add_callbacks( |
161 | 0 | [this] { |
162 | 0 | clone_element_tree_as_our_shadow_tree(referenced_element()); |
163 | 0 | m_load_event_delayer.clear(); |
164 | 0 | }, |
165 | 0 | [this] { |
166 | 0 | m_load_event_delayer.clear(); |
167 | 0 | }); |
168 | |
|
169 | 0 | if (m_resource_request->needs_fetching()) { |
170 | 0 | auto request = HTML::create_potential_CORS_request(vm(), url, Fetch::Infrastructure::Request::Destination::Image, HTML::CORSSettingAttribute::NoCORS); |
171 | 0 | request->set_client(&document().relevant_settings_object()); |
172 | 0 | m_resource_request->fetch_resource(realm(), request); |
173 | 0 | } |
174 | 0 | } |
175 | | |
176 | | // https://svgwg.org/svg2-draft/struct.html#UseShadowTree |
177 | | void SVGUseElement::clone_element_tree_as_our_shadow_tree(Element* to_clone) |
178 | 0 | { |
179 | 0 | shadow_root()->remove_all_children(); |
180 | |
|
181 | 0 | if (to_clone && is_valid_reference_element(*to_clone)) { |
182 | | // The ‘use’ element references another element, a copy of which is rendered in place of the ‘use’ in the document. |
183 | 0 | auto cloned_reference_node = MUST(to_clone->clone_node(nullptr, true)); |
184 | 0 | shadow_root()->append_child(cloned_reference_node).release_value_but_fixme_should_propagate_errors(); |
185 | 0 | } |
186 | 0 | } |
187 | | |
188 | | bool SVGUseElement::is_valid_reference_element(Element const& reference_element) const |
189 | 0 | { |
190 | | // If the referenced element that results from resolving the URL is not an SVG element, then the reference is invalid and the ‘use’ element is in error. |
191 | | // If the referenced element is a (shadow-including) ancestor of the ‘use’ element, then this is an invalid circular reference and the ‘use’ element is in error. |
192 | 0 | return reference_element.is_svg_element() && !reference_element.is_ancestor_of(*this); |
193 | 0 | } |
194 | | |
195 | | // https://www.w3.org/TR/SVG11/shapes.html#RectElementXAttribute |
196 | | JS::NonnullGCPtr<SVGAnimatedLength> SVGUseElement::x() const |
197 | 0 | { |
198 | | // FIXME: Populate the unit type when it is parsed (0 here is "unknown"). |
199 | | // FIXME: Create a proper animated value when animations are supported. |
200 | 0 | auto base_length = SVGLength::create(realm(), 0, m_x.value_or(0)); |
201 | 0 | auto anim_length = SVGLength::create(realm(), 0, m_x.value_or(0)); |
202 | 0 | return SVGAnimatedLength::create(realm(), move(base_length), move(anim_length)); |
203 | 0 | } |
204 | | |
205 | | // https://www.w3.org/TR/SVG11/shapes.html#RectElementYAttribute |
206 | | JS::NonnullGCPtr<SVGAnimatedLength> SVGUseElement::y() const |
207 | 0 | { |
208 | | // FIXME: Populate the unit type when it is parsed (0 here is "unknown"). |
209 | | // FIXME: Create a proper animated value when animations are supported. |
210 | 0 | auto base_length = SVGLength::create(realm(), 0, m_y.value_or(0)); |
211 | 0 | auto anim_length = SVGLength::create(realm(), 0, m_y.value_or(0)); |
212 | 0 | return SVGAnimatedLength::create(realm(), move(base_length), move(anim_length)); |
213 | 0 | } |
214 | | |
215 | | JS::NonnullGCPtr<SVGAnimatedLength> SVGUseElement::width() const |
216 | 0 | { |
217 | | // FIXME: Implement this properly. |
218 | 0 | return SVGAnimatedLength::create(realm(), SVGLength::create(realm(), 0, 0), SVGLength::create(realm(), 0, 0)); |
219 | 0 | } |
220 | | |
221 | | JS::NonnullGCPtr<SVGAnimatedLength> SVGUseElement::height() const |
222 | 0 | { |
223 | | // FIXME: Implement this properly. |
224 | 0 | return SVGAnimatedLength::create(realm(), SVGLength::create(realm(), 0, 0), SVGLength::create(realm(), 0, 0)); |
225 | 0 | } |
226 | | |
227 | | // https://svgwg.org/svg2-draft/struct.html#TermInstanceRoot |
228 | | JS::GCPtr<SVGElement> SVGUseElement::instance_root() const |
229 | 0 | { |
230 | 0 | return const_cast<DOM::ShadowRoot&>(*shadow_root()).first_child_of_type<SVGElement>(); |
231 | 0 | } |
232 | | |
233 | | JS::GCPtr<SVGElement> SVGUseElement::animated_instance_root() const |
234 | 0 | { |
235 | 0 | return instance_root(); |
236 | 0 | } |
237 | | |
238 | | JS::GCPtr<Layout::Node> SVGUseElement::create_layout_node(NonnullRefPtr<CSS::StyleProperties> style) |
239 | 0 | { |
240 | 0 | return heap().allocate_without_realm<Layout::SVGGraphicsBox>(document(), *this, move(style)); |
241 | 0 | } |
242 | | |
243 | | } |