Browse Source

LibWeb: Implement attr() types :^)

Support the optional `<attr-type>` parameter to the `attr()` function,
which allows parsing the attribute's value as a variety of types,
instead of always as a string.
Sam Atkins 1 year ago
parent
commit
07039af982

+ 25 - 0
Tests/LibWeb/Ref/css-attr-typed-fallback.html

@@ -0,0 +1,25 @@
+<!doctype html>
+<style>
+    div {
+        width: 100px;
+        height: 20px;
+        border: 1px solid black;
+    }
+    .string::before {
+        content: attr(foo string, "WHF!");
+    }
+    .length {
+        width: attr(foo length, 200px);
+    }
+    .px {
+        width: attr(foo px, 200px);
+    }
+    .color {
+        background-color: attr(foo color, lime);
+    }
+</style>
+<div class="string"></div>
+<div class="length" foo="90pizzas"></div>
+<div class="px" foo="twohundred"></div>
+<div class="color" foo="grunge"></div>
+<div class="color" foo="rgb(0,0,0)"></div>

+ 22 - 0
Tests/LibWeb/Ref/css-attr-typed-ref.html

@@ -0,0 +1,22 @@
+<!doctype html>
+<style>
+    div {
+        width: 100px;
+        height: 20px;
+        border: 1px solid black;
+    }
+    .length {
+        width: 200px;
+    }
+    .px {
+        width: 200px;
+    }
+    .color {
+        background-color: #00ff00;
+    }
+</style>
+<div>WHF!</div>
+<div class="length"></div>
+<div class="px"></div>
+<div class="color" foo="green"></div>
+<div class="color" foo="#00ff00"></div>

+ 25 - 0
Tests/LibWeb/Ref/css-attr-typed.html

@@ -0,0 +1,25 @@
+<!doctype html>
+<style>
+    div {
+        width: 100px;
+        height: 20px;
+        border: 1px solid black;
+    }
+    .string::before {
+        content: attr(foo string);
+    }
+    .length {
+        width: attr(foo length);
+    }
+    .px {
+        width: attr(foo px);
+    }
+    .color {
+        background-color: attr(foo color);
+    }
+</style>
+<div class="string" foo="WHF!"></div>
+<div class="length" foo="200px"></div>
+<div class="px" foo="200"></div>
+<div class="color" foo="lime"></div>
+<div class="color" foo="#00ff00"></div>

+ 2 - 0
Tests/LibWeb/Ref/manifest.json

@@ -4,6 +4,8 @@
     "css-any-link-selector.html": "css-any-link-selector-ref.html",
     "css-any-link-selector.html": "css-any-link-selector-ref.html",
     "css-attr-basic.html": "css-attr-ref.html",
     "css-attr-basic.html": "css-attr-ref.html",
     "css-attr-fallback.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-gradient-currentcolor.html": "css-gradient-currentcolor-ref.html",
     "css-gradients.html": "css-gradients-ref.html",
     "css-gradients.html": "css-gradients-ref.html",
     "css-invalid-var.html": "css-invalid-var-ref.html",
     "css-invalid-var.html": "css-invalid-var-ref.html",

+ 164 - 18
Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp

@@ -1056,6 +1056,11 @@ Vector<DeclarationOrAtRule> Parser::parse_a_list_of_declarations(TokenStream<T>&
     return consume_a_list_of_declarations(tokens);
     return consume_a_list_of_declarations(tokens);
 }
 }
 
 
+Optional<ComponentValue> Parser::parse_as_component_value()
+{
+    return parse_a_component_value(m_token_stream);
+}
+
 // 5.3.9. Parse a component value
 // 5.3.9. Parse a component value
 // https://www.w3.org/TR/css-syntax-3/#parse-component-value
 // https://www.w3.org/TR/css-syntax-3/#parse-component-value
 template<typename T>
 template<typename T>
@@ -6737,37 +6742,178 @@ bool Parser::expand_unresolved_values(DOM::Element& element, StringView property
 // https://drafts.csswg.org/css-values-5/#attr-substitution
 // 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<ComponentValue>& dest)
 bool Parser::substitute_attr_function(DOM::Element& element, StringView property_name, Function const& attr_function, Vector<ComponentValue>& dest)
 {
 {
+    // First, parse the arguments to attr():
+    // attr() = attr( <q-name> <attr-type>? , <declaration-value>?)
+    // <attr-type> = string | url | ident | color | number | percentage | length | angle | time | frequency | flex | <dimension-unit>
     TokenStream attr_contents { attr_function.values() };
     TokenStream attr_contents { attr_function.values() };
     attr_contents.skip_whitespace();
     attr_contents.skip_whitespace();
     if (!attr_contents.has_next_token())
     if (!attr_contents.has_next_token())
         return false;
         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;
         return false;
-    auto attr_name = attr_name_token.token().ident();
+    auto attribute_name = attr_contents.next_token().token().ident();
+    attr_contents.skip_whitespace();
 
 
-    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;
+    // - 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();
     }
     }
 
 
-    // 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();
+    // - Comma, then fallback values (optional)
+    bool has_fallback_values = false;
     if (attr_contents.has_next_token()) {
     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))
+        if (!attr_contents.peek_token().is(Token::Type::Comma))
             return false;
             return false;
-        return true;
+        (void)attr_contents.next_token(); // Comma
+        has_fallback_values = true;
+    }
+
+    // Then, run the substitution algorithm:
+
+    // 1. If the attr() function has a substitution value, replace the attr() function by the substitution value.
+    // 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 <dimension-token> 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 <hex-color> or a named color ident, the substitution value is that result as a <color>.
+            // 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 <dimension-token> 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 <custom-ident>, 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 <custom-ident>’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 <dimension-token> 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 <number-token>, 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 <percentage-token>, 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 <dimension-token> 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 <url> 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 <number-token>, 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.
+    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.
     // 3. Otherwise, the property containing the attr() function is invalid at computed-value time.
     return false;
     return false;
 }
 }

+ 2 - 0
Userland/Libraries/LibWeb/CSS/Parser/Parser.h

@@ -68,6 +68,8 @@ public:
 
 
     RefPtr<StyleValue> parse_as_css_value(PropertyID);
     RefPtr<StyleValue> parse_as_css_value(PropertyID);
 
 
+    Optional<ComponentValue> parse_as_component_value();
+
     static NonnullRefPtr<StyleValue> resolve_unresolved_style_value(Badge<StyleComputer>, ParsingContext const&, DOM::Element&, Optional<CSS::Selector::PseudoElement>, PropertyID, UnresolvedStyleValue const&);
     static NonnullRefPtr<StyleValue> resolve_unresolved_style_value(Badge<StyleComputer>, ParsingContext const&, DOM::Element&, Optional<CSS::Selector::PseudoElement>, PropertyID, UnresolvedStyleValue const&);
 
 
     [[nodiscard]] LengthOrCalculated parse_as_sizes_attribute();
     [[nodiscard]] LengthOrCalculated parse_as_sizes_attribute();