Skip to content

Commit

Permalink
JSLib, WebContent: Implement console.table
Browse files Browse the repository at this point in the history
- Expose table from console object
- Add new Table log level
- Create a JS object that represents table rows and columns
- Print table as HTML using WebContentConsoleClient
  • Loading branch information
GasimGasimzada committed Aug 11, 2024
1 parent dd8c693 commit 783ef6c
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 2 deletions.
59 changes: 57 additions & 2 deletions Base/res/ladybird/inspector.css
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,11 @@ html {
color: var(--tab-button-active-color);
}

.tab-controls button + button {
.tab-controls button+button {
border-left: 1px solid var(--tab-button-border);
}

details > :not(:first-child) {
details> :not(:first-child) {
display: list-item;
list-style: none inside;
margin-left: 1em;
Expand Down Expand Up @@ -271,3 +271,58 @@ details > :not(:first-child) {
padding-left: 10px;
padding-right: 10px;
}

.console-log-table {
width: 100%;
padding: 0 10px;
box-sizing: border-box;
}

.console-log-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
border: 1px solid gray;
}

.console-log-table thead {
border-bottom: 1px solid gray;
}

.console-log-table th {
position: sticky;
top: 0px;
border: 1px solid gray;
}

.console-log-table td {
border-left: 1px solid gray;
border-right: 1px solid gray;
}


@media (prefers-color-scheme: light) {
.console-log-table tbody tr:nth-of-type(2n+1) {
background-color: rgb(229, 229, 229);
}

.console-log-table tbody tr:hover {
background-color: rgb(199, 198, 198);
}
}

@media (prefers-color-scheme: dark) {
.console-log-table tbody tr:nth-of-type(2n+1) {
background-color: rgb(57, 57, 57);
}

.console-log-table tbody tr:hover {
background-color: rgb(80, 79, 79)
}
}

.console-log-table th,
.console-log-table td {
padding: 4px;
text-align: left;
}
140 changes: 140 additions & 0 deletions Userland/Libraries/LibJS/Console.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <LibJS/Console.h>
#include <LibJS/Print.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/StringConstructor.h>
#include <LibJS/Runtime/Temporal/Duration.h>
Expand Down Expand Up @@ -140,6 +141,145 @@ ThrowCompletionOr<Value> Console::log()
return js_undefined();
}

static ThrowCompletionOr<NonnullGCPtr<Object>> create_table_row(Realm& realm, Value index, Value value, Vector<Value>& columns, HashMap<PropertyKey, bool>& visited_columns, HashMap<PropertyKey, bool>& input_columns)
{
auto& vm = realm.vm();

auto item = Object::create(realm, nullptr);

auto add_column = [&](PropertyKey column_name) -> Optional<Completion> {
if (!visited_columns.contains(column_name)) {
visited_columns.set(column_name, true);

if (column_name.is_string()) {
columns.append(PrimitiveString::create(vm, column_name.as_string()));
} else if (column_name.is_symbol()) {
columns.append(column_name.as_symbol());
} else if (column_name.is_number()) {
columns.append(Value(column_name.as_number()));
}
}

return {};
};

// "(index)" column that that shows array indices
// or object keys
{
auto key = PropertyKey("(index)");
TRY(item->set(key, index, Object::ShouldThrowExceptions::No));

add_column(key);
}

if (TRY(value.is_array(vm))) {
auto& array = value.as_array();
for (auto const& prop : array.indexed_properties()) {
PropertyKey key(prop.index());

// If `properties` array is passed and the current index does
// not exist in it, do not add this column to the row.
if (input_columns.size() > 0 && !input_columns.contains(key)) {
continue;
}

TRY(item->set(key, TRY(array.get(key)), Object::ShouldThrowExceptions::No));
add_column(key);
}
} else if (value.is_object()) {
auto& object = value.as_object();
object.enumerate_object_properties([&](Value key_v) -> Optional<Completion> {
auto key = TRY(PropertyKey::from_value(vm, key_v));

// If `properties` array is passed and the current key does
// not exist in it, do not add this column to the row.
if (input_columns.size() > 0 && !input_columns.contains(key)) {
return {};
}

TRY(item->set(key, TRY(object.get(key)), Object::ShouldThrowExceptions::No));
add_column(key);

return {};
});
} else {
// The "Value" column
PropertyKey key("Value");
TRY(item->set(key, value, Object::ShouldThrowExceptions::No));

add_column(key);
}

return item;
}

// 1.1.7. table(tabularData, properties), https://console.spec.whatwg.org/#table
ThrowCompletionOr<Value> Console::table()
{
if (!m_client) {
return js_undefined();
}

auto& vm = realm().vm();

if (vm.argument_count() > 0) {
auto tabular_data = vm.argument(0);
auto properties = vm.argument(1);

HashMap<PropertyKey, bool> input_columns;

if (TRY(properties.is_array(vm))) {
auto& properties_array = properties.as_array().indexed_properties();
auto properties_storage = properties_array.storage();
for (auto const& col : properties_array) {
auto col_name = properties_storage->get(col.index()).value().value;
input_columns.set(TRY(PropertyKey::from_value(vm, col_name)), true);
}
}

Vector<Value> rows;
Vector<Value> columns;
HashMap<PropertyKey, bool> visited_columns;
if (TRY(tabular_data.is_array(vm))) {
auto& array = tabular_data.as_array();
for (auto const& prop : array.indexed_properties()) {
PropertyKey key(prop.index());
Value value = TRY(array.get(key));

auto item = TRY(create_table_row(realm(), Value(key.as_number()), value, columns, visited_columns, input_columns));
rows.append(item);
}

} else if (tabular_data.is_object()) {
auto& object = tabular_data.as_object();
object.enumerate_object_properties([&](Value key) -> Optional<Completion> {
auto index = TRY(PropertyKey::from_value(vm, key));
auto value = TRY(object.get(index));

auto item = TRY(create_table_row(realm(), key, value, columns, visited_columns, input_columns));
rows.append(item);

return {};
});
}

if (rows.size() > 0) {
auto table_rows = Array::create_from(realm(), rows);
auto table_cols = Array::create_from(realm(), columns);
auto table = Object::create(realm(), nullptr);

TRY(table->set(PropertyKey("rows"), table_rows, Object::ShouldThrowExceptions::No));
TRY(table->set(PropertyKey("columns"), table_cols, Object::ShouldThrowExceptions::No));

MarkedVector<Value> args(vm.heap());
args.append(Value(table));
return m_client->logger(LogLevel::Table, args);
}
}

return m_client->logger(LogLevel::Log, vm_arguments());
}

// 1.1.8. trace(...data), https://console.spec.whatwg.org/#trace
ThrowCompletionOr<Value> Console::trace()
{
Expand Down
2 changes: 2 additions & 0 deletions Userland/Libraries/LibJS/Console.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class Console : public Cell {
Log,
TimeEnd,
TimeLog,
Table,
Trace,
Warn,
};
Expand Down Expand Up @@ -73,6 +74,7 @@ class Console : public Cell {
ThrowCompletionOr<Value> error();
ThrowCompletionOr<Value> info();
ThrowCompletionOr<Value> log();
ThrowCompletionOr<Value> table();
ThrowCompletionOr<Value> trace();
ThrowCompletionOr<Value> warn();
ThrowCompletionOr<Value> dir();
Expand Down
1 change: 1 addition & 0 deletions Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ namespace JS {
P(supportedLocalesOf) \
P(supportedValuesOf) \
P(symmetricDifference) \
P(table) \
P(take) \
P(tan) \
P(tanh) \
Expand Down
8 changes: 8 additions & 0 deletions Userland/Libraries/LibJS/Runtime/ConsoleObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ void ConsoleObject::initialize(Realm& realm)
define_native_function(realm, vm.names.error, error, 0, attr);
define_native_function(realm, vm.names.info, info, 0, attr);
define_native_function(realm, vm.names.log, log, 0, attr);
define_native_function(realm, vm.names.table, table, 0, attr);
define_native_function(realm, vm.names.trace, trace, 0, attr);
define_native_function(realm, vm.names.warn, warn, 0, attr);
define_native_function(realm, vm.names.dir, dir, 0, attr);
Expand Down Expand Up @@ -94,6 +95,13 @@ JS_DEFINE_NATIVE_FUNCTION(ConsoleObject::log)
return console_object.console().log();
}

// 1.1.7. table(tabularData, properties), https://console.spec.whatwg.org/#table
JS_DEFINE_NATIVE_FUNCTION(ConsoleObject::table)
{
auto& console_object = *vm.current_realm()->intrinsics().console_object();
return console_object.console().table();
}

// 1.1.8. trace(...data), https://console.spec.whatwg.org/#trace
JS_DEFINE_NATIVE_FUNCTION(ConsoleObject::trace)
{
Expand Down
1 change: 1 addition & 0 deletions Userland/Libraries/LibJS/Runtime/ConsoleObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class ConsoleObject final : public Object {
JS_DECLARE_NATIVE_FUNCTION(log);
JS_DECLARE_NATIVE_FUNCTION(trace);
JS_DECLARE_NATIVE_FUNCTION(warn);
JS_DECLARE_NATIVE_FUNCTION(table);
JS_DECLARE_NATIVE_FUNCTION(dir);
JS_DECLARE_NATIVE_FUNCTION(count);
JS_DECLARE_NATIVE_FUNCTION(count_reset);
Expand Down
78 changes: 78 additions & 0 deletions Userland/Services/WebContent/WebContentConsoleClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <AK/MemoryStream.h>
#include <AK/StringBuilder.h>
#include <AK/TemporaryChange.h>
#include <LibJS/MarkupGenerator.h>
#include <LibJS/Print.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/GlobalEnvironment.h>
#include <LibJS/Runtime/ObjectEnvironment.h>
Expand Down Expand Up @@ -147,6 +149,82 @@ JS::ThrowCompletionOr<JS::Value> WebContentConsoleClient::printer(JS::Console::L
auto styling = escape_html_entities(m_current_message_style.string_view());
m_current_message_style.clear();

if (log_level == JS::Console::LogLevel::Table) {
auto& vm = m_console->realm().vm();

auto table_args = arguments.get<JS::MarkedVector<JS::Value>>();
auto& table = table_args.at(0).as_object();
auto& columns = TRY(table.get(JS::PropertyKey("columns"))).as_array().indexed_properties();
auto& rows = TRY(table.get(JS::PropertyKey("rows"))).as_array().indexed_properties();

StringBuilder html;

html.appendff("<div class=\"console-log-table\">");
html.appendff("<table>");
html.appendff("<thead>");
html.appendff("<tr>");
for (auto const& col : columns) {
auto index = col.index();
auto value = columns.storage()->get(index).value().value;
html.appendff("<td>{}</td>", value);
}

html.appendff("</tr>");
html.appendff("</thead>");
html.appendff("<tbody>");

for (auto const& row : rows) {
auto row_index = row.index();
auto& row_obj = rows.storage()->get(row_index).value().value.as_object();
html.appendff("<tr>");

for (auto const& col : columns) {
auto col_index = col.index();
auto col_name = columns.storage()->get(col_index).value().value;

auto property_key = TRY(JS::PropertyKey::from_value(vm, col_name));
auto cell = TRY(row_obj.get(property_key));
html.appendff("<td>");
if (TRY(cell.is_array(vm))) {
AllocatingMemoryStream stream;
JS::PrintContext ctx { vm, stream, true };
TRY_OR_THROW_OOM(vm, stream.write_until_depleted(" "sv.bytes()));
TRY_OR_THROW_OOM(vm, JS::print(cell, ctx));
auto output = TRY_OR_THROW_OOM(vm, String::from_stream(stream, stream.used_buffer_size()));

auto size = cell.as_array().indexed_properties().array_like_size();
html.appendff("<details><summary>Array({})</summary>{}</details>", size, output);

} else if (cell.is_object()) {
AllocatingMemoryStream stream;
JS::PrintContext ctx { vm, stream, true };
TRY_OR_THROW_OOM(vm, stream.write_until_depleted(" "sv.bytes()));
TRY_OR_THROW_OOM(vm, JS::print(cell, ctx));
auto output = TRY_OR_THROW_OOM(vm, String::from_stream(stream, stream.used_buffer_size()));

html.appendff("<details><summary>Object({{...}})</summary>{}</details>", output);
} else if (cell.is_function() || cell.is_constructor()) {
html.appendff("ƒ");
} else if (!cell.is_undefined()) {
html.appendff("{}", cell);
}
html.appendff("</td>");
}

html.appendff("</tr>");
}

html.appendff("</tbody>");
html.appendff("</table>");
html.appendff("</div>");
print_html(html.string_view());

auto output = TRY(generically_format_values(table_args));
m_console->output_debug_message(log_level, output);

return JS::js_undefined();
}

if (log_level == JS::Console::LogLevel::Trace) {
auto trace = arguments.get<JS::Console::Trace>();
StringBuilder html;
Expand Down

0 comments on commit 783ef6c

Please sign in to comment.