diff --git a/Applications/Browser/BrowserConsoleClient.cpp b/Applications/Browser/BrowserConsoleClient.cpp new file mode 100644 index 0000000000..a444814df2 --- /dev/null +++ b/Applications/Browser/BrowserConsoleClient.cpp @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2020, Hunter Salyer + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "BrowserConsoleClient.h" +#include "ConsoleWidget.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Browser { + +JS::Value BrowserConsoleClient::log() +{ + m_console_widget.print_html(interpreter().join_arguments()); + return JS::js_undefined(); +} + +JS::Value BrowserConsoleClient::info() +{ + StringBuilder html; + html.append(""); + html.append("(i) "); + html.append(interpreter().join_arguments()); + html.append(""); + m_console_widget.print_html(html.string_view()); + return JS::js_undefined(); +} + +JS::Value BrowserConsoleClient::debug() +{ + StringBuilder html; + html.append(""); + html.append("(d) "); + html.append(interpreter().join_arguments()); + html.append(""); + m_console_widget.print_html(html.string_view()); + return JS::js_undefined(); +} + +JS::Value BrowserConsoleClient::warn() +{ + StringBuilder html; + html.append(""); + html.append("(w) "); + html.append(interpreter().join_arguments()); + html.append(""); + m_console_widget.print_html(html.string_view()); + return JS::js_undefined(); +} + +JS::Value BrowserConsoleClient::error() +{ + StringBuilder html; + html.append(""); + html.append("(e) "); + html.append(interpreter().join_arguments()); + html.append(""); + m_console_widget.print_html(html.string_view()); + return JS::js_undefined(); +} + +JS::Value BrowserConsoleClient::clear() +{ + m_console_widget.clear_output(); + return JS::js_undefined(); +} + +JS::Value BrowserConsoleClient::trace() +{ + StringBuilder html; + html.append(interpreter().join_arguments()); + auto trace = interpreter().get_trace(); + for (auto& function_name : trace) { + if (function_name.is_empty()) + function_name = "<anonymous>"; + html.appendf(" -> %s
", function_name.characters()); + } + m_console_widget.print_html(html.string_view()); + return JS::js_undefined(); +} + +JS::Value BrowserConsoleClient::count() +{ + auto label = interpreter().argument_count() ? interpreter().argument(0).to_string_without_side_effects() : "default"; + auto counter_value = m_console.counter_increment(label); + m_console_widget.print_html(String::format("%s: %u", label.characters(), counter_value)); + return JS::js_undefined(); +} + +JS::Value BrowserConsoleClient::count_reset() +{ + auto label = interpreter().argument_count() ? interpreter().argument(0).to_string_without_side_effects() : "default"; + if (m_console.counter_reset(label)) { + m_console_widget.print_html(String::format("%s: 0", label.characters())); + } else { + m_console_widget.print_html(String::format("\"%s\" doesn't have a count", label.characters())); + } + return JS::js_undefined(); +} + +} diff --git a/Applications/Browser/BrowserConsoleClient.h b/Applications/Browser/BrowserConsoleClient.h new file mode 100644 index 0000000000..faef20a512 --- /dev/null +++ b/Applications/Browser/BrowserConsoleClient.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020, Hunter Salyer + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#pragma once + +#include +#include +#include +#include + +namespace Browser { + +class ConsoleWidget; + +class BrowserConsoleClient final : public JS::ConsoleClient { +public: + BrowserConsoleClient(JS::Console& console, ConsoleWidget& console_widget) + : ConsoleClient(console) + , m_console_widget(console_widget) + { + } + +private: + virtual JS::Value log() override; + virtual JS::Value info() override; + virtual JS::Value debug() override; + virtual JS::Value warn() override; + virtual JS::Value error() override; + virtual JS::Value clear() override; + virtual JS::Value trace() override; + virtual JS::Value count() override; + virtual JS::Value count_reset() override; + + ConsoleWidget& m_console_widget; +}; + +} diff --git a/Applications/Browser/CMakeLists.txt b/Applications/Browser/CMakeLists.txt index a75572ccde..d1132f8a6d 100644 --- a/Applications/Browser/CMakeLists.txt +++ b/Applications/Browser/CMakeLists.txt @@ -1,5 +1,7 @@ set(SOURCES BookmarksBarWidget.cpp + BrowserConsoleClient.cpp + ConsoleWidget.cpp DownloadWidget.cpp InspectorWidget.cpp main.cpp diff --git a/Applications/Browser/ConsoleWidget.cpp b/Applications/Browser/ConsoleWidget.cpp new file mode 100644 index 0000000000..801904da8d --- /dev/null +++ b/Applications/Browser/ConsoleWidget.cpp @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2020, Hunter Salyer + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "ConsoleWidget.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Browser { + +ConsoleWidget::ConsoleWidget() +{ + set_layout(); + set_fill_with_background_color(true); + + auto base_document = adopt(*new Web::Document); + base_document->append_child(adopt(*new Web::DocumentType(base_document))); + auto html_element = create_element(base_document, "html"); + base_document->append_child(html_element); + auto head_element = create_element(base_document, "head"); + html_element->append_child(head_element); + auto style_element = create_element(base_document, "style"); + style_element->append_child(adopt(*new Web::Text(base_document, "div { font-family: Csilla; font-weight: lighter; }"))); + head_element->append_child(style_element); + auto body_element = create_element(base_document, "body"); + html_element->append_child(body_element); + m_console_output_container = body_element; + + m_console_output_view = add(); + m_console_output_view->set_document(base_document); + + m_console_input = add(); + m_console_input->set_syntax_highlighter(make()); + m_console_input->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + m_console_input->set_preferred_size(0, 22); + // FIXME: Syntax Highlighting breaks the cursor's position on non fixed-width fonts. + m_console_input->set_font(Gfx::Font::default_fixed_width_font()); + + m_console_input->on_return_pressed = [this] { + auto js_source = m_console_input->text(); + m_console_input->clear(); + print_source_line(js_source); + + auto parser = JS::Parser(JS::Lexer(js_source)); + auto program = parser.parse_program(); + + if (parser.has_errors()) { + auto error = parser.errors()[0]; + m_interpreter->throw_exception(error.to_string()); + } else { + m_interpreter->run(*program); + } + + if (m_interpreter->exception()) { + StringBuilder output_html; + output_html.append("Uncaught exception: "); + print_value(m_interpreter->exception()->value(), output_html); + print_html(output_html.string_view()); + + m_interpreter->clear_exception(); + return; + } + + StringBuilder output_html; + print_value(m_interpreter->last_value(), output_html); + print_html(output_html.string_view()); + }; +} + +ConsoleWidget::~ConsoleWidget() +{ +} + +void ConsoleWidget::set_interpreter(WeakPtr interpreter) +{ + if (m_interpreter.ptr() == interpreter.ptr()) + return; + + m_interpreter = interpreter; + m_console_client = adopt_own(*new BrowserConsoleClient(interpreter->console(), *this)); + interpreter->console().set_client(*m_console_client.ptr()); + + clear_output(); +} + +void ConsoleWidget::print_source_line(const StringView& source) +{ + StringBuilder html; + html.append("> "); + // FIXME: Support output highlighting + html.append(source); + + print_html(html.string_view()); +} + +void ConsoleWidget::print_value(JS::Value value, StringBuilder& output_html, HashTable seen_objects) +{ + // FIXME: Support output highlighting + + if (value.is_empty()) { + output_html.append("<empty>"); + return; + } + + if (value.is_object()) { + if (seen_objects.contains(&value.as_object())) { + // FIXME: Maybe we should only do this for circular references, + // not for all reoccurring objects. + output_html.appendf("<already printed Object %p>", &value.as_object()); + return; + } + seen_objects.set(&value.as_object()); + } + + if (value.is_array()) + return print_array(static_cast(value.as_object()), output_html, seen_objects); + + if (value.is_object()) { + auto& object = value.as_object(); + if (object.is_function()) + return print_function(object, output_html, seen_objects); + if (object.is_date()) + return print_date(object, output_html, seen_objects); + if (object.is_error()) + return print_error(object, output_html, seen_objects); + return print_object(object, output_html, seen_objects); + } + + if (value.is_string()) + output_html.append('"'); + output_html.append(value.to_string_without_side_effects()); + if (value.is_string()) + output_html.append('"'); +} + +void ConsoleWidget::print_array(const JS::Array& array, StringBuilder& html_output, HashTable& seen_objects) +{ + html_output.append("[ "); + for (size_t i = 0; i < array.elements().size(); ++i) { + print_value(array.elements()[i], html_output, seen_objects); + if (i != array.elements().size() - 1) + html_output.append(", "); + } + html_output.append(" ]"); +} + +void ConsoleWidget::print_object(const JS::Object& object, StringBuilder& html_output, HashTable& seen_objects) +{ + html_output.append("{ "); + + for (size_t i = 0; i < object.elements().size(); ++i) { + if (object.elements()[i].is_empty()) + continue; + html_output.appendf("\"m%zu\": ", i); + print_value(object.elements()[i], html_output, seen_objects); + if (i != object.elements().size() - 1) + html_output.append(", "); + } + + if (!object.elements().is_empty() && object.shape().property_count()) + html_output.append(", "); + + size_t index = 0; + for (auto& it : object.shape().property_table_ordered()) { + html_output.appendf("\"%s\": ", it.key.characters()); + print_value(object.get_direct(it.value.offset), html_output, seen_objects); + if (index != object.shape().property_count() - 1) + html_output.append(", "); + ++index; + } + + html_output.append(" }"); +} + +void ConsoleWidget::print_function(const JS::Object& function, StringBuilder& html_output, HashTable&) +{ + html_output.appendf("[%s]", function.class_name()); +} + +void ConsoleWidget::print_date(const JS::Object& date, StringBuilder& html_output, HashTable&) +{ + html_output.appendf("Date %s", static_cast(date).string().characters()); +} + +void ConsoleWidget::print_error(const JS::Object& object, StringBuilder& html_output, HashTable&) +{ + auto& error = static_cast(object); + html_output.appendf("[%s]", error.name().characters()); + if (!error.message().is_empty()) + html_output.appendf(": %s", error.message().characters()); +} + +void ConsoleWidget::print_html(const StringView& line) +{ + auto paragraph = create_element(m_console_output_container->document(), "p"); + paragraph->set_inner_html(line); + + m_console_output_container->append_child(paragraph); + m_console_output_container->set_needs_style_update(true); + m_console_output_container->document().schedule_style_update(); + m_console_output_container->document().invalidate_layout(); +} + +void ConsoleWidget::clear_output() +{ + const_cast(m_console_output_view->document()->body())->remove_all_children(); +} + +} diff --git a/Applications/Browser/ConsoleWidget.h b/Applications/Browser/ConsoleWidget.h new file mode 100644 index 0000000000..c6b2fb5811 --- /dev/null +++ b/Applications/Browser/ConsoleWidget.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020, Hunter Salyer + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#pragma once + +#include "BrowserConsoleClient.h" +#include +#include +#include + +namespace Browser { + +class ConsoleWidget final : public GUI::Widget { + C_OBJECT(ConsoleWidget) +public: + virtual ~ConsoleWidget(); + + void set_interpreter(WeakPtr); + void print_source_line(const StringView&); + void print_html(const StringView&); + void clear_output(); + +private: + ConsoleWidget(); + + void print_value(JS::Value, StringBuilder& output_html, HashTable seen_objects = {}); + void print_array(const JS::Array&, StringBuilder& output_html, HashTable&); + void print_object(const JS::Object&, StringBuilder& output_html, HashTable&); + void print_function(const JS::Object&, StringBuilder& output_html, HashTable&); + void print_date(const JS::Object&, StringBuilder& output_html, HashTable&); + void print_error(const JS::Object&, StringBuilder& output_html, HashTable&); + + RefPtr m_console_input; + RefPtr m_console_output_view; + RefPtr m_console_output_container; + WeakPtr m_interpreter; + OwnPtr m_console_client; +}; + +} diff --git a/Applications/Browser/Tab.cpp b/Applications/Browser/Tab.cpp index c6a19f8c83..eaee09a8eb 100644 --- a/Applications/Browser/Tab.cpp +++ b/Applications/Browser/Tab.cpp @@ -26,8 +26,8 @@ #include "Tab.h" #include "BookmarksBarWidget.h" +#include "ConsoleWidget.h" #include "DownloadWidget.h" -#include "History.h" #include "InspectorWidget.h" #include "WindowActions.h" #include @@ -44,7 +44,7 @@ #include #include #include -#include +#include #include #include #include @@ -55,7 +55,6 @@ #include #include #include -#include #include namespace Browser { @@ -285,6 +284,21 @@ Tab::Tab() }, this)); + inspect_menu.add_action(GUI::Action::create( + "Open JS Console", { Mod_Ctrl, Key_I }, [this](auto&) { + if (!m_console_window) { + m_console_window = GUI::Window::construct(); + m_console_window->set_rect(100, 100, 500, 300); + m_console_window->set_title("JS Console"); + m_console_window->set_main_widget(); + } + auto* console_widget = static_cast(m_console_window->main_widget()); + console_widget->set_interpreter(m_html_widget->document()->interpreter().make_weak_ptr()); + m_console_window->show(); + m_console_window->move_to_front(); + }, + this)); + auto& debug_menu = m_menubar->add_menu("Debug"); debug_menu.add_action(GUI::Action::create( "Dump DOM tree", [this](auto&) { diff --git a/Applications/Browser/Tab.h b/Applications/Browser/Tab.h index 78abe4ddc8..2846c4ed0c 100644 --- a/Applications/Browser/Tab.h +++ b/Applications/Browser/Tab.h @@ -29,8 +29,8 @@ #include "History.h" #include #include -#include #include +#include namespace Browser { @@ -67,6 +67,7 @@ private: RefPtr m_location_box; RefPtr m_bookmark_button; RefPtr m_dom_inspector_window; + RefPtr m_console_window; RefPtr m_statusbar; RefPtr m_menubar; RefPtr m_toolbar_container; diff --git a/Libraries/LibJS/Interpreter.h b/Libraries/LibJS/Interpreter.h index 5757e92fc7..0fe178eeb6 100644 --- a/Libraries/LibJS/Interpreter.h +++ b/Libraries/LibJS/Interpreter.h @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -69,7 +70,7 @@ struct Argument { typedef Vector ArgumentVector; -class Interpreter { +class Interpreter : public Weakable { public: template static NonnullOwnPtr create(Args&&... args)