diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index d85221b8549..c91dac76459 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -132,6 +132,7 @@ set(SOURCES CSS/Number.cpp CSS/PageSelector.cpp CSS/ParsedFontFace.cpp + CSS/Parser/ArbitrarySubstitutionFunctions.cpp CSS/Parser/ComponentValue.cpp CSS/Parser/DescriptorParsing.cpp CSS/Parser/GradientParsing.cpp diff --git a/Libraries/LibWeb/CSS/Parser/ArbitrarySubstitutionFunctions.cpp b/Libraries/LibWeb/CSS/Parser/ArbitrarySubstitutionFunctions.cpp new file mode 100644 index 00000000000..2836202cb2f --- /dev/null +++ b/Libraries/LibWeb/CSS/Parser/ArbitrarySubstitutionFunctions.cpp @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2025, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +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 to_arbitrary_substitution_function(FlyString const& name) +{ + if (name.equals_ignoring_ascii_case("attr"sv)) + return ArbitrarySubstitutionFunction::Attr; + if (name.equals_ignoring_ascii_case("var"sv)) + return ArbitrarySubstitutionFunction::Var; + return {}; +} + +bool contains_guaranteed_invalid_value(Vector 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 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 in arguments. + // Let second arg be the ? passed after the comma, or null if there was no comma. + auto const& first_argument = arguments.first(); + auto const second_argument = arguments.get(1); + + FlyString attribute_name; + Optional maybe_syntax = {}; + auto failure = [&] -> Vector { + // This is step 6, but defined here for convenience. + + // 1. If second arg is null, and syntax was omitted, return an empty CSS . + if (!second_argument.has_value() && !maybe_syntax.has_value()) + 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 ?. + // 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 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 }; + // = [ ? '|' ]? + // 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(); + + // = type( ) | raw-string | + // FIXME: Support type() + bool is_dimension_unit = false; + 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)) { + maybe_syntax = first_argument_tokens.consume_a_token().token().ident(); + } else { + is_dimension_unit = syntax_ident == "%"sv + || Angle::unit_from_name(syntax_ident).has_value() + || Flex::unit_from_name(syntax_ident).has_value() + || Frequency::unit_from_name(syntax_ident).has_value() + || Length::unit_from_name(syntax_ident).has_value() + || Resolution::unit_from_name(syntax_ident).has_value() + || Time::unit_from_name(syntax_ident).has_value(); + if (is_dimension_unit) + maybe_syntax = first_argument_tokens.consume_a_token().token().ident(); + } + } + 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 null or the keyword raw-string, return a CSS whose value is attr value. + // NOTE: No parsing or modification of any kind is performed on the value. + if (!maybe_syntax.has_value() || maybe_syntax->equals_ignoring_ascii_case("raw-string"sv)) + return { Token::create_string(*attribute_value) }; + auto syntax = maybe_syntax.release_value(); + + // 5. Substitute arbitrary substitution functions in attr value, with «"attribute", attr name» as the substitution + // context, then parse with a 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() }); + + // FIXME: Parse using the syntax. For now we just handle `` here. + TokenStream value_tokens { substituted_values }; + value_tokens.discard_whitespace(); + auto const& component_value = value_tokens.consume_a_token(); + value_tokens.discard_whitespace(); + if (value_tokens.has_next_token()) + return failure(); + + if (component_value.is(Token::Type::Number) && is_dimension_unit) + return { Token::create_dimension(component_value.token().number_value(), syntax) }; + + return failure(); + + // 6. FAILURE: + // NB: Step 6 is a lambda defined at the top of the function. +} + +// https://drafts.csswg.org/css-variables-1/#replace-a-var-function +static Vector 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 in arguments. + // Let second arg be the ? passed after the comma, or null if there was no comma. + auto const& first_argument = arguments.first(); + auto const second_argument = arguments.get(1); + + // 2. Substitute arbitrary substitution functions in first arg, then parse it as a . + // If parsing returned a , 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 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); + if (custom_property_value->is_guaranteed_invalid()) { + result = { ComponentValue { GuaranteedInvalidValue {} } }; + } else if (custom_property_value->is_unresolved()) { + result = custom_property_value->as_unresolved().values(); + } else { + dbgln_if(CSS_PARSER_DEBUG, "Custom property `{}` is an unsupported type: {}", custom_property_name, to_underlying(custom_property_value->type())); + result = { ComponentValue { GuaranteedInvalidValue {} } }; + } + } + + // 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 void substitute_arbitrary_substitution_functions_step_2(DOM::AbstractElement& element, GuardedSubstitutionContexts& guarded_contexts, TokenStream& source, Vector& 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 result_after_processing; + substitute_arbitrary_substitution_functions_step_2(element, guarded_contexts, result_stream, result_after_processing); + dest.extend(result_after_processing); + } + continue; + } + + Vector function_values; + TokenStream source_function_contents { source_function.value }; + 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 block_values; + 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); + } +} + +// https://drafts.csswg.org/css-values-5/#substitute-arbitrary-substitution-function +Vector substitute_arbitrary_substitution_functions(DOM::AbstractElement& element, GuardedSubstitutionContexts& guarded_contexts, Vector const& values, Optional 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 new_values; + TokenStream source { values }; + substitute_arbitrary_substitution_functions_step_2(element, guarded_contexts, source, new_values); + + // 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 parse_according_to_argument_grammar(ArbitrarySubstitutionFunction function, Vector const& values) +{ + // Equivalent to ` , ?`, used by multiple argument grammars. + auto parse_declaration_value_then_optional_declaration_value = [](Vector const& values) -> Optional { + TokenStream tokens { values }; + + auto first_argument = Parser::parse_declaration_value(tokens, Parser::StopAtComma::Yes); + if (!first_argument.has_value()) + return OptionalNone {}; + + if (!tokens.has_next_token()) + return ArbitrarySubstitutionFunctionArguments { first_argument.release_value() }; + + VERIFY(tokens.next_token().is(Token::Type::Comma)); + tokens.discard_a_token(); // , + + auto second_argument = Parser::parse_declaration_value(tokens, Parser::StopAtComma::No); + if (tokens.has_next_token()) + return OptionalNone {}; + return ArbitrarySubstitutionFunctionArguments { first_argument.release_value(), second_argument.value_or({}) }; + }; + + switch (function) { + case ArbitrarySubstitutionFunction::Attr: + // https://drafts.csswg.org/css-values-5/#attr-notation + // = attr( , ? ) + return parse_declaration_value_then_optional_declaration_value(values); + case ArbitrarySubstitutionFunction::Var: + // https://drafts.csswg.org/css-variables/#funcdef-var + // = var( , ? ) + return parse_declaration_value_then_optional_declaration_value(values); + } + VERIFY_NOT_REACHED(); +} + +// https://drafts.csswg.org/css-values-5/#replace-an-arbitrary-substitution-function +Vector 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::Var: + return replace_a_var_function(element, guarded_contexts, arguments); + } + VERIFY_NOT_REACHED(); +} + +} diff --git a/Libraries/LibWeb/CSS/Parser/ArbitrarySubstitutionFunctions.h b/Libraries/LibWeb/CSS/Parser/ArbitrarySubstitutionFunctions.h new file mode 100644 index 00000000000..9ec1ad856ba --- /dev/null +++ b/Libraries/LibWeb/CSS/Parser/ArbitrarySubstitutionFunctions.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Web::CSS::Parser { + +// https://drafts.csswg.org/css-values-5/#substitution-context +struct SubstitutionContext { + enum class DependencyType : u8 { + Property, + Attribute, + Function, + }; + DependencyType dependency_type; + String first; + Optional second {}; + + bool is_cyclic { false }; + + bool operator==(SubstitutionContext const&) const; + String to_string() const; +}; + +class GuardedSubstitutionContexts { +public: + void guard(SubstitutionContext&); + void unguard(SubstitutionContext const&); + +private: + Vector m_contexts; +}; + +enum class ArbitrarySubstitutionFunction : u8 { + Attr, + Var, +}; +[[nodiscard]] Optional to_arbitrary_substitution_function(FlyString const& name); + +bool contains_guaranteed_invalid_value(Vector const&); + +[[nodiscard]] Vector substitute_arbitrary_substitution_functions(DOM::AbstractElement&, GuardedSubstitutionContexts&, Vector const&, Optional = {}); + +using ArbitrarySubstitutionFunctionArguments = Vector>; +[[nodiscard]] Optional parse_according_to_argument_grammar(ArbitrarySubstitutionFunction, Vector const&); + +[[nodiscard]] Vector replace_an_arbitrary_substitution_function(DOM::AbstractElement&, GuardedSubstitutionContexts&, ArbitrarySubstitutionFunction, ArbitrarySubstitutionFunctionArguments const&); + +} diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index 95c3e59812d..44e6eeeaa7b 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -134,7 +134,7 @@ public: Vector parse_as_list_of_component_values(); - static NonnullRefPtr resolve_unresolved_style_value(ParsingParams const&, DOM::Element&, Optional, PropertyIDOrCustomPropertyName, UnresolvedStyleValue const&); + static NonnullRefPtr resolve_unresolved_style_value(ParsingParams const&, DOM::Element&, Optional, PropertyIDOrCustomPropertyName, UnresolvedStyleValue const&, Optional = {}); [[nodiscard]] LengthOrCalculated parse_as_sizes_attribute(DOM::Element const& element, HTML::HTMLImageElement const* img = nullptr); @@ -513,10 +513,7 @@ private: OwnPtr parse_supports_feature(TokenStream&); - NonnullRefPtr resolve_unresolved_style_value(DOM::Element&, Optional, PropertyIDOrCustomPropertyName, UnresolvedStyleValue const&); - bool expand_variables(DOM::Element&, Optional, FlyString const& property_name, HashMap>& dependencies, TokenStream& source, Vector& dest); - bool expand_unresolved_values(DOM::Element&, FlyString const& property_name, TokenStream& source, Vector& dest); - bool substitute_attr_function(DOM::Element& element, FlyString const& property_name, Function const& attr_function, Vector& dest); + NonnullRefPtr resolve_unresolved_style_value(DOM::AbstractElement&, GuardedSubstitutionContexts&, PropertyIDOrCustomPropertyName, UnresolvedStyleValue const&); static bool has_ignored_vendor_prefix(StringView); diff --git a/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp b/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp index 386c8ed4798..0103dc4b240 100644 --- a/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp @@ -19,8 +19,8 @@ #include #include #include +#include #include -#include #include #include #include @@ -4273,342 +4273,56 @@ RefPtr Parser::parse_font_source_value(TokenStream Parser::resolve_unresolved_style_value(ParsingParams const& context, DOM::Element& element, Optional pseudo_element, PropertyIDOrCustomPropertyName property, UnresolvedStyleValue const& unresolved) +NonnullRefPtr Parser::resolve_unresolved_style_value(ParsingParams const& context, DOM::Element& element, Optional pseudo_element, PropertyIDOrCustomPropertyName property, UnresolvedStyleValue const& unresolved, Optional existing_guarded_contexts) { // Unresolved always contains a var() or attr(), unless it is a custom property's value, in which case we shouldn't be trying // to produce a different CSSStyleValue from it. VERIFY(unresolved.contains_arbitrary_substitution_function()); + DOM::AbstractElement abstract_element { element, pseudo_element }; auto parser = Parser::create(context, ""sv); - return parser.resolve_unresolved_style_value(element, pseudo_element, property, unresolved); + if (existing_guarded_contexts.has_value()) + return parser.resolve_unresolved_style_value(abstract_element, existing_guarded_contexts.value(), property, unresolved); + GuardedSubstitutionContexts guarded_contexts; + return parser.resolve_unresolved_style_value(abstract_element, guarded_contexts, property, unresolved); } -class PropertyDependencyNode : public RefCounted { -public: - static NonnullRefPtr create(FlyString name) - { - return adopt_ref(*new PropertyDependencyNode(move(name))); - } - - void add_child(NonnullRefPtr new_child) - { - for (auto const& child : m_children) { - if (child->m_name == new_child->m_name) - return; - } - - // We detect self-reference already. - VERIFY(new_child->m_name != m_name); - m_children.append(move(new_child)); - } - - bool has_cycles() - { - if (m_marked) - return true; - - TemporaryChange change { m_marked, true }; - for (auto& child : m_children) { - if (child->has_cycles()) - return true; - } - return false; - } - -private: - explicit PropertyDependencyNode(FlyString name) - : m_name(move(name)) - { - } - - FlyString m_name; - Vector> m_children; - bool m_marked { false }; -}; - -NonnullRefPtr Parser::resolve_unresolved_style_value(DOM::Element& element, Optional pseudo_element, PropertyIDOrCustomPropertyName property, UnresolvedStyleValue const& unresolved) +// https://drafts.csswg.org/css-values-5/#property-replacement +NonnullRefPtr Parser::resolve_unresolved_style_value(DOM::AbstractElement& element, GuardedSubstitutionContexts& guarded_contexts, PropertyIDOrCustomPropertyName property, UnresolvedStyleValue const& unresolved) { - TokenStream unresolved_values_without_variables_expanded { unresolved.values() }; - Vector values_with_variables_expanded; + // AD-HOC: Report that we might rely on custom properties. + // FIXME: This over-invalidates. Find a way of invalidating only when we need to - specifically, when var() is used. + element.element().set_style_uses_css_custom_properties(true); + // To replace substitution functions in a property prop: auto const& property_name = property.visit( [](PropertyID const& property_id) { return string_from_property_id(property_id); }, [](FlyString const& name) { return name; }); + auto const& property_id = property.visit( + [](PropertyID const& property_id) { return property_id; }, + [](FlyString const&) { return PropertyID::Custom; }); - HashMap> dependencies; - ScopeGuard mark_element_if_uses_custom_properties = [&] { - for (auto const& name : dependencies.keys()) { - if (is_a_custom_property_name_string(name)) { - element.set_style_uses_css_custom_properties(true); - return; - } - } - }; - if (!expand_variables(element, pseudo_element, property_name, dependencies, unresolved_values_without_variables_expanded, values_with_variables_expanded)) + // 1. Substitute arbitrary substitution functions in prop’s value, given «"property", prop’s name» as the + // substitution context. Let result be the returned component value sequence. + auto result = substitute_arbitrary_substitution_functions(element, guarded_contexts, unresolved.values(), SubstitutionContext { SubstitutionContext::DependencyType::Property, property_name.to_string() }); + + // 2. If result contains the guaranteed-invalid value, prop is invalid at computed-value time; return. + if (contains_guaranteed_invalid_value(result)) return GuaranteedInvalidStyleValue::create(); - TokenStream unresolved_values_with_variables_expanded { values_with_variables_expanded }; - Vector expanded_values; - if (!expand_unresolved_values(element, property_name, unresolved_values_with_variables_expanded, expanded_values)) + // 3. Parse result according to prop’s grammar. If this returns failure, prop is invalid at computed-value time; return. + // NB: Custom properties have no grammar as such, so we skip this step for them. + // FIXME: Parse according to @property syntax once we support that. + if (property_id == PropertyID::Custom) + return UnresolvedStyleValue::create(move(result), false, {}); + + auto expanded_value_tokens = TokenStream { result }; + auto parsed_value = parse_css_value(property_id, expanded_value_tokens); + if (parsed_value.is_error()) return GuaranteedInvalidStyleValue::create(); - return property.visit( - [&](PropertyID const& property_id) -> NonnullRefPtr { - auto expanded_value_tokens = TokenStream { expanded_values }; - if (auto parsed_value = parse_css_value(property_id, expanded_value_tokens); !parsed_value.is_error()) - return parsed_value.release_value(); - - return GuaranteedInvalidStyleValue::create(); - }, - [&](FlyString const&) -> NonnullRefPtr { - return UnresolvedStyleValue::create(move(expanded_values), false, {}); - }); -} - -static RefPtr get_custom_property(DOM::Element const& element, Optional pseudo_element, FlyString const& custom_property_name) -{ - if (pseudo_element.has_value()) { - if (auto it = element.custom_properties(pseudo_element).find(custom_property_name); it != element.custom_properties(pseudo_element).end()) - return it->value.value; - } - - for (auto const* current_element = &element; current_element; current_element = current_element->parent_or_shadow_host_element()) { - if (auto it = current_element->custom_properties({}).find(custom_property_name); it != current_element->custom_properties({}).end()) - return it->value.value; - } - return nullptr; -} - -bool Parser::expand_variables(DOM::Element& element, Optional pseudo_element, FlyString const& property_name, HashMap>& dependencies, TokenStream& source, Vector& dest) -{ - // Arbitrary large value chosen to avoid the billion-laughs attack. - // https://www.w3.org/TR/css-variables-1/#long-variables - size_t const MAX_VALUE_COUNT = 16384; - if (source.remaining_token_count() + dest.size() > MAX_VALUE_COUNT) { - dbgln("Stopped expanding CSS variables: maximum length reached."); - return false; - } - - auto get_dependency_node = [&](FlyString const& name) -> NonnullRefPtr { - if (auto existing = dependencies.get(name); existing.has_value()) - return *existing.value(); - auto new_node = PropertyDependencyNode::create(name); - dependencies.set(name, new_node); - return new_node; - }; - - while (source.has_next_token()) { - auto const& value = source.consume_a_token(); - if (value.is_block()) { - auto const& source_block = value.block(); - Vector block_values; - TokenStream source_block_contents { source_block.value }; - if (!expand_variables(element, pseudo_element, property_name, dependencies, source_block_contents, block_values)) - return false; - dest.empend(SimpleBlock { source_block.token, move(block_values) }); - continue; - } - if (!value.is_function()) { - dest.empend(value.token()); - continue; - } - if (!value.function().name.equals_ignoring_ascii_case("var"sv)) { - auto const& source_function = value.function(); - Vector function_values; - TokenStream source_function_contents { source_function.value }; - if (!expand_variables(element, pseudo_element, property_name, dependencies, source_function_contents, function_values)) - return false; - dest.empend(Function { source_function.name, move(function_values) }); - continue; - } - - TokenStream var_contents { value.function().value }; - var_contents.discard_whitespace(); - if (!var_contents.has_next_token()) - return false; - - auto const& custom_property_name_token = var_contents.consume_a_token(); - if (!custom_property_name_token.is(Token::Type::Ident)) - return false; - auto custom_property_name = custom_property_name_token.token().ident(); - if (!custom_property_name.bytes_as_string_view().starts_with("--"sv)) - return false; - - // Detect dependency cycles. https://www.w3.org/TR/css-variables-1/#cycles - // We do not do this by the spec, since we are not keeping a graph of var dependencies around, - // but rebuilding it every time. - if (custom_property_name == property_name) - return false; - auto parent = get_dependency_node(property_name); - auto child = get_dependency_node(custom_property_name); - parent->add_child(child); - if (parent->has_cycles()) - return false; - - if (auto custom_property_value = get_custom_property(element, pseudo_element, custom_property_name); - custom_property_value && custom_property_value->is_unresolved()) { - // FIXME: We should properly cascade here instead of doing a basic fallback for CSS-wide keywords. - TokenStream custom_property_tokens { custom_property_value->as_unresolved().values() }; - - auto dest_size_before = dest.size(); - if (!expand_variables(element, pseudo_element, custom_property_name, dependencies, custom_property_tokens, dest)) - return false; - - // If the size of dest has increased, then the custom property is not the initial guaranteed-invalid value. - // If it hasn't increased, then it is the initial guaranteed-invalid value, and thus we should move on to the fallback value. - if (dest_size_before < dest.size()) - continue; - - dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Expanding custom property '{}' did not return any tokens, treating it as invalid and moving on to the fallback value.", custom_property_name); - } - - // Use the provided fallback value, if any. - var_contents.discard_whitespace(); - if (var_contents.has_next_token()) { - auto const& comma_token = var_contents.consume_a_token(); - if (!comma_token.is(Token::Type::Comma)) - return false; - var_contents.discard_whitespace(); - if (!expand_variables(element, pseudo_element, property_name, dependencies, var_contents, dest)) - return false; - } - } - return true; -} - -bool Parser::expand_unresolved_values(DOM::Element& element, FlyString const& property_name, TokenStream& source, Vector& dest) -{ - while (source.has_next_token()) { - auto const& value = source.consume_a_token(); - if (value.is_function()) { - if (value.function().name.equals_ignoring_ascii_case("attr"sv)) { - if (!substitute_attr_function(element, property_name, value.function(), dest)) - return false; - continue; - } - - auto const& source_function = value.function(); - Vector function_values; - TokenStream source_function_contents { source_function.value }; - if (!expand_unresolved_values(element, property_name, source_function_contents, function_values)) - return false; - 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 block_values; - if (!expand_unresolved_values(element, property_name, source_block_values, block_values)) - return false; - dest.empend(SimpleBlock { source_block.token, move(block_values) }); - continue; - } - dest.empend(value.token()); - } - - return true; -} - -// 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& dest) -{ - // attr() = attr( ? , ?) - // = [ ? '|' ]? - // = type( ) | raw-string | - // The production matches any identifier that is an ASCII case-insensitive match for the name of a CSS dimension unit, such as px, or the %. - TokenStream attr_contents { attr_function.value }; - attr_contents.discard_whitespace(); - if (!attr_contents.has_next_token()) - return false; - - // - Attribute name - // FIXME: Support optional attribute namespace - if (!attr_contents.next_token().is(Token::Type::Ident)) - return false; - auto attribute_name = attr_contents.consume_a_token().token().ident(); - attr_contents.discard_whitespace(); - - // - Attribute type (optional) - 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(); - } - - // - Comma, then fallback values (optional) - bool has_fallback_values = false; - if (attr_contents.has_next_token()) { - if (!attr_contents.next_token().is(Token::Type::Comma)) - return false; - (void)attr_contents.consume_a_token(); // Comma - has_fallback_values = true; - } - - // Then, run the substitution algorithm: - - // 1. If the attr() function has a substitution value, replace the attr() function by the substitution value. - // https://drafts.csswg.org/css-values-5/#attr-types - if (element.has_attribute(attribute_name)) { - auto parse_string_as_component_value = [this](String const& string) { - auto tokens = Tokenizer::tokenize(string, "utf-8"sv); - TokenStream stream { tokens }; - return parse_a_component_value(stream); - }; - - auto attribute_value = element.get_attribute_value(attribute_name); - 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 { - // 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 = parse_string_as_component_value(attribute_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 flex_unit = Flex::unit_from_name(attribute_type); flex_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. - if (has_fallback_values) - return expand_unresolved_values(element, property_name, attr_contents, dest); - - if (attribute_type.equals_ignoring_ascii_case("raw-string"_fly_string)) { - // If the argument is string, defaults to the empty string if omitted - dest.empend(Token::create_string({})); - return true; - } - - // 3. Otherwise, the property containing the attr() function is invalid at computed-value time. - return false; + // 4. Otherwise, replace prop’s value with the parsed result. + return parsed_value.release_value(); } } diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index 6342fd450ad..027fdf7837e 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -3095,7 +3096,7 @@ void StyleComputer::unload_fonts_from_sheet(CSSStyleSheet& sheet) } } -NonnullRefPtr StyleComputer::compute_value_of_custom_property(DOM::AbstractElement abstract_element, FlyString const& name) +NonnullRefPtr StyleComputer::compute_value_of_custom_property(DOM::AbstractElement abstract_element, FlyString const& name, Optional guarded_contexts) { // https://drafts.csswg.org/css-variables/#propdef- // The computed value of a custom property is its specified value with any arbitrary-substitution functions replaced. @@ -3129,7 +3130,7 @@ NonnullRefPtr StyleComputer::compute_value_of_custom_proper return value.release_nonnull(); auto& unresolved = value->as_unresolved(); - return Parser::Parser::resolve_unresolved_style_value(Parser::ParsingParams {}, abstract_element.element(), abstract_element.pseudo_element(), name, unresolved); + return Parser::Parser::resolve_unresolved_style_value(Parser::ParsingParams {}, abstract_element.element(), abstract_element.pseudo_element(), name, unresolved, guarded_contexts); } void StyleComputer::compute_custom_properties(ComputedProperties&, DOM::AbstractElement abstract_element) const diff --git a/Libraries/LibWeb/CSS/StyleComputer.h b/Libraries/LibWeb/CSS/StyleComputer.h index f4f71ee518b..97df447cc07 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.h +++ b/Libraries/LibWeb/CSS/StyleComputer.h @@ -193,7 +193,7 @@ public: [[nodiscard]] inline bool should_reject_with_ancestor_filter(Selector const&) const; - static NonnullRefPtr compute_value_of_custom_property(DOM::AbstractElement, FlyString const& custom_property); + static NonnullRefPtr compute_value_of_custom_property(DOM::AbstractElement, FlyString const& custom_property, Optional = {}); private: enum class ComputeStyleMode { diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 23e96949c77..486292f5c60 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -335,6 +335,7 @@ struct StyleSheetIdentifier; namespace Web::CSS::Parser { class ComponentValue; +class GuardedSubstitutionContexts; class Parser; class Token; class Tokenizer; diff --git a/Tests/LibWeb/Layout/expected/css-attr-typed-fallback.txt b/Tests/LibWeb/Layout/expected/css-attr-typed-fallback.txt index 478b4773edf..b8a4095d4fb 100644 --- a/Tests/LibWeb/Layout/expected/css-attr-typed-fallback.txt +++ b/Tests/LibWeb/Layout/expected/css-attr-typed-fallback.txt @@ -8,9 +8,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline TextNode <#text> BlockContainer <(anonymous)> at (8,30) content-size 784x0 children: inline TextNode <#text> - BlockContainer at (9,31) content-size 100x20 children: inline - InlineNode <(anonymous)> - TextNode <#text> + BlockContainer at (9,31) content-size 100x20 children: not-inline BlockContainer <(anonymous)> at (8,52) content-size 784x0 children: inline TextNode <#text> BlockContainer at (9,53) content-size 200x20 children: not-inline diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-variables/variable-css-wide-keywords.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-variables/variable-css-wide-keywords.txt index 968c57802a7..ba07e6608b9 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-variables/variable-css-wide-keywords.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-variables/variable-css-wide-keywords.txt @@ -2,19 +2,19 @@ Harness status: OK Found 30 tests -4 Pass -26 Fail +9 Pass +21 Fail Pass `initial` as a value for an unregistered custom property -Fail `inherit` as a value for an unregistered custom property -Fail `unset` as a value for an unregistered custom property +Pass `inherit` as a value for an unregistered custom property +Pass `unset` as a value for an unregistered custom property Fail `revert` as a value for an unregistered custom property Pass `revert-layer` as a value for an unregistered custom property Fail `initial` as a value for a non-inheriting registered custom property Fail `initial` as a value for an inheriting registered custom property -Fail `inherit` as a value for a non-inheriting registered custom property -Fail `inherit` as a value for an inheriting registered custom property +Pass `inherit` as a value for a non-inheriting registered custom property +Pass `inherit` as a value for an inheriting registered custom property Fail `unset` as a value for a non-inheriting registered custom property -Fail `unset` as a value for an inheriting registered custom property +Pass `unset` as a value for an inheriting registered custom property Fail `revert` as a value for a non-inheriting registered custom property Fail `revert` as a value for an inheriting registered custom property Pass `revert-layer` as a value for a non-inheriting registered custom property diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-variables/variable-cycles.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-variables/variable-cycles.txt index fd901d37310..d1ab4929882 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-variables/variable-cycles.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-variables/variable-cycles.txt @@ -2,16 +2,15 @@ Harness status: OK Found 11 tests -4 Pass -7 Fail +11 Pass Pass Self-cycle Pass Simple a/b cycle Pass Three-var cycle -Fail Cycle that starts in the middle of a chain -Fail Cycle with extra edge -Fail Cycle with extra edge (2) -Fail Cycle with extra edge (3) -Fail Cycle with secondary cycle -Fail Cycle with overlapping secondary cycle -Fail Cycle with deeper secondary cycle +Pass Cycle that starts in the middle of a chain +Pass Cycle with extra edge +Pass Cycle with extra edge (2) +Pass Cycle with extra edge (3) +Pass Cycle with secondary cycle +Pass Cycle with overlapping secondary cycle +Pass Cycle with deeper secondary cycle Pass Cycle in unused fallback \ No newline at end of file