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();