LibWeb: Remove now-invalid attr() type support

Previously the type argument in attr() could be the name of a CSS type
on its own. This has changed, and now only `raw-string`
(previously `string`) or the name of a dimension unit is allowed. Other
types and more complex grammar use the `type()` function, which we
don't yet support.

I've updated the syntax comment, but not the algorithm itself, which
will be reimplemented in a later commit.
This commit is contained in:
Sam Atkins 2025-03-03 16:17:41 +00:00 committed by Tim Ledbetter
commit 8f01297182
Notes: github-actions[bot] 2025-07-09 15:46:32 +00:00
7 changed files with 22 additions and 153 deletions

View file

@ -4450,9 +4450,10 @@ bool Parser::expand_unresolved_values(DOM::Element& element, FlyString const& pr
// https://drafts.csswg.org/css-values-5/#attr-substitution
bool Parser::substitute_attr_function(DOM::Element& element, FlyString const& 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>
// attr() = attr( <attr-name> <attr-type>? , <declaration-value>?)
// <attr-name> = [ <ident-token>? '|' ]? <ident-token>
// <attr-type> = type( <syntax> ) | raw-string | <attr-unit>
// The <attr-unit> production matches any identifier that is an ASCII case-insensitive match for the name of a CSS dimension unit, such as px, or the <delim-token> %.
TokenStream attr_contents { attr_function.value };
attr_contents.discard_whitespace();
if (!attr_contents.has_next_token())
@ -4466,7 +4467,7 @@ bool Parser::substitute_attr_function(DOM::Element& element, FlyString const& pr
attr_contents.discard_whitespace();
// - Attribute type (optional)
auto attribute_type = "string"_fly_string;
auto attribute_type = "raw-string"_fly_string;
if (attr_contents.next_token().is(Token::Type::Ident)) {
attribute_type = attr_contents.consume_a_token().token().ident();
attr_contents.discard_whitespace();
@ -4493,117 +4494,12 @@ bool Parser::substitute_attr_function(DOM::Element& element, FlyString const& pr
};
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 = parse_string_as_component_value(attribute_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 = parse_string_as_component_value(attribute_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("flex"_fly_string)) {
// Parse a component value from the attributes value.
auto component_value = parse_string_as_component_value(attribute_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 (Flex::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("frequency"_fly_string)) {
// Parse a component value from the attributes value.
auto component_value = parse_string_as_component_value(attribute_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 = MUST(attribute_value.trim(Infra::ASCII_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(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 = parse_string_as_component_value(attribute_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 = parse_string_as_component_value(attribute_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 = parse_string_as_component_value(attribute_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)) {
if (attribute_type.equals_ignoring_ascii_case("raw-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(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 = parse_string_as_component_value(attribute_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(attribute_value));
return true;
} else {
// Dimension units
// Parse a component value from the attributes value.
@ -4642,7 +4538,7 @@ bool Parser::substitute_attr_function(DOM::Element& element, FlyString const& pr
if (has_fallback_values)
return expand_unresolved_values(element, property_name, attr_contents, dest);
if (attribute_type.equals_ignoring_ascii_case("string"_fly_string)) {
if (attribute_type.equals_ignoring_ascii_case("raw-string"_fly_string)) {
// If the <attr-type> argument is string, defaults to the empty string if omitted
dest.empend(Token::create_string({}));
return true;

View file

@ -6,10 +6,10 @@
border: 1px solid black;
}
.string::before {
content: attr(foo string, "WHF!");
content: attr(foo raw-string, "WHF!");
}
.string-no-fallback::before {
content: attr(foo string);
content: attr(foo raw-string);
}
.px {
width: attr(foo px, 200px);

View file

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

View file

@ -7,24 +7,15 @@
border: 1px solid black;
}
.string::before {
content: attr(foo string, "WHF!");
content: attr(foo raw-string, "WHF!");
}
.string-no-fallback::before {
content: attr(foo string);
}
.length {
width: attr(foo length, 200px);
content: attr(foo raw-string);
}
.px {
width: attr(foo px, 200px);
}
.color {
background-color: attr(foo color, lime);
}
</style>
<div class="string"></div>
<div class="string-no-fallback"></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>

View file

@ -7,21 +7,12 @@
border: 1px solid black;
}
.string::before {
content: attr(foo string);
}
.length {
width: attr(foo length);
content: attr(foo raw-string);
}
.px {
width: attr(foo px);
}
.color {
background-color: attr(foo color);
}
</style>
<div class="string" foo="WHF!"></div>
<div class="string" foo=""></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>

View file

@ -2,8 +2,8 @@ attr(foo)
attr( foo )
attr(foo, "fallback")
attr( foo , "fallback" )
attr(foo string)
attr( foo string )
attr(foo string, "fallback")
attr( foo string , "fallback" )
attr(foo raw-string)
attr( foo raw-string )
attr(foo raw-string, "fallback")
attr( foo raw-string , "fallback" )
attr(foo) attr(bar) attr(baz)

View file

@ -13,12 +13,12 @@
serialize('attr(foo, "fallback")');
// FIXME: This should produce `attr(foo, "fallback")` but doesn't yet.
serialize('attr( foo , "fallback" )');
serialize('attr(foo string)');
// FIXME: This should produce `attr(foo string)` but doesn't yet.
serialize('attr( foo string )');
serialize('attr(foo string, "fallback")');
// FIXME: This should produce `attr(foo string, "fallback")` but doesn't yet.
serialize('attr( foo string , "fallback" )');
serialize('attr(foo raw-string)');
// FIXME: This should produce `attr(foo raw-string)` but doesn't yet.
serialize('attr( foo raw-string )');
serialize('attr(foo raw-string, "fallback")');
// FIXME: This should produce `attr(foo raw-string, "fallback")` but doesn't yet.
serialize('attr( foo raw-string , "fallback" )');
serialize(' attr(foo) attr(bar) attr(baz) ');
});
</script>