LibWeb/SVG: Implement SVGAnimatedInteger

This is basically the same as SVGAnimatedNumber, but stores an i32
instead.
This commit is contained in:
Luke Wilde 2026-02-10 15:03:12 +00:00 committed by Andreas Kling
parent 66e9e2de4b
commit a89b02e5c3
10 changed files with 280 additions and 15 deletions

View file

@ -931,6 +931,7 @@ set(SOURCES
SVG/Path.cpp
SVG/SVGAElement.cpp
SVG/SVGAnimatedEnumeration.cpp
SVG/SVGAnimatedInteger.cpp
SVG/SVGAnimatedLength.cpp
SVG/SVGAnimatedLengthList.cpp
SVG/SVGAnimatedNumber.cpp

View file

@ -1125,6 +1125,7 @@ namespace Web::SVG {
class Path;
class SVGAnimatedEnumeration;
class SVGAnimatedInteger;
class SVGAnimatedLength;
class SVGAnimatedLengthList;
class SVGAnimatedNumber;

View file

@ -68,6 +68,20 @@ Optional<float> AttributeParser::parse_length(StringView input)
return {};
}
Optional<i32> AttributeParser::parse_integer(StringView input)
{
AttributeParser parser { input };
parser.parse_whitespace();
auto result_or_error = parser.parse_integer();
if (result_or_error.is_error())
return {};
parser.parse_whitespace();
if (parser.done())
return result_or_error.value();
return {};
}
float NumberPercentage::resolve_relative_to(float length) const
{
if (!m_is_percentage)
@ -302,6 +316,20 @@ ErrorOr<float> AttributeParser::parse_coordinate()
return parse_length();
}
// https://www.w3.org/TR/SVG11/types.html#DataTypeInteger
ErrorOr<i32> AttributeParser::parse_integer()
{
if (!match_integer())
return Error::from_string_literal("Expected integer");
auto parse_result = AK::parse_first_number<i32>(m_lexer.remaining(), TrimWhitespace::No);
if (!parse_result.has_value())
return Error::from_string_literal("Integer out of range");
m_lexer.ignore(parse_result->characters_parsed);
return parse_result->value;
}
ErrorOr<Vector<float>> AttributeParser::parse_coordinate_pair()
{
Vector<float> coordinates;
@ -779,15 +807,20 @@ bool AttributeParser::match_comma_whitespace() const
bool AttributeParser::match_coordinate() const
{
return match_length();
return match_length(AllowDot::Yes);
}
bool AttributeParser::match_number() const
{
return match_length();
return match_length(AllowDot::Yes);
}
bool AttributeParser::match_length() const
bool AttributeParser::match_integer() const
{
return match_length(AllowDot::No);
}
bool AttributeParser::match_length(AllowDot allow_dot) const
{
if (done())
return false;
@ -796,7 +829,7 @@ bool AttributeParser::match_length() const
if (ch() == '-' || ch() == '+')
offset++;
if (ch(offset) == '.')
if (allow_dot == AllowDot::Yes && ch(offset) == '.')
offset++;
return !done() && is_ascii_digit(ch(offset));

View file

@ -141,6 +141,7 @@ public:
static Optional<float> parse_coordinate(StringView input);
static Optional<float> parse_length(StringView input);
static Optional<i32> parse_integer(StringView input);
static Optional<NumberPercentage> parse_number_percentage(StringView input);
static Optional<float> parse_positive_length(StringView input);
static Vector<Gfx::FloatPoint> parse_points(StringView input);
@ -171,6 +172,7 @@ private:
ErrorOr<float> parse_length();
ErrorOr<float> parse_coordinate();
ErrorOr<i32> parse_integer();
ErrorOr<Vector<float>> parse_coordinate_pair();
ErrorOr<Vector<float>> parse_coordinate_sequence();
ErrorOr<Vector<Vector<float>>> parse_coordinate_pair_sequence();
@ -185,11 +187,17 @@ private:
// -1 if negative, +1 otherwise
int parse_sign();
enum class AllowDot {
No,
Yes,
};
bool match_whitespace() const;
bool match_comma_whitespace() const;
bool match_coordinate() const;
bool match_length() const;
bool match_length(AllowDot allow_dot) const;
bool match_number() const;
bool match_integer() const;
bool match(char c) const { return !done() && ch() == c; }
bool done() const { return m_lexer.is_eof(); }

View file

@ -0,0 +1,157 @@
/*
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
* Copyright (c) 2026, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/SVGAnimatedIntegerPrototype.h>
#include <LibWeb/SVG/AttributeParser.h>
#include <LibWeb/SVG/SVGAnimatedInteger.h>
namespace Web::SVG {
GC_DEFINE_ALLOCATOR(SVGAnimatedInteger);
GC::Ref<SVGAnimatedInteger> SVGAnimatedInteger::create(JS::Realm& realm, GC::Ref<SVGElement> element,
DOM::QualifiedName reflected_attribute, WebIDL::Long initial_value, SupportsSecondValue supports_second_value,
ValueRepresented value_represented)
{
return realm.create<SVGAnimatedInteger>(realm, element, move(reflected_attribute), initial_value,
supports_second_value, value_represented);
}
SVGAnimatedInteger::SVGAnimatedInteger(JS::Realm& realm, GC::Ref<SVGElement> element, DOM::QualifiedName reflected_attribute,
WebIDL::Long initial_value, SupportsSecondValue supports_second_value, ValueRepresented value_represented)
: PlatformObject(realm)
, m_element(element)
, m_reflected_attribute(move(reflected_attribute))
, m_initial_value(initial_value)
, m_supports_second_value(supports_second_value)
, m_value_represented(value_represented)
{
}
SVGAnimatedInteger::~SVGAnimatedInteger() = default;
// https://svgwg.org/svg2-draft/types.html#__svg__SVGAnimatedInteger__baseVal
WebIDL::Long SVGAnimatedInteger::base_val() const
{
// On getting baseVal or animVal, the following steps are run:
return get_base_or_anim_value();
}
// https://svgwg.org/svg2-draft/types.html#__svg__SVGAnimatedInteger__baseVal
void SVGAnimatedInteger::set_base_val(WebIDL::Long new_value)
{
// 1. Let value be the value being assigned to baseVal.
auto value = new_value;
// 2. Let new be a list of integers.
Vector<WebIDL::Long, 2> new_;
// 3. If the reflected attribute is defined to take an integer followed by an optional second integer, then:
if (m_supports_second_value == SupportsSecondValue::Yes) {
// 1. Let current be the value of the reflected attribute (using the attribute's initial value if it is not
// present or invalid).
auto current = m_element->get_attribute_value(m_reflected_attribute.local_name(), m_reflected_attribute.namespace_());
auto current_values = MUST(current.split(' '));
// 2. Let first be the first integer in current.
auto first = current_values.size() > 0 ? parse_value_or_initial(current_values[0]) : m_initial_value;
// 3. Let second be the second integer in current if it has been explicitly specified, and if not, the implicit
// value as described in the definition of the attribute.
// NB: All known usages of <number-optional-number> specify that a missing second number defaults to the value
// of the first number.
auto second = current_values.size() > 1 && !current_values[1].is_empty()
? parse_value_or_initial(current_values[1])
: first;
// 4. If this SVGAnimatedInteger object reflects the first integer, then set first to value. Otherwise, set second
// to value.
if (m_value_represented == ValueRepresented::First)
first = value;
else
second = value;
// 5. Append first to new.
new_.unchecked_append(first);
// 6. Append second to new.
new_.unchecked_append(second);
}
// 4. Otherwise, the reflected attribute is defined to take a single integer value. Append value to new.
else {
new_.unchecked_append(value);
}
// 5. Set the content attribute to a string consisting of each integer in new serialized to an implementation
// specific string that, if parsed as an <number> using CSS syntax, would return that integer,
// joined and separated by a single U+0020 SPACE character.
auto new_attribute_value = MUST(String::join(' ', new_));
m_element->set_attribute_value(m_reflected_attribute.local_name(), new_attribute_value, m_reflected_attribute.prefix(), m_reflected_attribute.namespace_());
}
// https://svgwg.org/svg2-draft/types.html#__svg__SVGAnimatedInteger__animVal
WebIDL::Long SVGAnimatedInteger::anim_val() const
{
// On getting baseVal or animVal, the following steps are run:
return get_base_or_anim_value();
}
WebIDL::Long SVGAnimatedInteger::parse_value_or_initial(StringView number_value) const
{
auto value = AttributeParser::parse_integer(number_value);
if (!value.has_value())
return m_initial_value;
return value.release_value();
}
// https://svgwg.org/svg2-draft/types.html#__svg__SVGAnimatedInteger__baseVal
WebIDL::Long SVGAnimatedInteger::get_base_or_anim_value() const
{
// 1. Let value be the value of the reflected attribute (using the attribute's initial value if it is not present or
// invalid).
auto value = m_element->get_attribute_value(m_reflected_attribute.local_name(), m_reflected_attribute.namespace_());
// 2. If the reflected attribute is defined to take an integer followed by an optional second integer, then:
if (m_supports_second_value == SupportsSecondValue::Yes) {
// 1. If this SVGAnimatedInteger object reflects the first integer, then return the first value in value.
auto values = MUST(value.split(' '));
if (values.is_empty())
return m_initial_value;
if (m_value_represented == ValueRepresented::First)
return parse_value_or_initial(values[0]);
// 2. Otherwise, this SVGAnimatedInteger object reflects the second integer. Return the second value in value if
// it has been explicitly specified, and if not, return the implicit value as described in the definition of
// the attribute.
// NB: All known usages of <number-optional-number> specify that a missing second number defaults to the value
// of the first number.
VERIFY(m_value_represented == ValueRepresented::Second);
if (values.size() > 1 && !values[1].is_empty())
return parse_value_or_initial(values[1]);
return parse_value_or_initial(values[0]);
}
// 3. Otherwise, the reflected attribute is defined to take a single integer value. Return value.
return parse_value_or_initial(value);
}
void SVGAnimatedInteger::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGAnimatedInteger);
Base::initialize(realm);
}
void SVGAnimatedInteger::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_element);
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
* Copyright (c) 2026, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/SVG/SVGElement.h>
namespace Web::SVG {
// https://svgwg.org/svg2-draft/types.html#InterfaceSVGAnimatedInteger
class SVGAnimatedInteger final : public Bindings::PlatformObject {
WEB_PLATFORM_OBJECT(SVGAnimatedInteger, Bindings::PlatformObject);
GC_DECLARE_ALLOCATOR(SVGAnimatedInteger);
public:
enum class SupportsSecondValue : u8 {
Yes,
No,
};
enum class ValueRepresented : u8 {
First,
Second,
};
[[nodiscard]] static GC::Ref<SVGAnimatedInteger> create(JS::Realm&, GC::Ref<SVGElement>,
DOM::QualifiedName reflected_attribute, WebIDL::Long initial_value, SupportsSecondValue = SupportsSecondValue::No,
ValueRepresented = ValueRepresented::First);
virtual ~SVGAnimatedInteger() override;
WebIDL::Long base_val() const;
void set_base_val(WebIDL::Long);
WebIDL::Long anim_val() const;
private:
SVGAnimatedInteger(JS::Realm&, GC::Ref<SVGElement>, DOM::QualifiedName, WebIDL::Long, SupportsSecondValue, ValueRepresented);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Visitor&) override;
WebIDL::Long parse_value_or_initial(StringView) const;
WebIDL::Long get_base_or_anim_value() const;
GC::Ref<SVGElement> m_element;
DOM::QualifiedName m_reflected_attribute;
WebIDL::Long m_initial_value;
SupportsSecondValue m_supports_second_value;
ValueRepresented m_value_represented;
};
}

View file

@ -0,0 +1,6 @@
// https://svgwg.org/svg2-draft/types.html#InterfaceSVGAnimatedInteger
[Exposed=Window]
interface SVGAnimatedInteger {
attribute long baseVal;
readonly attribute long animVal;
};

View file

@ -386,6 +386,7 @@ libweb_js_bindings(TrustedTypes/TrustedTypePolicy)
libweb_js_bindings(TrustedTypes/TrustedTypePolicyFactory)
libweb_js_bindings(SVG/SVGAElement)
libweb_js_bindings(SVG/SVGAnimatedEnumeration)
libweb_js_bindings(SVG/SVGAnimatedInteger)
libweb_js_bindings(SVG/SVGAnimatedLength)
libweb_js_bindings(SVG/SVGAnimatedLengthList)
libweb_js_bindings(SVG/SVGAnimatedNumber)

View file

@ -369,6 +369,7 @@ ResizeObserverSize
Response
SVGAElement
SVGAnimatedEnumeration
SVGAnimatedInteger
SVGAnimatedLength
SVGAnimatedLengthList
SVGAnimatedNumber

View file

@ -2,8 +2,8 @@ Harness status: OK
Found 1781 tests
1068 Pass
713 Fail
1076 Pass
705 Fail
Pass idl_test setup
Pass idl_test validation
Pass Partial interface Document: original interface defined
@ -290,14 +290,14 @@ Fail SVGAnimatedEnumeration must be primary interface of objects.text.lengthAdju
Fail Stringification of objects.text.lengthAdjust
Fail SVGAnimatedEnumeration interface: objects.text.lengthAdjust must inherit property "baseVal" with the proper type
Fail SVGAnimatedEnumeration interface: objects.text.lengthAdjust must inherit property "animVal" with the proper type
Fail SVGAnimatedInteger interface: existence and properties of interface object
Fail SVGAnimatedInteger interface object length
Fail SVGAnimatedInteger interface object name
Fail SVGAnimatedInteger interface: existence and properties of interface prototype object
Fail SVGAnimatedInteger interface: existence and properties of interface prototype object's "constructor" property
Fail SVGAnimatedInteger interface: existence and properties of interface prototype object's @@unscopables property
Fail SVGAnimatedInteger interface: attribute baseVal
Fail SVGAnimatedInteger interface: attribute animVal
Pass SVGAnimatedInteger interface: existence and properties of interface object
Pass SVGAnimatedInteger interface object length
Pass SVGAnimatedInteger interface object name
Pass SVGAnimatedInteger interface: existence and properties of interface prototype object
Pass SVGAnimatedInteger interface: existence and properties of interface prototype object's "constructor" property
Pass SVGAnimatedInteger interface: existence and properties of interface prototype object's @@unscopables property
Pass SVGAnimatedInteger interface: attribute baseVal
Pass SVGAnimatedInteger interface: attribute animVal
Fail SVGAnimatedInteger must be primary interface of objects.feConvolveMatrix.orderX
Fail Stringification of objects.feConvolveMatrix.orderX
Fail SVGAnimatedInteger interface: objects.feConvolveMatrix.orderX must inherit property "baseVal" with the proper type