Coverage Report

Created: 2025-03-04 07:22

/src/serenity/Userland/Libraries/LibJS/Console.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright (c) 2020, Emanuele Torre <torreemanuele6@gmail.com>
3
 * Copyright (c) 2020-2023, Linus Groh <linusg@serenityos.org>
4
 * Copyright (c) 2021-2022, Sam Atkins <atkinssj@serenityos.org>
5
 * Copyright (c) 2024, Gasim Gasimzada <gasim@gasimzada.net>
6
 *
7
 * SPDX-License-Identifier: BSD-2-Clause
8
 */
9
10
#include <AK/MemoryStream.h>
11
#include <AK/StringBuilder.h>
12
#include <LibJS/Console.h>
13
#include <LibJS/Print.h>
14
#include <LibJS/Runtime/AbstractOperations.h>
15
#include <LibJS/Runtime/Array.h>
16
#include <LibJS/Runtime/Completion.h>
17
#include <LibJS/Runtime/StringConstructor.h>
18
#include <LibJS/Runtime/Temporal/Duration.h>
19
#include <LibJS/Runtime/ValueInlines.h>
20
21
namespace JS {
22
23
JS_DEFINE_ALLOCATOR(Console);
24
JS_DEFINE_ALLOCATOR(ConsoleClient);
25
26
Console::Console(Realm& realm)
27
62
    : m_realm(realm)
28
62
{
29
62
}
30
31
62
Console::~Console() = default;
32
33
void Console::visit_edges(Visitor& visitor)
34
0
{
35
0
    Base::visit_edges(visitor);
36
0
    visitor.visit(m_realm);
37
0
    visitor.visit(m_client);
38
0
}
39
40
// 1.1.1. assert(condition, ...data), https://console.spec.whatwg.org/#assert
41
ThrowCompletionOr<Value> Console::assert_()
42
0
{
43
0
    auto& vm = realm().vm();
44
45
    // 1. If condition is true, return.
46
0
    auto condition = vm.argument(0).to_boolean();
47
0
    if (condition)
48
0
        return js_undefined();
49
50
    // 2. Let message be a string without any formatting specifiers indicating generically an assertion failure (such as "Assertion failed").
51
0
    auto message = PrimitiveString::create(vm, "Assertion failed"_string);
52
53
    // NOTE: Assemble `data` from the function arguments.
54
0
    MarkedVector<Value> data { vm.heap() };
55
0
    if (vm.argument_count() > 1) {
56
0
        data.ensure_capacity(vm.argument_count() - 1);
57
0
        for (size_t i = 1; i < vm.argument_count(); ++i) {
58
0
            data.append(vm.argument(i));
59
0
        }
60
0
    }
61
62
    // 3. If data is empty, append message to data.
63
0
    if (data.is_empty()) {
64
0
        data.append(message);
65
0
    }
66
    // 4. Otherwise:
67
0
    else {
68
        // 1. Let first be data[0].
69
0
        auto& first = data[0];
70
        // 2. If first is not a String, then prepend message to data.
71
0
        if (!first.is_string()) {
72
0
            data.prepend(message);
73
0
        }
74
        // 3. Otherwise:
75
0
        else {
76
            // 1. Let concat be the concatenation of message, U+003A (:), U+0020 SPACE, and first.
77
0
            auto concat = TRY_OR_THROW_OOM(vm, String::formatted("{}: {}", message->utf8_string(), MUST(first.to_string(vm))));
78
            // 2. Set data[0] to concat.
79
0
            data[0] = PrimitiveString::create(vm, move(concat));
80
0
        }
81
0
    }
82
83
    // 5. Perform Logger("assert", data).
84
0
    if (m_client)
85
0
        TRY(m_client->logger(LogLevel::Assert, data));
86
0
    return js_undefined();
87
0
}
88
89
// 1.1.2. clear(), https://console.spec.whatwg.org/#clear
90
Value Console::clear()
91
0
{
92
    // 1. Empty the appropriate group stack.
93
0
    m_group_stack.clear();
94
95
    // 2. If possible for the environment, clear the console. (Otherwise, do nothing.)
96
0
    if (m_client)
97
0
        m_client->clear();
98
0
    return js_undefined();
99
0
}
100
101
// 1.1.3. debug(...data), https://console.spec.whatwg.org/#debug
102
ThrowCompletionOr<Value> Console::debug()
103
0
{
104
    // 1. Perform Logger("debug", data).
105
0
    if (m_client) {
106
0
        auto data = vm_arguments();
107
0
        return m_client->logger(LogLevel::Debug, data);
108
0
    }
109
0
    return js_undefined();
110
0
}
111
112
// 1.1.4. error(...data), https://console.spec.whatwg.org/#error
113
ThrowCompletionOr<Value> Console::error()
114
0
{
115
    // 1. Perform Logger("error", data).
116
0
    if (m_client) {
117
0
        auto data = vm_arguments();
118
0
        return m_client->logger(LogLevel::Error, data);
119
0
    }
120
0
    return js_undefined();
121
0
}
122
123
// 1.1.5. info(...data), https://console.spec.whatwg.org/#info
124
ThrowCompletionOr<Value> Console::info()
125
0
{
126
    // 1. Perform Logger("info", data).
127
0
    if (m_client) {
128
0
        auto data = vm_arguments();
129
0
        return m_client->logger(LogLevel::Info, data);
130
0
    }
131
0
    return js_undefined();
132
0
}
133
134
// 1.1.6. log(...data), https://console.spec.whatwg.org/#log
135
ThrowCompletionOr<Value> Console::log()
136
0
{
137
    // 1. Perform Logger("log", data).
138
0
    if (m_client) {
139
0
        auto data = vm_arguments();
140
0
        return m_client->logger(LogLevel::Log, data);
141
0
    }
142
0
    return js_undefined();
143
0
}
144
145
// To [create table row] given tabularDataItem, rowIndex, list finalColumns, and optional list properties, perform the following steps:
146
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)
147
0
{
148
0
    auto& vm = realm.vm();
149
150
0
    auto add_column = [&](PropertyKey const& column_name) -> Optional<Completion> {
151
        // In order to not iterate over the final_columns to find if a column is
152
        // already in the list, an additional hash map is used to identify
153
        // if a column is already visited without needing to loop through the whole
154
        // array.
155
0
        if (!visited_columns.contains(column_name)) {
156
0
            visited_columns.set(column_name, true);
157
158
0
            if (column_name.is_string()) {
159
0
                final_columns.append(PrimitiveString::create(vm, column_name.as_string()));
160
0
            } else if (column_name.is_symbol()) {
161
0
                final_columns.append(column_name.as_symbol());
162
0
            } else if (column_name.is_number()) {
163
0
                final_columns.append(Value(column_name.as_number()));
164
0
            }
165
0
        }
166
167
0
        return {};
168
0
    };
169
170
    // 1. Let `row` be a new map
171
0
    auto row = Object::create(realm, nullptr);
172
173
    // 2. Set `row["(index)"]` to `rowIndex`
174
0
    {
175
0
        auto key = PropertyKey("(index)");
176
0
        TRY(row->set(key, row_index, Object::ShouldThrowExceptions::No));
177
178
0
        add_column(key);
179
0
    }
180
181
    // 3. If `tabularDataItem` is a list, then:
182
0
    if (TRY(tabular_data_item.is_array(vm))) {
183
0
        auto& array = tabular_data_item.as_array();
184
185
        // 3.1. Let `indices` be get the indices of `tabularDataItem`
186
0
        auto& indices = array.indexed_properties();
187
188
        // 3.2. For each `index` of `indices`
189
0
        for (auto const& prop : indices) {
190
0
            PropertyKey key(prop.index());
191
192
            // 3.2.1. Let `value` be `tabularDataItem[index]`
193
0
            Value value = TRY(array.get(key));
194
195
            // 3.2.2. If `properties` is not empty and `properties` does not contain `index`, continue
196
0
            if (properties.size() > 0 && !properties.contains(key)) {
197
0
                continue;
198
0
            }
199
200
            // 3.2.3. Set `row[index]` to `value`
201
0
            TRY(row->set(key, value, Object::ShouldThrowExceptions::No));
202
203
            // 3.2.4. If `finalColumns` does not contain `index`, append `index` to `finalColumns`
204
0
            add_column(key);
205
0
        }
206
0
    }
207
    // 4. Otherwise, if `tabularDataItem` is a map, then:
208
0
    else if (tabular_data_item.is_object()) {
209
0
        auto& object = tabular_data_item.as_object();
210
211
        // 4.1. For each `key` -> `value` of `tabularDataItem`
212
0
        object.enumerate_object_properties([&](Value key_v) -> Optional<Completion> {
213
0
            auto key = TRY(PropertyKey::from_value(vm, key_v));
214
215
            // 4.1.1. If `properties` is not empty and `properties` does not contain `key`, continue
216
0
            if (properties.size() > 0 && !properties.contains(key)) {
217
0
                return {};
218
0
            }
219
220
            // 4.1.2. Set `row[key]` to `value`
221
0
            TRY(row->set(key, TRY(object.get(key)), Object::ShouldThrowExceptions::No));
222
223
            // 4.1.3. If `finalColumns` does not contain `key`, append `key` to `finalColumns`
224
0
            add_column(key);
225
226
0
            return {};
227
0
        });
228
0
    }
229
    // 5. Otherwise,
230
0
    else {
231
0
        PropertyKey key("Value");
232
        // 5.1. Set `row["Value"]` to `tabularDataItem`
233
0
        TRY(row->set(key, tabular_data_item, Object::ShouldThrowExceptions::No));
234
235
        // 5.2. If `finalColumns` does not contain "Value", append "Value" to `finalColumns`
236
0
        add_column(key);
237
0
    }
238
239
    // 6. Return row
240
0
    return row;
241
0
}
242
243
// 1.1.7. table(tabularData, properties), https://console.spec.whatwg.org/#table, WIP
244
ThrowCompletionOr<Value> Console::table()
245
0
{
246
0
    if (!m_client) {
247
0
        return js_undefined();
248
0
    }
249
250
0
    auto& vm = realm().vm();
251
252
0
    if (vm.argument_count() > 0) {
253
0
        auto tabular_data = vm.argument(0);
254
0
        auto properties_arg = vm.argument(1);
255
256
0
        HashMap<PropertyKey, bool> properties;
257
258
0
        if (TRY(properties_arg.is_array(vm))) {
259
0
            auto& properties_array = properties_arg.as_array().indexed_properties();
260
0
            auto* properties_storage = properties_array.storage();
261
0
            for (auto const& col : properties_array) {
262
0
                auto col_name = properties_storage->get(col.index()).value().value;
263
0
                properties.set(TRY(PropertyKey::from_value(vm, col_name)), true);
264
0
            }
265
0
        }
266
267
        // 1. Let `finalRows` be the new list, initially empty
268
0
        Vector<Value> final_rows;
269
270
        // 2. Let `finalColumns` be the new list, initially empty
271
0
        Vector<Value> final_columns;
272
273
0
        HashMap<PropertyKey, bool> visited_columns;
274
275
        // 3. If `tabularData` is a list, then:
276
0
        if (TRY(tabular_data.is_array(vm))) {
277
0
            auto& array = tabular_data.as_array();
278
279
            // 3.1. Let `indices` be get the indices of `tabularData`
280
0
            auto& indices = array.indexed_properties();
281
282
            // 3.2. For each `index` of `indices`
283
0
            for (auto const& prop : indices) {
284
0
                PropertyKey index(prop.index());
285
286
                // 3.2.1. Let `value` be `tabularData[index]`
287
0
                Value value = TRY(array.get(index));
288
289
                // 3.2.2. Perform create table row with `value`, `key`, `finalColumns`, and `properties` that returns `row`
290
0
                auto row = TRY(create_table_row(realm(), Value(index.as_number()), value, final_columns, visited_columns, properties));
291
292
                // 3.2.3. Append `row` to `finalRows`
293
0
                final_rows.append(row);
294
0
            }
295
296
0
        }
297
        // 4. Otherwise, if `tabularData` is a map, then:
298
0
        else if (tabular_data.is_object()) {
299
0
            auto& object = tabular_data.as_object();
300
301
            // 4.1. For each `key` -> `value` of `tabularData`
302
0
            object.enumerate_object_properties([&](Value key) -> Optional<Completion> {
303
0
                auto index = TRY(PropertyKey::from_value(vm, key));
304
0
                auto value = TRY(object.get(index));
305
306
                // 4.1.1. Perform create table row with `key`, `value`, `finalColumns`, and `properties` that returns `row`
307
0
                auto row = TRY(create_table_row(realm(), key, value, final_columns, visited_columns, properties));
308
309
                // 4.1.2. Append `row` to `finalRows`
310
0
                final_rows.append(row);
311
312
0
                return {};
313
0
            });
314
0
        }
315
316
        // 5. If `finalRows` is not empty, then:
317
0
        if (final_rows.size() > 0) {
318
0
            auto table_rows = Array::create_from(realm(), final_rows);
319
0
            auto table_cols = Array::create_from(realm(), final_columns);
320
321
            // 5.1. Let `finalData` to be a new map:
322
0
            auto final_data = Object::create(realm(), nullptr);
323
324
            // 5.2. Set `finalData["rows"]` to `finalRows`
325
0
            TRY(final_data->set(PropertyKey("rows"), table_rows, Object::ShouldThrowExceptions::No));
326
327
            // 5.3. Set finalData["columns"] to finalColumns
328
0
            TRY(final_data->set(PropertyKey("columns"), table_cols, Object::ShouldThrowExceptions::No));
329
330
            // 5.4. Perform `Printer("table", finalData)`
331
0
            MarkedVector<Value> args(vm.heap());
332
0
            args.append(Value(final_data));
333
0
            return m_client->printer(LogLevel::Table, args);
334
0
        }
335
0
    }
336
337
    // 6. Otherwise, perform `Printer("log", tabularData)`
338
0
    return m_client->printer(LogLevel::Log, vm_arguments());
339
0
}
340
341
// 1.1.8. trace(...data), https://console.spec.whatwg.org/#trace
342
ThrowCompletionOr<Value> Console::trace()
343
0
{
344
0
    if (!m_client)
345
0
        return js_undefined();
346
347
0
    auto& vm = realm().vm();
348
349
    // 1. Let trace be some implementation-defined, potentially-interactive representation of the callstack from where this function was called.
350
0
    Console::Trace trace;
351
0
    auto& execution_context_stack = vm.execution_context_stack();
352
    // NOTE: -2 to skip the console.trace() execution context
353
0
    for (ssize_t i = execution_context_stack.size() - 2; i >= 0; --i) {
354
0
        auto const& function_name = execution_context_stack[i]->function_name;
355
0
        trace.stack.append((!function_name || function_name->is_empty())
356
0
                ? "<anonymous>"_string
357
0
                : function_name->utf8_string());
358
0
    }
359
360
    // 2. Optionally, let formattedData be the result of Formatter(data), and incorporate formattedData as a label for trace.
361
0
    if (vm.argument_count() > 0) {
362
0
        auto data = vm_arguments();
363
0
        auto formatted_data = TRY(m_client->formatter(data));
364
0
        trace.label = TRY(value_vector_to_string(formatted_data));
365
0
    }
366
367
    // 3. Perform Printer("trace", « trace »).
368
0
    return m_client->printer(Console::LogLevel::Trace, trace);
369
0
}
370
371
// 1.1.9. warn(...data), https://console.spec.whatwg.org/#warn
372
ThrowCompletionOr<Value> Console::warn()
373
0
{
374
    // 1. Perform Logger("warn", data).
375
0
    if (m_client) {
376
0
        auto data = vm_arguments();
377
0
        return m_client->logger(LogLevel::Warn, data);
378
0
    }
379
0
    return js_undefined();
380
0
}
381
382
// 1.1.10. dir(item, options), https://console.spec.whatwg.org/#dir
383
ThrowCompletionOr<Value> Console::dir()
384
0
{
385
0
    auto& vm = realm().vm();
386
387
    // 1. Let object be item with generic JavaScript object formatting applied.
388
    // NOTE: Generic formatting is performed by ConsoleClient::printer().
389
0
    auto object = vm.argument(0);
390
391
    // 2. Perform Printer("dir", « object », options).
392
0
    if (m_client) {
393
0
        MarkedVector<Value> printer_arguments { vm.heap() };
394
0
        TRY_OR_THROW_OOM(vm, printer_arguments.try_append(object));
395
396
0
        return m_client->printer(LogLevel::Dir, move(printer_arguments));
397
0
    }
398
399
0
    return js_undefined();
400
0
}
401
402
static ThrowCompletionOr<String> label_or_fallback(VM& vm, StringView fallback)
403
0
{
404
0
    return vm.argument_count() > 0 && !vm.argument(0).is_undefined()
405
0
        ? vm.argument(0).to_string(vm)
406
0
        : TRY_OR_THROW_OOM(vm, String::from_utf8(fallback));
407
0
}
408
409
// 1.2.1. count(label), https://console.spec.whatwg.org/#count
410
ThrowCompletionOr<Value> Console::count()
411
0
{
412
0
    auto& vm = realm().vm();
413
414
    // NOTE: "default" is the default value in the IDL. https://console.spec.whatwg.org/#ref-for-count
415
0
    auto label = TRY(label_or_fallback(vm, "default"sv));
416
417
    // 1. Let map be the associated count map.
418
0
    auto& map = m_counters;
419
420
    // 2. If map[label] exists, set map[label] to map[label] + 1.
421
0
    if (auto found = map.find(label); found != map.end()) {
422
0
        map.set(label, found->value + 1);
423
0
    }
424
    // 3. Otherwise, set map[label] to 1.
425
0
    else {
426
0
        map.set(label, 1);
427
0
    }
428
429
    // 4. Let concat be the concatenation of label, U+003A (:), U+0020 SPACE, and ToString(map[label]).
430
0
    auto concat = TRY_OR_THROW_OOM(vm, String::formatted("{}: {}", label, map.get(label).value()));
431
432
    // 5. Perform Logger("count", « concat »).
433
0
    MarkedVector<Value> concat_as_vector { vm.heap() };
434
0
    concat_as_vector.append(PrimitiveString::create(vm, move(concat)));
435
0
    if (m_client)
436
0
        TRY(m_client->logger(LogLevel::Count, concat_as_vector));
437
0
    return js_undefined();
438
0
}
439
440
// 1.2.2. countReset(label), https://console.spec.whatwg.org/#countreset
441
ThrowCompletionOr<Value> Console::count_reset()
442
0
{
443
0
    auto& vm = realm().vm();
444
445
    // NOTE: "default" is the default value in the IDL. https://console.spec.whatwg.org/#ref-for-countreset
446
0
    auto label = TRY(label_or_fallback(vm, "default"sv));
447
448
    // 1. Let map be the associated count map.
449
0
    auto& map = m_counters;
450
451
    // 2. If map[label] exists, set map[label] to 0.
452
0
    if (auto found = map.find(label); found != map.end()) {
453
0
        map.set(label, 0);
454
0
    }
455
    // 3. Otherwise:
456
0
    else {
457
        // 1. Let message be a string without any formatting specifiers indicating generically
458
        //    that the given label does not have an associated count.
459
0
        auto message = TRY_OR_THROW_OOM(vm, String::formatted("\"{}\" doesn't have a count", label));
460
        // 2. Perform Logger("countReset", « message »);
461
0
        MarkedVector<Value> message_as_vector { vm.heap() };
462
0
        message_as_vector.append(PrimitiveString::create(vm, move(message)));
463
0
        if (m_client)
464
0
            TRY(m_client->logger(LogLevel::CountReset, message_as_vector));
465
0
    }
466
467
0
    return js_undefined();
468
0
}
469
470
// 1.3.1. group(...data), https://console.spec.whatwg.org/#group
471
ThrowCompletionOr<Value> Console::group()
472
0
{
473
    // 1. Let group be a new group.
474
0
    Group group;
475
476
    // 2. If data is not empty, let groupLabel be the result of Formatter(data).
477
0
    String group_label {};
478
0
    auto data = vm_arguments();
479
0
    if (!data.is_empty()) {
480
0
        auto formatted_data = TRY(m_client->formatter(data));
481
0
        group_label = TRY(value_vector_to_string(formatted_data));
482
0
    }
483
    // ... Otherwise, let groupLabel be an implementation-chosen label representing a group.
484
0
    else {
485
0
        group_label = "Group"_string;
486
0
    }
487
488
    // 3. Incorporate groupLabel as a label for group.
489
0
    group.label = group_label;
490
491
    // 4. Optionally, if the environment supports interactive groups, group should be expanded by default.
492
    // NOTE: This is handled in Printer.
493
494
    // 5. Perform Printer("group", « group »).
495
0
    if (m_client)
496
0
        TRY(m_client->printer(LogLevel::Group, group));
497
498
    // 6. Push group onto the appropriate group stack.
499
0
    m_group_stack.append(group);
500
501
0
    return js_undefined();
502
0
}
503
504
// 1.3.2. groupCollapsed(...data), https://console.spec.whatwg.org/#groupcollapsed
505
ThrowCompletionOr<Value> Console::group_collapsed()
506
0
{
507
    // 1. Let group be a new group.
508
0
    Group group;
509
510
    // 2. If data is not empty, let groupLabel be the result of Formatter(data).
511
0
    String group_label {};
512
0
    auto data = vm_arguments();
513
0
    if (!data.is_empty()) {
514
0
        auto formatted_data = TRY(m_client->formatter(data));
515
0
        group_label = TRY(value_vector_to_string(formatted_data));
516
0
    }
517
    // ... Otherwise, let groupLabel be an implementation-chosen label representing a group.
518
0
    else {
519
0
        group_label = "Group"_string;
520
0
    }
521
522
    // 3. Incorporate groupLabel as a label for group.
523
0
    group.label = group_label;
524
525
    // 4. Optionally, if the environment supports interactive groups, group should be collapsed by default.
526
    // NOTE: This is handled in Printer.
527
528
    // 5. Perform Printer("groupCollapsed", « group »).
529
0
    if (m_client)
530
0
        TRY(m_client->printer(LogLevel::GroupCollapsed, group));
531
532
    // 6. Push group onto the appropriate group stack.
533
0
    m_group_stack.append(group);
534
535
0
    return js_undefined();
536
0
}
537
538
// 1.3.3. groupEnd(), https://console.spec.whatwg.org/#groupend
539
ThrowCompletionOr<Value> Console::group_end()
540
0
{
541
0
    if (m_group_stack.is_empty())
542
0
        return js_undefined();
543
544
    // 1. Pop the last group from the group stack.
545
0
    m_group_stack.take_last();
546
0
    if (m_client)
547
0
        m_client->end_group();
548
549
0
    return js_undefined();
550
0
}
551
552
// 1.4.1. time(label), https://console.spec.whatwg.org/#time
553
ThrowCompletionOr<Value> Console::time()
554
0
{
555
0
    auto& vm = realm().vm();
556
557
    // NOTE: "default" is the default value in the IDL. https://console.spec.whatwg.org/#ref-for-time
558
0
    auto label = TRY(label_or_fallback(vm, "default"sv));
559
560
    // 1. If the associated timer table contains an entry with key label, return, optionally reporting
561
    //    a warning to the console indicating that a timer with label `label` has already been started.
562
0
    if (m_timer_table.contains(label)) {
563
0
        if (m_client) {
564
0
            MarkedVector<Value> timer_already_exists_warning_message_as_vector { vm.heap() };
565
566
0
            auto message = TRY_OR_THROW_OOM(vm, String::formatted("Timer '{}' already exists.", label));
567
0
            timer_already_exists_warning_message_as_vector.append(PrimitiveString::create(vm, move(message)));
568
569
0
            TRY(m_client->printer(LogLevel::Warn, move(timer_already_exists_warning_message_as_vector)));
570
0
        }
571
0
        return js_undefined();
572
0
    }
573
574
    // 2. Otherwise, set the value of the entry with key label in the associated timer table to the current time.
575
0
    m_timer_table.set(label, Core::ElapsedTimer::start_new());
576
0
    return js_undefined();
577
0
}
578
579
// 1.4.2. timeLog(label, ...data), https://console.spec.whatwg.org/#timelog
580
ThrowCompletionOr<Value> Console::time_log()
581
0
{
582
0
    auto& vm = realm().vm();
583
584
    // NOTE: "default" is the default value in the IDL. https://console.spec.whatwg.org/#ref-for-timelog
585
0
    auto label = TRY(label_or_fallback(vm, "default"sv));
586
587
    // 1. Let timerTable be the associated timer table.
588
589
    // 2. Let startTime be timerTable[label].
590
0
    auto maybe_start_time = m_timer_table.find(label);
591
592
    // NOTE: Warn if the timer doesn't exist. Not part of the spec yet, but discussed here: https://github.com/whatwg/console/issues/134
593
0
    if (maybe_start_time == m_timer_table.end()) {
594
0
        if (m_client) {
595
0
            MarkedVector<Value> timer_does_not_exist_warning_message_as_vector { vm.heap() };
596
597
0
            auto message = TRY_OR_THROW_OOM(vm, String::formatted("Timer '{}' does not exist.", label));
598
0
            timer_does_not_exist_warning_message_as_vector.append(PrimitiveString::create(vm, move(message)));
599
600
0
            TRY(m_client->printer(LogLevel::Warn, move(timer_does_not_exist_warning_message_as_vector)));
601
0
        }
602
0
        return js_undefined();
603
0
    }
604
0
    auto start_time = maybe_start_time->value;
605
606
    // 3. Let duration be a string representing the difference between the current time and startTime, in an implementation-defined format.
607
0
    auto duration = TRY(format_time_since(start_time));
608
609
    // 4. Let concat be the concatenation of label, U+003A (:), U+0020 SPACE, and duration.
610
0
    auto concat = TRY_OR_THROW_OOM(vm, String::formatted("{}: {}", label, duration));
611
612
    // 5. Prepend concat to data.
613
0
    MarkedVector<Value> data { vm.heap() };
614
0
    data.ensure_capacity(vm.argument_count());
615
0
    data.append(PrimitiveString::create(vm, move(concat)));
616
0
    for (size_t i = 1; i < vm.argument_count(); ++i)
617
0
        data.append(vm.argument(i));
618
619
    // 6. Perform Printer("timeLog", data).
620
0
    if (m_client)
621
0
        TRY(m_client->printer(LogLevel::TimeLog, move(data)));
622
0
    return js_undefined();
623
0
}
624
625
// 1.4.3. timeEnd(label), https://console.spec.whatwg.org/#timeend
626
ThrowCompletionOr<Value> Console::time_end()
627
0
{
628
0
    auto& vm = realm().vm();
629
630
    // NOTE: "default" is the default value in the IDL. https://console.spec.whatwg.org/#ref-for-timeend
631
0
    auto label = TRY(label_or_fallback(vm, "default"sv));
632
633
    // 1. Let timerTable be the associated timer table.
634
635
    // 2. Let startTime be timerTable[label].
636
0
    auto maybe_start_time = m_timer_table.find(label);
637
638
    // NOTE: Warn if the timer doesn't exist. Not part of the spec yet, but discussed here: https://github.com/whatwg/console/issues/134
639
0
    if (maybe_start_time == m_timer_table.end()) {
640
0
        if (m_client) {
641
0
            MarkedVector<Value> timer_does_not_exist_warning_message_as_vector { vm.heap() };
642
643
0
            auto message = TRY_OR_THROW_OOM(vm, String::formatted("Timer '{}' does not exist.", label));
644
0
            timer_does_not_exist_warning_message_as_vector.append(PrimitiveString::create(vm, move(message)));
645
646
0
            TRY(m_client->printer(LogLevel::Warn, move(timer_does_not_exist_warning_message_as_vector)));
647
0
        }
648
0
        return js_undefined();
649
0
    }
650
0
    auto start_time = maybe_start_time->value;
651
652
    // 3. Remove timerTable[label].
653
0
    m_timer_table.remove(label);
654
655
    // 4. Let duration be a string representing the difference between the current time and startTime, in an implementation-defined format.
656
0
    auto duration = TRY(format_time_since(start_time));
657
658
    // 5. Let concat be the concatenation of label, U+003A (:), U+0020 SPACE, and duration.
659
0
    auto concat = TRY_OR_THROW_OOM(vm, String::formatted("{}: {}", label, duration));
660
661
    // 6. Perform Printer("timeEnd", « concat »).
662
0
    if (m_client) {
663
0
        MarkedVector<Value> concat_as_vector { vm.heap() };
664
0
        concat_as_vector.append(PrimitiveString::create(vm, move(concat)));
665
0
        TRY(m_client->printer(LogLevel::TimeEnd, move(concat_as_vector)));
666
0
    }
667
0
    return js_undefined();
668
0
}
669
670
MarkedVector<Value> Console::vm_arguments()
671
0
{
672
0
    auto& vm = realm().vm();
673
674
0
    MarkedVector<Value> arguments { vm.heap() };
675
0
    arguments.ensure_capacity(vm.argument_count());
676
0
    for (size_t i = 0; i < vm.argument_count(); ++i) {
677
0
        arguments.append(vm.argument(i));
678
0
    }
679
0
    return arguments;
680
0
}
681
682
void Console::output_debug_message(LogLevel log_level, String const& output) const
683
0
{
684
0
    switch (log_level) {
685
0
    case Console::LogLevel::Debug:
686
0
        dbgln("\033[32;1m(js debug)\033[0m {}", output);
687
0
        break;
688
0
    case Console::LogLevel::Error:
689
0
        dbgln("\033[32;1m(js error)\033[0m {}", output);
690
0
        break;
691
0
    case Console::LogLevel::Info:
692
0
        dbgln("\033[32;1m(js info)\033[0m {}", output);
693
0
        break;
694
0
    case Console::LogLevel::Log:
695
0
        dbgln("\033[32;1m(js log)\033[0m {}", output);
696
0
        break;
697
0
    case Console::LogLevel::Warn:
698
0
        dbgln("\033[32;1m(js warn)\033[0m {}", output);
699
0
        break;
700
0
    default:
701
0
        dbgln("\033[32;1m(js)\033[0m {}", output);
702
0
        break;
703
0
    }
704
0
}
705
706
void Console::report_exception(JS::Error const& exception, bool in_promise) const
707
0
{
708
0
    if (m_client)
709
0
        m_client->report_exception(exception, in_promise);
710
0
}
711
712
ThrowCompletionOr<String> Console::value_vector_to_string(MarkedVector<Value> const& values)
713
0
{
714
0
    auto& vm = realm().vm();
715
0
    StringBuilder builder;
716
717
0
    for (auto const& item : values) {
718
0
        if (!builder.is_empty())
719
0
            builder.append(' ');
720
721
0
        builder.append(TRY(item.to_string(vm)));
722
0
    }
723
724
0
    return MUST(builder.to_string());
725
0
}
726
727
ThrowCompletionOr<String> Console::format_time_since(Core::ElapsedTimer timer)
728
0
{
729
0
    auto& vm = realm().vm();
730
731
0
    auto elapsed_ms = timer.elapsed_time().to_milliseconds();
732
0
    auto duration = TRY(Temporal::balance_duration(vm, 0, 0, 0, 0, elapsed_ms, 0, "0"_sbigint, "year"sv));
733
734
0
    auto append = [&](auto& builder, auto format, auto number) {
735
0
        if (!builder.is_empty())
736
0
            builder.append(' ');
737
0
        builder.appendff(format, number);
738
0
    };
739
740
0
    StringBuilder builder;
741
742
0
    if (duration.days > 0)
743
0
        append(builder, "{:.0} day(s)"sv, duration.days);
744
0
    if (duration.hours > 0)
745
0
        append(builder, "{:.0} hour(s)"sv, duration.hours);
746
0
    if (duration.minutes > 0)
747
0
        append(builder, "{:.0} minute(s)"sv, duration.minutes);
748
0
    if (duration.seconds > 0 || duration.milliseconds > 0) {
749
0
        double combined_seconds = duration.seconds + (0.001 * duration.milliseconds);
750
0
        append(builder, "{:.3} seconds"sv, combined_seconds);
751
0
    }
752
753
0
    return MUST(builder.to_string());
754
0
}
755
756
ConsoleClient::ConsoleClient(Console& console)
757
0
    : m_console(console)
758
0
{
759
0
}
760
761
0
ConsoleClient::~ConsoleClient() = default;
762
763
void ConsoleClient::visit_edges(Visitor& visitor)
764
0
{
765
0
    Base::visit_edges(visitor);
766
0
    visitor.visit(m_console);
767
0
}
768
769
// 2.1. Logger(logLevel, args), https://console.spec.whatwg.org/#logger
770
ThrowCompletionOr<Value> ConsoleClient::logger(Console::LogLevel log_level, MarkedVector<Value> const& args)
771
0
{
772
0
    auto& vm = m_console->realm().vm();
773
774
    // 1. If args is empty, return.
775
0
    if (args.is_empty())
776
0
        return js_undefined();
777
778
    // 2. Let first be args[0].
779
0
    auto first = args[0];
780
781
    // 3. Let rest be all elements following first in args.
782
0
    size_t rest_size = args.size() - 1;
783
784
    // 4. If rest is empty, perform Printer(logLevel, « first ») and return.
785
0
    if (rest_size == 0) {
786
0
        MarkedVector<Value> first_as_vector { vm.heap() };
787
0
        first_as_vector.append(first);
788
0
        return printer(log_level, move(first_as_vector));
789
0
    }
790
791
    // 5. Otherwise, perform Printer(logLevel, Formatter(args)).
792
0
    else {
793
0
        auto formatted = TRY(formatter(args));
794
0
        TRY(printer(log_level, formatted));
795
0
    }
796
797
    // 6. Return undefined.
798
0
    return js_undefined();
799
0
}
800
801
// 2.2. Formatter(args), https://console.spec.whatwg.org/#formatter
802
ThrowCompletionOr<MarkedVector<Value>> ConsoleClient::formatter(MarkedVector<Value> const& args)
803
0
{
804
0
    auto& realm = m_console->realm();
805
0
    auto& vm = realm.vm();
806
807
    // 1. If args’s size is 1, return args.
808
0
    if (args.size() == 1)
809
0
        return args;
810
811
    // 2. Let target be the first element of args.
812
0
    auto target = (!args.is_empty()) ? TRY(args.first().to_string(vm)) : String {};
813
814
    // 3. Let current be the second element of args.
815
0
    auto current = (args.size() > 1) ? args[1] : js_undefined();
816
817
    // 4. Find the first possible format specifier specifier, from the left to the right in target.
818
0
    auto find_specifier = [](StringView target) -> Optional<StringView> {
819
0
        size_t start_index = 0;
820
0
        while (start_index < target.length()) {
821
0
            auto maybe_index = target.find('%', start_index);
822
0
            if (!maybe_index.has_value())
823
0
                return {};
824
825
0
            auto index = maybe_index.value();
826
0
            if (index + 1 >= target.length())
827
0
                return {};
828
829
0
            switch (target[index + 1]) {
830
0
            case 'c':
831
0
            case 'd':
832
0
            case 'f':
833
0
            case 'i':
834
0
            case 'o':
835
0
            case 'O':
836
0
            case 's':
837
0
                return target.substring_view(index, 2);
838
0
            }
839
840
0
            start_index = index + 1;
841
0
        }
842
0
        return {};
843
0
    };
844
0
    auto maybe_specifier = find_specifier(target);
845
846
    // 5. If no format specifier was found, return args.
847
0
    if (!maybe_specifier.has_value()) {
848
0
        return args;
849
0
    }
850
    // 6. Otherwise:
851
0
    else {
852
0
        auto specifier = maybe_specifier.release_value();
853
0
        Optional<Value> converted;
854
855
        // 1. If specifier is %s, let converted be the result of Call(%String%, undefined, « current »).
856
0
        if (specifier == "%s"sv) {
857
0
            converted = TRY(call(vm, *realm.intrinsics().string_constructor(), js_undefined(), current));
858
0
        }
859
        // 2. If specifier is %d or %i:
860
0
        else if (specifier.is_one_of("%d"sv, "%i"sv)) {
861
            // 1. If current is a Symbol, let converted be NaN
862
0
            if (current.is_symbol()) {
863
0
                converted = js_nan();
864
0
            }
865
            // 2. Otherwise, let converted be the result of Call(%parseInt%, undefined, « current, 10 »).
866
0
            else {
867
0
                converted = TRY(call(vm, *realm.intrinsics().parse_int_function(), js_undefined(), current, Value { 10 }));
868
0
            }
869
0
        }
870
        // 3. If specifier is %f:
871
0
        else if (specifier == "%f"sv) {
872
            // 1. If current is a Symbol, let converted be NaN
873
0
            if (current.is_symbol()) {
874
0
                converted = js_nan();
875
0
            }
876
            // 2. Otherwise, let converted be the result of Call(% parseFloat %, undefined, « current »).
877
0
            else {
878
0
                converted = TRY(call(vm, *realm.intrinsics().parse_float_function(), js_undefined(), current));
879
0
            }
880
0
        }
881
        // 4. If specifier is %o, optionally let converted be current with optimally useful formatting applied.
882
0
        else if (specifier == "%o"sv) {
883
            // TODO: "Optimally-useful formatting"
884
0
            converted = current;
885
0
        }
886
        // 5. If specifier is %O, optionally let converted be current with generic JavaScript object formatting applied.
887
0
        else if (specifier == "%O"sv) {
888
            // TODO: "generic JavaScript object formatting"
889
0
            converted = current;
890
0
        }
891
        // 6. TODO: process %c
892
0
        else if (specifier == "%c"sv) {
893
            // NOTE: This has no spec yet. `%c` specifiers treat the argument as CSS styling for the log message.
894
0
            add_css_style_to_current_message(TRY(current.to_string(vm)));
895
0
            converted = PrimitiveString::create(vm, String {});
896
0
        }
897
898
        // 7. If any of the previous steps set converted, replace specifier in target with converted.
899
0
        if (converted.has_value())
900
0
            target = TRY_OR_THROW_OOM(vm, target.replace(specifier, TRY(converted->to_string(vm)), ReplaceMode::FirstOnly));
901
0
    }
902
903
    // 7. Let result be a list containing target together with the elements of args starting from the third onward.
904
0
    MarkedVector<Value> result { vm.heap() };
905
0
    result.ensure_capacity(args.size() - 1);
906
0
    result.empend(PrimitiveString::create(vm, move(target)));
907
0
    for (size_t i = 2; i < args.size(); ++i)
908
0
        result.unchecked_append(args[i]);
909
910
    // 8. Return Formatter(result).
911
0
    return formatter(result);
912
0
}
913
914
ThrowCompletionOr<String> ConsoleClient::generically_format_values(MarkedVector<Value> const& values)
915
0
{
916
0
    AllocatingMemoryStream stream;
917
0
    auto& vm = m_console->realm().vm();
918
0
    PrintContext ctx { vm, stream, true };
919
0
    bool first = true;
920
0
    for (auto const& value : values) {
921
0
        if (!first)
922
0
            TRY_OR_THROW_OOM(vm, stream.write_until_depleted(" "sv.bytes()));
923
0
        TRY_OR_THROW_OOM(vm, JS::print(value, ctx));
924
0
        first = false;
925
0
    }
926
    // FIXME: Is it possible we could end up serializing objects to invalid UTF-8?
927
0
    return TRY_OR_THROW_OOM(vm, String::from_stream(stream, stream.used_buffer_size()));
928
0
}
929
930
}