Console.cpp 33 KB

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