Explorar el Código

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.
Sam Atkins hace 3 años
padre
commit
d702678d16

+ 66 - 1
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();

+ 9 - 0
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 };
 };
 
 }

+ 96 - 7
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();

+ 15 - 1
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;

+ 3 - 0
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)                            \

+ 21 - 0
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();
+}
+
 }

+ 3 - 0
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);
 };
 
 }

+ 31 - 4
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);
 

+ 8 - 3
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;
 };

+ 29 - 9
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)