LibWeb/CSS: Implement env() as an arbitrary substitution function

Technically, env() should not be an ASF. (😱) This is why some tests
still fail - env() as specced is expected to have its syntax checked
fully at parse-time, whereas ASFs are not properly syntax-checked until
later. However, I think this approach was worth doing for a few reasons:

- env() behaves like an ASF otherwise. (It is replaced with a set of
  arbitrary component-values that are not known until computed-value
  time.)
- env() was defined before the ASF concept existed, so I strongly
  suspect it will be updated in the future to match that definition,
  with a couple of adjustments. (eg, env() is allowed in some extra
  places compared to var() and attr().)
- This was much quicker and easier to implement (under 3 hours in total)
  compared to the greater amount of work to implement a whole separate
  system just for env().
- Most of these tests are marked tentative, and the spec definition of
  env() is still somewhat in flux, so failing some is not a huge deal.

If in the future I turn out to be wrong on this, we can convert it to
its own special thing.
This commit is contained in:
Sam Atkins 2025-08-05 12:32:54 +01:00 committed by Alexander Kalenik
commit 89b59cb5c3
Notes: github-actions[bot] 2025-08-07 14:41:43 +00:00
11 changed files with 122 additions and 50 deletions

View file

@ -12,6 +12,7 @@
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
#include <LibWeb/CSS/StyleValues/UnresolvedStyleValue.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
namespace Web::CSS::Parser {
@ -64,6 +65,8 @@ Optional<ArbitrarySubstitutionFunction> to_arbitrary_substitution_function(FlySt
{
if (name.equals_ignoring_ascii_case("attr"sv))
return ArbitrarySubstitutionFunction::Attr;
if (name.equals_ignoring_ascii_case("env"sv))
return ArbitrarySubstitutionFunction::Env;
if (name.equals_ignoring_ascii_case("var"sv))
return ArbitrarySubstitutionFunction::Var;
return {};
@ -198,6 +201,64 @@ static Vector<ComponentValue> replace_an_attr_function(DOM::AbstractElement& ele
// NB: Step 6 is a lambda defined at the top of the function.
}
// https://drafts.csswg.org/css-env/#substitute-an-env
static Vector<ComponentValue> replace_an_env_function(DOM::AbstractElement& element, GuardedSubstitutionContexts& guarded_contexts, ArbitrarySubstitutionFunctionArguments const& arguments)
{
// AD-HOC: env() is not defined as an ASF (and was defined before the ASF concept was), but behaves a lot like one.
// So, this is a combination of the spec's "substitute an env()" algorithm linked above, and the "replace a FOO function()" algorithms.
auto const& first_argument = arguments.first();
auto const second_argument = arguments.get(1);
// AD-HOC: Substitute ASFs in the first argument.
auto substituted_first_argument = substitute_arbitrary_substitution_functions(element, guarded_contexts, first_argument);
// AD-HOC: Parse the arguments.
// env() = env( <custom-ident> <integer [0,∞]>*, <declaration-value>? )
TokenStream first_argument_tokens { substituted_first_argument };
first_argument_tokens.discard_whitespace();
auto& name_token = first_argument_tokens.consume_a_token();
if (!name_token.is(Token::Type::Ident))
return { ComponentValue { GuaranteedInvalidValue {} } };
auto& name = name_token.token().ident();
first_argument_tokens.discard_whitespace();
Vector<i64> indices;
// FIXME: Are non-literal <integer>s allowed here?
while (first_argument_tokens.has_next_token()) {
auto& maybe_integer = first_argument_tokens.consume_a_token();
if (!maybe_integer.is(Token::Type::Number))
return { ComponentValue { GuaranteedInvalidValue {} } };
auto& number = maybe_integer.token().number();
if (number.is_integer() && number.integer_value() >= 0)
indices.append(number.integer_value());
else
return { ComponentValue { GuaranteedInvalidValue {} } };
first_argument_tokens.discard_whitespace();
}
// 1. If the name provided by the first argument of the env() function is a recognized environment variable name,
// the number of supplied integers matches the number of dimensions of the environment variable referenced by
// that name, and values of the indices correspond to a known sub-value, replace the env() function by the value
// of the named environment variable.
if (auto environment_variable = environment_variable_from_string(name);
environment_variable.has_value() && indices.size() == environment_variable_dimension_count(*environment_variable)) {
auto result = element.document().environment_variable_value(*environment_variable, indices);
if (result.has_value())
return result.release_value();
}
// 2. Otherwise, if the env() function has a fallback value as its second argument, replace the env() function by
// the fallback value. If there are any env() references in the fallback, substitute them as well.
// AD-HOC: Substitute all ASFs in the result.
if (second_argument.has_value())
return substitute_arbitrary_substitution_functions(element, guarded_contexts, second_argument.value());
// 3. Otherwise, the property or descriptor containing the env() function is invalid at computed-value time.
return { ComponentValue { GuaranteedInvalidValue {} } };
}
// https://drafts.csswg.org/css-variables-1/#replace-a-var-function
static Vector<ComponentValue> replace_a_var_function(DOM::AbstractElement& element, GuardedSubstitutionContexts& guarded_contexts, ArbitrarySubstitutionFunctionArguments const& arguments)
{
@ -386,6 +447,11 @@ Optional<ArbitrarySubstitutionFunctionArguments> parse_according_to_argument_gra
// https://drafts.csswg.org/css-values-5/#attr-notation
// <attr-args> = attr( <declaration-value> , <declaration-value>? )
return parse_declaration_value_then_optional_declaration_value(values);
case ArbitrarySubstitutionFunction::Env:
// https://drafts.csswg.org/css-env/#env-function
// AD-HOC: This doesn't have an argument-grammar definition.
// However, it follows the same format of "some CVs, then an optional comma and a fallback".
return parse_declaration_value_then_optional_declaration_value(values);
case ArbitrarySubstitutionFunction::Var:
// https://drafts.csswg.org/css-variables/#funcdef-var
// <var-args> = var( <declaration-value> , <declaration-value>? )
@ -400,6 +466,8 @@ Vector<ComponentValue> replace_an_arbitrary_substitution_function(DOM::AbstractE
switch (function) {
case ArbitrarySubstitutionFunction::Attr:
return replace_an_attr_function(element, guarded_contexts, arguments);
case ArbitrarySubstitutionFunction::Env:
return replace_an_env_function(element, guarded_contexts, arguments);
case ArbitrarySubstitutionFunction::Var:
return replace_a_var_function(element, guarded_contexts, arguments);
}

View file

@ -38,6 +38,7 @@ private:
enum class ArbitrarySubstitutionFunction : u8 {
Attr,
Env,
Var,
};
[[nodiscard]] Optional<ArbitrarySubstitutionFunction> to_arbitrary_substitution_function(FlyString const& name);

View file

@ -87,10 +87,12 @@ String Function::original_source_text() const
void Function::contains_arbitrary_substitution_function(SubstitutionFunctionsPresence& presence) const
{
if (name.equals_ignoring_ascii_case("var"sv))
presence.var = true;
else if (name.equals_ignoring_ascii_case("attr"sv))
if (name.equals_ignoring_ascii_case("attr"sv))
presence.attr = true;
else if (name.equals_ignoring_ascii_case("env"sv))
presence.env = true;
else if (name.equals_ignoring_ascii_case("var"sv))
presence.var = true;
for (auto const& component_value : value) {
if (component_value.is_function())
component_value.function().contains_arbitrary_substitution_function(presence);

View file

@ -63,9 +63,10 @@ struct Declaration {
struct SubstitutionFunctionsPresence {
bool attr { false };
bool env { false };
bool var { false };
bool has_any() const { return attr || var; }
bool has_any() const { return attr || env || var; }
};
// https://drafts.csswg.org/css-syntax/#simple-block

View file

@ -2,5 +2,5 @@ Harness status: OK
Found 1 tests
1 Fail
Fail Test that CSS env vars work with @support
1 Pass
Pass Test that CSS env vars work with @support

View file

@ -2,6 +2,6 @@ Harness status: OK
Found 2 tests
2 Fail
Fail env() is substituted into a custom property
Fail Substitution of unrecognized env() causes guaranteed-invalid
2 Pass
Pass env() is substituted into a custom property
Pass Substitution of unrecognized env() causes guaranteed-invalid

View file

@ -2,13 +2,13 @@ Harness status: OK
Found 8 tests
5 Pass
3 Fail
Fail e.style['width'] = "env(safe-area-inset-top)" should set the property value
Fail e.style['width'] = "env(safe-area-inset-top,)" should set the property value
Fail e.style['width'] = "env(safe-area-inset-top, )" should set the property value
Pass e.style['width'] = "env(safe-area-inset-top ())" should not set the property value
Pass e.style['width'] = "env(safe-area-inset-top () )" should not set the property value
Pass e.style['width'] = "env(safe-area-inset-top() )" should not set the property value
Pass e.style['width'] = "env(safe-area-inset-top (),)" should not set the property value
Pass e.style['width'] = "env(safe-area-inset-top(),)" should not set the property value
3 Pass
5 Fail
Pass e.style['width'] = "env(safe-area-inset-top)" should set the property value
Pass e.style['width'] = "env(safe-area-inset-top,)" should set the property value
Pass e.style['width'] = "env(safe-area-inset-top, )" should set the property value
Fail e.style['width'] = "env(safe-area-inset-top ())" should not set the property value
Fail e.style['width'] = "env(safe-area-inset-top () )" should not set the property value
Fail e.style['width'] = "env(safe-area-inset-top() )" should not set the property value
Fail e.style['width'] = "env(safe-area-inset-top (),)" should not set the property value
Fail e.style['width'] = "env(safe-area-inset-top(),)" should not set the property value

View file

@ -2,5 +2,5 @@ Harness status: OK
Found 1 tests
1 Fail
Fail Test that nested var() fallback values work with CSS env vars
1 Pass
Pass Test that nested var() fallback values work with CSS env vars

View file

@ -4,11 +4,11 @@ Found 8 tests
4 Pass
4 Fail
Pass CSS Environment variable value "env(test1 test2, green)" must not successfully parse
Pass CSS Environment variable value "env(test1 10 20 test2, green)" must not successfully parse
Pass CSS Environment variable value "env(test 0.1, green)" must not successfully parse
Pass CSS Environment variable value "env(test -1, green)" must not successfully parse
Fail CSS Environment variable value "env(test 0, green)" must successfully parse and roundtrip
Fail CSS Environment variable value "env(test 0,)" must successfully parse and roundtrip
Fail CSS Environment variable value "env(test 0)" must successfully parse and roundtrip
Fail CSS Environment variable value "env(test 0 1 2 3 4, green)" must successfully parse and roundtrip
Fail CSS Environment variable value "env(test1 test2, green)" must not successfully parse
Fail CSS Environment variable value "env(test1 10 20 test2, green)" must not successfully parse
Fail CSS Environment variable value "env(test 0.1, green)" must not successfully parse
Fail CSS Environment variable value "env(test -1, green)" must not successfully parse
Pass CSS Environment variable value "env(test 0, green)" must successfully parse and roundtrip
Pass CSS Environment variable value "env(test 0,)" must successfully parse and roundtrip
Pass CSS Environment variable value "env(test 0)" must successfully parse and roundtrip
Pass CSS Environment variable value "env(test 0 1 2 3 4, green)" must successfully parse and roundtrip

View file

@ -2,23 +2,23 @@ Harness status: OK
Found 18 tests
4 Pass
14 Fail
15 Pass
3 Fail
Pass rgb(0, 128, 0)
Fail background-color: env(test) rgba(0, 0, 0, 0)
Fail background-color: ENV(test) rgba(0, 0, 0, 0)
Fail background-color: env(test) !important rgba(0, 0, 0, 0)
Fail background-color: env(test, 10px) rgba(0, 0, 0, 0)
Fail background-color: env(test, blue) rgb(0, 0, 255)
Fail background-color: env(test, env(another)) rgba(0, 0, 0, 0)
Fail background-color: env(test, env(another, blue)) rgb(0, 0, 255)
Fail background-color: env(-test) rgba(0, 0, 0, 0)
Fail background-color: env(--test) rgba(0, 0, 0, 0)
Pass background-color: env(10px) rgb(0, 128, 0)
Pass background-color: env(env(test)) rgb(0, 128, 0)
Fail background-color: env( test) rgba(0, 0, 0, 0)
Fail background-color: env(test ) rgba(0, 0, 0, 0)
Fail background-color: env( test ) rgba(0, 0, 0, 0)
Fail background-color: env(test /**/, blue) rgb(0, 0, 255)
Fail background-color: env(test, {}) rgba(0, 0, 0, 0)
Pass background-color: env(test, {) rgb(0, 128, 0)
Pass background-color: env(test) rgba(0, 0, 0, 0)
Pass background-color: ENV(test) rgba(0, 0, 0, 0)
Pass background-color: env(test) !important rgba(0, 0, 0, 0)
Pass background-color: env(test, 10px) rgba(0, 0, 0, 0)
Pass background-color: env(test, blue) rgb(0, 0, 255)
Pass background-color: env(test, env(another)) rgba(0, 0, 0, 0)
Pass background-color: env(test, env(another, blue)) rgb(0, 0, 255)
Pass background-color: env(-test) rgba(0, 0, 0, 0)
Pass background-color: env(--test) rgba(0, 0, 0, 0)
Fail background-color: env(10px) rgb(0, 128, 0)
Fail background-color: env(env(test)) rgb(0, 128, 0)
Pass background-color: env( test) rgba(0, 0, 0, 0)
Pass background-color: env(test ) rgba(0, 0, 0, 0)
Pass background-color: env( test ) rgba(0, 0, 0, 0)
Pass background-color: env(test /**/, blue) rgb(0, 0, 255)
Pass background-color: env(test, {}) rgba(0, 0, 0, 0)
Fail background-color: env(test, {) rgb(0, 128, 0)

View file

@ -2,5 +2,5 @@ Harness status: OK
Found 1 tests
1 Fail
Fail Test unknown env() names will override previous values
1 Pass
Pass Test unknown env() names will override previous values