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.
This commit is contained in:
Sam Atkins 2026-06-04 16:59:14 +01:00
parent 121105eb83
commit 52e1d30404
16 changed files with 728 additions and 32 deletions

View file

@ -9,6 +9,7 @@
#include <AK/JsonValue.h>
#include <LibDevTools/Actors/InspectorActor.h>
#include <LibDevTools/Actors/PageStyleActor.h>
#include <LibDevTools/Actors/StyleRuleActor.h>
#include <LibDevTools/Actors/TabActor.h>
#include <LibDevTools/Actors/WalkerActor.h>
#include <LibDevTools/DevToolsDelegate.h>
@ -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<StyleRuleActor>(*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<Web::UniqueNodeID::Type>("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> PageStyleActor::create(DevToolsServer& devtools, String name, WeakPtr<InspectorActor> inspector)
{
return adopt_ref(*new PageStyleActor(devtools, move(name), move(inspector)));
@ -130,10 +167,19 @@ PageStyleActor::PageStyleActor(DevToolsServer& devtools, String name, WeakPtr<In
PageStyleActor::~PageStyleActor()
{
clear_style_rule_actors();
if (auto tab = InspectorActor::tab_for(m_inspector))
devtools().delegate().stop_listening_for_dom_properties(tab->description());
}
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;

View file

@ -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<InspectorActor> m_inspector;
Vector<Message, 1> m_pending_inspect_requests;
Vector<String> m_style_rule_actors;
};
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2026, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonArray.h>
#include <LibDevTools/Actors/StyleRuleActor.h>
namespace DevTools {
NonnullRefPtr<StyleRuleActor> 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);
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2026, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/JsonObject.h>
#include <AK/NonnullRefPtr.h>
#include <LibDevTools/Actor.h>
namespace DevTools {
class DEVTOOLS_API StyleRuleActor final : public Actor {
public:
static constexpr auto base_name = "style-rule"sv;
static NonnullRefPtr<StyleRuleActor> 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;
};
}

View file

@ -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

View file

@ -32,6 +32,7 @@ class ParentAccessibilityActor;
class PreferenceActor;
class ProcessActor;
class RootActor;
class StyleRuleActor;
class StyleSheetsActor;
class TabActor;
class TargetConfigurationActor;

View file

@ -643,16 +643,6 @@ void CSSStyleSheet::recalculate_rule_caches()
}
}
void CSSStyleSheet::set_source_text(String source)
{
m_source_text = move(source);
}
Optional<String> CSSStyleSheet::source_text(Badge<DOM::Document>) const
{
return m_source_text;
}
void CSSStyleSheet::add_critical_subresource(Subresource& subresource)
{
m_critical_subresources.append(subresource);

View file

@ -122,8 +122,8 @@ public:
bool disallow_modification() const { return m_disallow_modification; }
void set_source_text(String);
Optional<String> source_text(Badge<DOM::Document>) const;
void set_source_text(String source) { m_source_text = move(source); }
Optional<String> source_text() const { return m_source_text; }
void add_critical_subresource(Subresource&);
void remove_critical_subresource(Subresource&);

View file

@ -15,10 +15,12 @@
#include <AK/FixedBitmap.h>
#include <AK/Function.h>
#include <AK/HashMap.h>
#include <AK/JsonObject.h>
#include <AK/Math.h>
#include <AK/NeverDestroyed.h>
#include <AK/NonnullRawPtr.h>
#include <AK/QuickSort.h>
#include <AK/Utf8View.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibWeb/Animations/AnimationEffect.h>
#include <LibWeb/Animations/DocumentTimeline.h>
@ -33,6 +35,7 @@
#include <LibWeb/CSS/CSSScopeRule.h>
#include <LibWeb/CSS/CSSStyleProperties.h>
#include <LibWeb/CSS/CSSStyleRule.h>
#include <LibWeb/CSS/CSSStyleSheet.h>
#include <LibWeb/CSS/CSSTransition.h>
#include <LibWeb/CSS/CascadedProperties.h>
#include <LibWeb/CSS/ComputedProperties.h>
@ -81,6 +84,7 @@
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/HTML/AttributeNames.h>
#include <LibWeb/HTML/HTMLBRElement.h>
#include <LibWeb/HTML/HTMLHtmlElement.h>
#include <LibWeb/HTML/HTMLImageElement.h>
@ -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<Parser::DevToolsStyleDeclaration> 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<Parser::DevToolsStyleDeclaration> 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<size_t> 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<String> 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<u8> string_quote;
bool in_comment = false;
bool escaped = false;
Optional<size_t> 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<UniqueNodeID> 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<UniqueNodeID> 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<UniqueNodeID> 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<DOM::Element const> {
if (current_element.pseudo_element().has_value())
return &current_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<CascadedProperties> StyleComputer::compute_cascaded_values(DOM::AbstractElement abstract_element, bool did_match_any_pseudo_element_rules, ComputeStyleMode mode, MatchingRuleSet const& matching_rule_set) const

View file

@ -8,6 +8,7 @@
#pragma once
#include <AK/HashMap.h>
#include <AK/JsonArray.h>
#include <AK/Optional.h>
#include <AK/OwnPtr.h>
#include <LibWeb/Animations/KeyframeEffect.h>
@ -105,6 +106,7 @@ public:
[[nodiscard]] GC::Ref<ComputedProperties> compute_style(DOM::AbstractElement, Optional<bool&> did_change_custom_properties = {}) const;
[[nodiscard]] GC::Ref<ComputedProperties> compute_style_with_seeded_ancestors(DOM::AbstractElement);
[[nodiscard]] GC::Ptr<ComputedProperties> compute_pseudo_element_style_if_needed(DOM::AbstractElement, Optional<bool&> 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 };

View file

@ -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(),

View file

@ -35,6 +35,7 @@ struct MatchingRule {
GC::Ptr<CSSRule const> scope_rule; // Either CSSScopeRule or CSSImportRule
Optional<FlyString> default_namespace;
Selector const& selector;
size_t selector_index { 0 };
size_t style_sheet_index { 0 };
size_t rule_index { 0 };

View file

@ -7664,11 +7664,11 @@ Optional<String> 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<HTML::HTMLStyleElement>(*node).sheet())
return sheet->source_text({});
return sheet->source_text();
}
if (node->is_svg_style_element()) {
if (auto* sheet = as<SVG::SVGStyleElement>(*node).sheet())
return sheet->source_text({});
return sheet->source_text();
}
}
}
@ -7683,7 +7683,7 @@ Optional<String> 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<String> 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;
}

View file

@ -14,6 +14,7 @@ namespace WebView {
struct WEBVIEW_API DOMNodeProperties {
enum class Type {
AppliedStyleRules,
ComputedStyle,
Layout,
UsedFonts,

View file

@ -34,6 +34,7 @@
#include <LibWeb/CSS/StyleSheetList.h>
#include <LibWeb/Compositor/CompositorHost.h>
#include <LibWeb/CookieStore/CookieStore.h>
#include <LibWeb/DOM/AbstractElement.h>
#include <LibWeb/DOM/Attr.h>
#include <LibWeb/DOM/CharacterData.h>
#include <LibWeb/DOM/Document.h>
@ -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<Web::CSS::PseudoElement> 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<Web::DOM::Element>(*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;

View file

@ -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<Web::CSS::PseudoElement> 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<Web::CSS::PseudoElement> last_highlighted_pseudo_element;
mutable Optional<Web::UniqueNodeID> last_inspected_dom_node;
mutable Optional<Web::CSS::PseudoElement> last_inspected_pseudo_element;
mutable JsonObject last_inspected_dom_node_options;
mutable Optional<Web::UniqueNodeID> last_grid_root_node;
mutable Optional<Web::UniqueNodeID> last_current_grid_node;
mutable Optional<Web::UniqueNodeID> 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<int>("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<int>(), 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);