1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-31 14:48:14 +00:00

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.
This commit is contained in:
Sam Atkins 2023-09-04 12:57:12 +01:00 committed by Andreas Kling
parent 6f45df5ced
commit 07039af982
6 changed files with 240 additions and 18 deletions

View file

@ -1056,6 +1056,11 @@ Vector<DeclarationOrAtRule> Parser::parse_a_list_of_declarations(TokenStream<T>&
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
// https://www.w3.org/TR/css-syntax-3/#parse-component-value
template<typename T>
@ -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<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() };
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 attributes 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 attributes 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 attributes 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 attributes 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 attributes 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 attributes 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 attributes 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 attributes value.
// If the result is a <number-token>, the substitution value is a dimension with the results 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;