ladybird/Libraries/LibWeb/SVG/SVGElement.cpp
Andreas Kling c419bb526b LibWeb: Track connected SVG use elements in a per-document list
Every SVG element insertion, removal, attribute change, and children
change walked the entire document looking for use elements to notify
about possible referenced-subtree changes. On pages with large SVG
documents this is quadratic: loading chatgpt.com spent 7% of all CPU
samples in these full-document scans, nearly all of it while parsing
an SVG icon sprite sheet.

Instead, keep every use element connected to a document's node tree in
an intrusive list owned by that document, and only iterate that list
(usually empty or tiny) when an SVG element changes.

Subtleties:

- A use element inserted by the same subtree insertion as its
  referenced element, but after it in tree order, used to be found by
  the document-wide scan from the referenced element's insertion
  steps. Now SVGUseElement::inserted() re-resolves the reference if
  the shadow tree is still unpopulated. A new test covers both tree
  orders.

- Node.moveBefore() runs moving steps without insertion or removal
  hooks. Now SVGUseElement::moved_from() updates list membership when
  moving across document-tree and shadow-tree boundaries. A new test
  covers both directions.

- Removal hooks run after the subtree has been detached, so use
  elements being removed alongside the changed element may still be
  registered. Filter them out structurally via root().is_document(),
  since Node::is_connected() is a flag that is updated in hook order
  and can still be stale at this point.
2026-06-11 15:28:59 +02:00

374 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2020, Matthew Olsson <mattco@serenityos.org>
* Copyright (c) 2023, Preston Taylor <95388976+PrestonLTaylor@users.noreply.github.com>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/SVGElement.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/SVG/SVGDescElement.h>
#include <LibWeb/SVG/SVGElement.h>
#include <LibWeb/SVG/SVGForeignObjectElement.h>
#include <LibWeb/SVG/SVGSVGElement.h>
#include <LibWeb/SVG/SVGSymbolElement.h>
#include <LibWeb/SVG/SVGTitleElement.h>
#include <LibWeb/SVG/SVGUseElement.h>
#include <LibWeb/SVG/TagNames.h>
namespace Web::SVG {
GC_DEFINE_ALLOCATOR(SVGElement);
SVGElement::SVGElement(DOM::Document& document, DOM::QualifiedName qualified_name)
: Element(document, move(qualified_name))
{
}
void SVGElement::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGElement);
Base::initialize(realm);
}
struct NamedPropertyID {
NamedPropertyID(CSS::PropertyID property_id, FlyString name, Vector<FlyString> supported_elements = {})
: id(property_id)
, name(move(name))
, supported_elements(move(supported_elements))
{
}
NamedPropertyID(CSS::PropertyID property_id, Vector<FlyString> supported_elements = {})
: NamedPropertyID(property_id, CSS::string_from_property_id(property_id), move(supported_elements))
{
}
CSS::PropertyID id;
FlyString name;
Vector<FlyString> supported_elements;
};
static ReadonlySpan<NamedPropertyID> attribute_style_properties()
{
// https://svgwg.org/svg2-draft/styling.html#PresentationAttributes
static auto const& properties = *new Array {
// FIXME: The `fill` attribute and CSS `fill` property are not the same! But our support is limited enough that they are equivalent for now.
NamedPropertyID(CSS::PropertyID::Fill),
// FIXME: The `stroke` attribute and CSS `stroke` property are not the same! But our support is limited enough that they are equivalent for now.
NamedPropertyID(CSS::PropertyID::ClipPath),
NamedPropertyID(CSS::PropertyID::ClipRule),
NamedPropertyID(CSS::PropertyID::Color),
NamedPropertyID(CSS::PropertyID::ColorInterpolation),
NamedPropertyID(CSS::PropertyID::Cursor),
NamedPropertyID(CSS::PropertyID::Cx, { SVG::TagNames::circle, SVG::TagNames::ellipse }),
NamedPropertyID(CSS::PropertyID::Cy, { SVG::TagNames::circle, SVG::TagNames::ellipse }),
NamedPropertyID(CSS::PropertyID::Direction),
NamedPropertyID(CSS::PropertyID::Display),
NamedPropertyID(CSS::PropertyID::DominantBaseline),
NamedPropertyID(CSS::PropertyID::FillOpacity),
NamedPropertyID(CSS::PropertyID::FillRule),
NamedPropertyID(CSS::PropertyID::Filter),
NamedPropertyID(CSS::PropertyID::FloodColor),
NamedPropertyID(CSS::PropertyID::FloodOpacity),
NamedPropertyID(CSS::PropertyID::FontFamily),
NamedPropertyID(CSS::PropertyID::FontSize),
NamedPropertyID(CSS::PropertyID::FontStyle),
NamedPropertyID(CSS::PropertyID::FontVariant),
NamedPropertyID(CSS::PropertyID::FontWeight),
NamedPropertyID(CSS::PropertyID::FontWidth, "font-stretch"_fly_string),
NamedPropertyID(CSS::PropertyID::Height, { SVG::TagNames::foreignObject, SVG::TagNames::image, SVG::TagNames::rect, SVG::TagNames::svg, SVG::TagNames::symbol, SVG::TagNames::use }),
NamedPropertyID(CSS::PropertyID::ImageRendering),
NamedPropertyID(CSS::PropertyID::LetterSpacing),
NamedPropertyID(CSS::PropertyID::Mask),
NamedPropertyID(CSS::PropertyID::MaskType),
NamedPropertyID(CSS::PropertyID::Opacity),
NamedPropertyID(CSS::PropertyID::Overflow),
NamedPropertyID(CSS::PropertyID::PaintOrder),
NamedPropertyID(CSS::PropertyID::PointerEvents),
NamedPropertyID(CSS::PropertyID::R, { SVG::TagNames::circle }),
NamedPropertyID(CSS::PropertyID::Rx, { SVG::TagNames::ellipse, SVG::TagNames::rect }),
NamedPropertyID(CSS::PropertyID::Ry, { SVG::TagNames::ellipse, SVG::TagNames::rect }),
NamedPropertyID(CSS::PropertyID::ShapeRendering),
NamedPropertyID(CSS::PropertyID::StopColor),
NamedPropertyID(CSS::PropertyID::StopOpacity),
NamedPropertyID(CSS::PropertyID::Stroke),
NamedPropertyID(CSS::PropertyID::StrokeDasharray),
NamedPropertyID(CSS::PropertyID::StrokeDashoffset),
NamedPropertyID(CSS::PropertyID::StrokeLinecap),
NamedPropertyID(CSS::PropertyID::StrokeLinejoin),
NamedPropertyID(CSS::PropertyID::StrokeMiterlimit),
NamedPropertyID(CSS::PropertyID::StrokeOpacity),
NamedPropertyID(CSS::PropertyID::StrokeWidth),
NamedPropertyID(CSS::PropertyID::TextAnchor),
NamedPropertyID(CSS::PropertyID::TextDecoration),
NamedPropertyID(CSS::PropertyID::TextRendering),
NamedPropertyID(CSS::PropertyID::TextOverflow),
NamedPropertyID(CSS::PropertyID::Transform, SVG::AttributeNames::gradientTransform, { SVG::TagNames::linearGradient, SVG::TagNames::radialGradient }),
NamedPropertyID(CSS::PropertyID::Transform, SVG::AttributeNames::patternTransform, { SVG::TagNames::pattern }),
NamedPropertyID(CSS::PropertyID::TransformOrigin),
NamedPropertyID(CSS::PropertyID::UnicodeBidi),
NamedPropertyID(CSS::PropertyID::Visibility),
NamedPropertyID(CSS::PropertyID::WhiteSpace),
NamedPropertyID(CSS::PropertyID::Width, { SVG::TagNames::foreignObject, SVG::TagNames::image, SVG::TagNames::rect, SVG::TagNames::svg, SVG::TagNames::symbol, SVG::TagNames::use }),
NamedPropertyID(CSS::PropertyID::WordSpacing),
NamedPropertyID(CSS::PropertyID::WritingMode),
NamedPropertyID(CSS::PropertyID::X, { SVG::TagNames::foreignObject, SVG::TagNames::image, SVG::TagNames::rect, SVG::TagNames::svg, SVG::TagNames::symbol, SVG::TagNames::use }),
NamedPropertyID(CSS::PropertyID::Y, { SVG::TagNames::foreignObject, SVG::TagNames::image, SVG::TagNames::rect, SVG::TagNames::svg, SVG::TagNames::symbol, SVG::TagNames::use }),
};
return properties;
}
bool SVGElement::is_presentational_hint(FlyString const& name) const
{
if (Base::is_presentational_hint(name))
return true;
return any_of(attribute_style_properties(), [&](auto& property) { return name.equals_ignoring_ascii_case(property.name); });
}
void SVGElement::apply_presentational_hints(Vector<CSS::StyleProperty>& properties) const
{
Base::apply_presentational_hints(properties);
CSS::Parser::ParsingParams parsing_context { document(), CSS::Parser::ParsingMode::SVGPresentationAttribute };
for_each_attribute([&](auto& name, auto& value) {
for (auto& property : attribute_style_properties()) {
if (!name.equals_ignoring_ascii_case(property.name))
continue;
if (!property.supported_elements.is_empty() && !property.supported_elements.contains_slow(local_name()))
continue;
if (property.id == CSS::PropertyID::Mask) {
if (auto style_value = parse_css_value(parsing_context, value, CSS::PropertyID::Mask))
properties.append({ .property_id = CSS::PropertyID::Mask, .value = style_value.release_nonnull() });
} else {
if (auto style_value = parse_css_value(parsing_context, value, property.id))
properties.append({ .property_id = property.id, .value = style_value.release_nonnull() });
}
break;
}
});
}
bool SVGElement::should_include_in_accessibility_tree() const
{
bool has_title_or_desc = false;
auto role = role_from_role_attribute_value();
for_each_child_of_type<SVGElement>([&has_title_or_desc](auto& child) {
if ((is<SVGTitleElement>(child) || is<SVGDescElement>(child)) && !child.text_content()->utf16_view().trim_ascii_whitespace().is_empty()) {
has_title_or_desc = true;
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
// https://w3c.github.io/svg-aam/#include_elements
// TODO: Add support for the SVG tabindex attribute, and include a check for it here.
return has_title_or_desc
|| (aria_label().has_value() && !aria_label()->bytes_as_string_view().trim_whitespace().is_empty())
|| (aria_labelled_by().has_value() && !aria_labelled_by()->bytes_as_string_view().trim_whitespace().is_empty())
|| (aria_described_by().has_value() && !aria_described_by()->bytes_as_string_view().trim_whitespace().is_empty())
|| (role.has_value() && ARIA::is_abstract_role(role.value()) && role != ARIA::Role::none && role != ARIA::Role::presentation);
}
Optional<ARIA::Role> SVGElement::default_role() const
{
// https://w3c.github.io/svg-aam/#mapping_role_table
if (local_name() == TagNames::a && (has_attribute(SVG::AttributeNames::href) || has_attribute(AttributeNames::xlink_href)))
return ARIA::Role::link;
if (local_name().is_one_of(TagNames::foreignObject, TagNames::g)
&& should_include_in_accessibility_tree())
return ARIA::Role::group;
if (local_name() == TagNames::image && should_include_in_accessibility_tree())
return ARIA::Role::image;
if (local_name() == TagNames::circle && should_include_in_accessibility_tree())
return ARIA::Role::graphicssymbol;
if (local_name().is_one_of(TagNames::ellipse, TagNames::path, TagNames::polygon, TagNames::polyline)
&& should_include_in_accessibility_tree())
return ARIA::Role::graphicssymbol;
return ARIA::Role::generic;
}
void SVGElement::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
HTMLOrSVGOrMathMLElement::visit_edges(visitor);
visitor.visit(m_class_name_animated_string);
}
void SVGElement::attribute_changed(FlyString const& local_name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_)
{
Base::attribute_changed(local_name, old_value, value, namespace_);
HTMLOrSVGOrMathMLElement::attribute_changed(local_name, old_value, value, namespace_);
update_use_elements_that_reference_this();
}
WebIDL::ExceptionOr<void> SVGElement::cloned(DOM::Node& copy, bool clone_children) const
{
TRY(Base::cloned(copy, clone_children));
TRY(HTMLOrSVGOrMathMLElement::cloned(copy, clone_children));
return {};
}
void SVGElement::inserted()
{
Base::inserted();
HTMLOrSVGOrMathMLElement::inserted();
update_use_elements_that_reference_this();
}
void SVGElement::children_changed(ChildrenChangedMetadata const& metadata)
{
Base::children_changed(metadata);
update_use_elements_that_reference_this();
}
void SVGElement::update_use_elements_that_reference_this()
{
if (is<SVGUseElement>(this)
// If this element is in a shadow root, it already represents a clone and is not itself referenced.
|| is<DOM::ShadowRoot>(this->root())
// If this does not have an id it cannot be referenced, no point in notifying use elements.
|| !id().has_value()
// An unconnected node cannot have valid references.
// This also prevents searches for elements that are in the process of being constructed - as clones.
|| !this->is_connected()) {
return;
}
for (auto& use_element : document().svg_use_elements()) {
if (document().is_completely_loaded())
use_element.svg_element_changed(*this);
else
use_element.svg_element_changed_before_document_complete(*this);
}
}
void SVGElement::removed_from(IsSubtreeRoot is_subtree_root, Node* old_ancestor, Node& old_root)
{
Base::removed_from(is_subtree_root, old_ancestor, old_root);
auto is_use_element_shadow_root = [](Node& node) {
auto* shadow_root = as_if<DOM::ShadowRoot>(node);
return shadow_root && shadow_root->host() && is<SVGUseElement>(*shadow_root->host());
};
// If this element is in a shadow root hosted by a use element,
// it already represents a clone and is not itself referenced.
// NB: We check both old_root (for when the element is removed from the shadow tree directly)
// and root() (for when the use element host is removed from the document).
if (is_use_element_shadow_root(old_root) || is_use_element_shadow_root(root()))
return;
remove_from_use_element_that_reference_this();
}
void SVGElement::remove_from_use_element_that_reference_this()
{
if (is<SVGUseElement>(this) || !id().has_value()) {
return;
}
for (auto& use_element : document().svg_use_elements()) {
// Use elements that are part of the same subtree removal may still be registered, since removal hooks run
// after the subtree has been detached. They are no longer reachable from the document and should not be
// notified. NB: This must check the tree structure, not Node::is_connected(), because the connected flag of
// nodes later in tree order has not been updated yet when we get here.
if (!use_element.root().is_document())
continue;
use_element.svg_element_removed(*this);
}
}
void SVGElement::adjust_computed_style(CSS::ComputedProperties& computed_properties)
{
Base::adjust_computed_style(computed_properties);
// An <svg> element that is not in SVG layout participates in CSS box layout
// and may be positioned. That includes the outermost <svg>, and <svg>
// elements in HTML content inside <foreignObject>.
if (is<SVGSVGElement>(*this)) {
for (auto ancestor = parent_element(); ancestor; ancestor = ancestor->parent_element()) {
if (is<SVGForeignObjectElement>(*ancestor))
return;
if (is<SVGSVGElement>(*ancestor))
break;
}
if (!owner_svg_element())
return;
}
// SVG elements in SVG layout use SVG's coordinate system and must be forced
// to position: static.
computed_properties.set_property(CSS::PropertyID::Position, CSS::KeywordStyleValue::create(CSS::Keyword::Static));
}
// https://svgwg.org/svg2-draft/types.html#__svg__SVGElement__classNames
GC::Ref<SVGAnimatedString> SVGElement::class_name()
{
// The className IDL attribute reflects the class attribute.
if (!m_class_name_animated_string)
m_class_name_animated_string = SVGAnimatedString::create(realm(), *this, DOM::QualifiedName { AttributeNames::class_, OptionalNone {}, OptionalNone {} });
return *m_class_name_animated_string;
}
// https://svgwg.org/svg2-draft/types.html#__svg__SVGElement__ownerSVGElement
GC::Ptr<SVGSVGElement> SVGElement::owner_svg_element()
{
// The ownerSVGElement IDL attribute represents the nearest ancestor svg element.
// On getting ownerSVGElement, the nearest ancestor svg element is returned;
// if the current element is the outermost svg element, then null is returned.
return first_flat_tree_ancestor_of_type<SVGSVGElement>();
}
// https://svgwg.org/svg2-draft/types.html#__svg__SVGElement__viewportElement
GC::Ptr<SVGElement> SVGElement::viewport_element()
{
for (auto* node = parent(); node; node = node->parent_or_shadow_host()) {
// https://svgwg.org/svg2-draft/coords.html#EstablishingANewSVGViewport
// The following elements establish new SVG viewports:
// - The svg element
// - A symbol element that is instanced by a use element.
if (is<SVGSVGElement>(*node) || is<SVGSymbolElement>(*node)) {
return static_cast<SVGElement*>(node);
}
}
return nullptr;
}
GC::Ref<SVGAnimatedLength> SVGElement::fake_animated_length_fixme() const
{
// FIXME: All callers of this method must implement their animated length correctly.
auto base_length = SVGLength::create(realm(), 0, 0, SVGLength::ReadOnly::No);
auto anim_length = SVGLength::create(realm(), 0, 0, SVGLength::ReadOnly::Yes);
return SVGAnimatedLength::create(realm(), base_length, anim_length);
}
GC::Ref<SVGAnimatedLength> SVGElement::svg_animated_length_for_property(CSS::PropertyID property) const
{
// FIXME: Create a proper animated value when animations are supported.
auto make_length = [&](SVGLength::ReadOnly read_only) {
if (auto const computed_properties = this->computed_properties()) {
auto const& style_value = computed_properties->property(property);
if (!style_value.has_auto() && (style_value.is_length() || style_value.is_percentage() || style_value.is_calculated()))
return SVGLength::from_length_percentage(realm(), CSS::LengthPercentage::from_style_value(style_value), read_only);
}
return SVGLength::create(realm(), 0, 0, read_only);
};
return SVGAnimatedLength::create(
realm(),
make_length(SVGLength::ReadOnly::No),
make_length(SVGLength::ReadOnly::Yes));
}
}