From bf59e06d2af935e9b162778af66066c10ba0fc85 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Tue, 29 Aug 2023 11:44:18 -0400 Subject: [PATCH] Ladybird+LibWebView: Extract common JS console functionality to a helper This creates WebView::ConsoleClient to handle functionality that will be common to the JS consoles of all Ladybird chromes. This will let each chrome focus on just the UI. Note that this includes the `console.group` functionality that only the Serenity chrome previously had. This was a FIXME in the Qt chrome, and it is implemented such that all chromes will receive this functionality for free. --- Ladybird/Qt/ConsoleWidget.cpp | 130 +---------- Ladybird/Qt/ConsoleWidget.h | 25 +-- Ladybird/Qt/Tab.cpp | 18 +- Meta/Lagom/CMakeLists.txt | 3 +- .../Applications/Browser/ConsoleWidget.cpp | 176 +-------------- Userland/Applications/Browser/ConsoleWidget.h | 30 +-- Userland/Applications/Browser/Tab.cpp | 19 +- Userland/Libraries/LibWebView/CMakeLists.txt | 3 +- .../Libraries/LibWebView/ConsoleClient.cpp | 205 ++++++++++++++++++ Userland/Libraries/LibWebView/ConsoleClient.h | 56 +++++ Userland/Libraries/LibWebView/Forward.h | 1 + 11 files changed, 300 insertions(+), 366 deletions(-) create mode 100644 Userland/Libraries/LibWebView/ConsoleClient.cpp create mode 100644 Userland/Libraries/LibWebView/ConsoleClient.h diff --git a/Ladybird/Qt/ConsoleWidget.cpp b/Ladybird/Qt/ConsoleWidget.cpp index 7972b69b9b..6b44549a33 100644 --- a/Ladybird/Qt/ConsoleWidget.cpp +++ b/Ladybird/Qt/ConsoleWidget.cpp @@ -10,35 +10,26 @@ #include "ConsoleWidget.h" #include "StringUtils.h" #include "WebContentView.h" -#include -#include +#include #include #include #include -#include #include -#include #include namespace Ladybird { bool is_using_dark_system_theme(QWidget&); -ConsoleWidget::ConsoleWidget() +ConsoleWidget::ConsoleWidget(WebContentView& content_view) { setLayout(new QVBoxLayout); m_output_view = new WebContentView({}, WebView::EnableCallgrindProfiling::No, UseLagomNetworking::No); - m_output_view->use_native_user_style_sheet(); if (is_using_dark_system_theme(*this)) m_output_view->update_palette(WebContentView::PaletteMode::Dark); - m_output_view->load("data:text/html,"sv); - // Wait until our output WebView is loaded, and then request any messages that occurred before we existed - m_output_view->on_load_finish = [this](auto&) { - if (on_request_messages) - on_request_messages(0); - }; + m_console_client = make(content_view, *m_output_view); layout()->addWidget(m_output_view); @@ -59,117 +50,17 @@ ConsoleWidget::ConsoleWidget() clear_button->setText("X"); clear_button->setToolTip("Clear the console output"); QObject::connect(clear_button, &QPushButton::pressed, [this] { - clear_output(); + client().clear(); }); m_input->setFocus(); } -void ConsoleWidget::request_console_messages() -{ - VERIFY(!m_waiting_for_messages); - VERIFY(on_request_messages); - on_request_messages(m_highest_received_message_index + 1); - m_waiting_for_messages = true; -} - -void ConsoleWidget::notify_about_new_console_message(i32 message_index) -{ - if (message_index <= m_highest_received_message_index) { - dbgln("Notified about console message we already have"); - return; - } - if (message_index <= m_highest_notified_message_index) { - dbgln("Notified about console message we're already aware of"); - return; - } - - m_highest_notified_message_index = message_index; - if (!m_waiting_for_messages) - request_console_messages(); -} - -void ConsoleWidget::handle_console_messages(i32 start_index, Vector const& message_types, Vector const& messages) -{ - i32 end_index = start_index + message_types.size() - 1; - if (end_index <= m_highest_received_message_index) { - dbgln("Received old console messages"); - return; - } - - for (size_t i = 0; i < message_types.size(); i++) { - auto& type = message_types[i]; - auto& message = messages[i]; - - if (type == "html") { - print_html(message); - } else if (type == "clear") { - clear_output(); - } else if (type == "group") { - // FIXME: Implement. - } else if (type == "groupCollapsed") { - // FIXME: Implement. - } else if (type == "groupEnd") { - // FIXME: Implement. - } else { - VERIFY_NOT_REACHED(); - } - } - - m_highest_received_message_index = end_index; - m_waiting_for_messages = false; - - if (m_highest_received_message_index < m_highest_notified_message_index) - request_console_messages(); -} - -void ConsoleWidget::print_source_line(StringView source) -{ - StringBuilder html; - html.append(""sv); - html.append("> "sv); - html.append(""sv); - - html.append(JS::MarkupGenerator::html_from_source(source).release_value_but_fixme_should_propagate_errors()); - - print_html(html.string_view()); -} - -void ConsoleWidget::print_html(StringView line) -{ - StringBuilder builder; - - builder.append(R"~~~( - var p = document.createElement("p"); - p.innerHTML = ")~~~"sv); - builder.append_escaped_for_json(line); - builder.append(R"~~~(" - document.body.appendChild(p); -)~~~"sv); - - // FIXME: It should be sufficient to scrollTo a y value of document.documentElement.offsetHeight, - // but due to an unknown bug offsetHeight seems to not be properly updated after spamming - // a lot of document changes. - // - // The setTimeout makes the scrollTo async and allows the DOM to be updated. - builder.append("setTimeout(function() { window.scrollTo(0, 1_000_000_000); }, 0);"sv); - - m_output_view->run_javascript(builder.string_view()); -} - -void ConsoleWidget::clear_output() -{ - m_output_view->run_javascript(R"~~~( - document.body.innerHTML = ""; - )~~~"sv); -} +ConsoleWidget::~ConsoleWidget() = default; void ConsoleWidget::reset() { - clear_output(); - m_highest_notified_message_index = -1; - m_highest_received_message_index = -1; - m_waiting_for_messages = false; + m_console_client->reset(); } void ConsoleInputEdit::keyPressEvent(QKeyEvent* event) @@ -188,12 +79,14 @@ void ConsoleInputEdit::keyPressEvent(QKeyEvent* event) } break; } + case Qt::Key_Up: if (m_history_index > 0) { m_history_index--; setText(qstring_from_ak_deprecated_string(m_history.at(m_history_index))); } break; + case Qt::Key_Return: { auto js_source = ak_deprecated_string_from_qstring(text()); if (js_source.is_whitespace()) @@ -204,15 +97,12 @@ void ConsoleInputEdit::keyPressEvent(QKeyEvent* event) m_history_index = m_history.size(); } + m_console_widget.client().execute(js_source); clear(); - m_console_widget.print_source_line(js_source); - - if (m_console_widget.on_js_input) - m_console_widget.on_js_input(js_source); - break; } + default: QLineEdit::keyPressEvent(event); } diff --git a/Ladybird/Qt/ConsoleWidget.h b/Ladybird/Qt/ConsoleWidget.h index 2b08729984..df736c99a7 100644 --- a/Ladybird/Qt/ConsoleWidget.h +++ b/Ladybird/Qt/ConsoleWidget.h @@ -10,12 +10,12 @@ #pragma once #include -#include +#include #include +#include #include #include -class QLineEdit; namespace Ladybird { class WebContentView; @@ -23,30 +23,19 @@ class WebContentView; class ConsoleWidget final : public QWidget { Q_OBJECT public: - ConsoleWidget(); - virtual ~ConsoleWidget() = default; - - void notify_about_new_console_message(i32 message_index); - void handle_console_messages(i32 start_index, Vector const& message_types, Vector const& messages); - void print_source_line(StringView); - void print_html(StringView); - void reset(); + explicit ConsoleWidget(WebContentView& content_view); + virtual ~ConsoleWidget(); + WebView::ConsoleClient& client() { return *m_console_client; } WebContentView& view() { return *m_output_view; } - Function on_js_input; - Function on_request_messages; + void reset(); private: - void request_console_messages(); - void clear_output(); + OwnPtr m_console_client; WebContentView* m_output_view { nullptr }; QLineEdit* m_input { nullptr }; - - i32 m_highest_notified_message_index { -1 }; - i32 m_highest_received_message_index { -1 }; - bool m_waiting_for_messages { false }; }; class ConsoleInputEdit final : public QLineEdit { diff --git a/Ladybird/Qt/Tab.cpp b/Ladybird/Qt/Tab.cpp index 055cacfc5e..58f3d6f0b1 100644 --- a/Ladybird/Qt/Tab.cpp +++ b/Ladybird/Qt/Tab.cpp @@ -272,16 +272,6 @@ Tab::Tab(BrowserWindow* window, StringView webdriver_content_ipc_path, WebView:: m_inspector_widget->set_accessibility_json(accessibility_tree); }; - view().on_received_console_message = [this](auto message_index) { - if (m_console_widget) - m_console_widget->notify_about_new_console_message(message_index); - }; - - view().on_received_console_messages = [this](auto start_index, auto& message_types, auto& messages) { - if (m_console_widget) - m_console_widget->handle_console_messages(start_index, message_types, messages); - }; - auto* take_visible_screenshot_action = new QAction("Take &Visible Screenshot", this); take_visible_screenshot_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-image.png").arg(s_serenity_resource_root.characters()))); QObject::connect(take_visible_screenshot_action, &QAction::triggered, this, [this]() { @@ -702,7 +692,7 @@ void Tab::show_inspector_window(InspectorTarget inspector_target) void Tab::show_console_window() { if (!m_console_widget) { - m_console_widget = new Ladybird::ConsoleWidget; + m_console_widget = new Ladybird::ConsoleWidget(view()); m_console_widget->setWindowTitle("JS Console"); m_console_widget->resize(640, 480); @@ -719,12 +709,6 @@ void Tab::show_console_window() auto screen_position = QCursor::pos(); m_console_context_menu->exec(screen_position); }; - m_console_widget->on_js_input = [this](auto js_source) { - view().js_console_input(js_source); - }; - m_console_widget->on_request_messages = [this](i32 start_index) { - view().js_console_request_messages(start_index); - }; } m_console_widget->show(); diff --git a/Meta/Lagom/CMakeLists.txt b/Meta/Lagom/CMakeLists.txt index 15c76102b5..e76f66901a 100644 --- a/Meta/Lagom/CMakeLists.txt +++ b/Meta/Lagom/CMakeLists.txt @@ -430,6 +430,7 @@ if (BUILD_LAGOM) # WebView list(APPEND LIBWEBVIEW_SOURCES "../../Userland/Libraries/LibWebView/AccessibilityTreeModel.cpp") + list(APPEND LIBWEBVIEW_SOURCES "../../Userland/Libraries/LibWebView/ConsoleClient.cpp") list(APPEND LIBWEBVIEW_SOURCES "../../Userland/Libraries/LibWebView/DOMTreeModel.cpp") list(APPEND LIBWEBVIEW_SOURCES "../../Userland/Libraries/LibWebView/RequestServerAdapter.cpp") list(APPEND LIBWEBVIEW_SOURCES "../../Userland/Libraries/LibWebView/SourceHighlighter.cpp") @@ -463,7 +464,7 @@ if (BUILD_LAGOM) lagom_lib(LibWebView webview SOURCES ${LIBWEBVIEW_SOURCES} ${LIBWEBVIEW_GENERATED_SOURCES} - LIBS LibGfx LibGUI LibIPC LibWeb LibProtocol) + LIBS LibGfx LibGUI LibIPC LibJS LibWeb LibProtocol) foreach(header ${LIBWEBVIEW_GENERATED_SOURCES}) get_filename_component(subdirectory ${header} DIRECTORY) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${header}" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/${subdirectory}") diff --git a/Userland/Applications/Browser/ConsoleWidget.cpp b/Userland/Applications/Browser/ConsoleWidget.cpp index 6e19ec328c..6e30e14875 100644 --- a/Userland/Applications/Browser/ConsoleWidget.cpp +++ b/Userland/Applications/Browser/ConsoleWidget.cpp @@ -16,22 +16,18 @@ #include #include #include +#include +#include namespace Browser { -ConsoleWidget::ConsoleWidget() +ConsoleWidget::ConsoleWidget(WebView::OutOfProcessWebView& content_view) { set_layout(); set_fill_with_background_color(true); m_output_view = add(); - m_output_view->use_native_user_style_sheet(); - m_output_view->load("data:text/html,"sv); - // Wait until our output WebView is loaded, and then request any messages that occurred before we existed - m_output_view->on_load_finish = [this](auto&) { - if (on_request_messages) - on_request_messages(0); - }; + m_console_client = make(content_view, *m_output_view); auto& bottom_container = add(); bottom_container.set_layout(); @@ -52,10 +48,7 @@ ConsoleWidget::ConsoleWidget() m_input->add_current_text_to_history(); m_input->clear(); - print_source_line(js_source); - - if (on_js_input) - on_js_input(js_source); + m_console_client->execute(js_source); }; set_focus_proxy(m_input); @@ -65,168 +58,15 @@ ConsoleWidget::ConsoleWidget() clear_button.set_icon(g_icon_bag.delete_icon); clear_button.set_tooltip_deprecated("Clear the console output"); clear_button.on_click = [this](auto) { - clear_output(); + m_console_client->clear(); }; } -void ConsoleWidget::request_console_messages() -{ - VERIFY(!m_waiting_for_messages); - VERIFY(on_request_messages); - on_request_messages(m_highest_received_message_index + 1); - m_waiting_for_messages = true; -} - -void ConsoleWidget::notify_about_new_console_message(i32 message_index) -{ - if (message_index <= m_highest_received_message_index) { - dbgln("Notified about console message we already have"); - return; - } - if (message_index <= m_highest_notified_message_index) { - dbgln("Notified about console message we're already aware of"); - return; - } - - m_highest_notified_message_index = message_index; - if (!m_waiting_for_messages) - request_console_messages(); -} - -void ConsoleWidget::handle_console_messages(i32 start_index, Vector const& message_types, Vector const& messages) -{ - i32 end_index = start_index + message_types.size() - 1; - if (end_index <= m_highest_received_message_index) { - dbgln("Received old console messages"); - return; - } - - for (size_t i = 0; i < message_types.size(); i++) { - auto& type = message_types[i]; - auto& message = messages[i]; - - if (type == "html") { - print_html(message); - } else if (type == "clear") { - clear_output(); - } else if (type == "group") { - begin_group(message, true); - } else if (type == "groupCollapsed") { - begin_group(message, false); - } else if (type == "groupEnd") { - end_group(); - } else { - VERIFY_NOT_REACHED(); - } - } - - m_highest_received_message_index = end_index; - m_waiting_for_messages = false; - - if (m_highest_received_message_index < m_highest_notified_message_index) - request_console_messages(); -} - -void ConsoleWidget::print_source_line(StringView source) -{ - StringBuilder html; - html.append(""sv); - html.append("> "sv); - html.append(""sv); - - html.append(JS::MarkupGenerator::html_from_source(source).release_value_but_fixme_should_propagate_errors()); - - print_html(html.string_view()); -} - -void ConsoleWidget::print_html(StringView line) -{ - StringBuilder builder; - - int parent_id = m_group_stack.is_empty() ? 0 : m_group_stack.last().id; - if (parent_id == 0) { - builder.append(R"~~~( - var parentGroup = document.body; -)~~~"sv); - } else { - builder.appendff(R"~~~( - var parentGroup = document.getElementById("group_{}"); -)~~~", - parent_id); - } - - builder.append(R"~~~( - var p = document.createElement("p"); - p.innerHTML = ")~~~"sv); - builder.append_escaped_for_json(line); - builder.append(R"~~~(" - parentGroup.appendChild(p); -)~~~"sv); - m_output_view->run_javascript(builder.string_view()); - // FIXME: Make it scroll to the bottom, using `window.scrollTo()` in the JS above. - // We used to call `m_output_view->scroll_to_bottom();` here, but that does not work because - // it runs synchronously, meaning it happens before the HTML is output via IPC above. - // (See also: begin_group()) -} - -void ConsoleWidget::clear_output() -{ - m_group_stack.clear(); - m_output_view->run_javascript(R"~~~( - document.body.innerHTML = ""; - )~~~"sv); -} - -void ConsoleWidget::begin_group(StringView label, bool start_expanded) -{ - StringBuilder builder; - int parent_id = m_group_stack.is_empty() ? 0 : m_group_stack.last().id; - if (parent_id == 0) { - builder.append(R"~~~( - var parentGroup = document.body; -)~~~"sv); - } else { - builder.appendff(R"~~~( - var parentGroup = document.getElementById("group_{}"); -)~~~", - parent_id); - } - - Group group; - group.id = m_next_group_id++; - group.label = label; - - builder.appendff(R"~~~( - var group = document.createElement("details"); - group.id = "group_{}"; - var label = document.createElement("summary"); - label.innerHTML = ")~~~", - group.id); - builder.append_escaped_for_json(label); - builder.append(R"~~~("; - group.appendChild(label); - parentGroup.appendChild(group); -)~~~"sv); - - if (start_expanded) - builder.append("group.open = true;"sv); - - m_output_view->run_javascript(builder.string_view()); - // FIXME: Scroll console to bottom - see note in print_html() - m_group_stack.append(group); -} - -void ConsoleWidget::end_group() -{ - m_group_stack.take_last(); -} +ConsoleWidget::~ConsoleWidget() = default; void ConsoleWidget::reset() { - clear_output(); - m_highest_notified_message_index = -1; - m_highest_received_message_index = -1; - m_waiting_for_messages = false; + m_console_client->reset(); } } diff --git a/Userland/Applications/Browser/ConsoleWidget.h b/Userland/Applications/Browser/ConsoleWidget.h index 1b9f15aa83..08a5646206 100644 --- a/Userland/Applications/Browser/ConsoleWidget.h +++ b/Userland/Applications/Browser/ConsoleWidget.h @@ -9,47 +9,29 @@ #pragma once -#include "History.h" +#include #include -#include +#include namespace Browser { class ConsoleWidget final : public GUI::Widget { C_OBJECT(ConsoleWidget) public: - virtual ~ConsoleWidget() = default; + virtual ~ConsoleWidget(); - void notify_about_new_console_message(i32 message_index); - void handle_console_messages(i32 start_index, Vector const& message_types, Vector const& messages); - void print_source_line(StringView); - void print_html(StringView); void reset(); - Function on_js_input; - Function on_request_messages; - private: - ConsoleWidget(); + explicit ConsoleWidget(WebView::OutOfProcessWebView& content_view); void request_console_messages(); void clear_output(); - void begin_group(StringView label, bool start_expanded); - void end_group(); + + OwnPtr m_console_client; RefPtr m_input; RefPtr m_output_view; - - i32 m_highest_notified_message_index { -1 }; - i32 m_highest_received_message_index { -1 }; - bool m_waiting_for_messages { false }; - - struct Group { - int id { 0 }; - DeprecatedString label; - }; - Vector m_group_stack; - int m_next_group_id { 1 }; }; } diff --git a/Userland/Applications/Browser/Tab.cpp b/Userland/Applications/Browser/Tab.cpp index f08231be66..c5d048ad14 100644 --- a/Userland/Applications/Browser/Tab.cpp +++ b/Userland/Applications/Browser/Tab.cpp @@ -584,16 +584,6 @@ Tab::Tab(BrowserWindow& window) m_dom_inspector_widget->set_accessibility_json(accessibility_tree); }; - view().on_received_console_message = [this](auto message_index) { - if (m_console_widget) - m_console_widget->notify_about_new_console_message(message_index); - }; - - view().on_received_console_messages = [this](auto start_index, auto& message_types, auto& messages) { - if (m_console_widget) - m_console_widget->handle_console_messages(start_index, message_types, messages); - }; - auto focus_location_box_action = GUI::Action::create( "Focus location box", { Mod_Ctrl, Key_L }, Key_F6, [this](auto&) { m_location_box->set_focus(true); @@ -927,13 +917,8 @@ void Tab::show_console_window() console_window->resize(500, 300); console_window->set_title("JS Console"); console_window->set_icon(g_icon_bag.filetype_javascript); - m_console_widget = console_window->set_main_widget().release_value_but_fixme_should_propagate_errors(); - m_console_widget->on_js_input = [this](DeprecatedString const& js_source) { - m_web_content_view->js_console_input(js_source); - }; - m_console_widget->on_request_messages = [this](i32 start_index) { - m_web_content_view->js_console_request_messages(start_index); - }; + + m_console_widget = MUST(console_window->set_main_widget(view())); } auto* window = m_console_widget->window(); diff --git a/Userland/Libraries/LibWebView/CMakeLists.txt b/Userland/Libraries/LibWebView/CMakeLists.txt index 835751ac43..a06b92b748 100644 --- a/Userland/Libraries/LibWebView/CMakeLists.txt +++ b/Userland/Libraries/LibWebView/CMakeLists.txt @@ -1,6 +1,7 @@ set(SOURCES AccessibilityTreeModel.cpp AriaPropertiesStateModel.cpp + ConsoleClient.cpp DOMTreeModel.cpp OutOfProcessWebView.cpp RequestServerAdapter.cpp @@ -28,4 +29,4 @@ set(GENERATED_SOURCES ) serenity_lib(LibWebView webview) -target_link_libraries(LibWebView PRIVATE LibCore LibFileSystemAccessClient LibGfx LibGUI LibIPC LibProtocol LibWeb) +target_link_libraries(LibWebView PRIVATE LibCore LibFileSystemAccessClient LibGfx LibGUI LibIPC LibProtocol LibJS LibWeb) diff --git a/Userland/Libraries/LibWebView/ConsoleClient.cpp b/Userland/Libraries/LibWebView/ConsoleClient.cpp new file mode 100644 index 0000000000..da177d9560 --- /dev/null +++ b/Userland/Libraries/LibWebView/ConsoleClient.cpp @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace WebView { + +static constexpr auto CONSOLE_HTML = "data:text/html,"sv; + +// FIXME: It should be sufficient to scrollTo a y value of document.documentElement.offsetHeight, +// but due to an unknown bug offsetHeight seems to not be properly updated after spamming +// a lot of document changes. +// +// The setTimeout makes the scrollTo async and allows the DOM to be updated. +static constexpr auto SCROLL_TO_BOTTOM = "setTimeout(function() { window.scrollTo(0, 1_000_000_000); }, 0);"sv; + +ConsoleClient::ConsoleClient(ViewImplementation& content_web_view, ViewImplementation& console_web_view) + : m_content_web_view(content_web_view) + , m_console_web_view(console_web_view) +{ + m_content_web_view.on_received_console_message = [this](auto message_index) { + handle_console_message(message_index); + }; + + m_content_web_view.on_received_console_messages = [this](auto start_index, auto const& message_types, auto const& messages) { + handle_console_messages(start_index, message_types, messages); + }; + + // Wait until our output WebView is loaded, and then request any messages that occurred before we existed. + m_console_web_view.on_load_finish = [this](auto const&) { + request_console_messages(0); + }; + + m_console_web_view.use_native_user_style_sheet(); + m_console_web_view.load(CONSOLE_HTML); +} + +ConsoleClient::~ConsoleClient() +{ + m_content_web_view.on_received_console_message = nullptr; + m_content_web_view.on_received_console_messages = nullptr; +} + +void ConsoleClient::execute(StringView script) +{ + print_source(script); + m_content_web_view.js_console_input(script); +} + +void ConsoleClient::clear() +{ + m_console_web_view.js_console_input("document.body.innerHTML = \"\";"sv); + m_group_stack.clear(); +} + +void ConsoleClient::reset() +{ + clear(); + + m_highest_notified_message_index = -1; + m_highest_received_message_index = -1; + m_waiting_for_messages = false; +} + +void ConsoleClient::handle_console_message(i32 message_index) +{ + if (message_index <= m_highest_received_message_index) { + dbgln("Notified about console message we already have"); + return; + } + if (message_index <= m_highest_notified_message_index) { + dbgln("Notified about console message we're already aware of"); + return; + } + + m_highest_notified_message_index = message_index; + + if (!m_waiting_for_messages) + request_console_messages(); +} + +void ConsoleClient::handle_console_messages(i32 start_index, ReadonlySpan message_types, ReadonlySpan messages) +{ + auto end_index = start_index + static_cast(message_types.size()) - 1; + if (end_index <= m_highest_received_message_index) { + dbgln("Received old console messages"); + return; + } + + for (size_t i = 0; i < message_types.size(); ++i) { + auto const& type = message_types[i]; + auto const& message = messages[i]; + + if (type == "html"sv) + print_html(message); + else if (type == "clear"sv) + clear(); + else if (type == "group"sv) + begin_group(message, true); + else if (type == "groupCollapsed"sv) + begin_group(message, false); + else if (type == "groupEnd"sv) + end_group(); + else + VERIFY_NOT_REACHED(); + } + + m_highest_received_message_index = end_index; + m_waiting_for_messages = false; + + if (m_highest_received_message_index < m_highest_notified_message_index) + request_console_messages(); +} + +void ConsoleClient::print_source(StringView source) +{ + StringBuilder builder; + + builder.append("> "sv); + builder.append(MUST(JS::MarkupGenerator::html_from_source(source))); + + print_html(builder.string_view()); +} + +void ConsoleClient::print_html(StringView html) +{ + StringBuilder builder; + + if (m_group_stack.is_empty()) + builder.append("var parentGroup = document.body;"sv); + else + builder.appendff("var parentGroup = document.getElementById(\"group_{}\");", m_group_stack.last().id); + + builder.append(R"~~~( + var p = document.createElement("p"); + p.innerHTML = ")~~~"sv); + builder.append_escaped_for_json(html); + builder.append(R"~~~(" + parentGroup.appendChild(p); +)~~~"sv); + + builder.append(SCROLL_TO_BOTTOM); + + m_console_web_view.run_javascript(builder.string_view()); +} + +void ConsoleClient::request_console_messages() +{ + request_console_messages(m_highest_received_message_index + 1); +} + +void ConsoleClient::request_console_messages(i32 start_index) +{ + VERIFY(!m_waiting_for_messages); + + m_content_web_view.js_console_request_messages(start_index); + m_waiting_for_messages = true; +} + +void ConsoleClient::begin_group(StringView label, bool start_expanded) +{ + StringBuilder builder; + + if (m_group_stack.is_empty()) + builder.append("var parentGroup = document.body;"sv); + else + builder.appendff("var parentGroup = document.getElementById(\"group_{}\");", m_group_stack.last().id); + + Group group; + group.id = m_next_group_id++; + group.label = label; + + builder.appendff(R"~~~( + var group = document.createElement("details"); + group.id = "group_{}"; + var label = document.createElement("summary"); + label.innerHTML = ")~~~", + group.id); + builder.append_escaped_for_json(label); + builder.append(R"~~~("; + group.appendChild(label); + parentGroup.appendChild(group); +)~~~"sv); + + if (start_expanded) + builder.append("group.open = true;"sv); + + builder.append(SCROLL_TO_BOTTOM); + + m_console_web_view.run_javascript(builder.string_view()); + m_group_stack.append(group); +} + +void ConsoleClient::end_group() +{ + m_group_stack.take_last(); +} + +} diff --git a/Userland/Libraries/LibWebView/ConsoleClient.h b/Userland/Libraries/LibWebView/ConsoleClient.h new file mode 100644 index 0000000000..6f81c76d68 --- /dev/null +++ b/Userland/Libraries/LibWebView/ConsoleClient.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace WebView { + +class ConsoleClient { +public: + explicit ConsoleClient(ViewImplementation& content_web_view, ViewImplementation& console_web_view); + ~ConsoleClient(); + + void execute(StringView); + + void clear(); + void reset(); + +private: + void handle_console_message(i32 message_index); + void handle_console_messages(i32 start_index, ReadonlySpan message_types, ReadonlySpan messages); + + void print_source(StringView); + void print_html(StringView); + + void request_console_messages(); + void request_console_messages(i32 start_index); + + void begin_group(StringView label, bool start_expanded); + void end_group(); + + ViewImplementation& m_content_web_view; + ViewImplementation& m_console_web_view; + + i32 m_highest_notified_message_index { -1 }; + i32 m_highest_received_message_index { -1 }; + bool m_waiting_for_messages { false }; + + struct Group { + int id { 0 }; + DeprecatedString label; + }; + Vector m_group_stack; + int m_next_group_id { 1 }; +}; + +} diff --git a/Userland/Libraries/LibWebView/Forward.h b/Userland/Libraries/LibWebView/Forward.h index a89e644404..3d71fcc1c4 100644 --- a/Userland/Libraries/LibWebView/Forward.h +++ b/Userland/Libraries/LibWebView/Forward.h @@ -8,6 +8,7 @@ namespace WebView { +class ConsoleClient; class OutOfProcessWebView; class ViewImplementation; class WebContentClient;