diff --git a/Tests/LibWeb/Ref/css-attr-typed-fallback.html b/Tests/LibWeb/Ref/css-attr-typed-fallback.html
new file mode 100644
index 0000000000..fc94192a3f
--- /dev/null
+++ b/Tests/LibWeb/Ref/css-attr-typed-fallback.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/Tests/LibWeb/Ref/css-attr-typed-ref.html b/Tests/LibWeb/Ref/css-attr-typed-ref.html
new file mode 100644
index 0000000000..cefc680839
--- /dev/null
+++ b/Tests/LibWeb/Ref/css-attr-typed-ref.html
@@ -0,0 +1,22 @@
+
+
+WHF!
+
+
+
+
diff --git a/Tests/LibWeb/Ref/css-attr-typed.html b/Tests/LibWeb/Ref/css-attr-typed.html
new file mode 100644
index 0000000000..681573a496
--- /dev/null
+++ b/Tests/LibWeb/Ref/css-attr-typed.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/Tests/LibWeb/Ref/manifest.json b/Tests/LibWeb/Ref/manifest.json
index fe5d303f0c..f4f78e80cb 100644
--- a/Tests/LibWeb/Ref/manifest.json
+++ b/Tests/LibWeb/Ref/manifest.json
@@ -4,6 +4,8 @@
"css-any-link-selector.html": "css-any-link-selector-ref.html",
"css-attr-basic.html": "css-attr-ref.html",
"css-attr-fallback.html": "css-attr-ref.html",
+ "css-attr-typed.html": "css-attr-typed-ref.html",
+ "css-attr-typed-fallback.html": "css-attr-typed-ref.html",
"css-gradient-currentcolor.html": "css-gradient-currentcolor-ref.html",
"css-gradients.html": "css-gradients-ref.html",
"css-invalid-var.html": "css-invalid-var-ref.html",
diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp
index 22c4634c43..8f32ebdfca 100644
--- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp
+++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp
@@ -1056,6 +1056,11 @@ Vector Parser::parse_a_list_of_declarations(TokenStream&
return consume_a_list_of_declarations(tokens);
}
+Optional Parser::parse_as_component_value()
+{
+ return parse_a_component_value(m_token_stream);
+}
+
// 5.3.9. Parse a component value
// https://www.w3.org/TR/css-syntax-3/#parse-component-value
template
@@ -6737,36 +6742,177 @@ bool Parser::expand_unresolved_values(DOM::Element& element, StringView property
// https://drafts.csswg.org/css-values-5/#attr-substitution
bool Parser::substitute_attr_function(DOM::Element& element, StringView property_name, Function const& attr_function, Vector& dest)
{
+ // First, parse the arguments to attr():
+ // attr() = attr( ? , ?)
+ // = string | url | ident | color | number | percentage | length | angle | time | frequency | flex |
TokenStream attr_contents { attr_function.values() };
attr_contents.skip_whitespace();
if (!attr_contents.has_next_token())
return false;
- auto const& attr_name_token = attr_contents.next_token();
- if (!attr_name_token.is(Token::Type::Ident))
+ // - Attribute name
+ // FIXME: Support optional attribute namespace
+ if (!attr_contents.peek_token().is(Token::Type::Ident))
return false;
- auto attr_name = attr_name_token.token().ident();
+ auto attribute_name = attr_contents.next_token().token().ident();
+ attr_contents.skip_whitespace();
+
+ // - Attribute type (optional)
+ auto attribute_type = "string"_fly_string;
+ if (attr_contents.peek_token().is(Token::Type::Ident)) {
+ attribute_type = MUST(FlyString::from_utf8(attr_contents.next_token().token().ident()));
+ attr_contents.skip_whitespace();
+ }
+
+ // - Comma, then fallback values (optional)
+ bool has_fallback_values = false;
+ if (attr_contents.has_next_token()) {
+ if (!attr_contents.peek_token().is(Token::Type::Comma))
+ return false;
+ (void)attr_contents.next_token(); // Comma
+ has_fallback_values = true;
+ }
+
+ // Then, run the substitution algorithm:
- auto attr_value = element.get_attribute(attr_name);
// 1. If the attr() function has a substitution value, replace the attr() function by the substitution value.
- if (!attr_value.is_null()) {
- // FIXME: attr() should also accept an optional type argument, not just strings.
- dest.empend(Token::of_string(FlyString::from_deprecated_fly_string(attr_value).release_value_but_fixme_should_propagate_errors()));
- return true;
+ // https://drafts.csswg.org/css-values-5/#attr-types
+ if (element.has_attribute(attribute_name)) {
+ auto attribute_value = element.get_attribute_value(attribute_name);
+ if (attribute_type.equals_ignoring_ascii_case("angle"_fly_string)) {
+ // Parse a component value from the attribute’s value.
+ auto component_value = MUST(Parser::Parser::create(m_context, attribute_value)).parse_as_component_value();
+ // If the result is a whose unit matches the given type, the result is the substitution value.
+ // Otherwise, there is no substitution value.
+ if (component_value.has_value() && component_value->is(Token::Type::Dimension)) {
+ if (Angle::unit_from_name(component_value->token().dimension_unit()).has_value()) {
+ dest.append(component_value.release_value());
+ return true;
+ }
+ }
+ } else if (attribute_type.equals_ignoring_ascii_case("color"_fly_string)) {
+ // Parse a component value from the attribute’s value.
+ // If the result is a or a named color ident, the substitution value is that result as a .
+ // Otherwise there is no substitution value.
+ auto component_value = MUST(Parser::Parser::create(m_context, attribute_value)).parse_as_component_value();
+ if (component_value.has_value()) {
+ if ((component_value->is(Token::Type::Hash)
+ && Color::from_string(MUST(String::formatted("#{}", component_value->token().hash_value()))).has_value())
+ || (component_value->is(Token::Type::Ident)
+ && Color::from_string(component_value->token().ident()).has_value())) {
+ dest.append(component_value.release_value());
+ return true;
+ }
+ }
+ } else if (attribute_type.equals_ignoring_ascii_case("frequency"_fly_string)) {
+ // Parse a component value from the attribute’s value.
+ auto component_value = MUST(Parser::Parser::create(m_context, attribute_value)).parse_as_component_value();
+ // If the result is a whose unit matches the given type, the result is the substitution value.
+ // Otherwise, there is no substitution value.
+ if (component_value.has_value() && component_value->is(Token::Type::Dimension)) {
+ if (Frequency::unit_from_name(component_value->token().dimension_unit()).has_value()) {
+ dest.append(component_value.release_value());
+ return true;
+ }
+ }
+ } else if (attribute_type.equals_ignoring_ascii_case("ident"_fly_string)) {
+ // The substitution value is a CSS , whose value is the literal value of the attribute,
+ // with leading and trailing ASCII whitespace stripped. (No CSS parsing of the value is performed.)
+ // If the attribute value, after trimming, is the empty string, there is instead no substitution value.
+ // If the ’s value is a CSS-wide keyword or `default`, there is instead no substitution value.
+ auto substitution_value = attribute_value.trim_whitespace();
+ if (!substitution_value.is_empty()
+ && !substitution_value.equals_ignoring_ascii_case("default"sv)
+ && !is_css_wide_keyword(substitution_value)) {
+ dest.empend(Token::create_ident(MUST(FlyString::from_deprecated_fly_string(substitution_value))));
+ return true;
+ }
+ } else if (attribute_type.equals_ignoring_ascii_case("length"_fly_string)) {
+ // Parse a component value from the attribute’s value.
+ auto component_value = MUST(Parser::Parser::create(m_context, attribute_value)).parse_as_component_value();
+ // If the result is a whose unit matches the given type, the result is the substitution value.
+ // Otherwise, there is no substitution value.
+ if (component_value.has_value() && component_value->is(Token::Type::Dimension)) {
+ if (Length::unit_from_name(component_value->token().dimension_unit()).has_value()) {
+ dest.append(component_value.release_value());
+ return true;
+ }
+ }
+ } else if (attribute_type.equals_ignoring_ascii_case("number"_fly_string)) {
+ // Parse a component value from the attribute’s value.
+ // If the result is a , the result is the substitution value.
+ // Otherwise, there is no substitution value.
+ auto component_value = MUST(Parser::Parser::create(m_context, attribute_value)).parse_as_component_value();
+ if (component_value.has_value() && component_value->is(Token::Type::Number)) {
+ dest.append(component_value.release_value());
+ return true;
+ }
+ } else if (attribute_type.equals_ignoring_ascii_case("percentage"_fly_string)) {
+ // Parse a component value from the attribute’s value.
+ auto component_value = MUST(Parser::Parser::create(m_context, attribute_value)).parse_as_component_value();
+ // If the result is a , the result is the substitution value.
+ // Otherwise, there is no substitution value.
+ if (component_value.has_value() && component_value->is(Token::Type::Percentage)) {
+ dest.append(component_value.release_value());
+ return true;
+ }
+ } else if (attribute_type.equals_ignoring_ascii_case("string"_fly_string)) {
+ // The substitution value is a CSS string, whose value is the literal value of the attribute.
+ // (No CSS parsing or "cleanup" of the value is performed.)
+ // No value triggers fallback.
+ dest.empend(Token::create_string(MUST(FlyString::from_deprecated_fly_string(attribute_value))));
+ return true;
+ } else if (attribute_type.equals_ignoring_ascii_case("time"_fly_string)) {
+ // Parse a component value from the attribute’s value.
+ auto component_value = MUST(Parser::Parser::create(m_context, attribute_value)).parse_as_component_value();
+ // If the result is a whose unit matches the given type, the result is the substitution value.
+ // Otherwise, there is no substitution value.
+ if (component_value.has_value() && component_value->is(Token::Type::Dimension)) {
+ if (Time::unit_from_name(component_value->token().dimension_unit()).has_value()) {
+ dest.append(component_value.release_value());
+ return true;
+ }
+ }
+ } else if (attribute_type.equals_ignoring_ascii_case("url"_fly_string)) {
+ // The substitution value is a CSS value, whose url is the literal value of the attribute.
+ // (No CSS parsing or "cleanup" of the value is performed.)
+ // No value triggers fallback.
+ dest.empend(Token::create_url(MUST(FlyString::from_deprecated_fly_string(attribute_value))));
+ return true;
+ } else {
+ // Dimension units
+ // Parse a component value from the attribute’s value.
+ // If the result is a , the substitution value is a dimension with the result’s value, and the given unit.
+ // Otherwise, there is no substitution value.
+ auto component_value = MUST(Parser::Parser::create(m_context, attribute_value)).parse_as_component_value();
+ if (component_value.has_value() && component_value->is(Token::Type::Number)) {
+ if (attribute_value == "%"sv) {
+ dest.empend(Token::create_dimension(component_value->token().number_value(), attribute_type));
+ return true;
+ } else if (auto angle_unit = Angle::unit_from_name(attribute_type); angle_unit.has_value()) {
+ dest.empend(Token::create_dimension(component_value->token().number_value(), attribute_type));
+ return true;
+ } else if (auto frequency_unit = Frequency::unit_from_name(attribute_type); frequency_unit.has_value()) {
+ dest.empend(Token::create_dimension(component_value->token().number_value(), attribute_type));
+ return true;
+ } else if (auto length_unit = Length::unit_from_name(attribute_type); length_unit.has_value()) {
+ dest.empend(Token::create_dimension(component_value->token().number_value(), attribute_type));
+ return true;
+ } else if (auto time_unit = Time::unit_from_name(attribute_type); time_unit.has_value()) {
+ dest.empend(Token::create_dimension(component_value->token().number_value(), attribute_type));
+ return true;
+ } else {
+ // Not a dimension unit.
+ return false;
+ }
+ }
+ }
}
// 2. Otherwise, if the attr() function has a fallback value as its last argument, replace the attr() function by the fallback value.
// If there are any var() or attr() references in the fallback, substitute them as well.
- attr_contents.skip_whitespace();
- if (attr_contents.has_next_token()) {
- auto const& comma_token = attr_contents.next_token();
- if (!comma_token.is(Token::Type::Comma))
- return false;
- attr_contents.skip_whitespace();
- if (!expand_unresolved_values(element, property_name, attr_contents, dest))
- return false;
- return true;
- }
+ if (has_fallback_values)
+ return expand_unresolved_values(element, property_name, attr_contents, dest);
// 3. Otherwise, the property containing the attr() function is invalid at computed-value time.
return false;
diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h
index 06688f9a29..d40f318d00 100644
--- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h
+++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h
@@ -68,6 +68,8 @@ public:
RefPtr parse_as_css_value(PropertyID);
+ Optional parse_as_component_value();
+
static NonnullRefPtr resolve_unresolved_style_value(Badge, ParsingContext const&, DOM::Element&, Optional, PropertyID, UnresolvedStyleValue const&);
[[nodiscard]] LengthOrCalculated parse_as_sizes_attribute();