diff --git a/Base/res/ladybird/inspector.css b/Base/res/ladybird/inspector.css index 3d7ae6f4bd3..617270327bc 100644 --- a/Base/res/ladybird/inspector.css +++ b/Base/res/ladybird/inspector.css @@ -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; +} diff --git a/Userland/Libraries/LibJS/Console.cpp b/Userland/Libraries/LibJS/Console.cpp index 513a75eb9c8..ca5969fe056 100644 --- a/Userland/Libraries/LibJS/Console.cpp +++ b/Userland/Libraries/LibJS/Console.cpp @@ -2,6 +2,7 @@ * Copyright (c) 2020, Emanuele Torre * Copyright (c) 2020-2023, Linus Groh * Copyright (c) 2021-2022, Sam Atkins + * Copyright (c) 2024, Gasim Gasimzada * * SPDX-License-Identifier: BSD-2-Clause */ @@ -11,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -140,6 +142,202 @@ ThrowCompletionOr Console::log() return js_undefined(); } +// To [create table row] given tabularDataItem, rowIndex, list finalColumns, and optional list properties, perform the following steps: +static ThrowCompletionOr> create_table_row(Realm& realm, Value row_index, Value tabular_data_item, Vector& final_columns, HashMap& visited_columns, HashMap& properties) +{ + auto& vm = realm.vm(); + + auto add_column = [&](PropertyKey const& column_name) -> Optional { + // 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 { + 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 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 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 final_rows; + + // 2. Let `finalColumns` be the new list, initially empty + Vector final_columns; + + HashMap 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 { + 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 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 Console::trace() { diff --git a/Userland/Libraries/LibJS/Console.h b/Userland/Libraries/LibJS/Console.h index 30136e4093d..c546b5ef3e4 100644 --- a/Userland/Libraries/LibJS/Console.h +++ b/Userland/Libraries/LibJS/Console.h @@ -45,6 +45,7 @@ public: Log, TimeEnd, TimeLog, + Table, Trace, Warn, }; @@ -73,6 +74,7 @@ public: ThrowCompletionOr error(); ThrowCompletionOr info(); ThrowCompletionOr log(); + ThrowCompletionOr table(); ThrowCompletionOr trace(); ThrowCompletionOr warn(); ThrowCompletionOr dir(); diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 99950e5bde8..959234bd60f 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -513,6 +513,7 @@ namespace JS { P(supportedLocalesOf) \ P(supportedValuesOf) \ P(symmetricDifference) \ + P(table) \ P(take) \ P(tan) \ P(tanh) \ diff --git a/Userland/Libraries/LibJS/Runtime/ConsoleObject.cpp b/Userland/Libraries/LibJS/Runtime/ConsoleObject.cpp index fdb965ae4f5..433d269cfab 100644 --- a/Userland/Libraries/LibJS/Runtime/ConsoleObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/ConsoleObject.cpp @@ -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) { diff --git a/Userland/Libraries/LibJS/Runtime/ConsoleObject.h b/Userland/Libraries/LibJS/Runtime/ConsoleObject.h index c5f37eab28a..c2d0dbc337c 100644 --- a/Userland/Libraries/LibJS/Runtime/ConsoleObject.h +++ b/Userland/Libraries/LibJS/Runtime/ConsoleObject.h @@ -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); diff --git a/Userland/Services/WebContent/WebContentConsoleClient.cpp b/Userland/Services/WebContent/WebContentConsoleClient.cpp index d66b29b8de7..adfa7dd2a78 100644 --- a/Userland/Services/WebContent/WebContentConsoleClient.cpp +++ b/Userland/Services/WebContent/WebContentConsoleClient.cpp @@ -2,13 +2,16 @@ * Copyright (c) 2021, Brandon Scott * Copyright (c) 2020, Hunter Salyer * Copyright (c) 2021-2022, Sam Atkins + * Copyright (c) 2024, Gasim Gasimzada * * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include +#include #include #include #include @@ -147,6 +150,82 @@ JS::ThrowCompletionOr 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>(); + 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("
"); + html.appendff(""); + html.appendff(""); + html.appendff(""); + for (auto const& col : columns) { + auto index = col.index(); + auto value = columns.storage()->get(index).value().value; + html.appendff("", value); + } + + html.appendff(""); + html.appendff(""); + html.appendff(""); + + for (auto const& row : rows) { + auto row_index = row.index(); + auto& row_obj = rows.storage()->get(row_index).value().value.as_object(); + html.appendff(""); + + 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(""); + } + + html.appendff(""); + } + + html.appendff(""); + html.appendff("
{}
"); + 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("
Array({}){}
", 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("
Object({{...}}){}
", output); + } else if (cell.is_function() || cell.is_constructor()) { + html.appendff("ƒ"); + } else if (!cell.is_undefined()) { + html.appendff("{}", cell); + } + html.appendff("
"); + html.appendff("
"); + 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(); StringBuilder html;