Serenity Operating System
at master 691 lines 25 kB view raw
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}