LibJS+WebContent: Implement console.table

- 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
This commit is contained in:
Gasim Gasimzada 2024-08-09 23:24:26 +02:00 committed by Sam Atkins
parent a2a9a11466
commit 785180dd45
Notes: github-actions[bot] 2024-08-22 08:09:43 +00:00
7 changed files with 337 additions and 0 deletions

View file

@ -16,6 +16,9 @@
--console-warning-color: orange;
--console-input-color: rgb(57, 57, 57);
--console-input-focus-color: cyan;
--console-table-row-odd: rgb(57, 57, 57);
--console-table-row-hover: rgb(80, 79, 79);
--console-table-border: gray;
--property-table-head: rgb(57, 57, 57);
}
}
@ -37,6 +40,9 @@
--console-warning-color: darkorange;
--console-input-color: rgb(229, 229, 229);
--console-input-focus-color: blue;
--console-table-row-odd: rgb(229, 229, 229);
--console-table-row-hover: rgb(199, 198, 198);
--console-table-border: gray;
--property-table-head: rgb(229, 229, 229);
}
}
@ -283,3 +289,45 @@ 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 var(--console-table-border);
}
.console-log-table thead {
border-bottom: 1px solid var(--console-table-border);
}
.console-log-table th {
position: sticky;
top: 0px;
border: 1px solid var(--console-table-border);
}
.console-log-table td {
border-left: 1px solid var(--console-table-border);
border-right: 1px solid var(--console-table-border);
}
.console-log-table tbody tr:nth-of-type(2n + 1) {
background-color: var(--console-table-row-odd);
}
.console-log-table tbody tr:hover {
background-color: var(--console-table-row-hover);
}
.console-log-table th,
.console-log-table td {
padding: 4px;
text-align: left;
}

View file

@ -2,6 +2,7 @@
* Copyright (c) 2020, Emanuele Torre <torreemanuele6@gmail.com>
* Copyright (c) 2020-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2021-2022, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2024, Gasim Gasimzada <gasim@gasimzada.net>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -11,6 +12,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>
@ -140,6 +142,202 @@ ThrowCompletionOr<Value> Console::log()
return js_undefined();
}
// To [create table row] given tabularDataItem, rowIndex, list finalColumns, and optional list properties, perform the following steps:
static ThrowCompletionOr<NonnullGCPtr<Object>> create_table_row(Realm& realm, Value row_index, Value tabular_data_item, Vector<Value>& final_columns, HashMap<PropertyKey, bool>& visited_columns, HashMap<PropertyKey, bool>& properties)
{
auto& vm = realm.vm();
auto add_column = [&](PropertyKey const& column_name) -> Optional<Completion> {
// In order to not iterate over the final_columns to find if a column is
// already in the list, an additional hash map is used to identify
// if a column is already visited without needing to loop through the whole
// array.
if (!visited_columns.contains(column_name)) {
visited_columns.set(column_name, true);
if (column_name.is_string()) {
final_columns.append(PrimitiveString::create(vm, column_name.as_string()));
} else if (column_name.is_symbol()) {
final_columns.append(column_name.as_symbol());
} else if (column_name.is_number()) {
final_columns.append(Value(column_name.as_number()));
}
}
return {};
};
// 1. Let `row` be a new map
auto row = Object::create(realm, nullptr);
// 2. Set `row["(index)"]` to `rowIndex`
{
auto key = PropertyKey("(index)");
TRY(row->set(key, row_index, Object::ShouldThrowExceptions::No));
add_column(key);
}
// 3. If `tabularDataItem` is a list, then:
if (TRY(tabular_data_item.is_array(vm))) {
auto& array = tabular_data_item.as_array();
// 3.1. Let `indices` be get the indices of `tabularDataItem`
auto& indices = array.indexed_properties();
// 3.2. For each `index` of `indices`
for (auto const& prop : indices) {
PropertyKey key(prop.index());
// 3.2.1. Let `value` be `tabularDataItem[index]`
Value value = TRY(array.get(key));
// 3.2.2. If `properties` is not empty and `properties` does not contain `index`, continue
if (properties.size() > 0 && !properties.contains(key)) {
continue;
}
// 3.2.3. Set `row[index]` to `value`
TRY(row->set(key, value, Object::ShouldThrowExceptions::No));
// 3.2.4. If `finalColumns` does not contain `index`, append `index` to `finalColumns`
add_column(key);
}
}
// 4. Otherwise, if `tabularDataItem` is a map, then:
else if (tabular_data_item.is_object()) {
auto& object = tabular_data_item.as_object();
// 4.1. For each `key` -> `value` of `tabularDataItem`
object.enumerate_object_properties([&](Value key_v) -> Optional<Completion> {
auto key = TRY(PropertyKey::from_value(vm, key_v));
// 4.1.1. If `properties` is not empty and `properties` does not contain `key`, continue
if (properties.size() > 0 && !properties.contains(key)) {
return {};
}
// 4.1.2. Set `row[key]` to `value`
TRY(row->set(key, TRY(object.get(key)), Object::ShouldThrowExceptions::No));
// 4.1.3. If `finalColumns` does not contain `key`, append `key` to `finalColumns`
add_column(key);
return {};
});
}
// 5. Otherwise,
else {
PropertyKey key("Value");
// 5.1. Set `row["Value"]` to `tabularDataItem`
TRY(row->set(key, tabular_data_item, Object::ShouldThrowExceptions::No));
// 5.2. If `finalColumns` does not contain "Value", append "Value" to `finalColumns`
add_column(key);
}
// 6. Return row
return row;
}
// 1.1.7. table(tabularData, properties), https://console.spec.whatwg.org/#table, WIP
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_arg = vm.argument(1);
HashMap<PropertyKey, bool> properties;
if (TRY(properties_arg.is_array(vm))) {
auto& properties_array = properties_arg.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;
properties.set(TRY(PropertyKey::from_value(vm, col_name)), true);
}
}
// 1. Let `finalRows` be the new list, initially empty
Vector<Value> final_rows;
// 2. Let `finalColumns` be the new list, initially empty
Vector<Value> final_columns;
HashMap<PropertyKey, bool> visited_columns;
// 3. If `tabularData` is a list, then:
if (TRY(tabular_data.is_array(vm))) {
auto& array = tabular_data.as_array();
// 3.1. Let `indices` be get the indices of `tabularData`
auto& indices = array.indexed_properties();
// 3.2. For each `index` of `indices`
for (auto const& prop : indices) {
PropertyKey index(prop.index());
// 3.2.1. Let `value` be `tabularData[index]`
Value value = TRY(array.get(index));
// 3.2.2. Perform create table row with `value`, `key`, `finalColumns`, and `properties` that returns `row`
auto row = TRY(create_table_row(realm(), Value(index.as_number()), value, final_columns, visited_columns, properties));
// 3.2.3. Append `row` to `finalRows`
final_rows.append(row);
}
}
// 4. Otherwise, if `tabularData` is a map, then:
else if (tabular_data.is_object()) {
auto& object = tabular_data.as_object();
// 4.1. For each `key` -> `value` of `tabularData`
object.enumerate_object_properties([&](Value key) -> Optional<Completion> {
auto index = TRY(PropertyKey::from_value(vm, key));
auto value = TRY(object.get(index));
// 4.1.1. Perform create table row with `key`, `value`, `finalColumns`, and `properties` that returns `row`
auto row = TRY(create_table_row(realm(), key, value, final_columns, visited_columns, properties));
// 4.1.2. Append `row` to `finalRows`
final_rows.append(row);
return {};
});
}
// 5. If `finalRows` is not empty, then:
if (final_rows.size() > 0) {
auto table_rows = Array::create_from(realm(), final_rows);
auto table_cols = Array::create_from(realm(), final_columns);
// 5.1. Let `finalData` to be a new map:
auto final_data = Object::create(realm(), nullptr);
// 5.2. Set `finalData["rows"]` to `finalRows`
TRY(final_data->set(PropertyKey("rows"), table_rows, Object::ShouldThrowExceptions::No));
// 5.3. Set finalData["columns"] to finalColumns
TRY(final_data->set(PropertyKey("columns"), table_cols, Object::ShouldThrowExceptions::No));
// 5.4. Perform `Printer("table", finalData)`
MarkedVector<Value> args(vm.heap());
args.append(Value(final_data));
return m_client->printer(LogLevel::Table, args);
}
}
// 6. Otherwise, perform `Printer("log", tabularData)`
return m_client->printer(LogLevel::Log, vm_arguments());
}
// 1.1.8. trace(...data), https://console.spec.whatwg.org/#trace
ThrowCompletionOr<Value> Console::trace()
{

View file

@ -45,6 +45,7 @@ public:
Log,
TimeEnd,
TimeLog,
Table,
Trace,
Warn,
};
@ -73,6 +74,7 @@ public:
ThrowCompletionOr<Value> error();
ThrowCompletionOr<Value> info();
ThrowCompletionOr<Value> log();
ThrowCompletionOr<Value> table();
ThrowCompletionOr<Value> trace();
ThrowCompletionOr<Value> warn();
ThrowCompletionOr<Value> dir();

View file

@ -513,6 +513,7 @@ namespace JS {
P(supportedLocalesOf) \
P(supportedValuesOf) \
P(symmetricDifference) \
P(table) \
P(take) \
P(tan) \
P(tanh) \

View file

@ -45,6 +45,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);
@ -102,6 +103,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)
{

View file

@ -32,6 +32,7 @@ private:
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);

View file

@ -2,13 +2,16 @@
* Copyright (c) 2021, Brandon Scott <xeon.productions@gmail.com>
* Copyright (c) 2020, Hunter Salyer <thefalsehonesty@gmail.com>
* Copyright (c) 2021-2022, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2024, Gasim Gasimzada <gasim@gasimzada.net>
*
* 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>
@ -147,6 +150,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;