LibWeb: Respect presentation attributes that apply to not all elements
Some checks are pending
CI / macOS, arm64, Sanitizer_CI, Clang (push) Waiting to run
CI / Linux, x86_64, Fuzzers_CI, Clang (push) Waiting to run
CI / Linux, x86_64, Sanitizer_CI, GNU (push) Waiting to run
CI / Linux, x86_64, Sanitizer_CI, Clang (push) Waiting to run
Package the js repl as a binary artifact / Linux, arm64 (push) Waiting to run
Package the js repl as a binary artifact / macOS, arm64 (push) Waiting to run
Package the js repl as a binary artifact / Linux, x86_64 (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Label PRs with merge conflicts / auto-labeler (push) Waiting to run
Push notes / build (push) Waiting to run

Some SVG presentation attributes are only supported on certain
elements. We now support these special cases for attributes and
elements that we currently have implemented.
This commit is contained in:
Tim Ledbetter 2025-07-05 17:27:32 +01:00 committed by Tim Flynn
commit 05ef650a59
Notes: github-actions[bot] 2025-07-05 23:08:09 +00:00
5 changed files with 256 additions and 57 deletions

View file

@ -17,6 +17,7 @@
#include <LibWeb/SVG/SVGSymbolElement.h> #include <LibWeb/SVG/SVGSymbolElement.h>
#include <LibWeb/SVG/SVGTitleElement.h> #include <LibWeb/SVG/SVGTitleElement.h>
#include <LibWeb/SVG/SVGUseElement.h> #include <LibWeb/SVG/SVGUseElement.h>
#include <LibWeb/SVG/TagNames.h>
namespace Web::SVG { namespace Web::SVG {
@ -32,75 +33,92 @@ void SVGElement::initialize(JS::Realm& realm)
} }
struct NamedPropertyID { struct NamedPropertyID {
NamedPropertyID(CSS::PropertyID property_id) NamedPropertyID(CSS::PropertyID property_id, Vector<FlyString> supported_elements = {})
: id(property_id) : id(property_id)
, name(CSS::string_from_property_id(property_id)) , name(CSS::string_from_property_id(property_id))
, supported_elements(move(supported_elements))
{ {
} }
CSS::PropertyID id; CSS::PropertyID id;
StringView name; FlyString name;
Vector<FlyString> supported_elements;
}; };
static Array const attribute_style_properties { static ReadonlySpan<NamedPropertyID> attribute_style_properties()
// 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), static Array const properties = {
// FIXME: The `stroke` attribute and CSS `stroke` property are not the same! But our support is limited enough that they are equivalent for now. // 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::ClipPath), NamedPropertyID(CSS::PropertyID::Fill),
NamedPropertyID(CSS::PropertyID::ClipRule), // 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::Color), NamedPropertyID(CSS::PropertyID::ClipPath),
NamedPropertyID(CSS::PropertyID::Cursor), NamedPropertyID(CSS::PropertyID::ClipRule),
NamedPropertyID(CSS::PropertyID::Direction), NamedPropertyID(CSS::PropertyID::Color),
NamedPropertyID(CSS::PropertyID::Display), NamedPropertyID(CSS::PropertyID::Cursor),
NamedPropertyID(CSS::PropertyID::FillOpacity), NamedPropertyID(CSS::PropertyID::Cx, { SVG::TagNames::circle, SVG::TagNames::ellipse }),
NamedPropertyID(CSS::PropertyID::FillRule), NamedPropertyID(CSS::PropertyID::Cy, { SVG::TagNames::circle, SVG::TagNames::ellipse }),
NamedPropertyID(CSS::PropertyID::FontFamily), NamedPropertyID(CSS::PropertyID::Direction),
NamedPropertyID(CSS::PropertyID::FontSize), NamedPropertyID(CSS::PropertyID::Display),
NamedPropertyID(CSS::PropertyID::FontStyle), NamedPropertyID(CSS::PropertyID::FillOpacity),
NamedPropertyID(CSS::PropertyID::FontWeight), NamedPropertyID(CSS::PropertyID::FillRule),
NamedPropertyID(CSS::PropertyID::ImageRendering), NamedPropertyID(CSS::PropertyID::FontFamily),
NamedPropertyID(CSS::PropertyID::LetterSpacing), NamedPropertyID(CSS::PropertyID::FontSize),
NamedPropertyID(CSS::PropertyID::Mask), NamedPropertyID(CSS::PropertyID::FontStyle),
NamedPropertyID(CSS::PropertyID::MaskType), NamedPropertyID(CSS::PropertyID::FontWeight),
NamedPropertyID(CSS::PropertyID::Opacity), 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::Overflow), NamedPropertyID(CSS::PropertyID::ImageRendering),
NamedPropertyID(CSS::PropertyID::PointerEvents), NamedPropertyID(CSS::PropertyID::LetterSpacing),
NamedPropertyID(CSS::PropertyID::StopColor), NamedPropertyID(CSS::PropertyID::Mask),
NamedPropertyID(CSS::PropertyID::StopOpacity), NamedPropertyID(CSS::PropertyID::MaskType),
NamedPropertyID(CSS::PropertyID::Stroke), NamedPropertyID(CSS::PropertyID::Opacity),
NamedPropertyID(CSS::PropertyID::StrokeDasharray), NamedPropertyID(CSS::PropertyID::Overflow),
NamedPropertyID(CSS::PropertyID::StrokeDashoffset), NamedPropertyID(CSS::PropertyID::PointerEvents),
NamedPropertyID(CSS::PropertyID::StrokeLinecap), NamedPropertyID(CSS::PropertyID::R, { SVG::TagNames::circle }),
NamedPropertyID(CSS::PropertyID::StrokeLinejoin), NamedPropertyID(CSS::PropertyID::Rx, { SVG::TagNames::ellipse, SVG::TagNames::rect }),
NamedPropertyID(CSS::PropertyID::StrokeMiterlimit), NamedPropertyID(CSS::PropertyID::Ry, { SVG::TagNames::ellipse, SVG::TagNames::rect }),
NamedPropertyID(CSS::PropertyID::StrokeOpacity), NamedPropertyID(CSS::PropertyID::StopColor),
NamedPropertyID(CSS::PropertyID::StrokeWidth), NamedPropertyID(CSS::PropertyID::StopOpacity),
NamedPropertyID(CSS::PropertyID::TextAnchor), NamedPropertyID(CSS::PropertyID::Stroke),
NamedPropertyID(CSS::PropertyID::TextRendering), NamedPropertyID(CSS::PropertyID::StrokeDasharray),
NamedPropertyID(CSS::PropertyID::TextOverflow), NamedPropertyID(CSS::PropertyID::StrokeDashoffset),
NamedPropertyID(CSS::PropertyID::TransformOrigin), NamedPropertyID(CSS::PropertyID::StrokeLinecap),
NamedPropertyID(CSS::PropertyID::UnicodeBidi), NamedPropertyID(CSS::PropertyID::StrokeLinejoin),
NamedPropertyID(CSS::PropertyID::Visibility), NamedPropertyID(CSS::PropertyID::StrokeMiterlimit),
NamedPropertyID(CSS::PropertyID::WhiteSpace), NamedPropertyID(CSS::PropertyID::StrokeOpacity),
NamedPropertyID(CSS::PropertyID::WordSpacing), NamedPropertyID(CSS::PropertyID::StrokeWidth),
NamedPropertyID(CSS::PropertyID::WritingMode), NamedPropertyID(CSS::PropertyID::TextAnchor),
}; NamedPropertyID(CSS::PropertyID::TextRendering),
NamedPropertyID(CSS::PropertyID::TextOverflow),
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 bool SVGElement::is_presentational_hint(FlyString const& name) const
{ {
if (Base::is_presentational_hint(name)) if (Base::is_presentational_hint(name))
return true; return true;
return any_of(attribute_style_properties, [&](auto& property) { return name.equals_ignoring_ascii_case(property.name); }); return any_of(attribute_style_properties(), [&](auto& property) { return name.equals_ignoring_ascii_case(property.name); });
} }
void SVGElement::apply_presentational_hints(GC::Ref<CSS::CascadedProperties> cascaded_properties) const void SVGElement::apply_presentational_hints(GC::Ref<CSS::CascadedProperties> cascaded_properties) const
{ {
CSS::Parser::ParsingParams parsing_context { document(), CSS::Parser::ParsingMode::SVGPresentationAttribute }; CSS::Parser::ParsingParams parsing_context { document(), CSS::Parser::ParsingMode::SVGPresentationAttribute };
for_each_attribute([&](auto& name, auto& value) { for_each_attribute([&](auto& name, auto& value) {
for (auto property : attribute_style_properties) { for (auto& property : attribute_style_properties()) {
if (!name.equals_ignoring_ascii_case(property.name)) if (!name.equals_ignoring_ascii_case(property.name))
continue; continue;
if (!property.supported_elements.is_empty() && !property.supported_elements.contains_slow(local_name()))
continue;
if (property.id == CSS::PropertyID::Mask) { if (property.id == CSS::PropertyID::Mask) {
// Mask is a shorthand property in CSS, but parse_css_value does not take that into account. For now, // Mask is a shorthand property in CSS, but parse_css_value does not take that into account. For now,
// just parse as 'mask-image' as anything else is currently not supported. // just parse as 'mask-image' as anything else is currently not supported.

View file

@ -3,7 +3,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <body> at (8,8) content-size 784x100 children: inline BlockContainer <body> at (8,8) content-size 784x100 children: inline
frag 0 from SVGSVGBox start: 0, length: 0, rect: [8,8 100x100] baseline: 100 frag 0 from SVGSVGBox start: 0, length: 0, rect: [8,8 100x100] baseline: 100
SVGSVGBox <svg> at (8,8) content-size 100x100 [SVG] children: inline SVGSVGBox <svg> at (8,8) content-size 100x100 [SVG] children: inline
SVGForeignObjectBox <foreignObject> at (8,8) content-size 0x0 children: not-inline SVGForeignObjectBox <foreignObject> at (8,8) content-size 100x100 children: not-inline
BlockContainer <div> at (8,8) content-size 100x18 children: inline BlockContainer <div> at (8,8) content-size 100x18 children: inline
frag 0 from TextNode start: 0, length: 3, rect: [8,8 27.15625x18] baseline: 13.796875 frag 0 from TextNode start: 0, length: 3, rect: [8,8 27.15625x18] baseline: 13.796875
"foo" "foo"
@ -13,6 +13,6 @@ ViewportPaintable (Viewport<#document>) [0,0 800x600]
PaintableWithLines (BlockContainer<HTML>) [0,0 800x116] PaintableWithLines (BlockContainer<HTML>) [0,0 800x116]
PaintableWithLines (BlockContainer<BODY>) [8,8 784x100] PaintableWithLines (BlockContainer<BODY>) [8,8 784x100]
SVGSVGPaintable (SVGSVGBox<svg>) [8,8 100x100] SVGSVGPaintable (SVGSVGBox<svg>) [8,8 100x100]
SVGForeignObjectPaintable (SVGForeignObjectBox<foreignObject>) [8,8 0x0] overflow: [8,8 100x18] SVGForeignObjectPaintable (SVGForeignObjectBox<foreignObject>) [8,8 100x100]
PaintableWithLines (BlockContainer<DIV>) [8,8 100x18] PaintableWithLines (BlockContainer<DIV>) [8,8 100x18]
TextPaintable (TextNode<#text>) TextPaintable (TextNode<#text>)

View file

@ -2,8 +2,8 @@ Harness status: OK
Found 53 tests Found 53 tests
42 Pass 48 Pass
11 Fail 5 Fail
Pass clip-path presentation attribute supported on a relevant element Pass clip-path presentation attribute supported on a relevant element
Pass clip-rule presentation attribute supported on a relevant element Pass clip-rule presentation attribute supported on a relevant element
Pass color presentation attribute supported on a relevant element Pass color presentation attribute supported on a relevant element
@ -22,7 +22,7 @@ Fail font-stretch presentation attribute supported on a relevant element
Pass font-style presentation attribute supported on a relevant element Pass font-style presentation attribute supported on a relevant element
Fail font-variant presentation attribute supported on a relevant element Fail font-variant presentation attribute supported on a relevant element
Pass font-weight presentation attribute supported on a relevant element Pass font-weight presentation attribute supported on a relevant element
Fail height presentation attribute supported on a relevant element Pass height presentation attribute supported on a relevant element
Pass image-rendering presentation attribute supported on a relevant element Pass image-rendering presentation attribute supported on a relevant element
Pass letter-spacing presentation attribute supported on a relevant element Pass letter-spacing presentation attribute supported on a relevant element
Pass mask-type presentation attribute supported on a relevant element Pass mask-type presentation attribute supported on a relevant element
@ -31,8 +31,8 @@ Pass opacity presentation attribute supported on a relevant element
Pass overflow presentation attribute supported on a relevant element Pass overflow presentation attribute supported on a relevant element
Pass pointer-events presentation attribute supported on a relevant element Pass pointer-events presentation attribute supported on a relevant element
Pass r presentation attribute supported on a relevant element Pass r presentation attribute supported on a relevant element
Fail rx presentation attribute supported on a relevant element Pass rx presentation attribute supported on a relevant element
Fail ry presentation attribute supported on a relevant element Pass ry presentation attribute supported on a relevant element
Pass stop-color presentation attribute supported on a relevant element Pass stop-color presentation attribute supported on a relevant element
Pass stop-opacity presentation attribute supported on a relevant element Pass stop-opacity presentation attribute supported on a relevant element
Pass stroke presentation attribute supported on a relevant element Pass stroke presentation attribute supported on a relevant element
@ -52,8 +52,8 @@ Fail transform presentation attribute supported on a relevant element
Pass unicode-bidi presentation attribute supported on a relevant element Pass unicode-bidi presentation attribute supported on a relevant element
Pass visibility presentation attribute supported on a relevant element Pass visibility presentation attribute supported on a relevant element
Pass white-space presentation attribute supported on a relevant element Pass white-space presentation attribute supported on a relevant element
Fail width presentation attribute supported on a relevant element Pass width presentation attribute supported on a relevant element
Pass word-spacing presentation attribute supported on a relevant element Pass word-spacing presentation attribute supported on a relevant element
Pass writing-mode presentation attribute supported on a relevant element Pass writing-mode presentation attribute supported on a relevant element
Fail x presentation attribute supported on a relevant element Pass x presentation attribute supported on a relevant element
Fail y presentation attribute supported on a relevant element Pass y presentation attribute supported on a relevant element

View file

@ -0,0 +1,32 @@
Harness status: OK
Found 26 tests
17 Pass
9 Fail
Pass cx and cy presentation attributes supported on circle element
Pass cx and cy presentation attributes supported on ellipse element
Pass x, y, width, and height presentation attributes supported on foreignObject element
Pass x, y, width, and height presentation attributes supported on image element
Pass x, y, width, and height presentation attributes supported on rect element
Pass x, y, width, and height presentation attributes supported on svg element
Pass x, y, width, and height presentation attributes supported on symbol element
Fail x, y, width, and height presentation attributes supported on use element
Pass r presentation attribute supported on circle element
Pass rx and ry presentation attributes supported on ellipse element
Pass rx and ry presentation attributes supported on rect element
Pass cx and cy presentation attributes not supported on other elements
Pass x, y, width, and height presentation attributes not supported on other elements
Pass r presentation attribute not supported on other elements
Pass rx and ry presentation attributes not supported on other elements
Fail fill presentation attribute not supported on animate
Fail fill presentation attribute not supported on animateMotion
Fail fill presentation attribute not supported on animateTransform
Fail fill presentation attribute not supported on set
Fail transform presentation attribute supported on g
Fail patternTransform presentation attribute supported on pattern
Fail gradientTransform presentation attribute supported on linearGradient
Fail gradientTransform presentation attribute supported on radialGradient
Pass transform presentation attribute not supported on pattern or gradient elements
Pass patternTransform presentation attribute not supported on g or gradient elements
Pass gradientTransform presentation attribute not supported on g or pattern elements

View file

@ -0,0 +1,149 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>SVG presentation attributes - special cases</title>
<link rel="help" href="https://svgwg.org/svg2-draft/styling.html#PresentationAttributes">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="presentation-attributes.js"></script>
<svg id="svg"></svg>
<script>
// 1. Test the special cases where presentation attributes are only allowed on
// a specific set of elements.
if (propertiesAreSupported(["cx", "cy"])) {
for (let e of ["circle", "ellipse"]) {
test(function() {
for (let p of ["cx", "cy"]) {
assertPresentationAttributeIsSupported(e, p, "1", p);
}
}, `cx and cy presentation attributes supported on ${e} element`);
}
}
if (propertiesAreSupported(["x", "y", "width", "height"])) {
for (let e of ["foreignObject", "image", "rect", "svg", "symbol", "use"]) {
test(function() {
for (let p of ["x", "y", "width", "height"]) {
assertPresentationAttributeIsSupported(e, p, "1", p);
}
}, `x, y, width, and height presentation attributes supported on ${e} element`);
}
}
if (CSS.supports("r", "initial")) {
test(function() {
assertPresentationAttributeIsSupported("circle", "r", "1", "r");
}, `r presentation attribute supported on circle element`);
}
if (propertiesAreSupported(["rx", "ry"])) {
for (let e of ["ellipse", "rect"]) {
test(function() {
for (let p of ["rx", "ry"]) {
assertPresentationAttributeIsSupported(e, p, "1", p);
}
}, `rx and ry presentation attributes supported on ${e} element`);
}
}
if (CSS.supports("d", "initial")) {
test(function() {
assertPresentationAttributeIsSupported("path", "d", "M0,0 L1,1", "d");
}, `d presentation attribute supported on path element`);
}
// 2. Test that for those presentation attributes only allowed on a specific
// set of elements, that they are not supported on some other SVG element.
// (We use 'g' as that element for testing.)
if (propertiesAreSupported(["cx", "cy"])) {
test(function() {
for (let p of ["cx", "cy"]) {
assertPresentationAttributeIsNotSupported("g", p, "1", p);
}
}, `cx and cy presentation attributes not supported on other elements`);
}
if (propertiesAreSupported(["x", "y", "width", "height"])) {
test(function() {
for (let p of ["x", "y", "width", "height"]) {
assertPresentationAttributeIsNotSupported("g", p, "1", p);
}
}, `x, y, width, and height presentation attributes not supported on other elements`);
}
if (CSS.supports("r", "initial")) {
test(function() {
assertPresentationAttributeIsNotSupported("g", "r", "1", "r");
}, `r presentation attribute not supported on other elements`);
}
if (propertiesAreSupported(["rx", "ry"])) {
test(function() {
for (let p of ["rx", "ry"]) {
assertPresentationAttributeIsNotSupported("g", p, "1", p);
}
}, `rx and ry presentation attributes not supported on other elements`);
}
if (CSS.supports("d", "initial")) {
test(function() {
assertPresentationAttributeIsNotSupported("g", "d", "M0,0 L1,1", "d");
}, `d presentation attribute not supported on other elements`);
}
// 3. Test that the fill presentation attribute is not supported on any
// animation elements.
if (CSS.supports("fill", "initial")) {
for (let e of ["animate", "animateMotion", "animateTransform", "set"]) {
test(function() {
assertPresentationAttributeIsNotSupported(e, "fill", "blue", "fill");
}, `fill presentation attribute not supported on ${e}`);
}
}
if (CSS.supports("transform", "initial")) {
// 4. Test support for the presentation attributes of the transform property,
// which have a different spelling depending on which element they're on.
test(function() {
assertPresentationAttributeIsSupported("g", "transform", "scale(2)", "transform");
}, `transform presentation attribute supported on g`);
test(function() {
assertPresentationAttributeIsSupported("pattern", "patternTransform", "scale(2)", "transform");
}, `patternTransform presentation attribute supported on pattern`);
for (let e of ["linearGradient", "radialGradient"]) {
test(function() {
assertPresentationAttributeIsSupported(e, "gradientTransform", "scale(2)", "transform");
}, `gradientTransform presentation attribute supported on ${e}`);
}
// 5. Test that the wrong spellings of the presentation attributes of the
// transform property are not supported.
test(function() {
for (let e of ["pattern", "linearGradient", "radialGradient"]) {
assertPresentationAttributeIsNotSupported(e, "transform", "scale(2)", "transform");
}
}, `transform presentation attribute not supported on pattern or gradient elements`);
test(function() {
for (let e of ["g", "linearGradient", "radialGradient"]) {
assertPresentationAttributeIsNotSupported(e, "patternTransform", "scale(2)", "transform");
}
}, `patternTransform presentation attribute not supported on g or gradient elements`);
test(function() {
for (let e of ["g", "pattern"]) {
assertPresentationAttributeIsNotSupported(e, "gradientTransform", "scale(2)", "transform");
}
}, `gradientTransform presentation attribute not supported on g or pattern elements`);
}
</script>