소스 검색

LibWeb: Parse and resolve UnresolvedStyleValues

If a property is custom or contains a `var()` reference, it cannot be
parsed into a proper StyleValue immediately, so we store it as an
UnresolvedStyleValue until the property is compute. Then, at compute
time, we resolve them by expanding out any `var()` references, and
parsing the result.

The implementation here is very naive, and involves copying the
UnresolvedStyleValue's tree of StyleComponentValueRules while copying
the contents of any `var()`s it finds along the way. This is quite an
expensive operation to do every time that the style is computed.
Sam Atkins 3 년 전
부모
커밋
23dc0dac88

+ 40 - 4
Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp

@@ -3435,8 +3435,19 @@ RefPtr<StyleValue> Parser::parse_as_css_value(PropertyID property_id)
 
 Result<NonnullRefPtr<StyleValue>, Parser::ParsingResult> Parser::parse_css_value(PropertyID property_id, TokenStream<StyleComponentValueRule>& tokens)
 {
+    auto block_contains_var = [](StyleBlockRule const& block, auto&& recurse) -> bool {
+        for (auto const& token : block.values()) {
+            if (token.is_function() && token.function().name().equals_ignoring_case("var"sv))
+                return true;
+            if (token.is_block() && recurse(token.block(), recurse))
+                return true;
+        }
+        return false;
+    };
+
     m_context.set_current_property_id(property_id);
     Vector<StyleComponentValueRule> component_values;
+    bool contains_var = false;
 
     while (tokens.has_next_token()) {
         auto& token = tokens.next_token();
@@ -3446,15 +3457,27 @@ Result<NonnullRefPtr<StyleValue>, Parser::ParsingResult> Parser::parse_css_value
             break;
         }
 
-        if (token.is(Token::Type::Whitespace))
-            continue;
+        if (property_id != PropertyID::Custom) {
+            if (token.is(Token::Type::Whitespace))
+                continue;
 
-        if (token.is(Token::Type::Ident) && has_ignored_vendor_prefix(token.token().ident()))
-            return ParsingResult::IncludesIgnoredVendorPrefix;
+            if (token.is(Token::Type::Ident) && has_ignored_vendor_prefix(token.token().ident()))
+                return ParsingResult::IncludesIgnoredVendorPrefix;
+        }
+
+        if (!contains_var) {
+            if (token.is_function() && token.function().name().equals_ignoring_case("var"sv))
+                contains_var = true;
+            else if (token.is_block() && block_contains_var(token.block(), block_contains_var))
+                contains_var = true;
+        }
 
         component_values.append(token);
     }
 
+    if (property_id == PropertyID::Custom || contains_var)
+        return { UnresolvedStyleValue::create(move(component_values), contains_var) };
+
     if (component_values.is_empty())
         return ParsingResult::SyntaxError;
 
@@ -4167,6 +4190,19 @@ bool Parser::has_ignored_vendor_prefix(StringView string)
     return true;
 }
 
+RefPtr<StyleValue> Parser::parse_css_value(Badge<StyleComputer>, ParsingContext const& context, PropertyID property_id, Vector<StyleComponentValueRule> const& tokens)
+{
+    if (tokens.is_empty() || property_id == CSS::PropertyID::Invalid || property_id == CSS::PropertyID::Custom)
+        return {};
+
+    CSS::Parser parser(context, "");
+    CSS::TokenStream<CSS::StyleComponentValueRule> token_stream { tokens };
+    auto result = parser.parse_css_value(property_id, token_stream);
+    if (result.is_error())
+        return {};
+    return result.release_value();
+}
+
 }
 
 namespace Web {

+ 2 - 0
Userland/Libraries/LibWeb/CSS/Parser/Parser.h

@@ -113,6 +113,8 @@ public:
 
     RefPtr<StyleValue> parse_as_css_value(PropertyID);
 
+    static RefPtr<StyleValue> parse_css_value(Badge<StyleComputer>, ParsingContext const&, PropertyID, Vector<StyleComponentValueRule> const&);
+
 private:
     enum class ParsingResult {
         Done,

+ 2 - 1
Userland/Libraries/LibWeb/CSS/Parser/StyleFunctionRule.h

@@ -20,7 +20,8 @@ class StyleFunctionRule : public RefCounted<StyleFunctionRule> {
     friend class Parser;
 
 public:
-    StyleFunctionRule(String name);
+    explicit StyleFunctionRule(String name);
+    StyleFunctionRule(String name, Vector<StyleComponentValueRule>&& values);
     ~StyleFunctionRule();
 
     String const& name() const { return m_name; }

+ 7 - 1
Userland/Libraries/LibWeb/CSS/Parser/StyleRules.cpp

@@ -57,7 +57,13 @@ StyleDeclarationRule::StyleDeclarationRule() { }
 StyleDeclarationRule::~StyleDeclarationRule() { }
 
 StyleFunctionRule::StyleFunctionRule(String name)
-    : m_name(name)
+    : m_name(move(name))
+{
+}
+
+StyleFunctionRule::StyleFunctionRule(String name, Vector<StyleComponentValueRule>&& values)
+    : m_name(move(name))
+    , m_values(move(values))
 {
 }
 StyleFunctionRule::~StyleFunctionRule() { }

+ 83 - 0
Userland/Libraries/LibWeb/CSS/StyleComputer.cpp

@@ -452,6 +452,85 @@ struct MatchingDeclarations {
     Vector<MatchingRule> author_rules;
 };
 
+bool StyleComputer::expand_unresolved_values(DOM::Element& element, Vector<StyleComponentValueRule> const& source, Vector<StyleComponentValueRule>& dest) const
+{
+    // FIXME: Do this better!
+    // We build a copy of the tree of StyleComponentValueRules, with all var()s replaced with their contents.
+    // This is a very naive solution, and we could do better if the CSS Parser could accept tokens one at a time.
+
+    // FIXME: Handle dependency cycles. https://www.w3.org/TR/css-variables-1/#cycles
+    // FIXME: Handle overly-long variables. https://www.w3.org/TR/css-variables-1/#long-variables
+
+    auto get_custom_property = [this, &element](auto& name) -> RefPtr<StyleValue> {
+        auto custom_property = resolve_custom_property(element, name);
+        if (custom_property.has_value())
+            return custom_property.value().value;
+        return nullptr;
+    };
+
+    for (auto& value : source) {
+        if (value.is_function()) {
+            if (value.function().name().equals_ignoring_case("var"sv)) {
+                auto& var_contents = value.function().values();
+                if (var_contents.is_empty())
+                    return false;
+
+                auto& custom_property_name_token = var_contents.first();
+                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.starts_with("--"))
+                    return false;
+
+                if (auto custom_property_value = get_custom_property(custom_property_name)) {
+                    VERIFY(custom_property_value->is_unresolved());
+                    if (!expand_unresolved_values(element, custom_property_value->as_unresolved().values(), dest))
+                        return false;
+                    continue;
+                }
+
+                // TODO: Handle fallback value
+            }
+
+            auto& source_function = value.function();
+            Vector<StyleComponentValueRule> function_values;
+            if (!expand_unresolved_values(element, source_function.values(), function_values))
+                return false;
+            NonnullRefPtr<StyleFunctionRule> function = adopt_ref(*new StyleFunctionRule(source_function.name(), move(function_values)));
+            dest.empend(function);
+            continue;
+        }
+        if (value.is_block()) {
+            auto& source_block = value.block();
+            Vector<StyleComponentValueRule> block_values;
+            if (!expand_unresolved_values(element, source_block.values(), block_values))
+                return false;
+            NonnullRefPtr<StyleBlockRule> block = adopt_ref(*new StyleBlockRule(source_block.token(), move(block_values)));
+            dest.empend(block);
+            continue;
+        }
+        dest.empend(value.token());
+    }
+
+    return true;
+}
+
+RefPtr<StyleValue> StyleComputer::resolve_unresolved_style_value(DOM::Element& element, PropertyID property_id, UnresolvedStyleValue const& unresolved) const
+{
+    // Unresolved always contains a var(), unless it is a custom property's value, in which case we shouldn't be trying
+    // to produce a different StyleValue from it.
+    VERIFY(unresolved.contains_var());
+
+    Vector<StyleComponentValueRule> expanded_values;
+    if (!expand_unresolved_values(element, unresolved.values(), expanded_values))
+        return {};
+
+    if (auto parsed_value = Parser::parse_css_value({}, ParsingContext { document() }, property_id, expanded_values))
+        return parsed_value.release_nonnull();
+
+    return {};
+}
+
 void StyleComputer::cascade_declarations(StyleProperties& style, DOM::Element& element, Vector<MatchingRule> const& matching_rules, CascadeOrigin cascade_origin, bool important) const
 {
     for (auto& match : matching_rules) {
@@ -466,6 +545,10 @@ void StyleComputer::cascade_declarations(StyleProperties& style, DOM::Element& e
                     property_value = resolved.value().value;
                 }
             }
+            if (property.value->is_unresolved()) {
+                if (auto resolved = resolve_unresolved_style_value(element, property.property_id, property.value->as_unresolved()))
+                    property_value = resolved.release_nonnull();
+            }
             set_property_expanding_shorthands(style, property.property_id, property_value, m_document);
         }
     }

+ 4 - 0
Userland/Libraries/LibWeb/CSS/StyleComputer.h

@@ -9,6 +9,7 @@
 #include <AK/NonnullRefPtrVector.h>
 #include <AK/OwnPtr.h>
 #include <LibWeb/CSS/CSSStyleDeclaration.h>
+#include <LibWeb/CSS/Parser/StyleComponentValueRule.h>
 #include <LibWeb/CSS/StyleProperties.h>
 #include <LibWeb/Forward.h>
 
@@ -60,6 +61,9 @@ private:
 
     void compute_defaulted_property_value(StyleProperties&, DOM::Element const*, CSS::PropertyID) const;
 
+    RefPtr<StyleValue> resolve_unresolved_style_value(DOM::Element&, PropertyID, UnresolvedStyleValue const&) const;
+    bool expand_unresolved_values(DOM::Element&, Vector<StyleComponentValueRule> const& source, Vector<StyleComponentValueRule>& dest) const;
+
     template<typename Callback>
     void for_each_stylesheet(CascadeOrigin, Callback) const;