Coverage Report

Created: 2026-02-16 07:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}