diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 247581966ff..97bcb85204a 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -82,6 +82,7 @@ set(SOURCES CSS/GridTrackPlacement.cpp CSS/GridTrackSize.cpp CSS/Interpolation.cpp + CSS/InvalidationSet.cpp CSS/Length.cpp CSS/LengthBox.cpp CSS/MediaList.cpp @@ -116,6 +117,7 @@ set(SOURCES CSS/Sizing.cpp CSS/StyleComputer.cpp CSS/StyleInvalidation.cpp + CSS/StyleInvalidationData.cpp CSS/StyleProperty.cpp CSS/StyleSheet.cpp CSS/StyleSheetIdentifier.cpp diff --git a/Libraries/LibWeb/CSS/InvalidationSet.cpp b/Libraries/LibWeb/CSS/InvalidationSet.cpp new file mode 100644 index 00000000000..8314feedee5 --- /dev/null +++ b/Libraries/LibWeb/CSS/InvalidationSet.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace Web::CSS { + +void InvalidationSet::include_all_from(InvalidationSet const& other) +{ + m_needs_invalidate_self |= other.m_needs_invalidate_self; + m_needs_invalidate_whole_subtree |= other.m_needs_invalidate_whole_subtree; + for (auto const& property : other.m_properties) + m_properties.set(property); +} + +bool InvalidationSet::is_empty() const +{ + return !m_needs_invalidate_self && !m_needs_invalidate_whole_subtree && m_properties.is_empty(); +} + +void InvalidationSet::for_each_property(Function const& callback) const +{ + if (m_needs_invalidate_self) + callback({ Property::Type::InvalidateSelf }); + if (m_needs_invalidate_whole_subtree) + callback({ Property::Type::InvalidateWholeSubtree }); + for (auto const& property : m_properties) + callback(property); +} + +} + +namespace AK { + +unsigned Traits::hash(Web::CSS::InvalidationSet::Property const& invalidation_set_property) +{ + return pair_int_hash(to_underlying(invalidation_set_property.type), invalidation_set_property.name.hash()); +} + +ErrorOr Formatter::format(FormatBuilder& builder, Web::CSS::InvalidationSet::Property const& invalidation_set_property) +{ + switch (invalidation_set_property.type) { + case Web::CSS::InvalidationSet::Property::Type::InvalidateSelf: { + TRY(builder.put_string("$"sv)); + return {}; + } + case Web::CSS::InvalidationSet::Property::Type::Class: { + TRY(builder.put_string("."sv)); + TRY(builder.put_string(invalidation_set_property.name)); + return {}; + } + case Web::CSS::InvalidationSet::Property::Type::Id: { + TRY(builder.put_string("#"sv)); + TRY(builder.put_string(invalidation_set_property.name)); + return {}; + } + case Web::CSS::InvalidationSet::Property::Type::TagName: { + TRY(builder.put_string(invalidation_set_property.name)); + return {}; + } + case Web::CSS::InvalidationSet::Property::Type::Attribute: { + TRY(builder.put_string("["sv)); + TRY(builder.put_string(invalidation_set_property.name)); + TRY(builder.put_string("]"sv)); + return {}; + } + case Web::CSS::InvalidationSet::Property::Type::InvalidateWholeSubtree: { + TRY(builder.put_string("*"sv)); + return {}; + } + default: + VERIFY_NOT_REACHED(); + } +} + +ErrorOr Formatter::format(FormatBuilder& builder, Web::CSS::InvalidationSet const& invalidation_set) +{ + bool first = true; + invalidation_set.for_each_property([&](auto const& property) { + if (!first) + builder.builder().append(", "sv); + builder.builder().appendff("{}", property); + }); + return {}; +} + +} diff --git a/Libraries/LibWeb/CSS/InvalidationSet.h b/Libraries/LibWeb/CSS/InvalidationSet.h new file mode 100644 index 00000000000..3799b713b01 --- /dev/null +++ b/Libraries/LibWeb/CSS/InvalidationSet.h @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace Web::CSS { + +class InvalidationSet { +public: + struct Property { + enum class Type : u8 { + InvalidateSelf, + InvalidateWholeSubtree, + Class, + Id, + TagName, + Attribute, + }; + + Type type; + FlyString name {}; + + bool operator==(Property const& other) const = default; + }; + + void include_all_from(InvalidationSet const& other); + + bool needs_invalidate_self() const { return m_needs_invalidate_self; } + void set_needs_invalidate_self() { m_needs_invalidate_self = true; } + + bool needs_invalidate_whole_subtree() const { return m_needs_invalidate_whole_subtree; } + void set_needs_invalidate_whole_subtree() { m_needs_invalidate_whole_subtree = true; } + + void set_needs_invalidate_class(FlyString const& name) { m_properties.set({ Property::Type::Class, name }); } + void set_needs_invalidate_id(FlyString const& name) { m_properties.set({ Property::Type::Id, name }); } + void set_needs_invalidate_tag_name(FlyString const& name) { m_properties.set({ Property::Type::TagName, name }); } + void set_needs_invalidate_attribute(FlyString const& name) { m_properties.set({ Property::Type::Attribute, name }); } + + bool is_empty() const; + void for_each_property(Function const& callback) const; + +private: + bool m_needs_invalidate_self { false }; + bool m_needs_invalidate_whole_subtree { false }; + HashTable m_properties; +}; + +} + +namespace AK { + +template<> +struct Traits : DefaultTraits { + static unsigned hash(Web::CSS::InvalidationSet::Property const&); + static bool equals(Web::CSS::InvalidationSet::Property const& a, Web::CSS::InvalidationSet::Property const& b) { return a == b; } +}; + +template<> +struct Formatter : Formatter { + ErrorOr format(FormatBuilder&, Web::CSS::InvalidationSet::Property const&); +}; + +template<> +struct Formatter : Formatter { + ErrorOr format(FormatBuilder&, Web::CSS::InvalidationSet const&); +}; + +} diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index f06e1ab782b..1bd57a09a97 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -427,6 +428,46 @@ Vector const& StyleComputer::get_hover_rules() const return m_hover_rules; } +InvalidationSet StyleComputer::invalidation_set_for_properties(Vector const& properties) const +{ + if (!m_style_invalidation_data) + return {}; + auto const& descendant_invalidation_sets = m_style_invalidation_data->descendant_invalidation_sets; + InvalidationSet result; + for (auto const& property : properties) { + if (auto it = descendant_invalidation_sets.find(property); it != descendant_invalidation_sets.end()) + result.include_all_from(it->value); + } + return result; +} + +bool StyleComputer::invalidation_property_used_in_has_selector(InvalidationSet::Property const& property) const +{ + if (!m_style_invalidation_data) + return true; + switch (property.type) { + case InvalidationSet::Property::Type::Id: + if (m_style_invalidation_data->ids_used_in_has_selectors.contains(property.name)) + return true; + break; + case InvalidationSet::Property::Type::Class: + if (m_style_invalidation_data->class_names_used_in_has_selectors.contains(property.name)) + return true; + break; + case InvalidationSet::Property::Type::Attribute: + if (m_style_invalidation_data->attribute_names_used_in_has_selectors.contains(property.name)) + return true; + break; + case InvalidationSet::Property::Type::TagName: + if (m_style_invalidation_data->tag_names_used_in_has_selectors.contains(property.name)) + return true; + break; + default: + break; + } + return false; +} + Vector StyleComputer::collect_matching_rules(DOM::Element const& element, CascadeOrigin cascade_origin, Optional pseudo_element, bool& did_match_any_hover_rules, FlyString const& qualified_layer_name) const { auto const& root_node = element.root(); @@ -2576,6 +2617,11 @@ NonnullOwnPtr StyleComputer::make_rule_cache_for_casca return static_cast(rule).parent_style_rule().absolutized_selectors(); VERIFY_NOT_REACHED(); }(); + + for (auto const& selector : absolutized_selectors) { + m_style_invalidation_data->build_invalidation_sets_for_selector(selector); + } + for (CSS::Selector const& selector : absolutized_selectors) { MatchingRule matching_rule { shadow_root, @@ -2842,6 +2888,7 @@ void StyleComputer::build_qualified_layer_names_cache() void StyleComputer::build_rule_cache() { m_selector_insights = make(); + m_style_invalidation_data = make(); if (auto user_style_source = document().page().user_style(); user_style_source.has_value()) { m_user_style_sheet = GC::make_root(parse_css_stylesheet(CSS::Parser::ParsingContext(document()), user_style_source.value())); @@ -2869,6 +2916,7 @@ void StyleComputer::invalidate_rule_cache() m_user_agent_rule_cache = nullptr; m_hover_rules.clear_with_capacity(); + m_style_invalidation_data = nullptr; } void StyleComputer::did_load_font(FlyString const&) diff --git a/Libraries/LibWeb/CSS/StyleComputer.h b/Libraries/LibWeb/CSS/StyleComputer.h index 2c60e60c1c1..54492fdbf99 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.h +++ b/Libraries/LibWeb/CSS/StyleComputer.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -149,6 +150,9 @@ public: Vector const& get_hover_rules() const; Vector collect_matching_rules(DOM::Element const&, CascadeOrigin, Optional, bool& did_match_any_hover_rules, FlyString const& qualified_layer_name = {}) const; + InvalidationSet invalidation_set_for_properties(Vector const&) const; + bool invalidation_property_used_in_has_selector(InvalidationSet::Property const&) const; + void invalidate_rule_cache(); Gfx::Font const& initial_font() const; @@ -278,6 +282,7 @@ private: OwnPtr m_selector_insights; Vector m_hover_rules; + OwnPtr m_style_invalidation_data; OwnPtr m_author_rule_cache; OwnPtr m_user_rule_cache; OwnPtr m_user_agent_rule_cache; diff --git a/Libraries/LibWeb/CSS/StyleInvalidationData.cpp b/Libraries/LibWeb/CSS/StyleInvalidationData.cpp new file mode 100644 index 00000000000..14b9ec26d1d --- /dev/null +++ b/Libraries/LibWeb/CSS/StyleInvalidationData.cpp @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2025, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace Web::CSS { + +// Iterates over the given selector, grouping consecutive simple selectors that have no combinator (Combinator::None). +// For example, given "div:not(.a) + .b[foo]", the callback is invoked twice: +// once for "div:not(.a)" and once for ".b[foo]". +template +static void for_each_consecutive_simple_selector_group(Selector const& selector, Callback callback) +{ + auto const& compound_selectors = selector.compound_selectors(); + int compound_selector_index = compound_selectors.size() - 1; + Vector simple_selectors; + Selector::Combinator combinator = Selector::Combinator::None; + bool is_rightmost = true; + while (compound_selector_index >= 0) { + if (!simple_selectors.is_empty()) { + callback(simple_selectors, combinator, is_rightmost); + simple_selectors.clear(); + is_rightmost = false; + } + + auto const& compound_selector = compound_selectors[compound_selector_index]; + for (auto const& simple_selector : compound_selector.simple_selectors) { + simple_selectors.append(simple_selector); + } + combinator = compound_selector.combinator; + + --compound_selector_index; + } + if (!simple_selectors.is_empty()) { + callback(simple_selectors, combinator, is_rightmost); + } +} + +static void collect_properties_used_in_has(Selector::SimpleSelector const& selector, StyleInvalidationData& style_invalidation_data, bool in_has) +{ + switch (selector.type) { + case Selector::SimpleSelector::Type::Id: { + if (in_has) + style_invalidation_data.ids_used_in_has_selectors.set(selector.name()); + break; + } + case Selector::SimpleSelector::Type::Class: { + if (in_has) + style_invalidation_data.class_names_used_in_has_selectors.set(selector.name()); + break; + } + case Selector::SimpleSelector::Type::Attribute: { + if (in_has) + style_invalidation_data.attribute_names_used_in_has_selectors.set(selector.attribute().qualified_name.name.lowercase_name); + break; + } + case Selector::SimpleSelector::Type::TagName: { + if (in_has) + style_invalidation_data.tag_names_used_in_has_selectors.set(selector.qualified_name().name.lowercase_name); + break; + } + case Selector::SimpleSelector::Type::PseudoClass: { + auto const& pseudo_class = selector.pseudo_class(); + for (auto const& child_selector : pseudo_class.argument_selector_list) { + for (auto const& compound_selector : child_selector->compound_selectors()) { + for (auto const& simple_selector : compound_selector.simple_selectors) { + collect_properties_used_in_has(simple_selector, style_invalidation_data, in_has || pseudo_class.type == PseudoClass::Has); + } + } + } + break; + } + default: + break; + } +} + +enum class ExcludePropertiesNestedInNotPseudoClass : bool { + No, + Yes, +}; + +static void build_invalidation_sets_for_simple_selector(Selector::SimpleSelector const& selector, InvalidationSet& invalidation_set, ExcludePropertiesNestedInNotPseudoClass exclude_properties_nested_in_not_pseudo_class, StyleInvalidationData& rule_invalidation_data) +{ + switch (selector.type) { + case Selector::SimpleSelector::Type::Class: + invalidation_set.set_needs_invalidate_class(selector.name()); + break; + case Selector::SimpleSelector::Type::Id: + invalidation_set.set_needs_invalidate_id(selector.name()); + break; + case Selector::SimpleSelector::Type::TagName: + invalidation_set.set_needs_invalidate_tag_name(selector.qualified_name().name.lowercase_name); + break; + case Selector::SimpleSelector::Type::Attribute: + invalidation_set.set_needs_invalidate_attribute(selector.attribute().qualified_name.name.lowercase_name); + break; + case Selector::SimpleSelector::Type::PseudoClass: { + auto const& pseudo_class = selector.pseudo_class(); + if (pseudo_class.type == PseudoClass::Has) + break; + if (exclude_properties_nested_in_not_pseudo_class == ExcludePropertiesNestedInNotPseudoClass::Yes && pseudo_class.type == PseudoClass::Not) + break; + for (auto const& nested_selector : pseudo_class.argument_selector_list) { + auto rightmost_invalidation_set_for_selector = rule_invalidation_data.build_invalidation_sets_for_selector(*nested_selector); + invalidation_set.include_all_from(rightmost_invalidation_set_for_selector); + } + break; + } + default: + break; + } +} + +InvalidationSet StyleInvalidationData::build_invalidation_sets_for_selector(Selector const& selector) +{ + auto const& compound_selectors = selector.compound_selectors(); + int compound_selector_index = compound_selectors.size() - 1; + VERIFY(compound_selector_index >= 0); + + InvalidationSet invalidation_set_for_rightmost_selector; + Selector::Combinator previous_compound_combinator = Selector::Combinator::None; + for_each_consecutive_simple_selector_group(selector, [&](Vector const& simple_selectors, Selector::Combinator combinator, bool is_rightmost) { + // Collect properties used in :has() so we can decide if only specific properties + // trigger descendant invalidation or if the entire document must be invalidated. + for (auto const& simple_selector : simple_selectors) { + bool in_has = false; + if (simple_selector.type == Selector::SimpleSelector::Type::PseudoClass) { + auto const& pseudo_class = simple_selector.pseudo_class(); + if (pseudo_class.type == PseudoClass::Has) + in_has = true; + } + collect_properties_used_in_has(simple_selector, *this, in_has); + } + + if (is_rightmost) { + // The rightmost selector is handled twice: + // 1) Include properties nested in :not() + // 2) Exclude properties nested in :not() + // + // This ensures we handle cases like: + // :not(.foo) => produce invalidation set .foo { $ } ($ = invalidate self) + // .bar :not(.foo) => produce invalidation sets .foo { $ } and .bar { * } (* = invalidate subtree) + // which means invalidation_set_for_rightmost_selector should be empty + for (auto const& simple_selector : simple_selectors) { + InvalidationSet s; + build_invalidation_sets_for_simple_selector(simple_selector, s, ExcludePropertiesNestedInNotPseudoClass::No, *this); + s.for_each_property([&](auto const& invalidation_property) { + auto& descendant_invalidation_set = descendant_invalidation_sets.ensure(invalidation_property, [] { return InvalidationSet {}; }); + descendant_invalidation_set.set_needs_invalidate_self(); + }); + } + + for (auto const& simple_selector : simple_selectors) { + build_invalidation_sets_for_simple_selector(simple_selector, invalidation_set_for_rightmost_selector, ExcludePropertiesNestedInNotPseudoClass::Yes, *this); + } + } else { + VERIFY(previous_compound_combinator != Selector::Combinator::None); + for (auto const& simple_selector : simple_selectors) { + InvalidationSet s; + build_invalidation_sets_for_simple_selector(simple_selector, s, ExcludePropertiesNestedInNotPseudoClass::No, *this); + s.for_each_property([&](auto const& invalidation_property) { + auto& descendant_invalidation_set = descendant_invalidation_sets.ensure(invalidation_property, [] { + return InvalidationSet {}; + }); + // If the rightmost selector's invalidation set is empty, it means there's no + // specific property-based invalidation, so we fall back to invalidating the whole subtree. + // If combinator to the right of current compound selector is NextSibling or SubsequentSibling, + // we also need to invalidate the whole subtree, because we don't support sibling invalidation sets. + if (AK::first_is_one_of(previous_compound_combinator, Selector::Combinator::NextSibling, Selector::Combinator::SubsequentSibling)) { + descendant_invalidation_set.set_needs_invalidate_whole_subtree(); + } else if (invalidation_set_for_rightmost_selector.is_empty()) { + descendant_invalidation_set.set_needs_invalidate_whole_subtree(); + } else { + descendant_invalidation_set.include_all_from(invalidation_set_for_rightmost_selector); + } + }); + } + } + + previous_compound_combinator = combinator; + }); + + return invalidation_set_for_rightmost_selector; +} + +} diff --git a/Libraries/LibWeb/CSS/StyleInvalidationData.h b/Libraries/LibWeb/CSS/StyleInvalidationData.h new file mode 100644 index 00000000000..fb33bada9b9 --- /dev/null +++ b/Libraries/LibWeb/CSS/StyleInvalidationData.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Web::CSS { + +struct StyleInvalidationData { + HashMap descendant_invalidation_sets; + HashTable ids_used_in_has_selectors; + HashTable class_names_used_in_has_selectors; + HashTable attribute_names_used_in_has_selectors; + HashTable tag_names_used_in_has_selectors; + + InvalidationSet build_invalidation_sets_for_selector(Selector const& selector); +}; + +} diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index 0d8f8f4a545..cbc837a9886 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -462,7 +462,7 @@ void Element::run_attribute_change_steps(FlyString const& local_name, Optional const& old_value, Optional const& new_value) { // FIXME: Only invalidate if the attribute can actually affect style. @@ -1970,6 +1993,29 @@ void Element::invalidate_style_after_attribute_change(FlyString const& attribute return; } + if (attribute_name == HTML::AttributeNames::class_) { + Vector old_classes; + Vector new_classes; + if (old_value.has_value()) + old_classes = old_value->bytes_as_string_view().split_view_if(Infra::is_ascii_whitespace); + if (new_value.has_value()) + new_classes = new_value->bytes_as_string_view().split_view_if(Infra::is_ascii_whitespace); + Vector changed_properties; + for (auto& old_class : old_classes) { + if (!new_classes.contains_slow(old_class)) { + changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::Class, .name = FlyString::from_utf8_without_validation(old_class.bytes()) }); + } + } + for (auto& new_class : new_classes) { + if (!old_classes.contains_slow(new_class)) { + changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::Class, .name = FlyString::from_utf8_without_validation(new_class.bytes()) }); + } + } + changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::Attribute, .name = HTML::AttributeNames::class_ }); + invalidate_style(StyleInvalidationReason::ElementAttributeChange, changed_properties); + return; + } + if (is_presentational_hint(attribute_name) || attribute_name_may_affect_selectors(*this, attribute_name)) { invalidate_style(StyleInvalidationReason::ElementAttributeChange); diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index 8535e8ca368..cf1e31aba2b 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -263,6 +263,7 @@ public: static GC::Ptr create_layout_node_for_display_type(DOM::Document&, CSS::Display const&, GC::Ref, Element*); bool affected_by_hover() const; + bool affected_by_invalidation_property(CSS::InvalidationSet::Property const&) const; void set_pseudo_element_node(Badge, CSS::Selector::PseudoElement::Type, GC::Ptr); GC::Ptr get_pseudo_element_node(CSS::Selector::PseudoElement::Type) const; @@ -420,7 +421,7 @@ protected: private: void make_html_uppercased_qualified_name(); - void invalidate_style_after_attribute_change(FlyString const& attribute_name); + void invalidate_style_after_attribute_change(FlyString const& attribute_name, Optional const& old_value, Optional const& new_value); WebIDL::ExceptionOr> insert_adjacent(StringView where, GC::Ref node); diff --git a/Libraries/LibWeb/DOM/Node.cpp b/Libraries/LibWeb/DOM/Node.cpp index b87db9fd437..d2470242565 100644 --- a/Libraries/LibWeb/DOM/Node.cpp +++ b/Libraries/LibWeb/DOM/Node.cpp @@ -470,6 +470,64 @@ void Node::invalidate_style(StyleInvalidationReason reason) document().schedule_style_update(); } +void Node::invalidate_style(StyleInvalidationReason reason, Vector const& properties) +{ + if (is_character_data()) + return; + + bool properties_used_in_has_selectors = false; + for (auto const& property : properties) { + properties_used_in_has_selectors |= document().style_computer().invalidation_property_used_in_has_selector(property); + } + if (properties_used_in_has_selectors) { + document().invalidate_style(reason); + return; + } + + auto invalidation_set = document().style_computer().invalidation_set_for_properties(properties); + if (invalidation_set.is_empty()) + return; + + if (invalidation_set.needs_invalidate_self()) { + set_needs_style_update(true); + } + + auto element_has_properties_from_invalidation_set = [&](Element& element) { + bool result = false; + invalidation_set.for_each_property([&](auto const& property) { + if (element.affected_by_invalidation_property(property)) + result = true; + }); + return result; + }; + + auto invalidate_entire_subtree = [&](Node& subtree_root) { + subtree_root.for_each_shadow_including_inclusive_descendant([&](Node& node) { + if (!node.is_element()) + return TraversalDecision::Continue; + auto& element = static_cast(node); + bool needs_style_recalculation = invalidation_set.needs_invalidate_whole_subtree() || element_has_properties_from_invalidation_set(element); + if (needs_style_recalculation) { + element.set_needs_style_update(true); + } else { + element.set_needs_inherited_style_update(true); + } + return TraversalDecision::Continue; + }); + }; + + invalidate_entire_subtree(*this); + + if (invalidation_set.needs_invalidate_whole_subtree()) { + for (auto* sibling = next_sibling(); sibling; sibling = sibling->next_sibling()) { + if (sibling->is_element()) + invalidate_entire_subtree(*sibling); + } + } + + document().schedule_style_update(); +} + String Node::child_text_content() const { if (!is(*this)) diff --git a/Libraries/LibWeb/DOM/Node.h b/Libraries/LibWeb/DOM/Node.h index 587e0405f0f..bd21c40e618 100644 --- a/Libraries/LibWeb/DOM/Node.h +++ b/Libraries/LibWeb/DOM/Node.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -297,6 +298,7 @@ public: void set_child_needs_style_update(bool b) { m_child_needs_style_update = b; } void invalidate_style(StyleInvalidationReason); + void invalidate_style(StyleInvalidationReason, Vector const&); void set_document(Badge, Document&);