ladybird/Libraries/LibWeb/CSS/Parser/ArbitrarySubstitutionFunctions.cpp
Sam Atkins 08dbbf24c2 LibWeb/CSS: Tokenize custom property values when substituting var()
Co-authored-by: Shannon Booth <shannon@serenityos.org>
2026-04-15 16:42:15 +02:00

637 lines
33 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/CSS/Parser/ArbitrarySubstitutionFunctions.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/Parser/Syntax.h>
#include <LibWeb/CSS/Parser/SyntaxParsing.h>
#include <LibWeb/CSS/PropertyName.h>
#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 {
bool SubstitutionContext::operator==(SubstitutionContext const& other) const
{
return dependency_type == other.dependency_type
&& first == other.first
&& second == other.second;
}
String SubstitutionContext::to_string() const
{
StringView type_name = [this] {
switch (dependency_type) {
case DependencyType::Property:
return "Property"sv;
case DependencyType::Attribute:
return "Attribute"sv;
case DependencyType::Function:
return "Function"sv;
}
VERIFY_NOT_REACHED();
}();
return MUST(String::formatted("{} {} {}", type_name, first, second));
}
void GuardedSubstitutionContexts::guard(SubstitutionContext& context)
{
for (auto& existing_context : m_contexts) {
if (existing_context == context) {
existing_context.is_cyclic = true;
context.is_cyclic = true;
return;
}
}
m_contexts.append(context);
}
void GuardedSubstitutionContexts::unguard(SubstitutionContext const& context)
{
[[maybe_unused]] auto const was_removed = m_contexts.remove_first_matching([context](auto const& other) {
return context == other;
});
VERIFY(was_removed);
}
Optional<ArbitrarySubstitutionFunction> to_arbitrary_substitution_function(FlyString const& name)
{
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("if"sv))
return ArbitrarySubstitutionFunction::If;
if (name.equals_ignoring_ascii_case("inherit"sv))
return ArbitrarySubstitutionFunction::Inherit;
if (name.equals_ignoring_ascii_case("var"sv))
return ArbitrarySubstitutionFunction::Var;
return {};
}
bool contains_guaranteed_invalid_value(Vector<ComponentValue> const& values)
{
for (auto const& value : values) {
if (value.contains_guaranteed_invalid_value())
return true;
}
return false;
}
// https://drafts.csswg.org/css-values-5/#replace-an-attr-function
static Vector<ComponentValue> replace_an_attr_function(DOM::AbstractElement& element, GuardedSubstitutionContexts& guarded_contexts, ArbitrarySubstitutionFunctionArguments const& arguments)
{
// 1. Let el be the element that the style containing the attr() function is being applied to.
// Let first arg be the first <declaration-value> in arguments.
// Let second arg be the <declaration-value>? passed after the comma, or null if there was no comma.
auto declaration_value_list = arguments.get<DeclarationValueList>();
auto const& first_argument = declaration_value_list.first();
auto const second_argument = declaration_value_list.get(1);
FlyString attribute_name;
struct RawStringKeyword { };
struct NumberKeyword { };
struct AttrUnit {
FlyString name;
};
Variant<Empty, NonnullOwnPtr<SyntaxNode>, RawStringKeyword, NumberKeyword, AttrUnit> syntax;
auto failure = [&] -> Vector<ComponentValue> {
// This is step 7, but defined here for convenience.
// 1. If second arg is null, and syntax was omitted, return an empty CSS <string>.
if (!second_argument.has_value() && syntax.has<Empty>())
return { Token::create_string({}) };
// 2. If second arg is null, return the guaranteed-invalid value.
if (!second_argument.has_value())
return { ComponentValue { GuaranteedInvalidValue {} } };
// 3. Substitute arbitrary substitution functions in second arg, and return the result.
return substitute_arbitrary_substitution_functions(element, guarded_contexts, second_argument.value());
};
// 2. Substitute arbitrary substitution functions in first arg, then parse it as <attr-name> <attr-type>?.
// If that returns failure, jump to the last step (labeled FAILURE).
// Otherwise, let attr name and syntax be the results of parsing (with syntax being null if <attr-type> was
// omitted), processed as specified in the definition of those arguments.
auto substituted = substitute_arbitrary_substitution_functions(element, guarded_contexts, first_argument);
TokenStream first_argument_tokens { substituted };
// <attr-name> = [ <ident-token>? '|' ]? <ident-token>
// FIXME: Support optional attribute namespace
if (!first_argument_tokens.next_token().is(Token::Type::Ident))
return failure();
attribute_name = first_argument_tokens.consume_a_token().token().ident();
first_argument_tokens.discard_whitespace();
// <attr-type> = type( <syntax> ) | raw-string | number | <attr-unit>
if (first_argument_tokens.next_token().is(Token::Type::Ident)) {
auto const& syntax_ident = first_argument_tokens.next_token().token().ident();
if (syntax_ident.equals_ignoring_ascii_case("raw-string"sv)) {
first_argument_tokens.discard_a_token(); // raw-string
syntax = RawStringKeyword {};
} else if (syntax_ident.equals_ignoring_ascii_case("number"sv)) {
first_argument_tokens.discard_a_token(); // number
syntax = NumberKeyword {};
} else if (dimension_for_unit(syntax_ident).has_value()) {
syntax = AttrUnit { first_argument_tokens.consume_a_token().token().ident() };
} else {
return failure();
}
} else if (first_argument_tokens.next_token().is_delim('%')) {
first_argument_tokens.discard_a_token(); // %
syntax = AttrUnit { "%"_fly_string };
} else if (first_argument_tokens.next_token().is_function("type"sv)) {
auto const& type_function = first_argument_tokens.consume_a_token().function();
if (auto parsed_syntax = parse_as_syntax(type_function.value)) {
syntax = parsed_syntax.release_nonnull();
} else {
return failure();
}
}
first_argument_tokens.discard_whitespace();
if (first_argument_tokens.has_next_token())
return failure();
// 3. If attr name exists as an attribute on el, let attr value be its value; otherwise jump to the last step (labeled FAILURE).
// FIXME: Attribute namespaces
auto attribute_value = element.element().get_attribute(attribute_name);
if (!attribute_value.has_value())
return failure();
// 4. If syntax is the keyword number or an <attr-unit> value, parse attr value against <attr-type>.
// If that succeeds, return the result; otherwise, jump to the last step (labeled FAILURE).
// NOTE: No parsing or modification of any kind is performed on the value.
auto parse_as_number = [&]() -> RefPtr<NumberStyleValue const> {
auto parser = Parser::create(ParsingParams { element.element().document() }, attribute_value.value());
auto unsubstituted_values = parser.parse_as_list_of_component_values();
auto syntax_node = TypeSyntaxNode::create("number"_fly_string);
auto parsed_value = parse_with_a_syntax(ParsingParams { element.document() }, unsubstituted_values, *syntax_node);
if (parsed_value->is_guaranteed_invalid())
return {};
// FIXME: The spec is ambiguous about what we should do for non-number-literals.
// Chromium treats them as invalid, so copy that for now.
// Spec issue: https://github.com/w3c/csswg-drafts/issues/12479
if (!parsed_value->is_number())
return {};
return parsed_value->as_number();
};
bool return_from_step_4 = false;
auto step_4_result = syntax.visit(
// https://drafts.csswg.org/css-values-5/#ref-for-typedef-attr-type%E2%91%A0
[&](NumberKeyword) -> Optional<Vector<ComponentValue>> {
// If given as the number keyword, it causes the attribute’s literal value, after stripping leading and
// trailing whitespace, to be parsed as a <number-token>. Values that fail to parse trigger fallback.
return_from_step_4 = true;
auto parsed_number = parse_as_number();
if (!parsed_number)
return {};
return Vector<ComponentValue> { Token::create_number(Number { Number::Type::Number, parsed_number->number() }) };
},
[&](AttrUnit const& attr_unit) -> Optional<Vector<ComponentValue>> {
// If given as an <attr-unit> value, the value is first parsed as if number keyword was specified, then the
// resulting numeric value is turned into a dimension with the corresponding unit, or a percentage if % was
// given. Same as for number <attr-type>, values that do not correspond to the <number-token> production
// trigger fallback.
return_from_step_4 = true;
auto parsed_number = parse_as_number();
if (!parsed_number)
return {};
if (attr_unit.name == "%"_fly_string)
return Vector<ComponentValue> { Token::create_percentage(Number { Number::Type::Number, parsed_number->number() }) };
return Vector<ComponentValue> { Token::create_dimension(parsed_number->number(), attr_unit.name) };
},
[&](auto&) -> Optional<Vector<ComponentValue>> {
return OptionalNone {};
});
if (return_from_step_4) {
if (step_4_result.has_value())
return step_4_result.release_value();
return failure();
}
// 5. If syntax is null or the keyword raw-string, return a CSS <string> whose value is attr value.
// NOTE: No parsing or modification of any kind is performed on the value.
if (syntax.visit(
[](Empty) { return true; },
[](RawStringKeyword) { return true; },
[](auto&) { return false; })) {
return { Token::create_string(*attribute_value) };
}
// 6. Substitute arbitrary substitution functions in attr value, with «"attribute", attr name» as the substitution
// context, then parse with a <syntax> attr value, with syntax and el. If that succeeds, return the result;
// otherwise, jump to the last step (labeled FAILURE).
auto parser = Parser::create(ParsingParams { element.element().document() }, attribute_value.value());
auto unsubstituted_values = parser.parse_as_list_of_component_values();
auto substituted_values = substitute_arbitrary_substitution_functions(element, guarded_contexts, unsubstituted_values,
SubstitutionContext { SubstitutionContext::DependencyType::Attribute, attribute_name.to_string() });
auto parsed_value = parse_with_a_syntax(ParsingParams { element.document() }, substituted_values, *syntax.get<NonnullOwnPtr<SyntaxNode>>());
if (parsed_value->is_guaranteed_invalid())
return failure();
return parsed_value->tokenize();
// 7. FAILURE:
// NB: Step 7 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 declaration_value_list = arguments.get<DeclarationValueList>();
auto const& first_argument = declaration_value_list.first();
auto const second_argument = declaration_value_list.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<i32> 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-values-5/#replace-an-if-function
static Vector<ComponentValue> replace_an_if_function(DOM::AbstractElement& element, GuardedSubstitutionContexts& guarded_contexts, ArbitrarySubstitutionFunctionArguments const& arguments)
{
// NB: We create a single parser and reuse that for parsing all the conditions
auto parser = Parser::create(ParsingParams { element.element().document() }, {});
// 1. For each <if-args-branch> branch in arguments:
for (auto const& branch : arguments.get<IfArgs>()) {
// 1. Substitute arbitrary substitution functions in the first <declaration-value> of branch, then parse the
// result as an <if-condition>. If parsing returns failure, continue; otherwise, let the result be condition.
auto substituted_condition = substitute_arbitrary_substitution_functions(element, guarded_contexts, branch.condition);
TokenStream<ComponentValue> tokens { substituted_condition };
auto maybe_parsed_if_condition = parser.parse_if_condition(tokens);
if (!maybe_parsed_if_condition)
continue;
// 2. Evaluate condition.
// If a <style-query> in condition tests the value of a property, and guarding a substitution context
// «"property", referenced-property-name» would mark it as a cyclic substitution context, that query
// evaluates to false.
// If the result of condition is false, continue.
// FIXME: Implement the above behavior once we support style queries.
auto condition_evaluation_result = maybe_parsed_if_condition->evaluate(&element.element().document());
if (condition_evaluation_result == MatchResult::False)
continue;
// 3. Substitute arbitrary substitution functions in the second <declaration-value> of branch, and return the result.
if (!branch.value.has_value())
return {};
return substitute_arbitrary_substitution_functions(element, guarded_contexts, branch.value.value());
}
// 2. Return nothing (an empty sequence of component values).
return {};
}
// https://drafts.csswg.org/css-values-5/#replace-an-inherit-function
static Vector<ComponentValue> replace_an_inherit_function(DOM::AbstractElement& element, GuardedSubstitutionContexts& guarded_contexts, ArbitrarySubstitutionFunctionArguments const& arguments)
{
// To replace an inherit() function, given a list of arguments:
auto declaration_value_list = arguments.get<DeclarationValueList>();
auto const& first_argument = declaration_value_list.first();
auto const second_argument = declaration_value_list.get(1);
// 1. Substitute arbitrary substitution functions in the first <declaration-value> of arguments, then parse it as a
// <custom-property-name>.
auto substituted_first_argument = substitute_arbitrary_substitution_functions(element, guarded_contexts, first_argument);
TokenStream first_argument_tokens { substituted_first_argument };
first_argument_tokens.discard_whitespace();
auto const& name_token = first_argument_tokens.consume_a_token();
first_argument_tokens.discard_whitespace();
// 2. If parsing returned a <custom-property-name>, and the inherited value of that custom property on the element
// does not contain the guaranteed-invalid value, return that inherited value.
if (name_token.is(Token::Type::Ident) && is_a_custom_property_name_string(name_token.token().ident()) && first_argument_tokens.is_empty()) {
if (auto element_to_inherit_style_from = element.element_to_inherit_style_from(); element_to_inherit_style_from.has_value()) {
if (auto const& inherited_value = element_to_inherit_style_from->get_custom_property(name_token.token().ident())) {
auto inherited_value_tokens = inherited_value->tokenize();
if (!contains_guaranteed_invalid_value(inherited_value_tokens))
return inherited_value_tokens;
}
}
}
// 3. Otherwise, if a second <declaration-value>? was passed in arguments, substitute arbitrary substitution
// functions in that argument, and return the result.
if (second_argument.has_value())
return substitute_arbitrary_substitution_functions(element, guarded_contexts, second_argument.value());
// 4. Otherwise, return the guaranteed-invalid value.
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)
{
// 1. Let el be the element that the style containing the var() function is being applied to.
// Let first arg be the first <declaration-value> in arguments.
// Let second arg be the <declaration-value>? passed after the comma, or null if there was no comma.
auto declaration_value_list = arguments.get<DeclarationValueList>();
auto const& first_argument = declaration_value_list.first();
auto const second_argument = declaration_value_list.get(1);
// 2. Substitute arbitrary substitution functions in first arg, then parse it as a <custom-property-name>.
// If parsing returned a <custom-property-name>, let result be the computed value of the corresponding custom
// property on el. Otherwise, let result be the guaranteed-invalid value.
auto substituted_first_argument = substitute_arbitrary_substitution_functions(element, guarded_contexts, first_argument);
TokenStream name_tokens { substituted_first_argument };
name_tokens.discard_whitespace();
auto& name_token = name_tokens.consume_a_token();
name_tokens.discard_whitespace();
Vector<ComponentValue> result;
if (name_tokens.has_next_token() || !name_token.is(Token::Type::Ident) || !is_a_custom_property_name_string(name_token.token().ident())) {
result = { ComponentValue { GuaranteedInvalidValue {} } };
} else {
// Look up the value of the custom property
auto& custom_property_name = name_token.token().ident();
auto custom_property_value = StyleComputer::compute_value_of_custom_property(element, custom_property_name, guarded_contexts);
result = custom_property_value->tokenize();
}
// FIXME: 3. If the custom property named by the var()’s first argument is animation-tainted, and the var() is being used
// in a property that is not animatable, set result to the guaranteed-invalid value.
// 4. If result contains the guaranteed-invalid value, and second arg was provided, set result to the result of substitute arbitrary substitution functions on second arg.
if (contains_guaranteed_invalid_value(result) && second_argument.has_value())
result = substitute_arbitrary_substitution_functions(element, guarded_contexts, second_argument.value());
// 5. Return result.
return result;
}
static ErrorOr<void> substitute_arbitrary_substitution_functions_step_2(DOM::AbstractElement& element, GuardedSubstitutionContexts& guarded_contexts, TokenStream<ComponentValue>& source, Vector<ComponentValue>& dest)
{
// Step 2 of https://drafts.csswg.org/css-values-5/#substitute-arbitrary-substitution-function
// 2. For each arbitrary substitution function func in values (ordered via a depth-first pre-order traversal) that
// is not nested in the contents of another arbitrary substitution function:
while (source.has_next_token()) {
auto const& value = source.consume_a_token();
if (value.is_function()) {
auto const& source_function = value.function();
if (auto maybe_function_id = to_arbitrary_substitution_function(source_function.name); maybe_function_id.has_value()) {
auto function_id = maybe_function_id.release_value();
// FIXME: 1. Substitute early-invoked functions in func’s contents, and let early result be the result.
auto const& early_result = source_function.value;
// 2. If early result contains the guaranteed-invalid value, replace func in values with the guaranteed-invalid
// value and continue.
if (contains_guaranteed_invalid_value(early_result)) {
dest.empend(GuaranteedInvalidValue {});
continue;
}
// 3. Parse early result according to func’s argument grammar. If this returns failure, replace func in values
// with the guaranteed-invalid value and continue; otherwise, let arguments be the result.
auto maybe_arguments = parse_according_to_argument_grammar(function_id, early_result);
if (!maybe_arguments.has_value()) {
dest.empend(GuaranteedInvalidValue {});
continue;
}
auto arguments = maybe_arguments.release_value();
// 4. Replace an arbitrary substitution function for func, given arguments, as defined by that function.
// Let result be the returned list of component values.
auto result = replace_an_arbitrary_substitution_function(element, guarded_contexts, function_id, arguments);
// 5. If result contains the guaranteed-invalid value, replace func in values with the guaranteed-invalid value.
// Otherwise, replace func in values with result.
if (contains_guaranteed_invalid_value(result)) {
dest.empend(GuaranteedInvalidValue {});
} else {
// NB: Because we're doing this in one pass recursively, we now need to substitute any ASFs in result.
TokenStream result_stream { result };
Vector<ComponentValue> result_after_processing;
TRY(substitute_arbitrary_substitution_functions_step_2(element, guarded_contexts, result_stream, result_after_processing));
// NB: Protect against the billion-laughs attack by limiting to an arbitrary large number of tokens.
// https://drafts.csswg.org/css-values-5/#long-substitution
if (source.remaining_token_count() + result_after_processing.size() > 16384) {
dest.clear();
dest.empend(GuaranteedInvalidValue {});
return Error::from_string_literal("Stopped expanding arbitrary substitution functions: maximum length reached.");
}
dest.extend(result_after_processing);
}
continue;
}
Vector<ComponentValue> function_values;
TokenStream source_function_contents { source_function.value };
TRY(substitute_arbitrary_substitution_functions_step_2(element, guarded_contexts, source_function_contents, function_values));
dest.empend(Function { source_function.name, move(function_values) });
continue;
}
if (value.is_block()) {
auto const& source_block = value.block();
TokenStream source_block_values { source_block.value };
Vector<ComponentValue> block_values;
TRY(substitute_arbitrary_substitution_functions_step_2(element, guarded_contexts, source_block_values, block_values));
dest.empend(SimpleBlock { source_block.token, move(block_values) });
continue;
}
dest.empend(value);
}
return {};
}
// https://drafts.csswg.org/css-values-5/#substitute-arbitrary-substitution-function
Vector<ComponentValue> substitute_arbitrary_substitution_functions(DOM::AbstractElement& element, GuardedSubstitutionContexts& guarded_contexts, Vector<ComponentValue> const& values, Optional<SubstitutionContext> context)
{
// To substitute arbitrary substitution functions in a sequence of component values values, given an optional
// substitution context context:
// 1. Guard context for the remainder of this algorithm. If context is marked as a cyclic substitution context,
// return the guaranteed-invalid value.
if (context.has_value()) {
guarded_contexts.guard(context.value());
if (context->is_cyclic)
return { ComponentValue { GuaranteedInvalidValue {} } };
}
ScopeGuard const guard { [&] {
if (context.has_value())
guarded_contexts.unguard(context.value());
} };
// 2. For each arbitrary substitution function func in values (ordered via a depth-first pre-order traversal) that
// is not nested in the contents of another arbitrary substitution function:
Vector<ComponentValue> new_values;
TokenStream source { values };
auto maybe_error = substitute_arbitrary_substitution_functions_step_2(element, guarded_contexts, source, new_values);
if (maybe_error.is_error())
return { ComponentValue { GuaranteedInvalidValue {} } };
// 3. If context is marked as a cyclic substitution context, return the guaranteed-invalid value.
// NOTE: Nested arbitrary substitution functions may have marked context as cyclic in step 2.
if (context.has_value() && context->is_cyclic)
return { ComponentValue { GuaranteedInvalidValue {} } };
// 4. Return values.
return new_values;
}
Optional<ArbitrarySubstitutionFunctionArguments> parse_according_to_argument_grammar(ArbitrarySubstitutionFunction function, Vector<ComponentValue> const& values)
{
// Equivalent to `<declaration-value> , <declaration-value>?`, used by multiple argument grammars.
auto parse_declaration_value_then_optional_declaration_value = [](TokenStream<ComponentValue>& tokens, Token::Type separator) -> Optional<DeclarationValueList> {
auto first_argument = Parser::parse_declaration_value(tokens, separator);
if (!first_argument.has_value())
return OptionalNone {};
if (!tokens.has_next_token())
return DeclarationValueList { first_argument.release_value() };
if (!tokens.next_token().is(separator))
return {};
tokens.discard_a_token(); // separator
auto second_argument = Parser::parse_declaration_value(tokens);
return DeclarationValueList { first_argument.release_value(), second_argument.value_or({}) };
};
TokenStream tokens { values };
auto return_if_no_remaining_tokens = [&](Optional<ArbitrarySubstitutionFunctionArguments> value) -> Optional<ArbitrarySubstitutionFunctionArguments> {
if (tokens.has_next_token())
return {};
return value;
};
switch (function) {
case ArbitrarySubstitutionFunction::Attr:
// https://drafts.csswg.org/css-values-5/#attr-notation
// <attr-args> = attr( <declaration-value> , <declaration-value>? )
// FIXME: It would be nice if we had a nice way to create an Optional<Variant<T>> from Optional<T> without these maps.
return return_if_no_remaining_tokens(parse_declaration_value_then_optional_declaration_value(tokens, Token::Type::Comma).map([](DeclarationValueList const& list) -> ArbitrarySubstitutionFunctionArguments { return list; }));
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 return_if_no_remaining_tokens(parse_declaration_value_then_optional_declaration_value(tokens, Token::Type::Comma).map([](DeclarationValueList const& list) -> ArbitrarySubstitutionFunctionArguments { return list; }));
case ArbitrarySubstitutionFunction::If: {
// https://drafts.csswg.org/css-values-5/#if-notation
// <if-args> = if( [ <if-args-branch> ; ]* <if-args-branch> ;? )
// <if-args-branch> = <declaration-value> : <declaration-value>?
IfArgs args;
while (tokens.has_next_token()) {
auto if_args_branch = parse_declaration_value_then_optional_declaration_value(tokens, Token::Type::Colon);
if (!if_args_branch.has_value())
break;
args.append({ if_args_branch->first(), if_args_branch->get(1).map([](auto const& value) { return value; }) });
if (!tokens.next_token().is(Token::Type::Semicolon))
break;
tokens.discard_a_token(); // ;
}
if (args.is_empty())
return {};
return return_if_no_remaining_tokens(args);
}
case ArbitrarySubstitutionFunction::Inherit:
// https://drafts.csswg.org/css-values-5/#inherit-notation
// <inherit-args> = inherit( <declaration-value>, <declaration-value>? )
return return_if_no_remaining_tokens(parse_declaration_value_then_optional_declaration_value(tokens, Token::Type::Comma).map([](DeclarationValueList const& list) -> ArbitrarySubstitutionFunctionArguments { return list; }));
case ArbitrarySubstitutionFunction::Var:
// https://drafts.csswg.org/css-variables/#funcdef-var
// <var-args> = var( <declaration-value> , <declaration-value>? )
return return_if_no_remaining_tokens(parse_declaration_value_then_optional_declaration_value(tokens, Token::Type::Comma).map([](DeclarationValueList const& list) -> ArbitrarySubstitutionFunctionArguments { return list; }));
}
VERIFY_NOT_REACHED();
}
// https://drafts.csswg.org/css-values-5/#replace-an-arbitrary-substitution-function
Vector<ComponentValue> replace_an_arbitrary_substitution_function(DOM::AbstractElement& element, GuardedSubstitutionContexts& guarded_contexts, ArbitrarySubstitutionFunction function, ArbitrarySubstitutionFunctionArguments const& arguments)
{
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::If:
return replace_an_if_function(element, guarded_contexts, arguments);
case ArbitrarySubstitutionFunction::Inherit:
return replace_an_inherit_function(element, guarded_contexts, arguments);
case ArbitrarySubstitutionFunction::Var:
return replace_a_var_function(element, guarded_contexts, arguments);
}
VERIFY_NOT_REACHED();
}
}