LibWebView+WebContent: Create a different console client for DevTools

Our existing WebContentConsoleClient is very specific to our home-grown
Inspector. It renders console output to an HTML string. For DevTools, we
will not want this behavior; we will want to send representations of raw
JS values.

This patch makes WebContentConsoleClient a base class to handle console
input from the user, either from the Inspector or from DevTools. It then
moves the HTML rendering needed for the Inspector to a new class,
InspectorConsoleClient. And we add a DevToolsConsoleClient (currently
just stubbed) to handle needs specific to DevTools.

We choose at runtime which console client to install, based on the
--devtools command line flag.
This commit is contained in:
Timothy Flynn 2025-02-24 09:48:13 -05:00 committed by Andreas Kling
commit 37f07c176a
Notes: github-actions[bot] 2025-02-28 12:09:47 +00:00
12 changed files with 419 additions and 229 deletions

View file

@ -84,6 +84,7 @@ ErrorOr<NonnullRefPtr<WebView::WebContentClient>> launch_web_content_process(
IPC::File image_decoder_socket,
Optional<IPC::File> request_server_socket)
{
auto const& chrome_options = WebView::Application::chrome_options();
auto const& web_content_options = WebView::Application::web_content_options();
Vector<ByteString> arguments {
@ -93,6 +94,8 @@ ErrorOr<NonnullRefPtr<WebView::WebContentClient>> launch_web_content_process(
web_content_options.executable_path.to_byte_string(),
};
if (chrome_options.devtools_port.has_value())
arguments.append("--devtools"sv);
if (web_content_options.config_path.has_value()) {
arguments.append("--config-path"sv);
arguments.append(web_content_options.config_path.value());

View file

@ -1,9 +1,11 @@
include(audio)
set(SOURCES
BackingStoreManager.cpp
ConnectionFromClient.cpp
ConsoleGlobalEnvironmentExtensions.cpp
BackingStoreManager.cpp
DevToolsConsoleClient.cpp
InspectorConsoleClient.cpp
PageClient.cpp
PageHost.cpp
WebContentConsoleClient.cpp

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/HTML/Window.h>
#include <WebContent/ConsoleGlobalEnvironmentExtensions.h>
#include <WebContent/DevToolsConsoleClient.h>
#include <WebContent/PageClient.h>
namespace WebContent {
GC_DEFINE_ALLOCATOR(DevToolsConsoleClient);
GC::Ref<DevToolsConsoleClient> DevToolsConsoleClient::create(JS::Realm& realm, JS::Console& console, PageClient& client)
{
auto& window = as<Web::HTML::Window>(realm.global_object());
auto console_global_environment_extensions = realm.create<ConsoleGlobalEnvironmentExtensions>(realm, window);
return realm.heap().allocate<DevToolsConsoleClient>(realm, console, client, console_global_environment_extensions);
}
DevToolsConsoleClient::DevToolsConsoleClient(JS::Realm& realm, JS::Console& console, PageClient& client, ConsoleGlobalEnvironmentExtensions& console_global_environment_extensions)
: WebContentConsoleClient(realm, console, client, console_global_environment_extensions)
{
}
DevToolsConsoleClient::~DevToolsConsoleClient() = default;
void DevToolsConsoleClient::handle_result(JS::Value result)
{
(void)result;
}
void DevToolsConsoleClient::report_exception(JS::Error const& exception, bool in_promise)
{
(void)exception;
(void)in_promise;
}
void DevToolsConsoleClient::send_messages(i32 start_index)
{
(void)start_index;
}
// 2.3. Printer(logLevel, args[, options]), https://console.spec.whatwg.org/#printer
JS::ThrowCompletionOr<JS::Value> DevToolsConsoleClient::printer(JS::Console::LogLevel log_level, PrinterArguments arguments)
{
(void)log_level;
(void)arguments;
return JS::js_undefined();
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Console.h>
#include <LibJS/Forward.h>
#include <LibWeb/Forward.h>
#include <WebContent/Forward.h>
#include <WebContent/WebContentConsoleClient.h>
namespace WebContent {
class DevToolsConsoleClient final : public WebContentConsoleClient {
GC_CELL(DevToolsConsoleClient, WebContentConsoleClient);
GC_DECLARE_ALLOCATOR(DevToolsConsoleClient);
public:
static GC::Ref<DevToolsConsoleClient> create(JS::Realm&, JS::Console&, PageClient&);
virtual ~DevToolsConsoleClient() override;
private:
DevToolsConsoleClient(JS::Realm&, JS::Console&, PageClient&, ConsoleGlobalEnvironmentExtensions&);
virtual void handle_result(JS::Value) override;
virtual void report_exception(JS::Error const&, bool) override;
virtual void send_messages(i32 start_index) override;
virtual JS::ThrowCompletionOr<JS::Value> printer(JS::Console::LogLevel log_level, PrinterArguments) override;
};
}

View file

@ -10,6 +10,8 @@ namespace WebContent {
class ConnectionFromClient;
class ConsoleGlobalEnvironmentExtensions;
class DevToolsConsoleClient;
class InspectorConsoleClient;
class PageHost;
class PageClient;
class WebContentConsoleClient;

View file

@ -0,0 +1,231 @@
/*
* Copyright (c) 2021, Brandon Scott <xeon.productions@gmail.com>
* Copyright (c) 2020, Hunter Salyer <thefalsehonesty@gmail.com>
* Copyright (c) 2021-2022, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2024, Gasim Gasimzada <gasim@gasimzada.net>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/MemoryStream.h>
#include <AK/String.h>
#include <LibJS/MarkupGenerator.h>
#include <LibJS/Print.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/VM.h>
#include <LibWeb/HTML/Window.h>
#include <WebContent/ConsoleGlobalEnvironmentExtensions.h>
#include <WebContent/InspectorConsoleClient.h>
#include <WebContent/PageClient.h>
namespace WebContent {
GC_DEFINE_ALLOCATOR(InspectorConsoleClient);
GC::Ref<InspectorConsoleClient> InspectorConsoleClient::create(JS::Realm& realm, JS::Console& console, PageClient& client)
{
auto& window = as<Web::HTML::Window>(realm.global_object());
auto console_global_environment_extensions = realm.create<ConsoleGlobalEnvironmentExtensions>(realm, window);
return realm.heap().allocate<InspectorConsoleClient>(realm, console, client, console_global_environment_extensions);
}
InspectorConsoleClient::InspectorConsoleClient(JS::Realm& realm, JS::Console& console, PageClient& client, ConsoleGlobalEnvironmentExtensions& console_global_environment_extensions)
: WebContentConsoleClient(realm, console, client, console_global_environment_extensions)
{
}
InspectorConsoleClient::~InspectorConsoleClient() = default;
void InspectorConsoleClient::handle_result(JS::Value result)
{
print_html(JS::MarkupGenerator::html_from_value(result).release_value_but_fixme_should_propagate_errors());
}
void InspectorConsoleClient::report_exception(JS::Error const& exception, bool in_promise)
{
print_html(JS::MarkupGenerator::html_from_error(exception, in_promise).release_value_but_fixme_should_propagate_errors());
}
void InspectorConsoleClient::send_messages(i32 start_index)
{
// FIXME: Cap the number of messages we send at once?
auto messages_to_send = m_message_log.size() - start_index;
if (messages_to_send < 1) {
// When the console is first created, it requests any messages that happened before
// then, by requesting with start_index=0. If we don't have any messages at all, that
// is still a valid request, and we can just ignore it.
if (start_index != 0)
m_client->console_peer_did_misbehave("Requested non-existent console message index.");
return;
}
// FIXME: Replace with a single Vector of message structs
Vector<String> message_types;
Vector<String> messages;
message_types.ensure_capacity(messages_to_send);
messages.ensure_capacity(messages_to_send);
for (size_t i = start_index; i < m_message_log.size(); i++) {
auto& message = m_message_log[i];
switch (message.type) {
case ConsoleOutput::Type::HTML:
message_types.append("html"_string);
break;
case ConsoleOutput::Type::Clear:
message_types.append("clear"_string);
break;
case ConsoleOutput::Type::BeginGroup:
message_types.append("group"_string);
break;
case ConsoleOutput::Type::BeginGroupCollapsed:
message_types.append("groupCollapsed"_string);
break;
case ConsoleOutput::Type::EndGroup:
message_types.append("groupEnd"_string);
break;
}
messages.append(message.data);
}
m_client->did_get_js_console_messages(start_index, move(message_types), move(messages));
}
// 2.3. Printer(logLevel, args[, options]), https://console.spec.whatwg.org/#printer
JS::ThrowCompletionOr<JS::Value> InspectorConsoleClient::printer(JS::Console::LogLevel log_level, PrinterArguments arguments)
{
auto styling = escape_html_entities(m_current_message_style.string_view());
m_current_message_style.clear();
if (log_level == JS::Console::LogLevel::Table) {
auto& vm = m_console->realm().vm();
auto table_args = arguments.get<GC::RootVector<JS::Value>>();
auto& table = table_args.at(0).as_object();
auto& columns = TRY(table.get(vm.names.columns)).as_array().indexed_properties();
auto& rows = TRY(table.get(vm.names.rows)).as_array().indexed_properties();
StringBuilder html;
html.appendff("<div class=\"console-log-table\">");
html.appendff("<table>");
html.appendff("<thead>");
html.appendff("<tr>");
for (auto const& col : columns) {
auto index = col.index();
auto value = columns.storage()->get(index).value().value;
html.appendff("<td>{}</td>", value);
}
html.appendff("</tr>");
html.appendff("</thead>");
html.appendff("<tbody>");
for (auto const& row : rows) {
auto row_index = row.index();
auto& row_obj = rows.storage()->get(row_index).value().value.as_object();
html.appendff("<tr>");
for (auto const& col : columns) {
auto col_index = col.index();
auto col_name = columns.storage()->get(col_index).value().value;
auto property_key = TRY(JS::PropertyKey::from_value(vm, col_name));
auto cell = TRY(row_obj.get(property_key));
html.appendff("<td>");
if (TRY(cell.is_array(vm))) {
AllocatingMemoryStream stream;
JS::PrintContext ctx { vm, stream, true };
TRY_OR_THROW_OOM(vm, stream.write_until_depleted(" "sv.bytes()));
TRY_OR_THROW_OOM(vm, JS::print(cell, ctx));
auto output = TRY_OR_THROW_OOM(vm, String::from_stream(stream, stream.used_buffer_size()));
auto size = cell.as_array().indexed_properties().array_like_size();
html.appendff("<details><summary>Array({})</summary>{}</details>", size, output);
} else if (cell.is_object()) {
AllocatingMemoryStream stream;
JS::PrintContext ctx { vm, stream, true };
TRY_OR_THROW_OOM(vm, stream.write_until_depleted(" "sv.bytes()));
TRY_OR_THROW_OOM(vm, JS::print(cell, ctx));
auto output = TRY_OR_THROW_OOM(vm, String::from_stream(stream, stream.used_buffer_size()));
html.appendff("<details><summary>Object({{...}})</summary>{}</details>", output);
} else if (cell.is_function() || cell.is_constructor()) {
html.appendff("ƒ");
} else if (!cell.is_undefined()) {
html.appendff("{}", cell);
}
html.appendff("</td>");
}
html.appendff("</tr>");
}
html.appendff("</tbody>");
html.appendff("</table>");
html.appendff("</div>");
print_html(MUST(html.to_string()));
auto output = TRY(generically_format_values(table_args));
m_console->output_debug_message(log_level, output);
return JS::js_undefined();
}
if (log_level == JS::Console::LogLevel::Trace) {
auto trace = arguments.get<JS::Console::Trace>();
StringBuilder html;
if (!trace.label.is_empty())
html.appendff("<span class='title' style='{}'>{}</span><br>", styling, escape_html_entities(trace.label));
html.append("<span class='trace'>"sv);
for (auto& function_name : trace.stack)
html.appendff("-> {}<br>", escape_html_entities(function_name));
html.append("</span>"sv);
print_html(MUST(html.to_string()));
return JS::js_undefined();
}
if (log_level == JS::Console::LogLevel::Group || log_level == JS::Console::LogLevel::GroupCollapsed) {
auto group = arguments.get<JS::Console::Group>();
begin_group(MUST(String::formatted("<span style='{}'>{}</span>", styling, escape_html_entities(group.label))), log_level == JS::Console::LogLevel::Group);
return JS::js_undefined();
}
auto output = TRY(generically_format_values(arguments.get<GC::RootVector<JS::Value>>()));
m_console->output_debug_message(log_level, output);
StringBuilder html;
switch (log_level) {
case JS::Console::LogLevel::Debug:
html.appendff("<span class=\"debug\" style=\"{}\">(d) "sv, styling);
break;
case JS::Console::LogLevel::Error:
html.appendff("<span class=\"error\" style=\"{}\">(e) "sv, styling);
break;
case JS::Console::LogLevel::Info:
html.appendff("<span class=\"info\" style=\"{}\">(i) "sv, styling);
break;
case JS::Console::LogLevel::Log:
html.appendff("<span class=\"log\" style=\"{}\"> "sv, styling);
break;
case JS::Console::LogLevel::Warn:
case JS::Console::LogLevel::CountReset:
html.appendff("<span class=\"warn\" style=\"{}\">(w) "sv, styling);
break;
default:
html.appendff("<span style=\"{}\">"sv, styling);
break;
}
html.append(escape_html_entities(output));
html.append("</span>"sv);
print_html(MUST(html.to_string()));
return JS::js_undefined();
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2021, Brandon Scott <xeon.productions@gmail.com>
* Copyright (c) 2020, Hunter Salyer <thefalsehonesty@gmail.com>
* Copyright (c) 2021-2022, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/StringBuilder.h>
#include <LibWeb/Forward.h>
#include <WebContent/Forward.h>
#include <WebContent/WebContentConsoleClient.h>
namespace WebContent {
class InspectorConsoleClient final : public WebContentConsoleClient {
GC_CELL(InspectorConsoleClient, WebContentConsoleClient);
GC_DECLARE_ALLOCATOR(InspectorConsoleClient);
public:
static GC::Ref<InspectorConsoleClient> create(JS::Realm&, JS::Console&, PageClient&);
virtual ~InspectorConsoleClient() override;
private:
InspectorConsoleClient(JS::Realm&, JS::Console&, PageClient&, ConsoleGlobalEnvironmentExtensions&);
virtual void handle_result(JS::Value) override;
virtual void report_exception(JS::Error const&, bool) override;
virtual void send_messages(i32 start_index) override;
virtual JS::ThrowCompletionOr<JS::Value> printer(JS::Console::LogLevel log_level, PrinterArguments) override;
virtual void add_css_style_to_current_message(StringView style) override
{
m_current_message_style.append(style);
m_current_message_style.append(';');
}
StringBuilder m_current_message_style;
};
}

View file

@ -24,6 +24,8 @@
#include <LibWeb/Painting/ViewportPaintable.h>
#include <LibWebView/Attribute.h>
#include <WebContent/ConnectionFromClient.h>
#include <WebContent/DevToolsConsoleClient.h>
#include <WebContent/InspectorConsoleClient.h>
#include <WebContent/PageClient.h>
#include <WebContent/PageHost.h>
#include <WebContent/WebContentClientEndpoint.h>
@ -33,6 +35,7 @@ namespace WebContent {
static PageClient::UseSkiaPainter s_use_skia_painter = PageClient::UseSkiaPainter::GPUBackendIfAvailable;
static bool s_is_headless { false };
static bool s_devtools_enabled { false };
GC_DEFINE_ALLOCATOR(PageClient);
@ -51,6 +54,11 @@ void PageClient::set_is_headless(bool is_headless)
s_is_headless = is_headless;
}
void PageClient::set_devtools_enabled(bool devtools_enabled)
{
s_devtools_enabled = devtools_enabled;
}
GC::Ref<PageClient> PageClient::create(JS::VM& vm, PageHost& page_host, u64 id)
{
return vm.heap().allocate<PageClient>(page_host, id);
@ -731,9 +739,13 @@ void PageClient::initialize_js_console(Web::DOM::Document& document)
return;
auto& realm = document.realm();
auto console_object = realm.intrinsics().console_object();
auto console_client = heap().allocate<WebContentConsoleClient>(console_object->console(), document.realm(), *this);
GC::Ptr<JS::ConsoleClient> console_client;
if (s_devtools_enabled)
console_client = DevToolsConsoleClient::create(document.realm(), console_object->console(), *this);
else
console_client = InspectorConsoleClient::create(document.realm(), console_object->console(), *this);
document.set_console_client(console_client);
}

View file

@ -37,6 +37,8 @@ public:
virtual bool is_headless() const override;
static void set_is_headless(bool);
static void set_devtools_enabled(bool);
virtual bool is_ready_to_paint() const override;
virtual Web::Page& page() override { return *m_page; }

View file

@ -7,17 +7,11 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/MemoryStream.h>
#include <AK/StringBuilder.h>
#include <LibJS/MarkupGenerator.h>
#include <LibJS/Print.h>
#include <LibJS/Runtime/GlobalEnvironment.h>
#include <LibJS/Runtime/ObjectEnvironment.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/VM.h>
#include <LibWeb/HTML/Scripting/ClassicScript.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Window.h>
#include <WebContent/ConsoleGlobalEnvironmentExtensions.h>
#include <WebContent/PageClient.h>
#include <WebContent/WebContentConsoleClient.h>
@ -26,12 +20,12 @@ namespace WebContent {
GC_DEFINE_ALLOCATOR(WebContentConsoleClient);
WebContentConsoleClient::WebContentConsoleClient(JS::Console& console, JS::Realm& realm, PageClient& client)
WebContentConsoleClient::WebContentConsoleClient(JS::Realm& realm, JS::Console& console, PageClient& client, ConsoleGlobalEnvironmentExtensions& console_global_environment_extensions)
: ConsoleClient(console)
, m_realm(realm)
, m_client(client)
, m_console_global_environment_extensions(console_global_environment_extensions)
{
auto& window = as<Web::HTML::Window>(realm.global_object());
m_console_global_environment_extensions = realm.create<ConsoleGlobalEnvironmentExtensions>(realm, window);
}
WebContentConsoleClient::~WebContentConsoleClient() = default;
@ -39,15 +33,13 @@ WebContentConsoleClient::~WebContentConsoleClient() = default;
void WebContentConsoleClient::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_realm);
visitor.visit(m_client);
visitor.visit(m_console_global_environment_extensions);
}
void WebContentConsoleClient::handle_input(StringView js_source)
{
if (!m_console_global_environment_extensions)
return;
auto& settings = Web::HTML::relevant_settings_object(*m_console_global_environment_extensions);
auto script = Web::HTML::ClassicScript::create("(console)", js_source, settings.realm(), settings.api_base_url());
@ -57,22 +49,22 @@ void WebContentConsoleClient::handle_input(StringView js_source)
auto result = script->run(Web::HTML::ClassicScript::RethrowErrors::No, with_scope);
if (result.value().has_value()) {
m_console_global_environment_extensions->set_most_recent_result(result.value().value());
print_html(JS::MarkupGenerator::html_from_value(*result.value()).release_value_but_fixme_should_propagate_errors());
m_console_global_environment_extensions->set_most_recent_result(*result.value());
handle_result(*result.value());
}
}
void WebContentConsoleClient::report_exception(JS::Error const& exception, bool in_promise)
{
print_html(JS::MarkupGenerator::html_from_error(exception, in_promise).release_value_but_fixme_should_propagate_errors());
}
void WebContentConsoleClient::print_html(String const& line)
{
m_message_log.append({ .type = ConsoleOutput::Type::HTML, .data = line });
m_client->did_output_js_console_message(m_message_log.size() - 1);
}
void WebContentConsoleClient::clear()
{
clear_output();
}
void WebContentConsoleClient::clear_output()
{
m_message_log.append({ .type = ConsoleOutput::Type::Clear, .data = String {} });
@ -91,189 +83,4 @@ void WebContentConsoleClient::end_group()
m_client->did_output_js_console_message(m_message_log.size() - 1);
}
void WebContentConsoleClient::send_messages(i32 start_index)
{
// FIXME: Cap the number of messages we send at once?
auto messages_to_send = m_message_log.size() - start_index;
if (messages_to_send < 1) {
// When the console is first created, it requests any messages that happened before
// then, by requesting with start_index=0. If we don't have any messages at all, that
// is still a valid request, and we can just ignore it.
if (start_index != 0)
m_client->console_peer_did_misbehave("Requested non-existent console message index.");
return;
}
// FIXME: Replace with a single Vector of message structs
Vector<String> message_types;
Vector<String> messages;
message_types.ensure_capacity(messages_to_send);
messages.ensure_capacity(messages_to_send);
for (size_t i = start_index; i < m_message_log.size(); i++) {
auto& message = m_message_log[i];
switch (message.type) {
case ConsoleOutput::Type::HTML:
message_types.append("html"_string);
break;
case ConsoleOutput::Type::Clear:
message_types.append("clear"_string);
break;
case ConsoleOutput::Type::BeginGroup:
message_types.append("group"_string);
break;
case ConsoleOutput::Type::BeginGroupCollapsed:
message_types.append("groupCollapsed"_string);
break;
case ConsoleOutput::Type::EndGroup:
message_types.append("groupEnd"_string);
break;
}
messages.append(message.data);
}
m_client->did_get_js_console_messages(start_index, move(message_types), move(messages));
}
void WebContentConsoleClient::clear()
{
clear_output();
}
// 2.3. Printer(logLevel, args[, options]), https://console.spec.whatwg.org/#printer
JS::ThrowCompletionOr<JS::Value> WebContentConsoleClient::printer(JS::Console::LogLevel log_level, PrinterArguments arguments)
{
auto styling = escape_html_entities(m_current_message_style.string_view());
m_current_message_style.clear();
if (log_level == JS::Console::LogLevel::Table) {
auto& vm = m_console->realm().vm();
auto table_args = arguments.get<GC::RootVector<JS::Value>>();
auto& table = table_args.at(0).as_object();
auto& columns = TRY(table.get(vm.names.columns)).as_array().indexed_properties();
auto& rows = TRY(table.get(vm.names.rows)).as_array().indexed_properties();
StringBuilder html;
html.appendff("<div class=\"console-log-table\">");
html.appendff("<table>");
html.appendff("<thead>");
html.appendff("<tr>");
for (auto const& col : columns) {
auto index = col.index();
auto value = columns.storage()->get(index).value().value;
html.appendff("<td>{}</td>", value);
}
html.appendff("</tr>");
html.appendff("</thead>");
html.appendff("<tbody>");
for (auto const& row : rows) {
auto row_index = row.index();
auto& row_obj = rows.storage()->get(row_index).value().value.as_object();
html.appendff("<tr>");
for (auto const& col : columns) {
auto col_index = col.index();
auto col_name = columns.storage()->get(col_index).value().value;
auto property_key = TRY(JS::PropertyKey::from_value(vm, col_name));
auto cell = TRY(row_obj.get(property_key));
html.appendff("<td>");
if (TRY(cell.is_array(vm))) {
AllocatingMemoryStream stream;
JS::PrintContext ctx { vm, stream, true };
TRY_OR_THROW_OOM(vm, stream.write_until_depleted(" "sv.bytes()));
TRY_OR_THROW_OOM(vm, JS::print(cell, ctx));
auto output = TRY_OR_THROW_OOM(vm, String::from_stream(stream, stream.used_buffer_size()));
auto size = cell.as_array().indexed_properties().array_like_size();
html.appendff("<details><summary>Array({})</summary>{}</details>", size, output);
} else if (cell.is_object()) {
AllocatingMemoryStream stream;
JS::PrintContext ctx { vm, stream, true };
TRY_OR_THROW_OOM(vm, stream.write_until_depleted(" "sv.bytes()));
TRY_OR_THROW_OOM(vm, JS::print(cell, ctx));
auto output = TRY_OR_THROW_OOM(vm, String::from_stream(stream, stream.used_buffer_size()));
html.appendff("<details><summary>Object({{...}})</summary>{}</details>", output);
} else if (cell.is_function() || cell.is_constructor()) {
html.appendff("ƒ");
} else if (!cell.is_undefined()) {
html.appendff("{}", cell);
}
html.appendff("</td>");
}
html.appendff("</tr>");
}
html.appendff("</tbody>");
html.appendff("</table>");
html.appendff("</div>");
print_html(MUST(html.to_string()));
auto output = TRY(generically_format_values(table_args));
m_console->output_debug_message(log_level, output);
return JS::js_undefined();
}
if (log_level == JS::Console::LogLevel::Trace) {
auto trace = arguments.get<JS::Console::Trace>();
StringBuilder html;
if (!trace.label.is_empty())
html.appendff("<span class='title' style='{}'>{}</span><br>", styling, escape_html_entities(trace.label));
html.append("<span class='trace'>"sv);
for (auto& function_name : trace.stack)
html.appendff("-> {}<br>", escape_html_entities(function_name));
html.append("</span>"sv);
print_html(MUST(html.to_string()));
return JS::js_undefined();
}
if (log_level == JS::Console::LogLevel::Group || log_level == JS::Console::LogLevel::GroupCollapsed) {
auto group = arguments.get<JS::Console::Group>();
begin_group(MUST(String::formatted("<span style='{}'>{}</span>", styling, escape_html_entities(group.label))), log_level == JS::Console::LogLevel::Group);
return JS::js_undefined();
}
auto output = TRY(generically_format_values(arguments.get<GC::RootVector<JS::Value>>()));
m_console->output_debug_message(log_level, output);
StringBuilder html;
switch (log_level) {
case JS::Console::LogLevel::Debug:
html.appendff("<span class=\"debug\" style=\"{}\">(d) "sv, styling);
break;
case JS::Console::LogLevel::Error:
html.appendff("<span class=\"error\" style=\"{}\">(e) "sv, styling);
break;
case JS::Console::LogLevel::Info:
html.appendff("<span class=\"info\" style=\"{}\">(i) "sv, styling);
break;
case JS::Console::LogLevel::Log:
html.appendff("<span class=\"log\" style=\"{}\"> "sv, styling);
break;
case JS::Console::LogLevel::Warn:
case JS::Console::LogLevel::CountReset:
html.appendff("<span class=\"warn\" style=\"{}\">(w) "sv, styling);
break;
default:
html.appendff("<span style=\"{}\">"sv, styling);
break;
}
html.append(escape_html_entities(output));
html.append("</span>"sv);
print_html(MUST(html.to_string()));
return JS::js_undefined();
}
}

View file

@ -8,18 +8,16 @@
#pragma once
#include <AK/String.h>
#include <AK/Vector.h>
#include <AK/Weakable.h>
#include <LibJS/Console.h>
#include <LibJS/Forward.h>
#include <LibJS/Runtime/Value.h>
#include <LibWeb/Forward.h>
#include <WebContent/ConsoleGlobalEnvironmentExtensions.h>
#include <WebContent/Forward.h>
namespace WebContent {
class WebContentConsoleClient final : public JS::ConsoleClient {
class WebContentConsoleClient : public JS::ConsoleClient {
GC_CELL(WebContentConsoleClient, JS::ConsoleClient);
GC_DECLARE_ALLOCATOR(WebContentConsoleClient);
@ -27,30 +25,27 @@ public:
virtual ~WebContentConsoleClient() override;
void handle_input(StringView js_source);
void send_messages(i32 start_index);
void report_exception(JS::Error const&, bool) override;
private:
WebContentConsoleClient(JS::Console&, JS::Realm&, PageClient&);
virtual void handle_result(JS::Value) = 0;
virtual void send_messages(i32 start_index) = 0;
protected:
WebContentConsoleClient(JS::Realm&, JS::Console&, PageClient&, ConsoleGlobalEnvironmentExtensions&);
virtual void visit_edges(JS::Cell::Visitor&) override;
virtual void clear() override;
virtual JS::ThrowCompletionOr<JS::Value> printer(JS::Console::LogLevel log_level, PrinterArguments) override;
virtual void add_css_style_to_current_message(StringView style) override
{
m_current_message_style.append(style);
m_current_message_style.append(';');
}
GC::Ref<PageClient> m_client;
GC::Ptr<ConsoleGlobalEnvironmentExtensions> m_console_global_environment_extensions;
void clear_output();
void print_html(String const& line);
virtual void clear() override;
void clear_output();
void begin_group(String const& label, bool start_expanded);
virtual void end_group() override;
GC::Ref<JS::Realm> m_realm;
GC::Ref<PageClient> m_client;
GC::Ref<ConsoleGlobalEnvironmentExtensions> m_console_global_environment_extensions;
struct ConsoleOutput {
enum class Type {
HTML,
@ -63,8 +58,6 @@ private:
String data;
};
Vector<ConsoleOutput> m_message_log;
StringBuilder m_current_message_style;
};
}

View file

@ -107,6 +107,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
bool collect_garbage_on_every_allocation = false;
bool is_headless = false;
bool disable_scrollbar_painting = false;
bool devtools = false;
StringView echo_server_port_string_view {};
Core::ArgsParser args_parser;
@ -129,6 +130,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
args_parser.add_option(disable_scrollbar_painting, "Don't paint horizontal or vertical viewport scrollbars", "disable-scrollbar-painting");
args_parser.add_option(echo_server_port_string_view, "Echo server port used in test internals", "echo-server-port", 0, "echo_server_port");
args_parser.add_option(is_headless, "Report that the browser is running in headless mode", "headless");
args_parser.add_option(devtools, "Report that the browser is running with Firefox DevTools support", "devtools");
args_parser.parse(arguments);
@ -155,6 +157,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
WebContent::PageClient::set_use_skia_painter(force_cpu_painting ? WebContent::PageClient::UseSkiaPainter::CPUBackend : WebContent::PageClient::UseSkiaPainter::GPUBackendIfAvailable);
WebContent::PageClient::set_is_headless(is_headless);
WebContent::PageClient::set_devtools_enabled(devtools);
if (enable_http_cache) {
Web::Fetch::Fetching::g_http_cache_enabled = true;