/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 | | } |