Serenity Operating System
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 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8
9#include <AK/MemoryStream.h>
10#include <LibJS/Console.h>
11#include <LibJS/Print.h>
12#include <LibJS/Runtime/AbstractOperations.h>
13#include <LibJS/Runtime/Completion.h>
14#include <LibJS/Runtime/StringConstructor.h>
15#include <LibJS/Runtime/Temporal/Duration.h>
16#include <LibJS/Runtime/ThrowableStringBuilder.h>
17
18namespace JS {
19
20Console::Console(Realm& realm)
21 : m_realm(realm)
22{
23}
24
25// 1.1.3. debug(...data), https://console.spec.whatwg.org/#debug
26ThrowCompletionOr<Value> Console::debug()
27{
28 // 1. Perform Logger("debug", data).
29 if (m_client) {
30 auto data = vm_arguments();
31 return m_client->logger(LogLevel::Debug, data);
32 }
33 return js_undefined();
34}
35
36// 1.1.4. error(...data), https://console.spec.whatwg.org/#error
37ThrowCompletionOr<Value> Console::error()
38{
39 // 1. Perform Logger("error", data).
40 if (m_client) {
41 auto data = vm_arguments();
42 return m_client->logger(LogLevel::Error, data);
43 }
44 return js_undefined();
45}
46
47// 1.1.5. info(...data), https://console.spec.whatwg.org/#info
48ThrowCompletionOr<Value> Console::info()
49{
50 // 1. Perform Logger("info", data).
51 if (m_client) {
52 auto data = vm_arguments();
53 return m_client->logger(LogLevel::Info, data);
54 }
55 return js_undefined();
56}
57
58// 1.1.6. log(...data), https://console.spec.whatwg.org/#log
59ThrowCompletionOr<Value> Console::log()
60{
61 // 1. Perform Logger("log", data).
62 if (m_client) {
63 auto data = vm_arguments();
64 return m_client->logger(LogLevel::Log, data);
65 }
66 return js_undefined();
67}
68
69// 1.1.9. warn(...data), https://console.spec.whatwg.org/#warn
70ThrowCompletionOr<Value> Console::warn()
71{
72 // 1. Perform Logger("warn", data).
73 if (m_client) {
74 auto data = vm_arguments();
75 return m_client->logger(LogLevel::Warn, data);
76 }
77 return js_undefined();
78}
79
80// 1.1.2. clear(), https://console.spec.whatwg.org/#clear
81Value Console::clear()
82{
83 // 1. Empty the appropriate group stack.
84 m_group_stack.clear();
85
86 // 2. If possible for the environment, clear the console. (Otherwise, do nothing.)
87 if (m_client)
88 m_client->clear();
89 return js_undefined();
90}
91
92// 1.1.8. trace(...data), https://console.spec.whatwg.org/#trace
93ThrowCompletionOr<Value> Console::trace()
94{
95 if (!m_client)
96 return js_undefined();
97
98 auto& vm = realm().vm();
99
100 // 1. Let trace be some implementation-specific, potentially-interactive representation of the callstack from where this function was called.
101 Console::Trace trace;
102 auto& execution_context_stack = vm.execution_context_stack();
103 // NOTE: -2 to skip the console.trace() execution context
104 for (ssize_t i = execution_context_stack.size() - 2; i >= 0; --i) {
105 auto const& function_name = execution_context_stack[i]->function_name;
106 trace.stack.append(function_name.is_empty()
107 ? TRY_OR_THROW_OOM(vm, "<anonymous>"_string)
108 : TRY_OR_THROW_OOM(vm, String::from_deprecated_string(function_name)));
109 }
110
111 // 2. Optionally, let formattedData be the result of Formatter(data), and incorporate formattedData as a label for trace.
112 if (vm.argument_count() > 0) {
113 auto data = vm_arguments();
114 auto formatted_data = TRY(m_client->formatter(data));
115 trace.label = TRY(value_vector_to_string(formatted_data));
116 }
117
118 // 3. Perform Printer("trace", « trace »).
119 return m_client->printer(Console::LogLevel::Trace, trace);
120}
121
122static ThrowCompletionOr<String> label_or_fallback(VM& vm, StringView fallback)
123{
124 return vm.argument_count() > 0
125 ? vm.argument(0).to_string(vm)
126 : TRY_OR_THROW_OOM(vm, String::from_utf8(fallback));
127}
128
129// 1.2.1. count(label), https://console.spec.whatwg.org/#count
130ThrowCompletionOr<Value> Console::count()
131{
132 auto& vm = realm().vm();
133
134 // NOTE: "default" is the default value in the IDL. https://console.spec.whatwg.org/#ref-for-count
135 auto label = TRY(label_or_fallback(vm, "default"sv));
136
137 // 1. Let map be the associated count map.
138 auto& map = m_counters;
139
140 // 2. If map[label] exists, set map[label] to map[label] + 1.
141 if (auto found = map.find(label); found != map.end()) {
142 map.set(label, found->value + 1);
143 }
144 // 3. Otherwise, set map[label] to 1.
145 else {
146 map.set(label, 1);
147 }
148
149 // 4. Let concat be the concatenation of label, U+003A (:), U+0020 SPACE, and ToString(map[label]).
150 auto concat = TRY_OR_THROW_OOM(vm, String::formatted("{}: {}", label, map.get(label).value()));
151
152 // 5. Perform Logger("count", « concat »).
153 MarkedVector<Value> concat_as_vector { vm.heap() };
154 concat_as_vector.append(PrimitiveString::create(vm, move(concat)));
155 if (m_client)
156 TRY(m_client->logger(LogLevel::Count, concat_as_vector));
157 return js_undefined();
158}
159
160// 1.2.2. countReset(label), https://console.spec.whatwg.org/#countreset
161ThrowCompletionOr<Value> Console::count_reset()
162{
163 auto& vm = realm().vm();
164
165 // NOTE: "default" is the default value in the IDL. https://console.spec.whatwg.org/#ref-for-countreset
166 auto label = TRY(label_or_fallback(vm, "default"sv));
167
168 // 1. Let map be the associated count map.
169 auto& map = m_counters;
170
171 // 2. If map[label] exists, set map[label] to 0.
172 if (auto found = map.find(label); found != map.end()) {
173 map.set(label, 0);
174 }
175 // 3. Otherwise:
176 else {
177 // 1. Let message be a string without any formatting specifiers indicating generically
178 // that the given label does not have an associated count.
179 auto message = TRY_OR_THROW_OOM(vm, String::formatted("\"{}\" doesn't have a count", label));
180 // 2. Perform Logger("countReset", « message »);
181 MarkedVector<Value> message_as_vector { vm.heap() };
182 message_as_vector.append(PrimitiveString::create(vm, move(message)));
183 if (m_client)
184 TRY(m_client->logger(LogLevel::CountReset, message_as_vector));
185 }
186
187 return js_undefined();
188}
189
190// 1.1.1. assert(condition, ...data), https://console.spec.whatwg.org/#assert
191ThrowCompletionOr<Value> Console::assert_()
192{
193 auto& vm = realm().vm();
194
195 // 1. If condition is true, return.
196 auto condition = vm.argument(0).to_boolean();
197 if (condition)
198 return js_undefined();
199
200 // 2. Let message be a string without any formatting specifiers indicating generically an assertion failure (such as "Assertion failed").
201 auto message = MUST_OR_THROW_OOM(PrimitiveString::create(vm, "Assertion failed"sv));
202
203 // NOTE: Assemble `data` from the function arguments.
204 MarkedVector<Value> data { vm.heap() };
205 if (vm.argument_count() > 1) {
206 data.ensure_capacity(vm.argument_count() - 1);
207 for (size_t i = 1; i < vm.argument_count(); ++i) {
208 data.append(vm.argument(i));
209 }
210 }
211
212 // 3. If data is empty, append message to data.
213 if (data.is_empty()) {
214 data.append(message);
215 }
216 // 4. Otherwise:
217 else {
218 // 1. Let first be data[0].
219 auto& first = data[0];
220 // 2. If Type(first) is not String, then prepend message to data.
221 if (!first.is_string()) {
222 data.prepend(message);
223 }
224 // 3. Otherwise:
225 else {
226 // 1. Let concat be the concatenation of message, U+003A (:), U+0020 SPACE, and first.
227 auto concat = TRY_OR_THROW_OOM(vm, String::formatted("{}: {}", TRY(message->utf8_string()), MUST(first.to_string(vm))));
228 // 2. Set data[0] to concat.
229 data[0] = PrimitiveString::create(vm, move(concat));
230 }
231 }
232
233 // 5. Perform Logger("assert", data).
234 if (m_client)
235 TRY(m_client->logger(LogLevel::Assert, data));
236 return js_undefined();
237}
238
239// 1.3.1. group(...data), https://console.spec.whatwg.org/#group
240ThrowCompletionOr<Value> Console::group()
241{
242 auto& vm = realm().vm();
243
244 // 1. Let group be a new group.
245 Group group;
246
247 // 2. If data is not empty, let groupLabel be the result of Formatter(data).
248 String group_label {};
249 auto data = vm_arguments();
250 if (!data.is_empty()) {
251 auto formatted_data = TRY(m_client->formatter(data));
252 group_label = TRY(value_vector_to_string(formatted_data));
253 }
254 // ... Otherwise, let groupLabel be an implementation-chosen label representing a group.
255 else {
256 group_label = TRY_OR_THROW_OOM(vm, "Group"_string);
257 }
258
259 // 3. Incorporate groupLabel as a label for group.
260 group.label = group_label;
261
262 // 4. Optionally, if the environment supports interactive groups, group should be expanded by default.
263 // NOTE: This is handled in Printer.
264
265 // 5. Perform Printer("group", « group »).
266 if (m_client)
267 TRY(m_client->printer(LogLevel::Group, group));
268
269 // 6. Push group onto the appropriate group stack.
270 m_group_stack.append(group);
271
272 return js_undefined();
273}
274
275// 1.3.2. groupCollapsed(...data), https://console.spec.whatwg.org/#groupcollapsed
276ThrowCompletionOr<Value> Console::group_collapsed()
277{
278 auto& vm = realm().vm();
279
280 // 1. Let group be a new group.
281 Group group;
282
283 // 2. If data is not empty, let groupLabel be the result of Formatter(data).
284 String group_label {};
285 auto data = vm_arguments();
286 if (!data.is_empty()) {
287 auto formatted_data = TRY(m_client->formatter(data));
288 group_label = TRY(value_vector_to_string(formatted_data));
289 }
290 // ... Otherwise, let groupLabel be an implementation-chosen label representing a group.
291 else {
292 group_label = TRY_OR_THROW_OOM(vm, "Group"_string);
293 }
294
295 // 3. Incorporate groupLabel as a label for group.
296 group.label = group_label;
297
298 // 4. Optionally, if the environment supports interactive groups, group should be collapsed by default.
299 // NOTE: This is handled in Printer.
300
301 // 5. Perform Printer("groupCollapsed", « group »).
302 if (m_client)
303 TRY(m_client->printer(LogLevel::GroupCollapsed, group));
304
305 // 6. Push group onto the appropriate group stack.
306 m_group_stack.append(group);
307
308 return js_undefined();
309}
310
311// 1.3.3. groupEnd(), https://console.spec.whatwg.org/#groupend
312ThrowCompletionOr<Value> Console::group_end()
313{
314 if (m_group_stack.is_empty())
315 return js_undefined();
316
317 // 1. Pop the last group from the group stack.
318 m_group_stack.take_last();
319 if (m_client)
320 m_client->end_group();
321
322 return js_undefined();
323}
324
325// 1.4.1. time(label), https://console.spec.whatwg.org/#time
326ThrowCompletionOr<Value> Console::time()
327{
328 auto& vm = realm().vm();
329
330 // NOTE: "default" is the default value in the IDL. https://console.spec.whatwg.org/#ref-for-time
331 auto label = TRY(label_or_fallback(vm, "default"sv));
332
333 // 1. If the associated timer table contains an entry with key label, return, optionally reporting
334 // a warning to the console indicating that a timer with label `label` has already been started.
335 if (m_timer_table.contains(label)) {
336 if (m_client) {
337 MarkedVector<Value> timer_already_exists_warning_message_as_vector { vm.heap() };
338
339 auto message = TRY_OR_THROW_OOM(vm, String::formatted("Timer '{}' already exists.", label));
340 timer_already_exists_warning_message_as_vector.append(PrimitiveString::create(vm, move(message)));
341
342 TRY(m_client->printer(LogLevel::Warn, move(timer_already_exists_warning_message_as_vector)));
343 }
344 return js_undefined();
345 }
346
347 // 2. Otherwise, set the value of the entry with key label in the associated timer table to the current time.
348 m_timer_table.set(label, Core::ElapsedTimer::start_new());
349 return js_undefined();
350}
351
352// 1.4.2. timeLog(label, ...data), https://console.spec.whatwg.org/#timelog
353ThrowCompletionOr<Value> Console::time_log()
354{
355 auto& vm = realm().vm();
356
357 // NOTE: "default" is the default value in the IDL. https://console.spec.whatwg.org/#ref-for-timelog
358 auto label = TRY(label_or_fallback(vm, "default"sv));
359
360 // 1. Let timerTable be the associated timer table.
361
362 // 2. Let startTime be timerTable[label].
363 auto maybe_start_time = m_timer_table.find(label);
364
365 // NOTE: Warn if the timer doesn't exist. Not part of the spec yet, but discussed here: https://github.com/whatwg/console/issues/134
366 if (maybe_start_time == m_timer_table.end()) {
367 if (m_client) {
368 MarkedVector<Value> timer_does_not_exist_warning_message_as_vector { vm.heap() };
369
370 auto message = TRY_OR_THROW_OOM(vm, String::formatted("Timer '{}' does not exist.", label));
371 timer_does_not_exist_warning_message_as_vector.append(PrimitiveString::create(vm, move(message)));
372
373 TRY(m_client->printer(LogLevel::Warn, move(timer_does_not_exist_warning_message_as_vector)));
374 }
375 return js_undefined();
376 }
377 auto start_time = maybe_start_time->value;
378
379 // 3. Let duration be a string representing the difference between the current time and startTime, in an implementation-defined format.
380 auto duration = TRY(format_time_since(start_time));
381
382 // 4. Let concat be the concatenation of label, U+003A (:), U+0020 SPACE, and duration.
383 auto concat = TRY_OR_THROW_OOM(vm, String::formatted("{}: {}", label, duration));
384
385 // 5. Prepend concat to data.
386 MarkedVector<Value> data { vm.heap() };
387 data.ensure_capacity(vm.argument_count());
388 data.append(PrimitiveString::create(vm, move(concat)));
389 for (size_t i = 1; i < vm.argument_count(); ++i)
390 data.append(vm.argument(i));
391
392 // 6. Perform Printer("timeLog", data).
393 if (m_client)
394 TRY(m_client->printer(LogLevel::TimeLog, move(data)));
395 return js_undefined();
396}
397
398// 1.4.3. timeEnd(label), https://console.spec.whatwg.org/#timeend
399ThrowCompletionOr<Value> Console::time_end()
400{
401 auto& vm = realm().vm();
402
403 // NOTE: "default" is the default value in the IDL. https://console.spec.whatwg.org/#ref-for-timeend
404 auto label = TRY(label_or_fallback(vm, "default"sv));
405
406 // 1. Let timerTable be the associated timer table.
407
408 // 2. Let startTime be timerTable[label].
409 auto maybe_start_time = m_timer_table.find(label);
410
411 // NOTE: Warn if the timer doesn't exist. Not part of the spec yet, but discussed here: https://github.com/whatwg/console/issues/134
412 if (maybe_start_time == m_timer_table.end()) {
413 if (m_client) {
414 MarkedVector<Value> timer_does_not_exist_warning_message_as_vector { vm.heap() };
415
416 auto message = TRY_OR_THROW_OOM(vm, String::formatted("Timer '{}' does not exist.", label));
417 timer_does_not_exist_warning_message_as_vector.append(PrimitiveString::create(vm, move(message)));
418
419 TRY(m_client->printer(LogLevel::Warn, move(timer_does_not_exist_warning_message_as_vector)));
420 }
421 return js_undefined();
422 }
423 auto start_time = maybe_start_time->value;
424
425 // 3. Remove timerTable[label].
426 m_timer_table.remove(label);
427
428 // 4. Let duration be a string representing the difference between the current time and startTime, in an implementation-defined format.
429 auto duration = TRY(format_time_since(start_time));
430
431 // 5. Let concat be the concatenation of label, U+003A (:), U+0020 SPACE, and duration.
432 auto concat = TRY_OR_THROW_OOM(vm, String::formatted("{}: {}", label, duration));
433
434 // 6. Perform Printer("timeEnd", « concat »).
435 if (m_client) {
436 MarkedVector<Value> concat_as_vector { vm.heap() };
437 concat_as_vector.append(PrimitiveString::create(vm, move(concat)));
438 TRY(m_client->printer(LogLevel::TimeEnd, move(concat_as_vector)));
439 }
440 return js_undefined();
441}
442
443MarkedVector<Value> Console::vm_arguments()
444{
445 auto& vm = realm().vm();
446
447 MarkedVector<Value> arguments { vm.heap() };
448 arguments.ensure_capacity(vm.argument_count());
449 for (size_t i = 0; i < vm.argument_count(); ++i) {
450 arguments.append(vm.argument(i));
451 }
452 return arguments;
453}
454
455void Console::output_debug_message(LogLevel log_level, String const& output) const
456{
457 switch (log_level) {
458 case Console::LogLevel::Debug:
459 dbgln("\033[32;1m(js debug)\033[0m {}", output);
460 break;
461 case Console::LogLevel::Error:
462 dbgln("\033[32;1m(js error)\033[0m {}", output);
463 break;
464 case Console::LogLevel::Info:
465 dbgln("\033[32;1m(js info)\033[0m {}", output);
466 break;
467 case Console::LogLevel::Log:
468 dbgln("\033[32;1m(js log)\033[0m {}", output);
469 break;
470 case Console::LogLevel::Warn:
471 dbgln("\033[32;1m(js warn)\033[0m {}", output);
472 break;
473 default:
474 dbgln("\033[32;1m(js)\033[0m {}", output);
475 break;
476 }
477}
478
479void Console::report_exception(JS::Error const& exception, bool in_promise) const
480{
481 if (m_client)
482 m_client->report_exception(exception, in_promise);
483}
484
485ThrowCompletionOr<String> Console::value_vector_to_string(MarkedVector<Value> const& values)
486{
487 auto& vm = realm().vm();
488 ThrowableStringBuilder builder(vm);
489
490 for (auto const& item : values) {
491 if (!builder.is_empty())
492 MUST_OR_THROW_OOM(builder.append(' '));
493
494 MUST_OR_THROW_OOM(builder.append(TRY(item.to_string(vm))));
495 }
496
497 return builder.to_string();
498}
499
500ThrowCompletionOr<String> Console::format_time_since(Core::ElapsedTimer timer)
501{
502 auto& vm = realm().vm();
503
504 auto elapsed_ms = timer.elapsed_time().to_milliseconds();
505 auto duration = TRY(Temporal::balance_duration(vm, 0, 0, 0, 0, elapsed_ms, 0, "0"_sbigint, "year"sv));
506
507 auto append = [&](ThrowableStringBuilder& builder, auto format, auto number) -> ThrowCompletionOr<void> {
508 if (!builder.is_empty())
509 MUST_OR_THROW_OOM(builder.append(' '));
510 MUST_OR_THROW_OOM(builder.appendff(format, number));
511 return {};
512 };
513
514 ThrowableStringBuilder builder(vm);
515
516 if (duration.days > 0)
517 MUST_OR_THROW_OOM(append(builder, "{:.0} day(s)"sv, duration.days));
518 if (duration.hours > 0)
519 MUST_OR_THROW_OOM(append(builder, "{:.0} hour(s)"sv, duration.hours));
520 if (duration.minutes > 0)
521 MUST_OR_THROW_OOM(append(builder, "{:.0} minute(s)"sv, duration.minutes));
522 if (duration.seconds > 0 || duration.milliseconds > 0) {
523 double combined_seconds = duration.seconds + (0.001 * duration.milliseconds);
524 MUST_OR_THROW_OOM(append(builder, "{:.3} seconds"sv, combined_seconds));
525 }
526
527 return builder.to_string();
528}
529
530// 2.1. Logger(logLevel, args), https://console.spec.whatwg.org/#logger
531ThrowCompletionOr<Value> ConsoleClient::logger(Console::LogLevel log_level, MarkedVector<Value> const& args)
532{
533 auto& vm = m_console.realm().vm();
534
535 // 1. If args is empty, return.
536 if (args.is_empty())
537 return js_undefined();
538
539 // 2. Let first be args[0].
540 auto first = args[0];
541
542 // 3. Let rest be all elements following first in args.
543 size_t rest_size = args.size() - 1;
544
545 // 4. If rest is empty, perform Printer(logLevel, « first ») and return.
546 if (rest_size == 0) {
547 MarkedVector<Value> first_as_vector { vm.heap() };
548 first_as_vector.append(first);
549 return printer(log_level, move(first_as_vector));
550 }
551
552 // 5. Otherwise, perform Printer(logLevel, Formatter(args)).
553 else {
554 auto formatted = TRY(formatter(args));
555 TRY(printer(log_level, formatted));
556 }
557
558 // 6. Return undefined.
559 return js_undefined();
560}
561
562// 2.2. Formatter(args), https://console.spec.whatwg.org/#formatter
563ThrowCompletionOr<MarkedVector<Value>> ConsoleClient::formatter(MarkedVector<Value> const& args)
564{
565 auto& realm = m_console.realm();
566 auto& vm = realm.vm();
567
568 // 1. If args’s size is 1, return args.
569 if (args.size() == 1)
570 return args;
571
572 // 2. Let target be the first element of args.
573 auto target = (!args.is_empty()) ? TRY(args.first().to_string(vm)) : String {};
574
575 // 3. Let current be the second element of args.
576 auto current = (args.size() > 1) ? args[1] : js_undefined();
577
578 // 4. Find the first possible format specifier specifier, from the left to the right in target.
579 auto find_specifier = [](StringView target) -> Optional<StringView> {
580 size_t start_index = 0;
581 while (start_index < target.length()) {
582 auto maybe_index = target.find('%');
583 if (!maybe_index.has_value())
584 return {};
585
586 auto index = maybe_index.value();
587 if (index + 1 >= target.length())
588 return {};
589
590 switch (target[index + 1]) {
591 case 'c':
592 case 'd':
593 case 'f':
594 case 'i':
595 case 'o':
596 case 'O':
597 case 's':
598 return target.substring_view(index, 2);
599 }
600
601 start_index = index + 1;
602 }
603 return {};
604 };
605 auto maybe_specifier = find_specifier(target);
606
607 // 5. If no format specifier was found, return args.
608 if (!maybe_specifier.has_value()) {
609 return args;
610 }
611 // 6. Otherwise:
612 else {
613 auto specifier = maybe_specifier.release_value();
614 Optional<Value> converted;
615
616 // 1. If specifier is %s, let converted be the result of Call(%String%, undefined, « current »).
617 if (specifier == "%s"sv) {
618 converted = TRY(call(vm, realm.intrinsics().string_constructor(), js_undefined(), current));
619 }
620 // 2. If specifier is %d or %i:
621 else if (specifier.is_one_of("%d"sv, "%i"sv)) {
622 // 1. If Type(current) is Symbol, let converted be NaN
623 if (current.is_symbol()) {
624 converted = js_nan();
625 }
626 // 2. Otherwise, let converted be the result of Call(%parseInt%, undefined, « current, 10 »).
627 else {
628 converted = TRY(call(vm, realm.intrinsics().parse_int_function(), js_undefined(), current, Value { 10 }));
629 }
630 }
631 // 3. If specifier is %f:
632 else if (specifier == "%f"sv) {
633 // 1. If Type(current) is Symbol, let converted be NaN
634 if (current.is_symbol()) {
635 converted = js_nan();
636 }
637 // 2. Otherwise, let converted be the result of Call(% parseFloat %, undefined, « current »).
638 else {
639 converted = TRY(call(vm, realm.intrinsics().parse_float_function(), js_undefined(), current));
640 }
641 }
642 // 4. If specifier is %o, optionally let converted be current with optimally useful formatting applied.
643 else if (specifier == "%o"sv) {
644 // TODO: "Optimally-useful formatting"
645 converted = current;
646 }
647 // 5. If specifier is %O, optionally let converted be current with generic JavaScript object formatting applied.
648 else if (specifier == "%O"sv) {
649 // TODO: "generic JavaScript object formatting"
650 converted = current;
651 }
652 // 6. TODO: process %c
653 else if (specifier == "%c"sv) {
654 // NOTE: This has no spec yet. `%c` specifiers treat the argument as CSS styling for the log message.
655 add_css_style_to_current_message(TRY(current.to_string(vm)));
656 converted = PrimitiveString::create(vm, String {});
657 }
658
659 // 7. If any of the previous steps set converted, replace specifier in target with converted.
660 if (converted.has_value())
661 target = TRY_OR_THROW_OOM(vm, target.replace(specifier, TRY(converted->to_string(vm)), ReplaceMode::FirstOnly));
662 }
663
664 // 7. Let result be a list containing target together with the elements of args starting from the third onward.
665 MarkedVector<Value> result { vm.heap() };
666 result.ensure_capacity(args.size() - 1);
667 result.empend(PrimitiveString::create(vm, move(target)));
668 for (size_t i = 2; i < args.size(); ++i)
669 result.unchecked_append(args[i]);
670
671 // 8. Return Formatter(result).
672 return formatter(result);
673}
674
675ThrowCompletionOr<String> ConsoleClient::generically_format_values(MarkedVector<Value> const& values)
676{
677 AllocatingMemoryStream stream;
678 auto& vm = m_console.realm().vm();
679 PrintContext ctx { vm, stream, true };
680 bool first = true;
681 for (auto const& value : values) {
682 if (!first)
683 TRY_OR_THROW_OOM(vm, stream.write_until_depleted(" "sv.bytes()));
684 TRY_OR_THROW_OOM(vm, JS::print(value, ctx));
685 first = false;
686 }
687 // FIXME: Is it possible we could end up serializing objects to invalid UTF-8?
688 return TRY_OR_THROW_OOM(vm, String::from_stream(stream, stream.used_buffer_size()));
689}
690
691}