summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Atkins <atkinssj@serenityos.org>2021-12-22 12:32:15 +0000
committerAndreas Kling <kling@serenityos.org>2021-12-27 21:44:07 +0100
commitd702678d16753f9bb81c6a46cffe0cb43133c267 (patch)
treee4c2ae790070460c059fefe6a19574b4fffe7bb2
parentff5e07d718148417e89aea20db478550742edc29 (diff)
downloadserenity-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.cpp67
-rw-r--r--Userland/Applications/Browser/ConsoleWidget.h9
-rw-r--r--Userland/Libraries/LibJS/Console.cpp103
-rw-r--r--Userland/Libraries/LibJS/Console.h16
-rw-r--r--Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h3
-rw-r--r--Userland/Libraries/LibJS/Runtime/ConsoleObject.cpp21
-rw-r--r--Userland/Libraries/LibJS/Runtime/ConsoleObject.h3
-rw-r--r--Userland/Services/WebContent/WebContentConsoleClient.cpp35
-rw-r--r--Userland/Services/WebContent/WebContentConsoleClient.h11
-rw-r--r--Userland/Utilities/js.cpp38
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)