Ladybird: Add quick & dirty port of the JS console from Browser :^)

This commit is contained in:
Andreas Kling 2022-07-17 14:47:26 +02:00 committed by Andrew Kaster
parent af5250b2cb
commit aa5f886128
Notes: sideshowbarker 2024-07-17 02:47:59 +09:00
6 changed files with 437 additions and 1 deletions

View file

@ -52,6 +52,16 @@ BrowserWindow::BrowserWindow(Core::EventLoop& event_loop)
}
});
auto* js_console_action = new QAction("Show &JS Console");
js_console_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-javascript.png").arg(s_serenity_resource_root.characters())));
js_console_action->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_J));
inspect_menu->addAction(js_console_action);
QObject::connect(js_console_action, &QAction::triggered, this, [this] {
if (m_current_tab) {
m_current_tab->view().show_js_console();
}
});
auto* debug_menu = menuBar()->addMenu("&Debug");
auto* dump_dom_tree_action = new QAction("Dump DOM Tree");

View file

@ -35,6 +35,8 @@ find_package(Qt6 REQUIRED COMPONENTS Core Widgets Network)
set(SOURCES
BrowserWindow.cpp
ConsoleClient.cpp
ConsoleGlobalObject.cpp
CookieJar.cpp
RequestManagerQt.cpp
main.cpp

197
Ladybird/ConsoleClient.cpp Normal file
View file

@ -0,0 +1,197 @@
/*
* Copyright (c) 2021, Brandon Scott <xeon.productions@gmail.com>
* Copyright (c) 2020, Hunter Salyer <thefalsehonesty@gmail.com>
* Copyright (c) 2021, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "ConsoleClient.h"
#include "ConsoleGlobalObject.h"
#include "WebView.h"
#include <LibJS/Interpreter.h>
#include <LibJS/MarkupGenerator.h>
#include <LibWeb/Bindings/WindowObject.h>
#include <LibWeb/HTML/Scripting/ClassicScript.h>
#include <LibWeb/HTML/Scripting/Environments.h>
namespace Ladybird {
ConsoleClient::ConsoleClient(JS::Console& console, WeakPtr<JS::Interpreter> interpreter, WebView& view)
: JS::ConsoleClient(console)
, m_view(view)
, m_interpreter(interpreter)
{
JS::DeferGC defer_gc(m_interpreter->heap());
auto& vm = m_interpreter->vm();
auto& global_object = m_interpreter->global_object();
auto console_global_object = m_interpreter->heap().allocate_without_global_object<ConsoleGlobalObject>(static_cast<Web::Bindings::WindowObject&>(global_object));
// NOTE: We need to push an execution context here for NativeFunction::create() to succeed during global object initialization.
// It gets removed immediately after creating the interpreter in Document::interpreter().
auto& eso = verify_cast<Web::HTML::EnvironmentSettingsObject>(*m_interpreter->realm().host_defined());
vm.push_execution_context(eso.realm_execution_context());
console_global_object->initialize_global_object();
vm.pop_execution_context();
m_console_global_object = JS::make_handle(console_global_object);
}
void ConsoleClient::handle_input(String const& js_source)
{
auto& settings = verify_cast<Web::HTML::EnvironmentSettingsObject>(*m_interpreter->realm().host_defined());
auto script = Web::HTML::ClassicScript::create("(console)", js_source, settings, settings.api_base_url());
// FIXME: Add parse error printouts back once ClassicScript can report parse errors.
auto result = script->run();
StringBuilder output_html;
if (result.is_abrupt()) {
output_html.append("Uncaught exception: "sv);
auto error = *result.release_error().value();
if (error.is_object())
output_html.append(JS::MarkupGenerator::html_from_error(error.as_object()));
else
output_html.append(JS::MarkupGenerator::html_from_value(error));
print_html(output_html.string_view());
return;
}
if (result.value().has_value())
print_html(JS::MarkupGenerator::html_from_value(*result.value()));
}
void ConsoleClient::print_html(String const& line)
{
m_message_log.append({ .type = ConsoleOutput::Type::HTML, .data = line });
m_view.did_output_js_console_message(m_message_log.size() - 1);
}
void ConsoleClient::clear_output()
{
m_message_log.append({ .type = ConsoleOutput::Type::Clear, .data = "" });
m_view.did_output_js_console_message(m_message_log.size() - 1);
}
void ConsoleClient::begin_group(String const& label, bool start_expanded)
{
m_message_log.append({ .type = start_expanded ? ConsoleOutput::Type::BeginGroup : ConsoleOutput::Type::BeginGroupCollapsed, .data = label });
m_view.did_output_js_console_message(m_message_log.size() - 1);
}
void ConsoleClient::end_group()
{
m_message_log.append({ .type = ConsoleOutput::Type::EndGroup, .data = "" });
m_view.did_output_js_console_message(m_message_log.size() - 1);
}
void ConsoleClient::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.
dbgln("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"sv);
break;
case ConsoleOutput::Type::Clear:
message_types.append("clear"sv);
break;
case ConsoleOutput::Type::BeginGroup:
message_types.append("group"sv);
break;
case ConsoleOutput::Type::BeginGroupCollapsed:
message_types.append("groupCollapsed"sv);
break;
case ConsoleOutput::Type::EndGroup:
message_types.append("groupEnd"sv);
break;
}
messages.append(message.data);
}
m_view.did_get_js_console_messages(start_index, message_types, messages);
}
void ConsoleClient::clear()
{
clear_output();
}
// 2.3. Printer(logLevel, args[, options]), https://console.spec.whatwg.org/#printer
JS::ThrowCompletionOr<JS::Value> ConsoleClient::printer(JS::Console::LogLevel log_level, PrinterArguments arguments)
{
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'>{}</span><br>", 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(html.string_view());
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(group.label, log_level == JS::Console::LogLevel::Group);
return JS::js_undefined();
}
auto output = String::join(' ', arguments.get<JS::MarkedVector<JS::Value>>());
m_console.output_debug_message(log_level, output);
StringBuilder html;
switch (log_level) {
case JS::Console::LogLevel::Debug:
html.append("<span class=\"debug\">(d) "sv);
break;
case JS::Console::LogLevel::Error:
html.append("<span class=\"error\">(e) "sv);
break;
case JS::Console::LogLevel::Info:
html.append("<span class=\"info\">(i) "sv);
break;
case JS::Console::LogLevel::Log:
html.append("<span class=\"log\"> "sv);
break;
case JS::Console::LogLevel::Warn:
case JS::Console::LogLevel::CountReset:
html.append("<span class=\"warn\">(w) "sv);
break;
default:
html.append("<span>"sv);
break;
}
html.append(escape_html_entities(output));
html.append("</span>"sv);
print_html(html.string_view());
return JS::js_undefined();
}
}

View file

@ -0,0 +1,114 @@
/*
* Copyright (c) 2021, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2022, Andreas Kling <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "ConsoleGlobalObject.h"
#include <LibJS/Runtime/Completion.h>
#include <LibWeb/Bindings/NodeWrapper.h>
#include <LibWeb/Bindings/NodeWrapperFactory.h>
#include <LibWeb/Bindings/WindowObject.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/Window.h>
namespace Ladybird {
ConsoleGlobalObject::ConsoleGlobalObject(Web::Bindings::WindowObject& parent_object)
: m_window_object(&parent_object)
{
}
void ConsoleGlobalObject::initialize_global_object()
{
Base::initialize_global_object();
// $0 magic variable
define_native_accessor("$0", inspected_node_getter, nullptr, 0);
}
void ConsoleGlobalObject::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_window_object);
}
JS::ThrowCompletionOr<JS::Object*> ConsoleGlobalObject::internal_get_prototype_of() const
{
return m_window_object->internal_get_prototype_of();
}
JS::ThrowCompletionOr<bool> ConsoleGlobalObject::internal_set_prototype_of(JS::Object* prototype)
{
return m_window_object->internal_set_prototype_of(prototype);
}
JS::ThrowCompletionOr<bool> ConsoleGlobalObject::internal_is_extensible() const
{
return m_window_object->internal_is_extensible();
}
JS::ThrowCompletionOr<bool> ConsoleGlobalObject::internal_prevent_extensions()
{
return m_window_object->internal_prevent_extensions();
}
JS::ThrowCompletionOr<Optional<JS::PropertyDescriptor>> ConsoleGlobalObject::internal_get_own_property(JS::PropertyKey const& property_name) const
{
if (auto result = TRY(m_window_object->internal_get_own_property(property_name)); result.has_value())
return result;
return Base::internal_get_own_property(property_name);
}
JS::ThrowCompletionOr<bool> ConsoleGlobalObject::internal_define_own_property(JS::PropertyKey const& property_name, JS::PropertyDescriptor const& descriptor)
{
return m_window_object->internal_define_own_property(property_name, descriptor);
}
JS::ThrowCompletionOr<bool> ConsoleGlobalObject::internal_has_property(JS::PropertyKey const& property_name) const
{
return TRY(Object::internal_has_property(property_name)) || TRY(m_window_object->internal_has_property(property_name));
}
JS::ThrowCompletionOr<JS::Value> ConsoleGlobalObject::internal_get(JS::PropertyKey const& property_name, JS::Value receiver) const
{
if (TRY(m_window_object->has_own_property(property_name)))
return m_window_object->internal_get(property_name, (receiver == this) ? m_window_object : receiver);
return Base::internal_get(property_name, receiver);
}
JS::ThrowCompletionOr<bool> ConsoleGlobalObject::internal_set(JS::PropertyKey const& property_name, JS::Value value, JS::Value receiver)
{
return m_window_object->internal_set(property_name, value, (receiver == this) ? m_window_object : receiver);
}
JS::ThrowCompletionOr<bool> ConsoleGlobalObject::internal_delete(JS::PropertyKey const& property_name)
{
return m_window_object->internal_delete(property_name);
}
JS::ThrowCompletionOr<JS::MarkedVector<JS::Value>> ConsoleGlobalObject::internal_own_property_keys() const
{
return m_window_object->internal_own_property_keys();
}
JS_DEFINE_NATIVE_FUNCTION(ConsoleGlobalObject::inspected_node_getter)
{
auto* this_object = TRY(vm.this_value(global_object).to_object(global_object));
if (!is<ConsoleGlobalObject>(this_object))
return vm.throw_completion<JS::TypeError>(global_object, JS::ErrorType::NotAnObjectOfType, "ConsoleGlobalObject");
auto console_global_object = static_cast<ConsoleGlobalObject*>(this_object);
auto& window = console_global_object->m_window_object->impl();
auto* inspected_node = window.associated_document().inspected_node();
if (!inspected_node)
return JS::js_undefined();
return Web::Bindings::wrap(global_object, *inspected_node);
}
}

View file

@ -8,6 +8,7 @@
#define AK_DONT_REPLACE_STD
#include "WebView.h"
#include "ConsoleClient.h"
#include "CookieJar.h"
#include "RequestManagerQt.h"
#include <AK/Assertions.h>
@ -30,12 +31,14 @@
#include <LibGfx/ImageDecoder.h>
#include <LibGfx/PNGWriter.h>
#include <LibGfx/Rect.h>
#include <LibJS/Interpreter.h>
#include <LibMain/Main.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/Cookie/ParsedCookie.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Dump.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/Scripting/ClassicScript.h>
#include <LibWeb/HTML/Storage.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/ImageDecoding.h>
@ -50,12 +53,26 @@
#include <LibWebSocket/WebSocket.h>
#include <QCursor>
#include <QIcon>
#include <QLineEdit>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPaintEvent>
#include <QPainter>
#include <QScrollBar>
#include <QTextEdit>
#include <QVBoxLayout>
#include <stdlib.h>
AK::String akstring_from_qstring(QString const& qstring)
{
return AK::String(qstring.toUtf8().data());
}
QString qstring_from_akstring(AK::String const& akstring)
{
return QString::fromUtf8(akstring.characters(), akstring.length());
}
String s_serenity_resource_root = [] {
auto const* source_dir = getenv("SERENITY_SOURCE_DIR");
if (source_dir) {
@ -162,6 +179,20 @@ public:
virtual void page_did_finish_loading(AK::URL const&) override
{
initialize_js_console();
m_console_client->send_messages(0);
}
void initialize_js_console()
{
auto* document = page().top_level_browsing_context().active_document();
auto interpreter = document->interpreter().make_weak_ptr();
if (m_interpreter.ptr() == interpreter.ptr())
return;
m_interpreter = interpreter;
m_console_client = make<Ladybird::ConsoleClient>(interpreter->global_object().console(), interpreter, m_view);
interpreter->global_object().console().set_client(*m_console_client.ptr());
}
virtual void page_did_change_selection() override
@ -290,7 +321,6 @@ public:
void set_should_show_line_box_borders(bool state) { m_should_show_line_box_borders = state; }
private:
HeadlessBrowserPageClient(WebView& view)
: m_view(view)
, m_page(make<Web::Page>(*this))
@ -301,6 +331,8 @@ private:
NonnullOwnPtr<Web::Page> m_page;
Browser::CookieJar m_cookie_jar;
OwnPtr<Ladybird::ConsoleClient> m_console_client;
WeakPtr<JS::Interpreter> m_interpreter;
RefPtr<Gfx::PaletteImpl> m_palette_impl;
Gfx::IntRect m_viewport_rect { 0, 0, 800, 600 };
Web::CSS::PreferredColorScheme m_preferred_color_scheme { Web::CSS::PreferredColorScheme::Auto };
@ -675,3 +707,69 @@ String WebView::source() const
return String::empty();
return document->source();
}
void WebView::run_javascript(String const& js_source) const
{
auto* active_document = const_cast<Web::DOM::Document*>(m_page_client->page().top_level_browsing_context().active_document());
if (!active_document)
return;
// This is partially based on "execute a javascript: URL request" https://html.spec.whatwg.org/multipage/browsing-the-web.html#javascript-protocol
// Let settings be browsingContext's active document's relevant settings object.
auto& settings = active_document->relevant_settings_object();
// Let baseURL be settings's API base URL.
auto base_url = settings.api_base_url();
// Let script be the result of creating a classic script given scriptSource, settings, baseURL, and the default classic script fetch options.
// FIXME: This doesn't pass in "default classic script fetch options"
// FIXME: What should the filename be here?
auto script = Web::HTML::ClassicScript::create("(client connection run_javascript)", js_source, settings, base_url);
// Let evaluationStatus be the result of running the classic script script.
auto evaluation_status = script->run();
if (evaluation_status.is_error())
dbgln("Exception :(");
}
void WebView::did_output_js_console_message(i32 message_index)
{
m_page_client->m_console_client->send_messages(message_index);
}
void WebView::did_get_js_console_messages(i32, Vector<String>, Vector<String> messages)
{
for (auto& message : messages) {
m_js_console_output_edit->append(qstring_from_akstring(message).trimmed());
}
}
void WebView::show_js_console()
{
if (!m_js_console_widget) {
m_js_console_widget = new QWidget;
m_js_console_widget->setWindowTitle("JS Console");
auto* layout = new QVBoxLayout;
m_js_console_widget->setLayout(layout);
m_js_console_output_edit = new QTextEdit;
m_js_console_output_edit->setReadOnly(true);
m_js_console_input_edit = new QLineEdit;
layout->addWidget(m_js_console_output_edit);
layout->addWidget(m_js_console_input_edit);
m_js_console_widget->resize(640, 480);
QObject::connect(m_js_console_input_edit, &QLineEdit::returnPressed, [this] {
auto code = m_js_console_input_edit->text().trimmed();
m_js_console_input_edit->clear();
m_js_console_output_edit->append(QString("> %1").arg(code));
m_page_client->m_console_client->handle_input(akstring_from_qstring(code));
});
}
m_js_console_widget->show();
m_js_console_input_edit->setFocus();
}

View file

@ -12,6 +12,10 @@
#include <AK/String.h>
#include <LibGfx/Forward.h>
#include <QAbstractScrollArea>
#include <QPointer>
class QTextEdit;
class QLineEdit;
class HeadlessBrowserPageClient;
@ -34,6 +38,13 @@ public:
String source() const;
void run_javascript(String const& js_source) const;
void did_output_js_console_message(i32 message_index);
void did_get_js_console_messages(i32 start_index, Vector<String> message_types, Vector<String> messages);
void show_js_console();
signals:
void linkHovered(QString, int timeout = 0);
void linkUnhovered();
@ -48,4 +59,8 @@ private:
qreal m_inverse_pixel_scaling_ratio { 1.0 };
bool m_should_show_line_box_borders { false };
QPointer<QWidget> m_js_console_widget;
QTextEdit* m_js_console_output_edit { nullptr };
QLineEdit* m_js_console_input_edit { nullptr };
};