diff --git a/Base/res/icons/16x16/download.png b/Base/res/icons/16x16/download.png new file mode 100644 index 0000000000..7836d3c341 Binary files /dev/null and b/Base/res/icons/16x16/download.png differ diff --git a/Base/res/ladybird/inspector.css b/Base/res/ladybird/inspector.css index 40688631e7..3d7ae6f4bd 100644 --- a/Base/res/ladybird/inspector.css +++ b/Base/res/ladybird/inspector.css @@ -92,7 +92,7 @@ body { display: flex; align-items: center; - justify-content: center; + justify-content: space-between; z-index: 100; } @@ -134,6 +134,30 @@ body { border-left: 1px solid var(--tab-button-border); } +.global-controls { + margin: 0 8px 0 8px; +} + +.global-controls button { + width: 24px; + height: 24px; + + border: none; + outline: none; + cursor: pointer; +} + +#export-inspector-button { + background-image: url("resource://icons/16x16/download.png"); + background-position: center; + background-repeat: no-repeat; + background-color: var(--tab-controls); +} + +#export-inspector-button:hover { + background-color: var(--tab-button-background); +} + .tab-content { height: calc(100% - 40px); diff --git a/Base/res/ladybird/inspector.js b/Base/res/ladybird/inspector.js index 913790978d..9a854c87d3 100644 --- a/Base/res/ladybird/inspector.js +++ b/Base/res/ladybird/inspector.js @@ -91,6 +91,11 @@ const scrollToElement = element => { window.scrollTo(0, position); }; +inspector.exportInspector = () => { + const html = document.documentElement.outerHTML; + inspector.exportInspectorHTML(html); +}; + inspector.reset = () => { let domTree = document.getElementById("dom-tree"); domTree.innerHTML = ""; diff --git a/Ladybird/cmake/ResourceFiles.cmake b/Ladybird/cmake/ResourceFiles.cmake index ff371ffcec..6f383a4e98 100644 --- a/Ladybird/cmake/ResourceFiles.cmake +++ b/Ladybird/cmake/ResourceFiles.cmake @@ -11,6 +11,7 @@ set(16x16_ICONS audio-volume-high.png audio-volume-muted.png close-tab.png + download.png edit-copy.png filetype-css.png filetype-folder-open.png diff --git a/Userland/Libraries/LibWeb/Internals/Inspector.cpp b/Userland/Libraries/LibWeb/Internals/Inspector.cpp index 761f2c80f9..97505b1438 100644 --- a/Userland/Libraries/LibWeb/Internals/Inspector.cpp +++ b/Userland/Libraries/LibWeb/Internals/Inspector.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Tim Flynn + * Copyright (c) 2023-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -76,4 +76,9 @@ void Inspector::execute_console_script(String const& script) global_object().browsing_context()->page().client().inspector_did_execute_console_script(script); } +void Inspector::export_inspector_html(String const& html) +{ + global_object().browsing_context()->page().client().inspector_did_export_inspector_html(html); +} + } diff --git a/Userland/Libraries/LibWeb/Internals/Inspector.h b/Userland/Libraries/LibWeb/Internals/Inspector.h index 0058984d72..faf1a08636 100644 --- a/Userland/Libraries/LibWeb/Internals/Inspector.h +++ b/Userland/Libraries/LibWeb/Internals/Inspector.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Tim Flynn + * Copyright (c) 2023-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -31,6 +31,8 @@ public: void execute_console_script(String const& script); + void export_inspector_html(String const& html); + private: explicit Inspector(JS::Realm&); diff --git a/Userland/Libraries/LibWeb/Internals/Inspector.idl b/Userland/Libraries/LibWeb/Internals/Inspector.idl index a87479bb35..d31b2b5dec 100644 --- a/Userland/Libraries/LibWeb/Internals/Inspector.idl +++ b/Userland/Libraries/LibWeb/Internals/Inspector.idl @@ -14,4 +14,6 @@ undefined executeConsoleScript(DOMString script); + undefined exportInspectorHTML(DOMString html); + }; diff --git a/Userland/Libraries/LibWeb/Page/Page.h b/Userland/Libraries/LibWeb/Page/Page.h index 55cb6d6ea2..30c10b9c07 100644 --- a/Userland/Libraries/LibWeb/Page/Page.h +++ b/Userland/Libraries/LibWeb/Page/Page.h @@ -377,6 +377,7 @@ public: virtual void inspector_did_replace_dom_node_attribute([[maybe_unused]] i32 node_id, [[maybe_unused]] size_t attribute_index, [[maybe_unused]] JS::NonnullGCPtr 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 const& tag, [[maybe_unused]] Optional const& attribute_index) { } virtual void inspector_did_execute_console_script([[maybe_unused]] String const& script) { } + virtual void inspector_did_export_inspector_html([[maybe_unused]] String const& html) { } virtual void schedule_repaint() = 0; virtual bool is_ready_to_paint() const = 0; diff --git a/Userland/Libraries/LibWebView/InspectorClient.cpp b/Userland/Libraries/LibWebView/InspectorClient.cpp index 923e58af41..74ddc0e883 100644 --- a/Userland/Libraries/LibWebView/InspectorClient.cpp +++ b/Userland/Libraries/LibWebView/InspectorClient.cpp @@ -7,7 +7,12 @@ #include #include #include +#include #include +#include +#include +#include +#include #include #include #include @@ -15,6 +20,9 @@ namespace WebView { +static constexpr auto INSPECTOR_CSS = "resource://ladybird/inspector.css"sv; +static constexpr auto INSPECTOR_JS = "resource://ladybird/inspector.js"sv; + static ErrorOr parse_json_tree(StringView json) { auto parsed_tree = TRY(JsonValue::from_string(json)); @@ -179,6 +187,47 @@ InspectorClient::InspectorClient(ViewImplementation& content_web_view, ViewImple m_content_web_view.js_console_input(script.to_byte_string()); }; + m_inspector_web_view.on_inspector_exported_inspector_html = [this](String const& html) { + auto inspector_path = LexicalPath::join(Core::StandardPaths::downloads_directory(), "inspector"sv); + + if (auto result = Core::Directory::create(inspector_path, Core::Directory::CreateDirectories::Yes); result.is_error()) { + append_console_warning(MUST(String::formatted("Unable to create {}: {}", inspector_path, result.error()))); + return; + } + + auto export_file = [&](auto name, auto const& contents) { + auto path = inspector_path.append(name); + + auto file = Core::File::open(path.string(), Core::File::OpenMode::Write); + if (file.is_error()) { + append_console_warning(MUST(String::formatted("Unable to open {}: {}", path, file.error()))); + return false; + } + + if (auto result = file.value()->write_until_depleted(contents); result.is_error()) { + append_console_warning(MUST(String::formatted("Unable to save {}: {}", path, result.error()))); + return false; + } + + return true; + }; + + auto inspector_css = MUST(Core::Resource::load_from_uri(INSPECTOR_CSS)); + auto inspector_js = MUST(Core::Resource::load_from_uri(INSPECTOR_JS)); + + auto inspector_html = MUST(html.replace(INSPECTOR_CSS, "inspector.css"sv, ReplaceMode::All)); + inspector_html = MUST(inspector_html.replace(INSPECTOR_JS, "inspector.js"sv, ReplaceMode::All)); + + if (!export_file("inspector.html"sv, inspector_html)) + return; + if (!export_file("inspector.css"sv, inspector_css->data())) + return; + if (!export_file("inspector.js"sv, inspector_js->data())) + return; + + append_console_message(MUST(String::formatted("Exported Inspector files to {}", inspector_path))); + }; + load_inspector(); } @@ -359,18 +408,22 @@ void InspectorClient::load_inspector() builder.append(HTML_HIGHLIGHTER_STYLE); - builder.append(R"~~~( + builder.appendff(R"~~~( - +
+
+
+ +
@@ -384,6 +437,7 @@ void InspectorClient::load_inspector()
+
@@ -391,6 +445,7 @@ void InspectorClient::load_inspector()
+
@@ -402,7 +457,8 @@ void InspectorClient::load_inspector()
-)~~~"sv); +)~~~", + INSPECTOR_CSS); auto generate_property_table = [&](auto name) { builder.appendff(R"~~~( @@ -435,14 +491,15 @@ void InspectorClient::load_inspector()
)~~~"sv); - builder.append(R"~~~( + builder.appendff(R"~~~( - + -)~~~"sv); +)~~~", + INSPECTOR_JS); m_inspector_web_view.load_html(builder.string_view()); } diff --git a/Userland/Libraries/LibWebView/ViewImplementation.h b/Userland/Libraries/LibWebView/ViewImplementation.h index dea99dd9e0..629f249fc7 100644 --- a/Userland/Libraries/LibWebView/ViewImplementation.h +++ b/Userland/Libraries/LibWebView/ViewImplementation.h @@ -218,6 +218,7 @@ public: Function const&)> on_inspector_replaced_dom_node_attribute; Function const&, Optional const&)> on_inspector_requested_dom_tree_context_menu; Function on_inspector_executed_console_script; + Function on_inspector_exported_inspector_html; Function on_request_worker_agent; virtual Web::DevicePixelSize viewport_size() const = 0; diff --git a/Userland/Libraries/LibWebView/WebContentClient.cpp b/Userland/Libraries/LibWebView/WebContentClient.cpp index e80723e3f9..ed2600d593 100644 --- a/Userland/Libraries/LibWebView/WebContentClient.cpp +++ b/Userland/Libraries/LibWebView/WebContentClient.cpp @@ -693,6 +693,14 @@ void WebContentClient::inspector_did_execute_console_script(u64 page_id, String } } +void WebContentClient::inspector_did_export_inspector_html(u64 page_id, String const& html) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) { + if (view->on_inspector_exported_inspector_html) + view->on_inspector_exported_inspector_html(html); + } +} + Messages::WebContentClient::RequestWorkerAgentResponse WebContentClient::request_worker_agent(u64 page_id) { if (auto view = view_for_page_id(page_id); view.has_value()) { diff --git a/Userland/Libraries/LibWebView/WebContentClient.h b/Userland/Libraries/LibWebView/WebContentClient.h index 9de57eb0d5..bd7a40666b 100644 --- a/Userland/Libraries/LibWebView/WebContentClient.h +++ b/Userland/Libraries/LibWebView/WebContentClient.h @@ -120,6 +120,7 @@ private: virtual void inspector_did_replace_dom_node_attribute(u64 page_id, i32 node_id, size_t attribute_index, Vector const& replacement_attributes) override; virtual void inspector_did_request_dom_tree_context_menu(u64 page_id, i32 node_id, Gfx::IntPoint position, String const& type, Optional const& tag, Optional const& attribute_index) override; 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; Optional view_for_page_id(u64, SourceLocation = SourceLocation::current()); diff --git a/Userland/Services/WebContent/PageClient.cpp b/Userland/Services/WebContent/PageClient.cpp index 1ed46b7f3c..00d40f6c8b 100644 --- a/Userland/Services/WebContent/PageClient.cpp +++ b/Userland/Services/WebContent/PageClient.cpp @@ -659,6 +659,11 @@ void PageClient::inspector_did_execute_console_script(String const& script) client().async_inspector_did_execute_console_script(m_id, script); } +void PageClient::inspector_did_export_inspector_html(String const& html) +{ + client().async_inspector_did_export_inspector_html(m_id, html); +} + ErrorOr PageClient::connect_to_webdriver(ByteString const& webdriver_ipc_path) { VERIFY(!m_webdriver); diff --git a/Userland/Services/WebContent/PageClient.h b/Userland/Services/WebContent/PageClient.h index 2220d62df8..41d91138d1 100644 --- a/Userland/Services/WebContent/PageClient.h +++ b/Userland/Services/WebContent/PageClient.h @@ -167,6 +167,7 @@ private: virtual void inspector_did_replace_dom_node_attribute(i32 node_id, size_t attribute_index, JS::NonnullGCPtr replacement_attributes) override; virtual void inspector_did_request_dom_tree_context_menu(i32 node_id, Web::CSSPixelPoint position, String const& type, Optional const& tag, Optional const& attribute_index) override; virtual void inspector_did_execute_console_script(String const& script) override; + virtual void inspector_did_export_inspector_html(String const& script) override; Web::Layout::Viewport* layout_root(); void setup_palette(); diff --git a/Userland/Services/WebContent/WebContentClient.ipc b/Userland/Services/WebContent/WebContentClient.ipc index 2a9cd05aef..45694704bb 100644 --- a/Userland/Services/WebContent/WebContentClient.ipc +++ b/Userland/Services/WebContent/WebContentClient.ipc @@ -101,5 +101,6 @@ endpoint WebContentClient inspector_did_replace_dom_node_attribute(u64 page_id, i32 node_id, size_t attribute_index, Vector replacement_attributes) =| inspector_did_request_dom_tree_context_menu(u64 page_id, i32 node_id, Gfx::IntPoint position, String type, Optional tag, Optional attribute_index) =| inspector_did_execute_console_script(u64 page_id, String script) =| + inspector_did_export_inspector_html(u64 page_id, String html) =| }