mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-21 23:20:20 +00:00
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:
parent
a2a9a11466
commit
785180dd45
Notes:
github-actions[bot]
2024-08-22 08:09:43 +00:00
Author: https://github.com/GasimGasimzada Commit: https://github.com/LadybirdBrowser/ladybird/commit/785180dd45e Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/1027 Reviewed-by: https://github.com/AtkinsSJ ✅
7 changed files with 337 additions and 0 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -513,6 +513,7 @@ namespace JS {
|
|||
P(supportedLocalesOf) \
|
||||
P(supportedValuesOf) \
|
||||
P(symmetricDifference) \
|
||||
P(table) \
|
||||
P(take) \
|
||||
P(tan) \
|
||||
P(tanh) \
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue