diff options
author | Sam Atkins <atkinssj@serenityos.org> | 2021-12-22 12:32:15 +0000 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2021-12-27 21:44:07 +0100 |
commit | d702678d16753f9bb81c6a46cffe0cb43133c267 (patch) | |
tree | e4c2ae790070460c059fefe6a19574b4fffe7bb2 | |
parent | ff5e07d718148417e89aea20db478550742edc29 (diff) | |
download | serenity-d702678d16753f9bb81c6a46cffe0cb43133c267.zip |
LibJS+WebContent+Browser+js: Implement console.group() methods
This implements:
- console.group()
- console.groupCollapsed()
- console.groupEnd()
In the Browser, we use `<details>` for the groups, which is not actually
implemented yet, so groups are always open.
In the REPL, groups are non-interactive, but still indent any output.
This looks weird since the console prompt and return values remain on
the far left, but this matches what Node does so it's probably fine. :^)
I expect `console.group()` is not used much outside of browsers.
-rw-r--r-- | Userland/Applications/Browser/ConsoleWidget.cpp | 67 | ||||
-rw-r--r-- | Userland/Applications/Browser/ConsoleWidget.h | 9 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Console.cpp | 103 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Console.h | 16 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h | 3 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/ConsoleObject.cpp | 21 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/ConsoleObject.h | 3 | ||||
-rw-r--r-- | Userland/Services/WebContent/WebContentConsoleClient.cpp | 35 | ||||
-rw-r--r-- | Userland/Services/WebContent/WebContentConsoleClient.h | 11 | ||||
-rw-r--r-- | Userland/Utilities/js.cpp | 38 |
10 files changed, 281 insertions, 25 deletions
diff --git a/Userland/Applications/Browser/ConsoleWidget.cpp b/Userland/Applications/Browser/ConsoleWidget.cpp index 347c216b0e..c52d4f8646 100644 --- a/Userland/Applications/Browser/ConsoleWidget.cpp +++ b/Userland/Applications/Browser/ConsoleWidget.cpp @@ -111,6 +111,12 @@ void ConsoleWidget::handle_console_messages(i32 start_index, const Vector<String 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(); } @@ -138,26 +144,85 @@ void ConsoleWidget::print_source_line(StringView source) 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; +)~~~"); + } else { + builder.appendff(R"~~~( + var parentGroup = document.getElementById("group_{}"); +)~~~", + parent_id); + } + builder.append(R"~~~( var p = document.createElement("p"); p.innerHTML = ")~~~"); builder.append_escaped_for_json(line); builder.append(R"~~~(" - document.body.appendChild(p); + parentGroup.appendChild(p); )~~~"); 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 = ""; )~~~"); } +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; +)~~~"); + } 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.innerText = ")~~~", + group.id); + builder.append_escaped_for_json(label); + builder.append(R"~~~("; + group.appendChild(label); + parentGroup.appendChild(group); +)~~~"); + + if (start_expanded) + builder.append("group.open = true;"); + + 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(); +} + void ConsoleWidget::reset() { clear_output(); diff --git a/Userland/Applications/Browser/ConsoleWidget.h b/Userland/Applications/Browser/ConsoleWidget.h index 6f2bedffc4..82eac6f07b 100644 --- a/Userland/Applications/Browser/ConsoleWidget.h +++ b/Userland/Applications/Browser/ConsoleWidget.h @@ -33,6 +33,8 @@ private: void request_console_messages(); void clear_output(); + void begin_group(StringView label, bool start_expanded); + void end_group(); RefPtr<GUI::TextBox> m_input; RefPtr<Web::OutOfProcessWebView> m_output_view; @@ -40,6 +42,13 @@ private: 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 }; + String label; + }; + Vector<Group> m_group_stack; + int m_next_group_id { 1 }; }; } diff --git a/Userland/Libraries/LibJS/Console.cpp b/Userland/Libraries/LibJS/Console.cpp index 8a9b6b57d6..5bf75bce2e 100644 --- a/Userland/Libraries/LibJS/Console.cpp +++ b/Userland/Libraries/LibJS/Console.cpp @@ -79,7 +79,8 @@ ThrowCompletionOr<Value> Console::warn() // 1.1.2. clear(), https://console.spec.whatwg.org/#clear Value Console::clear() { - // 1. TODO: Empty the appropriate group stack. + // 1. Empty the appropriate group stack. + m_group_stack.clear(); // 2. If possible for the environment, clear the console. (Otherwise, do nothing.) if (m_client) @@ -107,12 +108,7 @@ ThrowCompletionOr<Value> Console::trace() StringBuilder builder; auto data = vm_arguments(); auto formatted_data = TRY(m_client->formatter(data)); - for (auto const& item : formatted_data) { - if (!builder.is_empty()) - builder.append(' '); - builder.append(TRY(item.to_string(global_object()))); - } - trace.label = builder.to_string(); + trace.label = TRY(value_vector_to_string(formatted_data)); } // 3. Perform Printer("trace", « trace »). @@ -221,6 +217,88 @@ ThrowCompletionOr<Value> Console::assert_() return js_undefined(); } +// 1.3.1. group(...data), https://console.spec.whatwg.org/#group +ThrowCompletionOr<Value> Console::group() +{ + // 1. Let group be a new group. + Group group; + + // 2. If data is not empty, let groupLabel be the result of Formatter(data). + String group_label; + auto data = vm_arguments(); + if (!data.is_empty()) { + auto formatted_data = TRY(m_client->formatter(data)); + group_label = TRY(value_vector_to_string(formatted_data)); + } + // ... Otherwise, let groupLabel be an implementation-chosen label representing a group. + else { + group_label = "Group"; + } + + // 3. Incorporate groupLabel as a label for group. + group.label = group_label; + + // 4. Optionally, if the environment supports interactive groups, group should be expanded by default. + // NOTE: This is handled in Printer. + + // 5. Perform Printer("group", « group »). + if (m_client) + TRY(m_client->printer(LogLevel::Group, group)); + + // 6. Push group onto the appropriate group stack. + m_group_stack.append(group); + + return js_undefined(); +} + +// 1.3.2. groupCollapsed(...data), https://console.spec.whatwg.org/#groupcollapsed +ThrowCompletionOr<Value> Console::group_collapsed() +{ + // 1. Let group be a new group. + Group group; + + // 2. If data is not empty, let groupLabel be the result of Formatter(data). + String group_label; + auto data = vm_arguments(); + if (!data.is_empty()) { + auto formatted_data = TRY(m_client->formatter(data)); + group_label = TRY(value_vector_to_string(formatted_data)); + } + // ... Otherwise, let groupLabel be an implementation-chosen label representing a group. + else { + group_label = "Group"; + } + + // 3. Incorporate groupLabel as a label for group. + group.label = group_label; + + // 4. Optionally, if the environment supports interactive groups, group should be collapsed by default. + // NOTE: This is handled in Printer. + + // 5. Perform Printer("groupCollapsed", « group »). + if (m_client) + TRY(m_client->printer(LogLevel::GroupCollapsed, group)); + + // 6. Push group onto the appropriate group stack. + m_group_stack.append(group); + + return js_undefined(); +} + +// 1.3.3. groupEnd(), https://console.spec.whatwg.org/#groupend +ThrowCompletionOr<Value> Console::group_end() +{ + if (m_group_stack.is_empty()) + return js_undefined(); + + // 1. Pop the last group from the group stack. + m_group_stack.take_last(); + if (m_client) + m_client->end_group(); + + return js_undefined(); +} + Vector<Value> Console::vm_arguments() { Vector<Value> arguments; @@ -257,6 +335,17 @@ void Console::output_debug_message([[maybe_unused]] LogLevel log_level, [[maybe_ #endif } +ThrowCompletionOr<String> Console::value_vector_to_string(Vector<Value>& values) +{ + StringBuilder builder; + for (auto const& item : values) { + if (!builder.is_empty()) + builder.append(' '); + builder.append(TRY(item.to_string(global_object()))); + } + return builder.to_string(); +} + VM& ConsoleClient::vm() { return global_object().vm(); diff --git a/Userland/Libraries/LibJS/Console.h b/Userland/Libraries/LibJS/Console.h index e7f626c6a5..15e70d76ca 100644 --- a/Userland/Libraries/LibJS/Console.h +++ b/Userland/Libraries/LibJS/Console.h @@ -43,6 +43,10 @@ public: Warn, }; + struct Group { + String label; + }; + struct Trace { String label; Vector<String> stack; @@ -71,14 +75,21 @@ public: ThrowCompletionOr<Value> count(); ThrowCompletionOr<Value> count_reset(); ThrowCompletionOr<Value> assert_(); + ThrowCompletionOr<Value> group(); + ThrowCompletionOr<Value> group_collapsed(); + ThrowCompletionOr<Value> group_end(); void output_debug_message(LogLevel log_level, String output) const; private: + ThrowCompletionOr<String> value_vector_to_string(Vector<Value>&); + GlobalObject& m_global_object; ConsoleClient* m_client { nullptr }; HashMap<String, unsigned> m_counters; + + Vector<Group> m_group_stack; }; class ConsoleClient { @@ -88,11 +99,14 @@ public: { } + using PrinterArguments = Variant<Console::Group, Console::Trace, Vector<Value>>; + ThrowCompletionOr<Value> logger(Console::LogLevel log_level, Vector<Value>& args); ThrowCompletionOr<Vector<Value>> formatter(Vector<Value>& args); - virtual ThrowCompletionOr<Value> printer(Console::LogLevel log_level, Variant<Vector<Value>, Console::Trace>) = 0; + virtual ThrowCompletionOr<Value> printer(Console::LogLevel log_level, PrinterArguments) = 0; virtual void clear() = 0; + virtual void end_group() = 0; protected: virtual ~ConsoleClient() = default; diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 0de67f96ed..00b3e37752 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -240,6 +240,9 @@ namespace JS { P(getYear) \ P(global) \ P(globalThis) \ + P(group) \ + P(groupCollapsed) \ + P(groupEnd) \ P(groups) \ P(has) \ P(hasIndices) \ diff --git a/Userland/Libraries/LibJS/Runtime/ConsoleObject.cpp b/Userland/Libraries/LibJS/Runtime/ConsoleObject.cpp index 06778dd99f..1010cc42bf 100644 --- a/Userland/Libraries/LibJS/Runtime/ConsoleObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/ConsoleObject.cpp @@ -32,6 +32,9 @@ void ConsoleObject::initialize(GlobalObject& global_object) define_native_function(vm.names.countReset, count_reset, 0, attr); define_native_function(vm.names.clear, clear, 0, attr); define_native_function(vm.names.assert, assert_, 0, attr); + define_native_function(vm.names.group, group, 0, attr); + define_native_function(vm.names.groupCollapsed, group_collapsed, 0, attr); + define_native_function(vm.names.groupEnd, group_end, 0, attr); } ConsoleObject::~ConsoleObject() @@ -98,4 +101,22 @@ JS_DEFINE_NATIVE_FUNCTION(ConsoleObject::assert_) return global_object.console().assert_(); } +// 1.3.1. group(...data), https://console.spec.whatwg.org/#group +JS_DEFINE_NATIVE_FUNCTION(ConsoleObject::group) +{ + return global_object.console().group(); +} + +// 1.3.2. groupCollapsed(...data), https://console.spec.whatwg.org/#groupcollapsed +JS_DEFINE_NATIVE_FUNCTION(ConsoleObject::group_collapsed) +{ + return global_object.console().group_collapsed(); +} + +// 1.3.3. groupEnd(), https://console.spec.whatwg.org/#groupend +JS_DEFINE_NATIVE_FUNCTION(ConsoleObject::group_end) +{ + return global_object.console().group_end(); +} + } diff --git a/Userland/Libraries/LibJS/Runtime/ConsoleObject.h b/Userland/Libraries/LibJS/Runtime/ConsoleObject.h index 5f43c4eed9..2578641a02 100644 --- a/Userland/Libraries/LibJS/Runtime/ConsoleObject.h +++ b/Userland/Libraries/LibJS/Runtime/ConsoleObject.h @@ -29,6 +29,9 @@ private: JS_DECLARE_NATIVE_FUNCTION(count_reset); JS_DECLARE_NATIVE_FUNCTION(clear); JS_DECLARE_NATIVE_FUNCTION(assert_); + JS_DECLARE_NATIVE_FUNCTION(group); + JS_DECLARE_NATIVE_FUNCTION(group_collapsed); + JS_DECLARE_NATIVE_FUNCTION(group_end); }; } diff --git a/Userland/Services/WebContent/WebContentConsoleClient.cpp b/Userland/Services/WebContent/WebContentConsoleClient.cpp index 8db5c64835..d99316dbdf 100644 --- a/Userland/Services/WebContent/WebContentConsoleClient.cpp +++ b/Userland/Services/WebContent/WebContentConsoleClient.cpp @@ -69,13 +69,25 @@ void WebContentConsoleClient::handle_input(String const& js_source) void WebContentConsoleClient::print_html(String const& line) { - m_message_log.append({ .type = ConsoleOutput::Type::HTML, .html = line }); + m_message_log.append({ .type = ConsoleOutput::Type::HTML, .data = line }); m_client.async_did_output_js_console_message(m_message_log.size() - 1); } void WebContentConsoleClient::clear_output() { - m_message_log.append({ .type = ConsoleOutput::Type::Clear, .html = "" }); + m_message_log.append({ .type = ConsoleOutput::Type::Clear, .data = "" }); + m_client.async_did_output_js_console_message(m_message_log.size() - 1); +} + +void WebContentConsoleClient::begin_group(String const& label, bool start_expanded) +{ + m_message_log.append({ .type = start_expanded ? ConsoleOutput::Type::BeginGroup : ConsoleOutput::Type::BeginGroupCollapsed, .data = label }); + m_client.async_did_output_js_console_message(m_message_log.size() - 1); +} + +void WebContentConsoleClient::end_group() +{ + m_message_log.append({ .type = ConsoleOutput::Type::EndGroup, .data = "" }); m_client.async_did_output_js_console_message(m_message_log.size() - 1); } @@ -107,9 +119,18 @@ void WebContentConsoleClient::send_messages(i32 start_index) case ConsoleOutput::Type::Clear: message_types.append("clear"); break; + case ConsoleOutput::Type::BeginGroup: + message_types.append("group"); + break; + case ConsoleOutput::Type::BeginGroupCollapsed: + message_types.append("groupCollapsed"); + break; + case ConsoleOutput::Type::EndGroup: + message_types.append("groupEnd"); + break; } - messages.append(message.html); + messages.append(message.data); } m_client.async_did_get_js_console_messages(start_index, message_types, messages); @@ -121,7 +142,7 @@ void WebContentConsoleClient::clear() } // 2.3. Printer(logLevel, args[, options]), https://console.spec.whatwg.org/#printer -JS::ThrowCompletionOr<JS::Value> WebContentConsoleClient::printer(JS::Console::LogLevel log_level, Variant<Vector<JS::Value>, JS::Console::Trace> arguments) +JS::ThrowCompletionOr<JS::Value> WebContentConsoleClient::printer(JS::Console::LogLevel log_level, PrinterArguments arguments) { if (log_level == JS::Console::LogLevel::Trace) { auto trace = arguments.get<JS::Console::Trace>(); @@ -138,6 +159,12 @@ JS::ThrowCompletionOr<JS::Value> WebContentConsoleClient::printer(JS::Console::L 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<Vector<JS::Value>>()); m_console.output_debug_message(log_level, output); diff --git a/Userland/Services/WebContent/WebContentConsoleClient.h b/Userland/Services/WebContent/WebContentConsoleClient.h index 9086e678e0..d9e0167202 100644 --- a/Userland/Services/WebContent/WebContentConsoleClient.h +++ b/Userland/Services/WebContent/WebContentConsoleClient.h @@ -25,7 +25,7 @@ public: private: virtual void clear() override; - virtual JS::ThrowCompletionOr<JS::Value> printer(JS::Console::LogLevel log_level, Variant<Vector<JS::Value>, JS::Console::Trace>) override; + virtual JS::ThrowCompletionOr<JS::Value> printer(JS::Console::LogLevel log_level, PrinterArguments) override; ClientConnection& m_client; WeakPtr<JS::Interpreter> m_interpreter; @@ -33,14 +33,19 @@ private: void clear_output(); void print_html(String const& line); + void begin_group(String const& label, bool start_expanded); + virtual void end_group() override; struct ConsoleOutput { enum class Type { HTML, - Clear + Clear, + BeginGroup, + BeginGroupCollapsed, + EndGroup, }; Type type; - String html; + String data; }; Vector<ConsoleOutput> m_message_log; }; diff --git a/Userland/Utilities/js.cpp b/Userland/Utilities/js.cpp index d1ec0fef6a..917aebc30a 100644 --- a/Userland/Utilities/js.cpp +++ b/Userland/Utilities/js.cpp @@ -1125,51 +1125,71 @@ public: virtual void clear() override { js_out("\033[3J\033[H\033[2J"); + m_group_stack_depth = 0; fflush(stdout); } - virtual JS::ThrowCompletionOr<JS::Value> printer(JS::Console::LogLevel log_level, Variant<Vector<JS::Value>, JS::Console::Trace> arguments) override + virtual void end_group() override { + if (m_group_stack_depth > 0) + m_group_stack_depth--; + } + + // 2.3. Printer(logLevel, args[, options]), https://console.spec.whatwg.org/#printer + virtual JS::ThrowCompletionOr<JS::Value> printer(JS::Console::LogLevel log_level, PrinterArguments arguments) override + { + String indent = String::repeated(" ", m_group_stack_depth); + if (log_level == JS::Console::LogLevel::Trace) { auto trace = arguments.get<JS::Console::Trace>(); StringBuilder builder; if (!trace.label.is_empty()) - builder.appendff("\033[36;1m{}\033[0m\n", trace.label); + builder.appendff("{}\033[36;1m{}\033[0m\n", indent, trace.label); for (auto& function_name : trace.stack) - builder.appendff("-> {}\n", function_name); + builder.appendff("{}-> {}\n", indent, function_name); js_outln("{}", builder.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>(); + js_outln("{}\033[36;1m{}\033[0m", indent, group.label); + m_group_stack_depth++; + return JS::js_undefined(); + } + auto output = String::join(" ", arguments.get<Vector<JS::Value>>()); m_console.output_debug_message(log_level, output); switch (log_level) { case JS::Console::LogLevel::Debug: - js_outln("\033[36;1m{}\033[0m", output); + js_outln("{}\033[36;1m{}\033[0m", indent, output); break; case JS::Console::LogLevel::Error: case JS::Console::LogLevel::Assert: - js_outln("\033[31;1m{}\033[0m", output); + js_outln("{}\033[31;1m{}\033[0m", indent, output); break; case JS::Console::LogLevel::Info: - js_outln("(i) {}", output); + js_outln("{}(i) {}", indent, output); break; case JS::Console::LogLevel::Log: - js_outln("{}", output); + js_outln("{}{}", indent, output); break; case JS::Console::LogLevel::Warn: case JS::Console::LogLevel::CountReset: - js_outln("\033[33;1m{}\033[0m", output); + js_outln("{}\033[33;1m{}\033[0m", indent, output); break; default: - js_outln("{}", output); + js_outln("{}{}", indent, output); break; } return JS::js_undefined(); } + +private: + int m_group_stack_depth { 0 }; }; ErrorOr<int> serenity_main(Main::Arguments arguments) |