Inspector: Add a basic style sheet inspector

Choosing options from the `<select>` will load and display that style
sheet's source text, with some checks to make sure that the text that
just loaded is the one we currently want.

The UI is a little goofy when scrolling, as it uses `position: sticky`
which we don't implement yet. But that's just more motivation to
implement it! :^)
This commit is contained in:
Sam Atkins 2024-08-23 11:18:35 +01:00 committed by Sam Atkins
parent 49b2eb5f51
commit da171c3230
Notes: github-actions[bot] 2024-09-03 09:12:56 +00:00
18 changed files with 270 additions and 2 deletions

View file

@ -1,3 +1,7 @@
:root {
--code-font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
@media (prefers-color-scheme: dark) {
:root {
--background: rgb(23, 23, 23);
@ -174,6 +178,17 @@ body {
overflow: auto scroll;
}
.tab-header {
position: sticky;
top: 2px; /* FIXME: Remove this when https://github.com/LadybirdBrowser/ladybird/issues/1245 is resolved. */
left: 0;
right: 0;
background-color: var(--tab-controls);
border-top: 2px solid var(--background);
display: flex;
padding: 0.5em;
}
details > :not(:first-child) {
display: list-item;
list-style: none inside;
@ -204,7 +219,7 @@ details > :not(:first-child) {
}
.console {
font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-family: var(--code-font-family);
width: 100%;
height: 100%;
}
@ -330,3 +345,14 @@ details > :not(:first-child) {
padding: 4px;
text-align: left;
}
#style-sheet-picker {
flex-grow: 1;
}
#style-sheet-source {
font-size: 10pt;
font-family: var(--code-font-family);
white-space: pre;
padding: 0.5em;
}

View file

@ -199,6 +199,97 @@ inspector.addAttributeToDOMNodeID = nodeID => {
pendingEditDOMNode = null;
};
inspector.setStyleSheets = styleSheets => {
const styleSheetPicker = document.getElementById("style-sheet-picker");
const styleSheetSource = document.getElementById("style-sheet-source");
styleSheetPicker.replaceChildren();
styleSheetSource.innerHTML = "";
function addOption(styleSheet, text) {
const option = document.createElement("option");
option.innerText = text;
if (styleSheet.type) {
option.dataset["type"] = styleSheet.type;
}
if (styleSheet.domNodeId) {
option.dataset["domNodeId"] = styleSheet.domNodeId;
}
if (styleSheet.url) {
option.dataset["url"] = styleSheet.url;
}
styleSheetPicker.add(option);
}
if (styleSheets.length > 0) {
let styleElementIndex = 1;
for (const styleSheet of styleSheets) {
switch (styleSheet.type) {
case "StyleElement":
addOption(styleSheet, `Style element #${styleElementIndex++}`);
break;
case "LinkElement":
addOption(styleSheet, styleSheet.url);
break;
case "ImportRule":
addOption(styleSheet, styleSheet.url);
break;
case "UserAgent":
addOption(styleSheet, `User agent: ${styleSheet.url}`);
break;
case "UserStyle":
addOption(styleSheet, "User style");
break;
}
}
styleSheetPicker.disabled = false;
} else {
addOption({}, "No style sheets found");
styleSheetPicker.disabled = true;
}
styleSheetPicker.selectedIndex = 0;
if (!styleSheetPicker.disabled) {
loadStyleSheet();
}
};
const loadStyleSheet = () => {
const styleSheetPicker = document.getElementById("style-sheet-picker");
const styleSheetSource = document.getElementById("style-sheet-source");
const selectedOption = styleSheetPicker.selectedOptions[0];
styleSheetSource.innerHTML = "Loading...";
inspector.requestStyleSheetSource(
selectedOption.dataset["type"],
selectedOption.dataset["domNodeId"],
selectedOption.dataset["url"]
);
};
inspector.setStyleSheetSource = (identifier, sourceBase64) => {
const styleSheetPicker = document.getElementById("style-sheet-picker");
const styleSheetSource = document.getElementById("style-sheet-source");
const selectedOption = styleSheetPicker.selectedOptions[0];
// Make sure this is the source for the currently-selected style sheet.
// NOTE: These are != not !== intentionally.
if (
identifier.type != selectedOption.dataset["type"] ||
identifier.domNodeId != selectedOption.dataset["domNodeId"] ||
identifier.url != selectedOption.dataset["url"]
) {
console.log(
JSON.stringify(identifier),
"doesn't match",
JSON.stringify(selectedOption.dataset)
);
return;
}
styleSheetSource.innerHTML = decodeBase64(sourceBase64);
};
inspector.createPropertyTables = (computedStyle, resolvedStyle, customProperties) => {
const createPropertyTable = (tableID, properties) => {
let oldTable = document.getElementById(tableID);

View file

@ -326,6 +326,10 @@ Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client,
m_window->new_tab_from_content(html, Web::HTML::ActivateTab::Yes);
};
view().on_inspector_requested_style_sheet_source = [this](auto const& identifier) {
view().request_style_sheet_source(identifier);
};
view().on_navigate_back = [this]() {
back();
};

View file

@ -9,6 +9,7 @@
#include <LibWeb/Bindings/InspectorPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/CSS/StyleSheetIdentifier.h>
#include <LibWeb/DOM/NamedNodeMap.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/Window.h>
@ -76,6 +77,18 @@ void Inspector::request_dom_tree_context_menu(i32 node_id, i32 client_x, i32 cli
inspector_page_client().inspector_did_request_dom_tree_context_menu(node_id, { client_x, client_y }, type, tag, attribute_index.map([](auto index) { return static_cast<size_t>(index); }));
}
void Inspector::request_style_sheet_source(String const& type_string, Optional<i32> const& dom_node_unique_id, Optional<String> const& url)
{
auto type = CSS::style_sheet_identifier_type_from_string(type_string);
VERIFY(type.has_value());
inspector_page_client().inspector_did_request_style_sheet_source({
.type = type.value(),
.dom_element_unique_id = dom_node_unique_id,
.url = url,
});
}
void Inspector::execute_console_script(String const& script)
{
inspector_page_client().inspector_did_execute_console_script(script);

View file

@ -29,6 +29,8 @@ public:
void request_dom_tree_context_menu(i32 node_id, i32 client_x, i32 client_y, String const& type, Optional<String> const& tag, Optional<WebIDL::UnsignedLongLong> const& attribute_index);
void request_style_sheet_source(String const& type, Optional<i32> const& dom_node_unique_id, Optional<String> const& url);
void execute_console_script(String const& script);
void export_inspector_html(String const& html);

View file

@ -12,6 +12,8 @@
undefined requestDOMTreeContextMenu(long nodeID, long clientX, long clientY, DOMString type, DOMString? tag, unsigned long long? attributeIndex);
undefined requestStyleSheetSource(DOMString type, long? domNodeID, DOMString? url);
undefined executeConsoleScript(DOMString script);
undefined exportInspectorHTML(DOMString html);

View file

@ -29,6 +29,7 @@
#include <LibWeb/CSS/PreferredContrast.h>
#include <LibWeb/CSS/PreferredMotion.h>
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/CSS/StyleSheetIdentifier.h>
#include <LibWeb/Cookie/Cookie.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/ActivateTab.h>
@ -376,6 +377,7 @@ public:
virtual void inspector_did_add_dom_node_attributes([[maybe_unused]] i32 node_id, [[maybe_unused]] JS::NonnullGCPtr<DOM::NamedNodeMap> attributes) { }
virtual void inspector_did_replace_dom_node_attribute([[maybe_unused]] i32 node_id, [[maybe_unused]] size_t attribute_index, [[maybe_unused]] JS::NonnullGCPtr<DOM::NamedNodeMap> replacement_attributes) { }
virtual void inspector_did_request_dom_tree_context_menu([[maybe_unused]] i32 node_id, [[maybe_unused]] CSSPixelPoint position, [[maybe_unused]] String const& type, [[maybe_unused]] Optional<String> const& tag, [[maybe_unused]] Optional<size_t> const& attribute_index) { }
virtual void inspector_did_request_style_sheet_source([[maybe_unused]] CSS::StyleSheetIdentifier const& identifier) { }
virtual void inspector_did_execute_console_script([[maybe_unused]] String const& script) { }
virtual void inspector_did_export_inspector_html([[maybe_unused]] String const& html) { }

View file

@ -32,6 +32,14 @@ static ErrorOr<JsonValue> parse_json_tree(StringView json)
return parsed_tree;
}
static String style_sheet_identifier_to_json(Web::CSS::StyleSheetIdentifier const& identifier)
{
return MUST(String::formatted("{{ type: '{}', domNodeId: {}, url: '{}' }}"sv,
Web::CSS::style_sheet_identifier_type_to_string(identifier.type),
identifier.dom_element_unique_id.map([](auto& it) { return MUST(String::number(it)); }).value_or("undefined"_string),
identifier.url.value_or("undefined"_string)));
}
InspectorClient::InspectorClient(ViewImplementation& content_web_view, ViewImplementation& inspector_web_view)
: m_content_web_view(content_web_view)
, m_inspector_web_view(inspector_web_view)
@ -105,6 +113,26 @@ InspectorClient::InspectorClient(ViewImplementation& content_web_view, ViewImple
select_node(node_id);
};
m_content_web_view.on_received_style_sheet_list = [this](auto const& style_sheets) {
StringBuilder builder;
builder.append("inspector.setStyleSheets(["sv);
for (auto& style_sheet : style_sheets) {
builder.appendff("{}, "sv, style_sheet_identifier_to_json(style_sheet));
}
builder.append("]);"sv);
m_inspector_web_view.run_javascript(builder.string_view());
};
m_content_web_view.on_received_style_sheet_source = [this](Web::CSS::StyleSheetIdentifier const& identifier, String const& source) {
// TODO: Highlight it
auto escaped_source = escape_html_entities(source.bytes()).replace("\t"sv, " "sv, ReplaceMode::All);
auto script = MUST(String::formatted("inspector.setStyleSheetSource({}, \"{}\");",
style_sheet_identifier_to_json(identifier),
MUST(encode_base64(escaped_source.bytes()))));
m_inspector_web_view.run_javascript(script);
};
m_content_web_view.on_finshed_editing_dom_node = [this](auto const& node_id) {
m_pending_selection = node_id;
m_dom_tree_loaded = false;
@ -181,6 +209,10 @@ InspectorClient::InspectorClient(ViewImplementation& content_web_view, ViewImple
m_content_web_view.replace_dom_node_attribute(node_id, attribute.name, replacement_attributes);
};
m_inspector_web_view.on_inspector_requested_style_sheet_source = [this](auto const& identifier) {
m_content_web_view.request_style_sheet_source(identifier);
};
m_inspector_web_view.on_inspector_executed_console_script = [this](auto const& script) {
append_console_source(script);
@ -241,6 +273,8 @@ InspectorClient::~InspectorClient()
m_content_web_view.on_received_dom_node_properties = nullptr;
m_content_web_view.on_received_dom_tree = nullptr;
m_content_web_view.on_received_hovered_node_id = nullptr;
m_content_web_view.on_received_style_sheet_list = nullptr;
m_content_web_view.on_inspector_requested_style_sheet_source = nullptr;
}
void InspectorClient::inspect()
@ -250,6 +284,7 @@ void InspectorClient::inspect()
m_content_web_view.inspect_dom_tree();
m_content_web_view.inspect_accessibility_tree();
m_content_web_view.list_style_sheets();
}
void InspectorClient::reset()
@ -420,6 +455,7 @@ void InspectorClient::load_inspector()
<div class="tab-controls">
<button id="dom-tree-button" onclick="selectTopTab(this, 'dom-tree')">DOM Tree</button>
<button id="accessibility-tree-button" onclick="selectTopTab(this, 'accessibility-tree')">Accessibility Tree</button>
<button id="style-sheets-button" onclick="selectTopTab(this, 'style-sheets')">Style Sheets</button>
</div>
<div class="global-controls">
<button id="export-inspector-button" title="Export the Inspector to an HTML file" onclick="inspector.exportInspector()"></button>
@ -427,6 +463,14 @@ void InspectorClient::load_inspector()
</div>
<div id="dom-tree" class="tab-content html"></div>
<div id="accessibility-tree" class="tab-content"></div>
<div id="style-sheets" class="tab-content" style="padding: 0">
<div class="tab-header">
<select id="style-sheet-picker" disabled onchange="loadStyleSheet()">
<option value="." selected>No style sheets found</option>
</select>
</div>
<div id="style-sheet-source"></div>
</div>
</div>
<div id="inspector-separator" class="split-view-separator">
<svg viewBox="0 0 16 5" xmlns="http://www.w3.org/2000/svg">

View file

@ -305,6 +305,16 @@ void ViewImplementation::get_dom_node_html(i32 node_id)
client().async_get_dom_node_html(page_id(), node_id);
}
void ViewImplementation::list_style_sheets()
{
client().async_list_style_sheets(page_id());
}
void ViewImplementation::request_style_sheet_source(Web::CSS::StyleSheetIdentifier const& identifier)
{
client().async_request_style_sheet_source(page_id(), identifier);
}
void ViewImplementation::debug_request(ByteString const& request, ByteString const& argument)
{
client().async_debug_request(page_id(), request, argument);

View file

@ -97,6 +97,9 @@ public:
void remove_dom_node(i32 node_id);
void get_dom_node_html(i32 node_id);
void list_style_sheets();
void request_style_sheet_source(Web::CSS::StyleSheetIdentifier const&);
void debug_request(ByteString const& request, ByteString const& argument = {});
void run_javascript(StringView);
@ -182,6 +185,9 @@ public:
Function<void(ByteString const&)> on_received_dom_tree;
Function<void(Optional<DOMNodeProperties>)> on_received_dom_node_properties;
Function<void(ByteString const&)> on_received_accessibility_tree;
Function<void(Vector<Web::CSS::StyleSheetIdentifier>)> on_received_style_sheet_list;
Function<void(Web::CSS::StyleSheetIdentifier const&)> on_inspector_requested_style_sheet_source;
Function<void(Web::CSS::StyleSheetIdentifier const&, String const&)> on_received_style_sheet_source;
Function<void(i32 node_id)> on_received_hovered_node_id;
Function<void(Optional<i32> const& node_id)> on_finshed_editing_dom_node;
Function<void(String const&)> on_received_dom_node_html;

View file

@ -711,6 +711,30 @@ Messages::WebContentClient::RequestWorkerAgentResponse WebContentClient::request
return IPC::File {};
}
void WebContentClient::inspector_did_list_style_sheets(u64 page_id, Vector<Web::CSS::StyleSheetIdentifier> const& stylesheets)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_received_style_sheet_list)
view->on_received_style_sheet_list(stylesheets);
}
}
void WebContentClient::inspector_did_request_style_sheet_source(u64 page_id, Web::CSS::StyleSheetIdentifier const& identifier)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_inspector_requested_style_sheet_source)
view->on_inspector_requested_style_sheet_source(identifier);
}
}
void WebContentClient::did_request_style_sheet_source(u64 page_id, Web::CSS::StyleSheetIdentifier const& identifier, String const& source)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_received_style_sheet_source)
view->on_received_style_sheet_source(identifier, source);
}
}
Optional<ViewImplementation&> WebContentClient::view_for_page_id(u64 page_id, SourceLocation location)
{
if (auto view = m_views.get(page_id); view.has_value())

View file

@ -9,6 +9,7 @@
#include <AK/HashMap.h>
#include <AK/SourceLocation.h>
#include <LibIPC/ConnectionToServer.h>
#include <LibWeb/CSS/StyleSheetIdentifier.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/FileFilter.h>
#include <LibWeb/HTML/SelectItem.h>
@ -122,6 +123,9 @@ private:
virtual void inspector_did_execute_console_script(u64 page_id, String const& script) override;
virtual void inspector_did_export_inspector_html(u64 page_id, String const& html) override;
virtual Messages::WebContentClient::RequestWorkerAgentResponse request_worker_agent(u64 page_id) override;
virtual void inspector_did_list_style_sheets(u64 page_id, Vector<Web::CSS::StyleSheetIdentifier> const& stylesheets) override;
virtual void inspector_did_request_style_sheet_source(u64 page_id, Web::CSS::StyleSheetIdentifier const& identifier) override;
virtual void did_request_style_sheet_source(u64 page_id, Web::CSS::StyleSheetIdentifier const& identifier, String const& source) override;
Optional<ViewImplementation&> view_for_page_id(u64, SourceLocation = SourceLocation::current());

View file

@ -1,6 +1,6 @@
/*
* Copyright (c) 2020-2023, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2021-2024, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org>
* Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
@ -629,6 +629,28 @@ void ConnectionFromClient::get_hovered_node_id(u64 page_id)
async_did_get_hovered_node_id(page_id, node_id);
}
void ConnectionFromClient::list_style_sheets(u64 page_id)
{
auto page = this->page(page_id);
if (!page.has_value())
return;
async_inspector_did_list_style_sheets(page_id, page->list_style_sheets());
}
void ConnectionFromClient::request_style_sheet_source(u64 page_id, Web::CSS::StyleSheetIdentifier const& identifier)
{
auto page = this->page(page_id);
if (!page.has_value())
return;
if (auto* document = page->page().top_level_browsing_context().active_document()) {
auto stylesheet = document->get_style_sheet_source(identifier);
if (stylesheet.has_value())
async_did_request_style_sheet_source(page_id, identifier, stylesheet.value());
}
}
void ConnectionFromClient::set_dom_node_text(u64 page_id, i32 node_id, String const& text)
{
auto* dom_node = Web::DOM::Node::from_unique_id(node_id);

View file

@ -76,6 +76,9 @@ private:
virtual void inspect_accessibility_tree(u64 page_id) override;
virtual void get_hovered_node_id(u64 page_id) override;
virtual void list_style_sheets(u64 page_id) override;
virtual void request_style_sheet_source(u64 page_id, Web::CSS::StyleSheetIdentifier const& identifier) override;
virtual void set_dom_node_text(u64 page_id, i32 node_id, String const& text) override;
virtual void set_dom_node_tag(u64 page_id, i32 node_id, String const& name) override;
virtual void add_dom_node_attributes(u64 page_id, i32 node_id, Vector<WebView::Attribute> const& attributes) override;

View file

@ -2,6 +2,7 @@
* Copyright (c) 2020-2023, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021-2022, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
* Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -657,6 +658,11 @@ void PageClient::inspector_did_request_dom_tree_context_menu(i32 node_id, Web::C
client().async_inspector_did_request_dom_tree_context_menu(m_id, node_id, page().css_to_device_point(position).to_type<int>(), type, tag, attribute_index);
}
void PageClient::inspector_did_request_style_sheet_source(Web::CSS::StyleSheetIdentifier const& identifier)
{
client().async_inspector_did_request_style_sheet_source(m_id, identifier);
}
void PageClient::inspector_did_execute_console_script(String const& script)
{
client().async_inspector_did_execute_console_script(m_id, script);

View file

@ -169,6 +169,7 @@ private:
virtual void inspector_did_add_dom_node_attributes(i32 node_id, JS::NonnullGCPtr<Web::DOM::NamedNodeMap> attributes) override;
virtual void inspector_did_replace_dom_node_attribute(i32 node_id, size_t attribute_index, JS::NonnullGCPtr<Web::DOM::NamedNodeMap> replacement_attributes) override;
virtual void inspector_did_request_dom_tree_context_menu(i32 node_id, Web::CSSPixelPoint position, String const& type, Optional<String> const& tag, Optional<size_t> const& attribute_index) override;
virtual void inspector_did_request_style_sheet_source(Web::CSS::StyleSheetIdentifier const& stylesheet_source) override;
virtual void inspector_did_execute_console_script(String const& script) override;
virtual void inspector_did_export_inspector_html(String const& script) override;

View file

@ -5,6 +5,7 @@
#include <LibWeb/Cookie/Cookie.h>
#include <LibWeb/Cookie/ParsedCookie.h>
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/CSS/StyleSheetIdentifier.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/AudioPlayState.h>
#include <LibWeb/HTML/FileFilter.h>
@ -54,6 +55,10 @@ endpoint WebContentClient
did_finish_editing_dom_node(u64 page_id, Optional<i32> node_id) =|
did_get_dom_node_html(u64 page_id, String html) =|
inspector_did_list_style_sheets(u64 page_id, Vector<Web::CSS::StyleSheetIdentifier> style_sheets) =|
inspector_did_request_style_sheet_source(u64 page_id, Web::CSS::StyleSheetIdentifier identifier) =|
did_request_style_sheet_source(u64 page_id, Web::CSS::StyleSheetIdentifier identifier, String source) =|
did_take_screenshot(u64 page_id, Gfx::ShareableBitmap screenshot) =|
did_change_favicon(u64 page_id, Gfx::ShareableBitmap favicon) =|

View file

@ -5,6 +5,7 @@
#include <LibWeb/CSS/PreferredContrast.h>
#include <LibWeb/CSS/PreferredMotion.h>
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/CSS/StyleSheetIdentifier.h>
#include <LibWeb/HTML/ColorPickerUpdateState.h>
#include <LibWeb/HTML/SelectedFile.h>
#include <LibWeb/Page/InputEvent.h>
@ -45,6 +46,8 @@ endpoint WebContentServer
get_hovered_node_id(u64 page_id) =|
js_console_input(u64 page_id, ByteString js_source) =|
js_console_request_messages(u64 page_id, i32 start_index) =|
list_style_sheets(u64 page_id) =|
request_style_sheet_source(u64 page_id, Web::CSS::StyleSheetIdentifier identifier) =|
set_dom_node_text(u64 page_id, i32 node_id, String text) =|
set_dom_node_tag(u64 page_id, i32 node_id, String name) =|