From 52e1d3040481a1c93b49d8754f725d58a83dfa8e Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Thu, 4 Jun 2026 16:59:14 +0100 Subject: [PATCH] LibWeb+LibDevTools: Report applied style rules to Firefox Collect the style rules that apply to an inspected element and expose them through the existing DOM node inspection path. This gives Firefox's Rules panel real rule forms instead of the previous empty getApplied response. --- .../LibDevTools/Actors/PageStyleActor.cpp | 68 ++- Libraries/LibDevTools/Actors/PageStyleActor.h | 3 + .../LibDevTools/Actors/StyleRuleActor.cpp | 63 +++ Libraries/LibDevTools/Actors/StyleRuleActor.h | 32 ++ Libraries/LibDevTools/CMakeLists.txt | 1 + Libraries/LibDevTools/Forward.h | 1 + Libraries/LibWeb/CSS/CSSStyleSheet.cpp | 10 - Libraries/LibWeb/CSS/CSSStyleSheet.h | 4 +- Libraries/LibWeb/CSS/StyleComputer.cpp | 393 ++++++++++++++++++ Libraries/LibWeb/CSS/StyleComputer.h | 2 + Libraries/LibWeb/CSS/StyleScope.cpp | 4 +- Libraries/LibWeb/CSS/StyleScope.h | 1 + Libraries/LibWeb/DOM/Document.cpp | 8 +- Libraries/LibWebView/DOMNodeProperties.h | 1 + Services/WebContent/ConnectionFromClient.cpp | 27 +- Tests/LibDevTools/TestDevToolsProtocol.cpp | 142 ++++++- 16 files changed, 728 insertions(+), 32 deletions(-) create mode 100644 Libraries/LibDevTools/Actors/StyleRuleActor.cpp create mode 100644 Libraries/LibDevTools/Actors/StyleRuleActor.h diff --git a/Libraries/LibDevTools/Actors/PageStyleActor.cpp b/Libraries/LibDevTools/Actors/PageStyleActor.cpp index 60c1f2cb34..105fbff48f 100644 --- a/Libraries/LibDevTools/Actors/PageStyleActor.cpp +++ b/Libraries/LibDevTools/Actors/PageStyleActor.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -111,6 +112,42 @@ static void received_fonts(JsonObject& response, JsonValue const& fonts) response.set("fontFaces"sv, move(font_faces)); } +void PageStyleActor::received_applied_style_rules(JsonObject& response, JsonValue const& applied_style_rules) +{ + JsonArray entries; + clear_style_rule_actors(); + + if (applied_style_rules.is_array()) { + applied_style_rules.as_array().for_each([&](JsonValue const& value) { + if (!value.is_object()) + return; + + auto entry = value.as_object(); + auto rule = entry.get_object("rule"sv); + if (!rule.has_value()) + return; + + auto& style_rule_actor = devtools().register_actor(*rule); + m_style_rule_actors.append(style_rule_actor.name()); + entry.set("rule"sv, style_rule_actor.serialize_rule()); + + if (auto inherited_node_id = entry.get_integer("inheritedNodeId"sv); inherited_node_id.has_value()) { + JsonValue inherited { JsonValue {} }; + if (auto walker = InspectorActor::walker_for(m_inspector)) { + if (auto inherited_node_actor = walker->node_actor_name_for(Web::UniqueNodeID { *inherited_node_id }); inherited_node_actor.has_value()) + inherited = *inherited_node_actor; + } + entry.set("inherited"sv, move(inherited)); + entry.remove("inheritedNodeId"sv); + } + + entries.must_append(move(entry)); + }); + } + + response.set("entries"sv, move(entries)); +} + NonnullRefPtr PageStyleActor::create(DevToolsServer& devtools, String name, WeakPtr inspector) { return adopt_ref(*new PageStyleActor(devtools, move(name), move(inspector))); @@ -130,10 +167,19 @@ PageStyleActor::PageStyleActor(DevToolsServer& devtools, String name, WeakPtrdescription()); } +void PageStyleActor::clear_style_rule_actors() +{ + for (auto const& actor : m_style_rule_actors) + devtools().unregister_actor(actor); + m_style_rule_actors.clear(); +} + void PageStyleActor::handle_message(Message const& message) { JsonObject response; @@ -145,10 +191,7 @@ void PageStyleActor::handle_message(Message const& message) } if (message.type == "getApplied"sv) { - // FIXME: This provides information to the "styles" pane in the inspector tab, which allows toggling and editing - // styles live. We do not yet support figuring out the list of styles that apply to a specific node. - response.set("entries"sv, JsonArray {}); - send_response(message, move(response)); + inspect_dom_node(message, WebView::DOMNodeProperties::Type::AppliedStyleRules); return; } @@ -202,7 +245,19 @@ void PageStyleActor::inspect_dom_node(Message const& message, WebView::DOMNodePr return; } - devtools().delegate().inspect_dom_node(dom_node->tab->description(), property_type, dom_node->identifier.id, dom_node->identifier.pseudo_element); + JsonObject options; + if (property_type == WebView::DOMNodeProperties::Type::AppliedStyleRules) { + if (auto inherited = message.data.get_bool("inherited"sv); inherited.has_value()) + options.set("inherited"sv, *inherited); + if (auto matched_selectors = message.data.get_bool("matchedSelectors"sv); matched_selectors.has_value()) + options.set("matchedSelectors"sv, *matched_selectors); + if (auto skip_pseudo = message.data.get_bool("skipPseudo"sv); skip_pseudo.has_value()) + options.set("skipPseudo"sv, *skip_pseudo); + if (auto filter = message.data.get_string("filter"sv); filter.has_value()) + options.set("filter"sv, *filter); + } + + devtools().delegate().inspect_dom_node(dom_node->tab->description(), property_type, dom_node->identifier.id, dom_node->identifier.pseudo_element, move(options)); m_pending_inspect_requests.append({ .id = message.id }); } @@ -214,6 +269,9 @@ void PageStyleActor::received_dom_node_properties(WebView::DOMNodeProperties con JsonObject response; switch (properties.type) { + case WebView::DOMNodeProperties::Type::AppliedStyleRules: + received_applied_style_rules(response, properties.properties); + break; case WebView::DOMNodeProperties::Type::ComputedStyle: received_computed_style(response, properties.properties); break; diff --git a/Libraries/LibDevTools/Actors/PageStyleActor.h b/Libraries/LibDevTools/Actors/PageStyleActor.h index dddf1b7f9f..13b3c48f96 100644 --- a/Libraries/LibDevTools/Actors/PageStyleActor.h +++ b/Libraries/LibDevTools/Actors/PageStyleActor.h @@ -28,11 +28,14 @@ private: virtual void handle_message(Message const&) override; void inspect_dom_node(Message const&, WebView::DOMNodeProperties::Type); + void clear_style_rule_actors(); + void received_applied_style_rules(JsonObject&, JsonValue const&); void received_dom_node_properties(WebView::DOMNodeProperties const&); WeakPtr m_inspector; Vector m_pending_inspect_requests; + Vector m_style_rule_actors; }; } diff --git a/Libraries/LibDevTools/Actors/StyleRuleActor.cpp b/Libraries/LibDevTools/Actors/StyleRuleActor.cpp new file mode 100644 index 0000000000..f8713562bb --- /dev/null +++ b/Libraries/LibDevTools/Actors/StyleRuleActor.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace DevTools { + +NonnullRefPtr StyleRuleActor::create(DevToolsServer& devtools, String name, JsonObject rule) +{ + return adopt_ref(*new StyleRuleActor(devtools, move(name), move(rule))); +} + +StyleRuleActor::StyleRuleActor(DevToolsServer& devtools, String name, JsonObject rule) + : Actor(devtools, move(name)) + , m_rule(move(rule)) +{ + m_rule.set("actor"sv, this->name()); + + JsonObject traits; + // FIXME: Support modifying style rules inside the inspector. + traits.set("canSetRuleText"sv, false); + m_rule.set("traits"sv, move(traits)); + m_rule.set("ancestorData"sv, JsonArray {}); +} + +StyleRuleActor::~StyleRuleActor() = default; + +JsonObject StyleRuleActor::serialize_rule() const +{ + return m_rule; +} + +void StyleRuleActor::handle_message(Message const& message) +{ + JsonObject response; + + if (message.type == "getRuleText"sv) { + response.set("text"sv, m_rule.get_string("cssText"sv).value_or({})); + send_response(message, move(response)); + return; + } + + if (message.type == "modifyProperties"sv || message.type == "setRuleText"sv || message.type == "modifySelector"sv) { + response.set("error"sv, "unsupported"sv); + response.set("message"sv, "Live CSS rule editing is not supported"sv); + send_response(message, move(response)); + return; + } + + if (message.type == "getQueryContainerForNode"sv) { + response.set("container"sv, JsonValue {}); + send_response(message, move(response)); + return; + } + + send_unrecognized_packet_type_error(message); +} + +} diff --git a/Libraries/LibDevTools/Actors/StyleRuleActor.h b/Libraries/LibDevTools/Actors/StyleRuleActor.h new file mode 100644 index 0000000000..2dfdaddd65 --- /dev/null +++ b/Libraries/LibDevTools/Actors/StyleRuleActor.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace DevTools { + +class DEVTOOLS_API StyleRuleActor final : public Actor { +public: + static constexpr auto base_name = "style-rule"sv; + + static NonnullRefPtr create(DevToolsServer&, String name, JsonObject rule); + virtual ~StyleRuleActor() override; + + JsonObject serialize_rule() const; + +private: + StyleRuleActor(DevToolsServer&, String name, JsonObject rule); + + virtual void handle_message(Message const&) override; + + JsonObject m_rule; +}; + +} diff --git a/Libraries/LibDevTools/CMakeLists.txt b/Libraries/LibDevTools/CMakeLists.txt index 5e4a1ad653..28d18b84d5 100644 --- a/Libraries/LibDevTools/CMakeLists.txt +++ b/Libraries/LibDevTools/CMakeLists.txt @@ -18,6 +18,7 @@ set(SOURCES Actors/PreferenceActor.cpp Actors/ProcessActor.cpp Actors/RootActor.cpp + Actors/StyleRuleActor.cpp Actors/StyleSheetsActor.cpp Actors/TabActor.cpp Actors/TargetConfigurationActor.cpp diff --git a/Libraries/LibDevTools/Forward.h b/Libraries/LibDevTools/Forward.h index ce774df63e..6e6c9b350e 100644 --- a/Libraries/LibDevTools/Forward.h +++ b/Libraries/LibDevTools/Forward.h @@ -32,6 +32,7 @@ class ParentAccessibilityActor; class PreferenceActor; class ProcessActor; class RootActor; +class StyleRuleActor; class StyleSheetsActor; class TabActor; class TargetConfigurationActor; diff --git a/Libraries/LibWeb/CSS/CSSStyleSheet.cpp b/Libraries/LibWeb/CSS/CSSStyleSheet.cpp index 2309db9e1e..a007f522f8 100644 --- a/Libraries/LibWeb/CSS/CSSStyleSheet.cpp +++ b/Libraries/LibWeb/CSS/CSSStyleSheet.cpp @@ -643,16 +643,6 @@ void CSSStyleSheet::recalculate_rule_caches() } } -void CSSStyleSheet::set_source_text(String source) -{ - m_source_text = move(source); -} - -Optional CSSStyleSheet::source_text(Badge) const -{ - return m_source_text; -} - void CSSStyleSheet::add_critical_subresource(Subresource& subresource) { m_critical_subresources.append(subresource); diff --git a/Libraries/LibWeb/CSS/CSSStyleSheet.h b/Libraries/LibWeb/CSS/CSSStyleSheet.h index 065c5af7e2..f6ee6e7b9d 100644 --- a/Libraries/LibWeb/CSS/CSSStyleSheet.h +++ b/Libraries/LibWeb/CSS/CSSStyleSheet.h @@ -122,8 +122,8 @@ public: bool disallow_modification() const { return m_disallow_modification; } - void set_source_text(String); - Optional source_text(Badge) const; + void set_source_text(String source) { m_source_text = move(source); } + Optional source_text() const { return m_source_text; } void add_critical_subresource(Subresource&); void remove_critical_subresource(Subresource&); diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index dc74bd14eb..eff21fdef5 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -15,10 +15,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -33,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -81,6 +84,7 @@ #include #include #include +#include #include #include #include @@ -1491,6 +1495,395 @@ StyleComputer::MatchingRuleSet StyleComputer::build_matching_rule_set(DOM::Abstr return matching_rule_set; } +static bool custom_property_inherits(DOM::Document const& document, FlyString const& name) +{ + // A custom property inherits unless it has been registered with an explicit `inherits: false`. + auto registration = document.get_registered_custom_property(name); + return !registration.has_value() || registration->inherit; +} + +enum class IsCustomProperty : u8 { + No, + Yes, +}; + +enum class Inherits : u8 { + No, + Yes, +}; + +enum class NameIsValid : u8 { + No, + Yes, +}; + +enum class IsValid : u8 { + No, + Yes, +}; + +static JsonObject serialize_devtools_style_declaration( + String name, + String value, + Important important, + IsCustomProperty is_custom_property, + Inherits inherits, + NameIsValid is_name_valid, + IsValid is_valid) +{ + JsonObject serialized_property; + serialized_property.set("name"sv, move(name)); + serialized_property.set("value"sv, move(value)); + serialized_property.set("priority"sv, important == Important::Yes ? "important"sv : ""sv); + serialized_property.set("isCustomProperty"sv, is_custom_property == IsCustomProperty::Yes); + serialized_property.set("inherits"sv, inherits == Inherits::Yes); + serialized_property.set("isNameValid"sv, is_name_valid == NameIsValid::Yes); + serialized_property.set("isValid"sv, is_valid == IsValid::Yes); + return serialized_property; +} + +static JsonArray serialize_devtools_style_declarations(DOM::Document const& document, CSSStyleProperties const& declaration) +{ + JsonArray declarations; + + auto serialize_property = [&](String name, StyleProperty const& property, IsCustomProperty is_custom_property, Inherits inherits) { + declarations.must_append(serialize_devtools_style_declaration( + move(name), + property.value->to_string(SerializationMode::Normal), + property.important, + is_custom_property, + inherits, + NameIsValid::Yes, + IsValid::Yes)); + }; + + for (auto const& property : declaration.properties()) { + serialize_property( + string_from_property_id(property.property_id).to_string(), + property, + IsCustomProperty::No, + is_inherited_property(property.property_id) ? Inherits::Yes : Inherits::No); + } + + for (auto const& custom_property : declaration.custom_properties()) + serialize_property( + custom_property.key.to_string(), + custom_property.value, + IsCustomProperty::Yes, + custom_property_inherits(document, custom_property.key) ? Inherits::Yes : Inherits::No); + + return declarations; +} + +static JsonArray serialize_devtools_style_declarations(DOM::Document const& document, Vector const& declarations) +{ + JsonArray serialized_declarations; + + for (auto const& declaration : declarations) { + bool inherits = declaration.is_custom_property + ? custom_property_inherits(document, declaration.name) + : PropertyNameAndID::from_name(declaration.name) + .map([](auto const& property) { return !property.is_custom_property() && is_inherited_property(property.id()); }) + .value_or(false); + + serialized_declarations.must_append(serialize_devtools_style_declaration( + declaration.name.to_string(), + declaration.value, + declaration.important, + declaration.is_custom_property ? IsCustomProperty::Yes : IsCustomProperty::No, + inherits ? Inherits::Yes : Inherits::No, + declaration.is_name_valid ? NameIsValid::Yes : NameIsValid::No, + declaration.is_valid ? IsValid::Yes : IsValid::No)); + } + + return serialized_declarations; +} + +static Vector parse_devtools_style_declarations(DOM::Document const& document, StringView declaration_block) +{ + return Parser::parse_css_declaration_block_for_devtools(Parser::ParsingParams(document), declaration_block); +} + +static Optional source_offset_for_line_and_column(StringView source, SourcePosition const& position) +{ + size_t line = 0; + size_t column = 0; + + Utf8View source_code_points { source }; + for (auto it = source_code_points.begin(); it != source_code_points.end();) { + auto offset = source_code_points.byte_offset_of(it); + if (line == position.line && column == position.column) + return offset; + + auto code_point = *it; + ++it; + + if (code_point == '\r') { + if (offset + 1 < source.length() && source[offset + 1] == '\n') + ++it; + ++line; + column = 0; + } else if (code_point == '\n' || code_point == '\f') { + ++line; + column = 0; + } else { + ++column; + } + } + + if (line == position.line && column == position.column) + return source.length(); + + return {}; +} + +static Optional extract_css_declaration_block_from_source(CSSRule const& rule) +{ + if (rule.type() != CSSRule::Type::Style) + return {}; + + auto const* style_sheet = rule.parent_style_sheet(); + if (!style_sheet) + return {}; + + auto const source_text = style_sheet->source_text(); + if (!source_text.has_value()) + return {}; + + auto const& source = *source_text; + auto source_view = source.bytes_as_string_view(); + auto const& source_location = rule.source_location(); + if (!source_location.has_value()) + return {}; + + auto maybe_offset = source_offset_for_line_and_column(source_view, *source_location); + if (!maybe_offset.has_value()) + return {}; + + Optional string_quote; + bool in_comment = false; + bool escaped = false; + Optional block_start; + size_t block_depth = 0; + + for (size_t offset = *maybe_offset; offset < source_view.length(); ++offset) { + auto ch = source_view[offset]; + auto next_ch = offset + 1 < source_view.length() ? source_view[offset + 1] : '\0'; + + if (in_comment) { + if (ch == '*' && next_ch == '/') { + in_comment = false; + ++offset; + } + continue; + } + + if (string_quote.has_value()) { + if (escaped) { + escaped = false; + continue; + } + if (ch == '\\') { + escaped = true; + continue; + } + if (ch == *string_quote) + string_quote = {}; + continue; + } + + if (ch == '/' && next_ch == '*') { + in_comment = true; + ++offset; + continue; + } + + if (ch == '"' || ch == '\'') { + string_quote = ch; + continue; + } + + if (ch == '{') { + if (!block_start.has_value()) + block_start = offset + 1; + ++block_depth; + continue; + } + + if (ch == '}' && block_start.has_value()) { + VERIFY(block_depth > 0); + --block_depth; + if (block_depth == 0) + return MUST(String::from_utf8(source_view.substring_view(*block_start, offset - *block_start))); + } + } + + return {}; +} + +static bool has_inherited_declaration(DOM::Document const& document, CSSStyleProperties const& declaration) +{ + if (any_of(declaration.properties(), [](auto const& property) { + return CSS::is_inherited_property(property.property_id); + })) { + return true; + } + + return any_of(declaration.custom_properties(), [&](auto const& custom_property) { + return custom_property_inherits(document, custom_property.key); + }); +} + +static JsonArray serialize_devtools_selectors(MatchingRule const& rule) +{ + JsonArray selectors; + for (auto const& selector : rule.absolutized_selectors()) + selectors.must_append(selector->serialize()); + return selectors; +} + +static JsonArray serialize_devtools_selector_specificities(MatchingRule const& rule) +{ + JsonArray specificities; + for (auto const& selector : rule.absolutized_selectors()) + specificities.must_append(selector->specificity()); + return specificities; +} + +static JsonObject serialize_devtools_matching_rule(DOM::Document const& document, MatchingRule const& rule) +{ + auto const& declaration = rule.declaration(); + auto authored_text = extract_css_declaration_block_from_source(*rule.rule); + + JsonArray matched_selector_indexes; + matched_selector_indexes.must_append(rule.selector_index); + + JsonObject serialized_rule; + serialized_rule.set("type"sv, to_underlying(rule.rule->type())); + serialized_rule.set("className"sv, rule.rule->type() == CSSRule::Type::Style ? "CSSStyleRule"sv : "CSSNestedDeclarations"sv); + serialized_rule.set("selectors"sv, serialize_devtools_selectors(rule)); + serialized_rule.set("selectorsSpecificity"sv, serialize_devtools_selector_specificities(rule)); + serialized_rule.set("matchedSelectorIndexes"sv, move(matched_selector_indexes)); + serialized_rule.set("cssText"sv, rule.rule->css_text()); + if (authored_text.has_value()) { + serialized_rule.set("authoredText"sv, *authored_text); + serialized_rule.set("declarations"sv, serialize_devtools_style_declarations(document, parse_devtools_style_declarations(document, authored_text->bytes_as_string_view()))); + } else { + serialized_rule.set("authoredText"sv, declaration.serialized()); + serialized_rule.set("declarations"sv, serialize_devtools_style_declarations(document, declaration)); + } + serialized_rule.set("styleSheetIndex"sv, rule.style_sheet_index); + serialized_rule.set("ruleIndex"sv, rule.rule_index); + serialized_rule.set("isSystem"sv, rule.cascade_origin == CascadeOrigin::UserAgent); + + return serialized_rule; +} + +static JsonObject serialize_devtools_inline_style(DOM::Document const& document, DOM::AbstractElement abstract_element, CSSStyleProperties const& declaration) +{ + auto authored_text = abstract_element.element().get_attribute(HTML::AttributeNames::style); + + JsonObject serialized_rule; + serialized_rule.set("type"sv, 100); + serialized_rule.set("className"sv, 100); + serialized_rule.set("cssText"sv, declaration.serialized()); + if (authored_text.has_value()) { + serialized_rule.set("authoredText"sv, *authored_text); + serialized_rule.set("declarations"sv, serialize_devtools_style_declarations(document, parse_devtools_style_declarations(document, authored_text->bytes_as_string_view()))); + } else { + serialized_rule.set("authoredText"sv, declaration.serialized()); + serialized_rule.set("declarations"sv, serialize_devtools_style_declarations(document, declaration)); + } + serialized_rule.set("isSystem"sv, false); + serialized_rule.set("nodeId"sv, abstract_element.element().unique_id().value()); + return serialized_rule; +} + +static void append_devtools_applied_style_entry(JsonArray& entries, JsonObject rule, Optional inherited_node_id = {}) +{ + JsonObject entry; + + JsonValue matched_selector_indexes { JsonArray {} }; + if (auto value = rule.get("matchedSelectorIndexes"sv); value.has_value()) + matched_selector_indexes = *value; + rule.remove("matchedSelectorIndexes"sv); + auto is_system = rule.get_bool("isSystem"sv).value_or(false); + + entry.set("rule"sv, move(rule)); + entry.set("isSystem"sv, is_system); + entry.set("matchedSelectorIndexes"sv, move(matched_selector_indexes)); + if (inherited_node_id.has_value()) + entry.set("inheritedNodeId"sv, inherited_node_id->value()); + else + entry.set("inherited"sv, JsonValue {}); + + entries.must_append(move(entry)); +} + +static void append_devtools_rules_for_element(DOM::Document const& document, JsonArray& entries, auto const& matching_rule_set, bool include_user_agent_styles, Optional inherited_node_id = {}) +{ + auto should_include_rule = [&](MatchingRule const& rule) { + return !inherited_node_id.has_value() || has_inherited_declaration(document, rule.declaration()); + }; + + auto append_rules = [&](auto const& matching_rules) { + for (auto const& matching_rule : matching_rules.in_reverse()) { + auto const& rule = *matching_rule.rule; + if (!should_include_rule(rule)) + continue; + append_devtools_applied_style_entry(entries, serialize_devtools_matching_rule(document, rule), inherited_node_id); + } + }; + + for (auto const& context : matching_rule_set.author_contexts.in_reverse()) { + for (auto const& layer : context.author_rules.in_reverse()) + append_rules(layer.rules); + } + append_rules(matching_rule_set.user_rules); + if (include_user_agent_styles) + append_rules(matching_rule_set.user_agent_rules); +} + +JsonArray StyleComputer::collect_devtools_applied_style_rules(DOM::AbstractElement abstract_element, bool include_inherited, bool include_user_agent_styles) +{ + JsonArray entries; + + auto append_rules_for_abstract_element = [&](DOM::AbstractElement current_element, Optional inherited_node_id) { + if (auto inline_style = current_element.inline_style()) { + if (!inherited_node_id.has_value() || has_inherited_declaration(m_document, *inline_style)) + append_devtools_applied_style_entry(entries, serialize_devtools_inline_style(m_document, current_element, *inline_style), inherited_node_id); + } + + auto const first_ancestor = [&] -> GC::Ptr { + if (current_element.pseudo_element().has_value()) + return ¤t_element.element(); + return current_element.element().parent_or_shadow_host_element(); + }(); + + for (auto ancestor = first_ancestor; ancestor; ancestor = ancestor->parent_or_shadow_host_element()) + push_ancestor(*ancestor); + + ScopeGuard pop_ancestors = [&] { + for (auto ancestor = first_ancestor; ancestor; ancestor = ancestor->parent_or_shadow_host_element()) + pop_ancestor(*ancestor); + }; + + bool did_match_any_pseudo_element_rules = false; + auto matching_rule_set = build_matching_rule_set(current_element, did_match_any_pseudo_element_rules, ComputeStyleMode::Normal); + append_devtools_rules_for_element(m_document, entries, matching_rule_set, include_user_agent_styles, inherited_node_id); + }; + + append_rules_for_abstract_element(abstract_element, {}); + + if (!include_inherited) + return entries; + + for (auto current_element = abstract_element.element_to_inherit_style_from(); current_element.has_value(); current_element = current_element->element_to_inherit_style_from()) + append_rules_for_abstract_element(*current_element, current_element->element().unique_id()); + + return entries; +} + // https://www.w3.org/TR/css-cascade/#cascading // https://drafts.csswg.org/css-cascade-5/#layering GC::Ref StyleComputer::compute_cascaded_values(DOM::AbstractElement abstract_element, bool did_match_any_pseudo_element_rules, ComputeStyleMode mode, MatchingRuleSet const& matching_rule_set) const diff --git a/Libraries/LibWeb/CSS/StyleComputer.h b/Libraries/LibWeb/CSS/StyleComputer.h index c6aade87fa..3dafa3b604 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.h +++ b/Libraries/LibWeb/CSS/StyleComputer.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include #include @@ -105,6 +106,7 @@ public: [[nodiscard]] GC::Ref compute_style(DOM::AbstractElement, Optional did_change_custom_properties = {}) const; [[nodiscard]] GC::Ref compute_style_with_seeded_ancestors(DOM::AbstractElement); [[nodiscard]] GC::Ptr compute_pseudo_element_style_if_needed(DOM::AbstractElement, Optional did_change_custom_properties) const; + [[nodiscard]] JsonArray collect_devtools_applied_style_rules(DOM::AbstractElement, bool include_inherited, bool include_user_agent_styles); struct ScopedMatchingRule { MatchingRule const* rule { nullptr }; diff --git a/Libraries/LibWeb/CSS/StyleScope.cpp b/Libraries/LibWeb/CSS/StyleScope.cpp index 0692acaf46..6d3a55d9d1 100644 --- a/Libraries/LibWeb/CSS/StyleScope.cpp +++ b/Libraries/LibWeb/CSS/StyleScope.cpp @@ -410,7 +410,8 @@ void StyleScope::make_rule_cache_for_cascade_origin(CascadeOrigin cascade_origin if (scope_rule) collect_scope_boundary_selector_dependencies(*scope_rule, style_cache); - for (CSS::Selector const& selector : absolutized_selectors) { + for (size_t selector_index = 0; selector_index < absolutized_selectors.size(); ++selector_index) { + auto const& selector = *absolutized_selectors[selector_index]; MatchingRule matching_rule { .rule = &rule, .sheet = current_style_sheet, @@ -418,6 +419,7 @@ void StyleScope::make_rule_cache_for_cascade_origin(CascadeOrigin cascade_origin .scope_rule = scope_rule, .default_namespace = current_style_sheet.default_namespace(), .selector = selector, + .selector_index = selector_index, .style_sheet_index = style_sheet_index, .rule_index = rule_index, .specificity = selector.specificity(), diff --git a/Libraries/LibWeb/CSS/StyleScope.h b/Libraries/LibWeb/CSS/StyleScope.h index 2ab2e00e28..3c5dc82c35 100644 --- a/Libraries/LibWeb/CSS/StyleScope.h +++ b/Libraries/LibWeb/CSS/StyleScope.h @@ -35,6 +35,7 @@ struct MatchingRule { GC::Ptr scope_rule; // Either CSSScopeRule or CSSImportRule Optional default_namespace; Selector const& selector; + size_t selector_index { 0 }; size_t style_sheet_index { 0 }; size_t rule_index { 0 }; diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index bdf6f66079..6b965cf976 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -7664,11 +7664,11 @@ Optional Document::get_style_sheet_source(CSS::StyleSheetIdentifier cons if (auto* node = Node::from_unique_id(*identifier.dom_element_unique_id)) { if (node->is_html_style_element()) { if (auto* sheet = as(*node).sheet()) - return sheet->source_text({}); + return sheet->source_text(); } if (node->is_svg_style_element()) { if (auto* sheet = as(*node).sheet()) - return sheet->source_text({}); + return sheet->source_text(); } } } @@ -7683,7 +7683,7 @@ Optional Document::get_style_sheet_source(CSS::StyleSheetIdentifier cons if (m_style_sheets) { for (auto& style_sheet : m_style_sheets->sheets()) { if (auto match = find_style_sheet_with_url(identifier.url.value(), style_sheet); match.has_value()) - return match->source_text({}); + return match->source_text(); } } @@ -7694,7 +7694,7 @@ Optional Document::get_style_sheet_source(CSS::StyleSheetIdentifier cons return; if (auto match = find_style_sheet_with_url(identifier.url.value(), style_sheet); match.has_value()) - result = match->source_text({}); + result = match->source_text(); }); return result; } diff --git a/Libraries/LibWebView/DOMNodeProperties.h b/Libraries/LibWebView/DOMNodeProperties.h index b0cd33db7c..d33cd3ffbe 100644 --- a/Libraries/LibWebView/DOMNodeProperties.h +++ b/Libraries/LibWebView/DOMNodeProperties.h @@ -14,6 +14,7 @@ namespace WebView { struct WEBVIEW_API DOMNodeProperties { enum class Type { + AppliedStyleRules, ComputedStyle, Layout, UsedFonts, diff --git a/Services/WebContent/ConnectionFromClient.cpp b/Services/WebContent/ConnectionFromClient.cpp index 2a65464d78..2a825d2d63 100644 --- a/Services/WebContent/ConnectionFromClient.cpp +++ b/Services/WebContent/ConnectionFromClient.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -548,8 +549,6 @@ void ConnectionFromClient::inspect_dom_tree(u64 page_id) void ConnectionFromClient::inspect_dom_node(u64 page_id, WebView::DOMNodeProperties::Type property_type, Web::UniqueNodeID node_id, Optional pseudo_element, JsonValue options_value) { - (void)options_value; - auto page = this->page(page_id); if (!page.has_value()) return; @@ -557,8 +556,7 @@ void ConnectionFromClient::inspect_dom_node(u64 page_id, WebView::DOMNodePropert clear_inspected_dom_node(page_id); auto* node = Web::DOM::Node::from_unique_id(node_id); - // Nodes without layout (aka non-visible nodes) don't have style computed. - if (!node || !node->layout_node() || !node->is_element()) { + if (!node || !node->is_element()) { async_did_inspect_dom_node(page_id, { property_type, {} }); return; } @@ -566,6 +564,9 @@ void ConnectionFromClient::inspect_dom_node(u64 page_id, WebView::DOMNodePropert auto& element = as(*node); node->document().set_inspected_node(node); + Web::DOM::AbstractElement abstract_element { element, pseudo_element }; + node->document().update_style_for_element(abstract_element); + auto properties = element.computed_properties(pseudo_element); if (!properties) { @@ -573,6 +574,13 @@ void ConnectionFromClient::inspect_dom_node(u64 page_id, WebView::DOMNodePropert return; } + // Nodes without layout (aka non-visible nodes) do not have box metrics, but DevTools can still ask for their style + // rules and computed properties. + if (property_type == WebView::DOMNodeProperties::Type::Layout && !node->layout_node()) { + async_did_inspect_dom_node(page_id, { property_type, {} }); + return; + } + auto serialize_computed_style = [&]() { JsonObject serialized; @@ -647,9 +655,20 @@ void ConnectionFromClient::inspect_dom_node(u64 page_id, WebView::DOMNodePropert return serialized; }; + auto serialize_applied_style_rules = [&]() { + JsonObject const empty_options; + auto const& options = options_value.is_object() ? options_value.as_object() : empty_options; + auto include_inherited = options.get_bool("inherited"sv).value_or(false); + auto include_user_agent_styles = options.get_string("filter"sv).map([](auto const& filter) { return filter == "ua"sv; }).value_or(false); + return node->document().style_computer().collect_devtools_applied_style_rules(abstract_element, include_inherited, include_user_agent_styles); + }; + JsonValue serialized; switch (property_type) { + case WebView::DOMNodeProperties::Type::AppliedStyleRules: + serialized = serialize_applied_style_rules(); + break; case WebView::DOMNodeProperties::Type::ComputedStyle: serialized = serialize_computed_style(); break; diff --git a/Tests/LibDevTools/TestDevToolsProtocol.cpp b/Tests/LibDevTools/TestDevToolsProtocol.cpp index 7eaa8d816f..7214f12a2f 100644 --- a/Tests/LibDevTools/TestDevToolsProtocol.cpp +++ b/Tests/LibDevTools/TestDevToolsProtocol.cpp @@ -188,6 +188,84 @@ static WebView::DOMNodeProperties make_computed_style() return { WebView::DOMNodeProperties::Type::ComputedStyle, move(properties) }; } +static WebView::DOMNodeProperties make_applied_style_rules() +{ + JsonArray entries; + + JsonArray inline_declarations; + JsonObject inline_display; + inline_display.set("name"sv, "display"sv); + inline_display.set("value"sv, "grid"sv); + inline_display.set("priority"sv, ""sv); + inline_display.set("isCustomProperty"sv, false); + inline_display.set("isNameValid"sv, true); + inline_display.set("isValid"sv, true); + inline_display.set("inherits"sv, false); + inline_declarations.must_append(move(inline_display)); + + JsonObject inline_rule; + inline_rule.set("type"sv, 100); + inline_rule.set("className"sv, 100); + inline_rule.set("cssText"sv, "display: grid;"sv); + inline_rule.set("authoredText"sv, "display: grid;"sv); + inline_rule.set("declarations"sv, move(inline_declarations)); + + JsonObject inline_entry; + inline_entry.set("rule"sv, move(inline_rule)); + inline_entry.set("isSystem"sv, false); + inline_entry.set("matchedSelectorIndexes"sv, JsonArray {}); + inline_entry.set("inherited"sv, JsonValue {}); + entries.must_append(move(inline_entry)); + + JsonArray rule_declarations; + JsonObject rule_color; + rule_color.set("name"sv, "color"sv); + rule_color.set("value"sv, "rgb(1, 2, 3)"sv); + rule_color.set("priority"sv, ""sv); + rule_color.set("isCustomProperty"sv, false); + rule_color.set("isNameValid"sv, true); + rule_color.set("isValid"sv, true); + rule_color.set("inherits"sv, true); + rule_declarations.must_append(move(rule_color)); + + JsonObject rule_invalid_color; + rule_invalid_color.set("name"sv, "color"sv); + rule_invalid_color.set("value"sv, "invalid-value"sv); + rule_invalid_color.set("priority"sv, ""sv); + rule_invalid_color.set("isCustomProperty"sv, false); + rule_invalid_color.set("isNameValid"sv, true); + rule_invalid_color.set("isValid"sv, false); + rule_invalid_color.set("inherits"sv, true); + rule_declarations.must_append(move(rule_invalid_color)); + + JsonArray selectors; + selectors.must_append("body div.fixture"sv); + + JsonArray selector_specificities; + selector_specificities.must_append(0x101u); + + JsonArray matched_selector_indexes; + matched_selector_indexes.must_append(0u); + + JsonObject rule; + rule.set("type"sv, 1); + rule.set("className"sv, "CSSStyleRule"sv); + rule.set("selectors"sv, move(selectors)); + rule.set("selectorsSpecificity"sv, move(selector_specificities)); + rule.set("cssText"sv, "body div.fixture { color: rgb(1, 2, 3); }"sv); + rule.set("authoredText"sv, "color: rgb(1, 2, 3);"sv); + rule.set("declarations"sv, move(rule_declarations)); + + JsonObject rule_entry; + rule_entry.set("rule"sv, move(rule)); + rule_entry.set("isSystem"sv, false); + rule_entry.set("matchedSelectorIndexes"sv, move(matched_selector_indexes)); + rule_entry.set("inheritedNodeId"sv, 1u); + entries.must_append(move(rule_entry)); + + return { WebView::DOMNodeProperties::Type::AppliedStyleRules, move(entries) }; +} + static WebView::DOMNodeProperties make_layout() { JsonObject properties; @@ -420,14 +498,16 @@ public: virtual void inspect_dom_node(DevTools::TabDescription const&, WebView::DOMNodeProperties::Type type, Web::UniqueNodeID node_id, Optional pseudo_element, JsonObject options = {}) const override { - (void)options; ++inspect_dom_node_call_count; last_inspected_dom_node = node_id; last_inspected_pseudo_element = pseudo_element; + last_inspected_dom_node_options = move(options); Core::deferred_invoke([this, type] { VERIFY(on_dom_node_properties); - if (type == WebView::DOMNodeProperties::Type::ComputedStyle) + if (type == WebView::DOMNodeProperties::Type::AppliedStyleRules) + on_dom_node_properties(make_applied_style_rules()); + else if (type == WebView::DOMNodeProperties::Type::ComputedStyle) on_dom_node_properties(make_computed_style()); else if (type == WebView::DOMNodeProperties::Type::Layout) on_dom_node_properties(make_layout()); @@ -854,6 +934,7 @@ public: mutable Optional last_highlighted_pseudo_element; mutable Optional last_inspected_dom_node; mutable Optional last_inspected_pseudo_element; + mutable JsonObject last_inspected_dom_node_options; mutable Optional last_grid_root_node; mutable Optional last_current_grid_node; mutable Optional last_current_flexbox_node; @@ -1031,6 +1112,16 @@ static JsonObject get_tab(ProtocolClient& client) return tabs.at(0).as_object(); } +static size_t style_rule_actor_count(DevTools::DevToolsServer const& server) +{ + size_t count = 0; + for (auto const& actor : server.actor_registry()) { + if (actor.key.bytes_as_string_view().contains("-style-rule"sv)) + ++count; + } + return count; +} + static JsonObject get_frame_target(ProtocolClient& client, StringView tab_actor) { auto watcher_actor = actor_from(client.request(tab_actor, "getWatcher"sv), "actor"sv); @@ -1856,12 +1947,51 @@ TEST_CASE(styles_and_stylesheets) auto root_actor = walker.get_object("root"sv)->get_string("actor"sv).release_value(); auto div_actor = query_selector(client, walker_actor, root_actor, "div"sv); - JsonObject applied; - applied.set("to"sv, page_style_actor); - applied.set("type"sv, "getApplied"sv); - EXPECT(client.request(move(applied)).get_array("entries"sv)->is_empty()); + auto get_applied = [&] { + JsonObject applied; + applied.set("to"sv, page_style_actor); + applied.set("type"sv, "getApplied"sv); + applied.set("node"sv, div_actor); + applied.set("inherited"sv, true); + applied.set("matchedSelectors"sv, true); + return client.request(move(applied)).get_array("entries"sv).release_value(); + }; + + auto applied_entries = get_applied(); + EXPECT(session->delegate.last_inspected_dom_node_options.get_bool("inherited"sv).value()); + EXPECT(session->delegate.last_inspected_dom_node_options.get_bool("matchedSelectors"sv).value()); + VERIFY(applied_entries.size() == 2u); + EXPECT_EQ(style_rule_actor_count(*session->server), applied_entries.size()); + + auto inline_rule = applied_entries.at(0).as_object().get_object("rule"sv).release_value(); + auto inline_rule_actor = actor_from(inline_rule, "actor"sv); + EXPECT_EQ(inline_rule.get_integer("type"sv).value(), 100); + EXPECT(inline_rule.get_array("ancestorData"sv)->is_empty()); + EXPECT(!inline_rule.get_object("traits"sv)->get_bool("canSetRuleText"sv).value()); + EXPECT_EQ(inline_rule.get_array("declarations"sv)->at(0).as_object().get_string("name"sv).value(), "display"sv); + EXPECT(inline_rule.get_array("declarations"sv)->at(0).as_object().get_bool("isNameValid"sv).value()); + EXPECT(inline_rule.get_array("declarations"sv)->at(0).as_object().get_bool("isValid"sv).value()); + EXPECT_EQ(client.request(inline_rule_actor, "getRuleText"sv).get_string("text"sv).value(), "display: grid;"sv); + + auto inherited_rule_entry = applied_entries.at(1).as_object(); + auto inherited_rule = inherited_rule_entry.get_object("rule"sv).release_value(); + EXPECT_EQ(inherited_rule_entry.get_string("inherited"sv).value(), root_actor); + EXPECT_EQ(inherited_rule.get_array("selectors"sv)->at(0).as_string(), "body div.fixture"sv); + EXPECT_EQ(inherited_rule.get_array("declarations"sv)->at(0).as_object().get_string("name"sv).value(), "color"sv); + EXPECT(inherited_rule.get_array("declarations"sv)->at(0).as_object().get_bool("isNameValid"sv).value()); + EXPECT(inherited_rule.get_array("declarations"sv)->at(0).as_object().get_bool("isValid"sv).value()); + EXPECT(inherited_rule.get_array("declarations"sv)->at(1).as_object().get_bool("isNameValid"sv).value()); + EXPECT(!inherited_rule.get_array("declarations"sv)->at(1).as_object().get_bool("isValid"sv).value()); + EXPECT_EQ(inherited_rule_entry.get_array("matchedSelectorIndexes"sv)->at(0).as_integer(), 0); EXPECT(!client.request(page_style_actor, "isPositionEditable"sv).get_bool("value"sv).value()); + auto second_applied_entries = get_applied(); + VERIFY(second_applied_entries.size() == applied_entries.size()); + spin_until(session->loop, [&] { + return style_rule_actor_count(*session->server) == second_applied_entries.size() + && !session->server->actor_registry().contains(inline_rule_actor); + }); + JsonObject computed_request; computed_request.set("to"sv, page_style_actor); computed_request.set("type"sv, "getComputed"sv);