diff --git a/Meta/gn/secondary/Userland/Libraries/LibWebView/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWebView/BUILD.gn index 1523681adb..548e3b4c94 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWebView/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibWebView/BUILD.gn @@ -120,6 +120,7 @@ shared_library("LibWebView") { "CookieJar.cpp", "Database.cpp", "History.cpp", + "InspectorClient.cpp", "PropertyTableModel.cpp", "RequestServerAdapter.cpp", "SearchEngine.cpp", diff --git a/Userland/Libraries/LibWebView/CMakeLists.txt b/Userland/Libraries/LibWebView/CMakeLists.txt index 870ffda21a..0fc65abd7f 100644 --- a/Userland/Libraries/LibWebView/CMakeLists.txt +++ b/Userland/Libraries/LibWebView/CMakeLists.txt @@ -5,6 +5,7 @@ set(SOURCES CookieJar.cpp Database.cpp History.cpp + InspectorClient.cpp PropertyTableModel.cpp RequestServerAdapter.cpp SearchEngine.cpp diff --git a/Userland/Libraries/LibWebView/Forward.h b/Userland/Libraries/LibWebView/Forward.h index 062237509c..11a56d27c3 100644 --- a/Userland/Libraries/LibWebView/Forward.h +++ b/Userland/Libraries/LibWebView/Forward.h @@ -14,6 +14,7 @@ class ConsoleClient; class CookieJar; class Database; class History; +class InspectorClient; class OutOfProcessWebView; class ViewImplementation; class WebContentClient; diff --git a/Userland/Libraries/LibWebView/InspectorClient.cpp b/Userland/Libraries/LibWebView/InspectorClient.cpp new file mode 100644 index 0000000000..5eda8c8273 --- /dev/null +++ b/Userland/Libraries/LibWebView/InspectorClient.cpp @@ -0,0 +1,523 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace WebView { + +static ErrorOr parse_json_tree(StringView json) +{ + auto parsed_tree = TRY(JsonValue::from_string(json)); + if (!parsed_tree.is_object()) + return Error::from_string_literal("Expected tree to be a JSON object"); + + return parsed_tree; +} + +InspectorClient::InspectorClient(ViewImplementation& content_web_view, ViewImplementation& inspector_web_view) + : m_content_web_view(content_web_view) + , m_inspector_web_view(inspector_web_view) +{ + m_content_web_view.on_received_dom_tree = [this](auto const& dom_tree) { + if (auto result = parse_json_tree(dom_tree); result.is_error()) + dbgln("Failed to load DOM tree: {}", result.error()); + else + m_dom_tree = result.release_value(); + + maybe_load_inspector(); + }; + + m_content_web_view.on_received_accessibility_tree = [this](auto const& dom_tree) { + if (auto result = parse_json_tree(dom_tree); result.is_error()) + dbgln("Failed to load accessibility tree: {}", result.error()); + else + m_accessibility_tree = result.release_value(); + + maybe_load_inspector(); + }; + + m_inspector_web_view.enable_inspector_prototype(); + m_inspector_web_view.use_native_user_style_sheet(); + + m_inspector_web_view.on_inspector_loaded = [this]() { + m_inspector_loaded = true; + + if (m_pending_selection.has_value()) + select_node(m_pending_selection.release_value()); + else + select_default_node(); + }; + + m_inspector_web_view.on_inspector_selected_dom_node = [this](auto node_id, auto const& pseudo_element) { + auto inspected_node_properties = m_content_web_view.inspect_dom_node(node_id, pseudo_element); + + if (on_dom_node_properties_received) + on_dom_node_properties_received(move(inspected_node_properties)); + }; + + inspect(); +} + +InspectorClient::~InspectorClient() +{ + m_content_web_view.on_received_dom_tree = nullptr; + m_content_web_view.on_received_accessibility_tree = nullptr; +} + +void InspectorClient::inspect() +{ + m_content_web_view.inspect_dom_tree(); + m_content_web_view.inspect_accessibility_tree(); +} + +void InspectorClient::reset() +{ + m_dom_tree.clear(); + m_accessibility_tree.clear(); + + m_body_node_id.clear(); + m_pending_selection.clear(); + + m_inspector_loaded = false; +} + +void InspectorClient::select_hovered_node() +{ + auto hovered_node_id = m_content_web_view.get_hovered_node_id(); + select_node(hovered_node_id); +} + +void InspectorClient::select_default_node() +{ + if (m_body_node_id.has_value()) + select_node(*m_body_node_id); +} + +void InspectorClient::clear_selection() +{ + m_content_web_view.clear_inspected_dom_node(); + + static constexpr auto script = "inspector.clearInspectedDOMNode();"sv; + m_inspector_web_view.run_javascript(script); +} + +void InspectorClient::select_node(i32 node_id) +{ + if (!m_inspector_loaded) { + m_pending_selection = node_id; + return; + } + + auto script = MUST(String::formatted("inspector.inspectDOMNodeID({});", node_id)); + m_inspector_web_view.run_javascript(script); +} + +void InspectorClient::maybe_load_inspector() +{ + if (!m_dom_tree.has_value() || !m_accessibility_tree.has_value()) + return; + + StringBuilder builder; + + builder.append(R"~~~( + + + + + + + +
+
+ + +
+
+ +
+
+)~~~"sv); + + generate_dom_tree(builder); + + builder.append(R"~~~( +
+
+)~~~"sv); + + generate_accessibility_tree(builder); + + builder.append(R"~~~( +
+
+ + + + +)~~~"sv); + + m_inspector_web_view.load_html(builder.string_view()); +} + +template +static void generate_tree(StringBuilder& builder, JsonObject const& node, Generator&& generator) +{ + if (auto children = node.get_array("children"sv); children.has_value() && !children->is_empty()) { + auto name = node.get_deprecated_string("name"sv).value_or({}); + builder.append("
"sv); + + builder.append(""sv); + generator(node); + builder.append(""sv); + + children->for_each([&](auto const& child) { + builder.append("
"sv); + generate_tree(builder, child.as_object(), generator); + builder.append("
"sv); + }); + + builder.append("
"sv); + } else { + generator(node); + } +} + +void InspectorClient::generate_dom_tree(StringBuilder& builder) +{ + generate_tree(builder, m_dom_tree->as_object(), [&](JsonObject const& node) { + auto type = node.get_deprecated_string("type"sv).value_or("unknown"sv); + auto name = node.get_deprecated_string("name"sv).value_or({}); + + StringBuilder data_attributes; + auto append_data_attribute = [&](auto name, auto value) { + if (!data_attributes.is_empty()) + data_attributes.append(' '); + data_attributes.appendff("data-{}=\"{}\"", name, value); + }; + + if (auto pseudo_element = node.get_integer("pseudo-element"sv); pseudo_element.has_value()) { + append_data_attribute("id"sv, node.get_integer("parent-id"sv).value()); + append_data_attribute("pseudo-element"sv, *pseudo_element); + } else { + append_data_attribute("id"sv, node.get_integer("id"sv).value()); + } + + if (type == "text"sv) { + auto deprecated_text = node.get_deprecated_string("text"sv).release_value(); + deprecated_text = escape_html_entities(deprecated_text); + + if (auto text = MUST(Web::Infra::strip_and_collapse_whitespace(deprecated_text)); text.is_empty()) { + builder.appendff("", data_attributes.string_view()); + builder.append(name); + builder.append(""sv); + } else { + builder.appendff("", data_attributes.string_view()); + builder.append(text); + builder.append(""sv); + } + + return; + } + + if (type == "comment"sv) { + auto comment = node.get_deprecated_string("data"sv).release_value(); + comment = escape_html_entities(comment); + + builder.appendff("", data_attributes.string_view()); + builder.appendff("<!--{}-->", comment); + builder.append(""sv); + return; + } + + if (type == "shadow-root"sv) { + auto mode = node.get_deprecated_string("mode"sv).release_value(); + + builder.appendff("", data_attributes.string_view()); + builder.appendff("{} ({})", name, mode); + builder.append(""sv); + return; + } + + if (type != "element"sv) { + builder.appendff("", data_attributes.string_view()); + builder.appendff(name); + builder.append(""sv); + return; + } + + if (name.equals_ignoring_ascii_case("BODY"sv)) + m_body_node_id = node.get_integer("id"sv).value(); + + builder.appendff("", data_attributes.string_view()); + builder.append("<"sv); + builder.appendff("{}", name.to_lowercase()); + + if (auto attributes = node.get_object("attributes"sv); attributes.has_value()) { + attributes->for_each_member([&builder](auto const& name, auto const& value) { + builder.append(" "sv); + builder.appendff("{}", name); + builder.append('='); + builder.appendff("\"{}\"", value); + }); + } + + builder.append(">"sv); + builder.append(""sv); + }); +} + +void InspectorClient::generate_accessibility_tree(StringBuilder& builder) +{ + generate_tree(builder, m_accessibility_tree->as_object(), [&](JsonObject const& node) { + auto type = node.get_deprecated_string("type"sv).value_or("unknown"sv); + auto role = node.get_deprecated_string("role"sv).value_or({}); + + if (type == "text"sv) { + auto text = node.get_deprecated_string("text"sv).release_value(); + text = escape_html_entities(text); + + builder.appendff(""); + builder.append(MUST(Web::Infra::strip_and_collapse_whitespace(text))); + builder.append(""sv); + return; + } + + if (type != "element"sv) { + builder.appendff(""); + builder.appendff(role.to_lowercase()); + builder.append(""sv); + return; + } + + auto name = node.get_deprecated_string("name"sv).value_or({}); + auto description = node.get_deprecated_string("description"sv).value_or({}); + + builder.appendff(""); + builder.append(role.to_lowercase()); + builder.appendff(" name: \"{}\", description: \"{}\"", name, description); + builder.append(""sv); + }); +} + +} diff --git a/Userland/Libraries/LibWebView/InspectorClient.h b/Userland/Libraries/LibWebView/InspectorClient.h new file mode 100644 index 0000000000..d62c367e1b --- /dev/null +++ b/Userland/Libraries/LibWebView/InspectorClient.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +#pragma once + +namespace WebView { + +class InspectorClient { +public: + InspectorClient(ViewImplementation& content_web_view, ViewImplementation& inspector_web_view); + ~InspectorClient(); + + void inspect(); + void reset(); + + void select_hovered_node(); + void select_default_node(); + void clear_selection(); + + Function)> on_dom_node_properties_received; + +private: + void maybe_load_inspector(); + void generate_dom_tree(StringBuilder&); + void generate_accessibility_tree(StringBuilder&); + + void select_node(i32 node_id); + + ViewImplementation& m_content_web_view; + ViewImplementation& m_inspector_web_view; + + Optional m_dom_tree; + Optional m_accessibility_tree; + + Optional m_body_node_id; + Optional m_pending_selection; + + bool m_inspector_loaded { false }; +}; + +} diff --git a/Userland/Libraries/LibWebView/SourceHighlighter.h b/Userland/Libraries/LibWebView/SourceHighlighter.h index 2d4f204947..12d1c0852f 100644 --- a/Userland/Libraries/LibWebView/SourceHighlighter.h +++ b/Userland/Libraries/LibWebView/SourceHighlighter.h @@ -41,6 +41,9 @@ constexpr inline StringView HTML_HIGHLIGHTER_STYLE = R"~~~( .attribute-value { color: deepskyblue; } + .internal { + color: darkgrey; + } } @media (prefers-color-scheme: light) { @@ -56,6 +59,9 @@ constexpr inline StringView HTML_HIGHLIGHTER_STYLE = R"~~~( .attribute-value { color: blue; } + .internal { + color: dimgray; + } } )~~~"sv;