boa_runtime/console/
mod.rs

1//! Boa's implementation of JavaScript's `console` Web API object.
2//!
3//! The `console` object can be accessed from any global object.
4//!
5//! The specifics of how it works varies from browser to browser, but there is a de facto set of features that are typically provided.
6//!
7//! More information:
8//!  - [MDN documentation][mdn]
9//!  - [WHATWG `console` specification][spec]
10//!
11//! [spec]: https://console.spec.whatwg.org/
12//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Console
13
14#[cfg(test)]
15pub(crate) mod tests;
16
17use boa_engine::JsVariant;
18use boa_engine::property::Attribute;
19use boa_engine::{
20    Context, JsArgs, JsData, JsError, JsResult, JsString, JsSymbol, js_str, js_string,
21    native_function::NativeFunction,
22    object::{JsObject, ObjectInitializer},
23    value::{JsValue, Numeric},
24};
25use boa_gc::{Finalize, Trace};
26use rustc_hash::FxHashMap;
27use std::{
28    cell::RefCell, collections::hash_map::Entry, fmt::Write as _, io::Write, rc::Rc,
29    time::SystemTime,
30};
31
32/// A trait that can be used to forward console logs to an implementation.
33pub trait Logger: Trace {
34    /// Log a trace message (`console.trace`). By default, passes the message and the
35    /// code block names of each stack trace frame to `log`.
36    ///
37    /// # Errors
38    /// Returning an error will throw an exception in JavaScript.
39    fn trace(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
40        self.log(msg, state, context)?;
41
42        let stack_trace_dump = context
43            .stack_trace()
44            .map(|frame| frame.code_block().name())
45            .map(JsString::to_std_string_escaped)
46            .collect::<Vec<_>>();
47
48        for frame in stack_trace_dump {
49            self.log(frame, state, context)?;
50        }
51
52        Ok(())
53    }
54
55    /// Log a debug message (`console.debug`). By default, passes the message to `log`.
56    ///
57    /// # Errors
58    /// Returning an error will throw an exception in JavaScript.
59    fn debug(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
60        self.log(msg, state, context)
61    }
62
63    /// Log a log message (`console.log`).
64    ///
65    /// # Errors
66    /// Returning an error will throw an exception in JavaScript.
67    fn log(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()>;
68
69    /// Log an info message (`console.info`).
70    ///
71    /// # Errors
72    /// Returning an error will throw an exception in JavaScript.
73    fn info(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()>;
74
75    /// Log a warning message (`console.warn`).
76    ///
77    /// # Errors
78    /// Returning an error will throw an exception in JavaScript.
79    fn warn(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()>;
80
81    /// Log an error message (`console.error`).
82    ///
83    /// # Errors
84    /// Returning an error will throw an exception in JavaScript.
85    fn error(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()>;
86}
87
88/// The default implementation for logging from the console.
89///
90/// Implements the [`Logger`] trait and output errors to stderr and all
91/// the others to stdout. Will add indentation based on the number of
92/// groups.
93#[derive(Debug, Trace, Finalize)]
94pub struct DefaultLogger;
95
96impl Logger for DefaultLogger {
97    #[inline]
98    fn log(&self, msg: String, state: &ConsoleState, _context: &mut Context) -> JsResult<()> {
99        let indent = state.indent();
100        writeln!(std::io::stdout(), "{msg:>indent$}").map_err(JsError::from_rust)
101    }
102
103    #[inline]
104    fn info(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
105        self.log(msg, state, context)
106    }
107
108    #[inline]
109    fn warn(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
110        self.log(msg, state, context)
111    }
112
113    #[inline]
114    fn error(&self, msg: String, state: &ConsoleState, _context: &mut Context) -> JsResult<()> {
115        let indent = state.indent();
116        writeln!(std::io::stderr(), "{msg:>indent$}").map_err(JsError::from_rust)
117    }
118}
119
120/// A logger that drops all logging. Useful for testing.
121#[derive(Debug, Trace, Finalize)]
122pub struct NullLogger;
123
124impl Logger for NullLogger {
125    #[inline]
126    fn log(&self, _: String, _: &ConsoleState, _: &mut Context) -> JsResult<()> {
127        Ok(())
128    }
129
130    #[inline]
131    fn info(&self, _: String, _: &ConsoleState, _: &mut Context) -> JsResult<()> {
132        Ok(())
133    }
134
135    #[inline]
136    fn warn(&self, _: String, _: &ConsoleState, _: &mut Context) -> JsResult<()> {
137        Ok(())
138    }
139
140    #[inline]
141    fn error(&self, _: String, _: &ConsoleState, _: &mut Context) -> JsResult<()> {
142        Ok(())
143    }
144}
145
146/// This represents the `console` formatter.
147fn formatter(data: &[JsValue], context: &mut Context) -> JsResult<String> {
148    fn to_string(value: &JsValue, _context: &mut Context) -> String {
149        match value.variant() {
150            JsVariant::String(s) => s.to_std_string_escaped(),
151            _ => value.display().to_string(),
152        }
153    }
154
155    match data {
156        [] => Ok(String::new()),
157        [val] => Ok(to_string(val, context)),
158        data => {
159            let mut formatted = String::new();
160            let mut arg_index = 1;
161            let target = data
162                .get_or_undefined(0)
163                .to_string(context)?
164                .to_std_string_escaped();
165            let mut chars = target.chars();
166            while let Some(c) = chars.next() {
167                if c == '%' {
168                    let fmt = chars.next().unwrap_or('%');
169                    match fmt {
170                        /* integer */
171                        'd' | 'i' => {
172                            let arg = match data.get_or_undefined(arg_index).to_numeric(context)? {
173                                Numeric::Number(r) => (r.floor() + 0.0).to_string(),
174                                Numeric::BigInt(int) => int.to_string(),
175                            };
176                            formatted.push_str(&arg);
177                            arg_index += 1;
178                        }
179                        /* float */
180                        'f' => {
181                            let arg = data.get_or_undefined(arg_index).to_number(context)?;
182                            let _ = write!(formatted, "{arg:.6}");
183                            arg_index += 1;
184                        }
185                        /* object, FIXME: how to render this properly? */
186                        'o' | 'O' => {
187                            let arg = data.get_or_undefined(arg_index);
188                            formatted.push_str(&arg.display().to_string());
189                            arg_index += 1;
190                        }
191                        /* string */
192                        's' => {
193                            let arg = data.get_or_undefined(arg_index);
194
195                            // If a JS value implements `toString()`, call it.
196                            let mut written = false;
197                            if let Some(obj) = arg.as_object()
198                                && let Ok(to_string) = obj.get(js_string!("toString"), context)
199                                && let Some(to_string_fn) = to_string.as_function()
200                            {
201                                let arg =
202                                    to_string_fn.call(arg, &[], context)?.to_string(context)?;
203                                formatted.push_str(&arg.to_std_string_escaped());
204                                written = true;
205                            }
206
207                            if !written {
208                                let arg = arg.to_string(context)?.to_std_string_escaped();
209                                formatted.push_str(&arg);
210                            }
211
212                            arg_index += 1;
213                        }
214                        '%' => formatted.push('%'),
215                        /* TODO: %c is not implemented */
216                        c => {
217                            formatted.push('%');
218                            formatted.push(c);
219                        }
220                    }
221                } else {
222                    formatted.push(c);
223                }
224            }
225
226            /* unformatted data */
227            for rest in data.iter().skip(arg_index) {
228                formatted.push(' ');
229                formatted.push_str(&to_string(rest, context));
230            }
231
232            Ok(formatted)
233        }
234    }
235}
236
237/// The current state of the console, passed to the logger backend.
238/// This should not be copied or cloned. References are only valid
239/// for the current logging call.
240#[derive(Debug, Default, Trace, Finalize)]
241pub struct ConsoleState {
242    /// The map of console counters, used in `console.count()`.
243    count_map: FxHashMap<JsString, u32>,
244
245    /// The map of console timers, used in `console.time`, `console.timeLog`
246    /// and `console.timeEnd`.
247    timer_map: FxHashMap<JsString, u128>,
248
249    /// The current list of groups. Groups should be indented, but some logging
250    /// libraries may want to use them in a different way.
251    groups: Vec<String>,
252}
253
254impl ConsoleState {
255    /// Returns the indentation level that should be applied to logging.
256    #[must_use]
257    pub fn indent(&self) -> usize {
258        2 * self.groups.len()
259    }
260
261    /// Returns the current list of groups.
262    #[must_use]
263    pub fn groups(&self) -> &Vec<String> {
264        &self.groups
265    }
266
267    /// Returns the count map.
268    #[must_use]
269    pub fn count_map(&self) -> &FxHashMap<JsString, u32> {
270        &self.count_map
271    }
272
273    /// Returns the timer map.
274    #[must_use]
275    pub fn timer_map(&self) -> &FxHashMap<JsString, u128> {
276        &self.timer_map
277    }
278}
279
280/// This is the internal console object state.
281#[derive(Debug, Default, Trace, Finalize, JsData)]
282pub struct Console {
283    state: ConsoleState,
284}
285
286impl Console {
287    /// Name of the built-in `console` property.
288    pub const NAME: JsString = js_string!("console");
289
290    /// Modify the context to include the `console` object.
291    ///
292    /// # Errors
293    /// This function will return an error if the property cannot be defined on the global object.
294    pub fn register_with_logger<L>(logger: L, context: &mut Context) -> JsResult<()>
295    where
296        L: Logger + 'static,
297    {
298        let console = Self::init_with_logger(logger, context);
299        context.register_global_property(
300            Self::NAME,
301            console,
302            Attribute::WRITABLE | Attribute::CONFIGURABLE,
303        )?;
304
305        Ok(())
306    }
307
308    /// Initializes the `console` with a special logger.
309    #[allow(clippy::too_many_lines)]
310    pub fn init_with_logger<L>(logger: L, context: &mut Context) -> JsObject
311    where
312        L: Logger + 'static,
313    {
314        fn console_method<L: Logger + 'static>(
315            f: fn(&JsValue, &[JsValue], &Console, &L, &mut Context) -> JsResult<JsValue>,
316            state: Rc<RefCell<Console>>,
317            logger: Rc<L>,
318        ) -> NativeFunction {
319            // SAFETY: `Console` doesn't contain types that need tracing.
320            unsafe {
321                NativeFunction::from_closure(move |this, args, context| {
322                    f(this, args, &state.borrow(), &logger, context)
323                })
324            }
325        }
326        fn console_method_mut<L: Logger + 'static>(
327            f: fn(&JsValue, &[JsValue], &mut Console, &L, &mut Context) -> JsResult<JsValue>,
328            state: Rc<RefCell<Console>>,
329            logger: Rc<L>,
330        ) -> NativeFunction {
331            // SAFETY: `Console` doesn't contain types that need tracing.
332            unsafe {
333                NativeFunction::from_closure(move |this, args, context| {
334                    f(this, args, &mut state.borrow_mut(), &logger, context)
335                })
336            }
337        }
338
339        let state = Rc::new(RefCell::new(Self::default()));
340        let logger = Rc::new(logger);
341
342        ObjectInitializer::with_native_data_and_proto(
343            Self::default(),
344            JsObject::with_object_proto(context.realm().intrinsics()),
345            context,
346        )
347        .property(
348            JsSymbol::to_string_tag(),
349            Self::NAME,
350            Attribute::CONFIGURABLE,
351        )
352        .function(
353            console_method(Self::assert, state.clone(), logger.clone()),
354            js_string!("assert"),
355            0,
356        )
357        .function(
358            console_method_mut(Self::clear, state.clone(), logger.clone()),
359            js_string!("clear"),
360            0,
361        )
362        .function(
363            console_method(Self::debug, state.clone(), logger.clone()),
364            js_string!("debug"),
365            0,
366        )
367        .function(
368            console_method(Self::error, state.clone(), logger.clone()),
369            js_string!("error"),
370            0,
371        )
372        .function(
373            console_method(Self::info, state.clone(), logger.clone()),
374            js_string!("info"),
375            0,
376        )
377        .function(
378            console_method(Self::log, state.clone(), logger.clone()),
379            js_string!("log"),
380            0,
381        )
382        .function(
383            console_method(Self::trace, state.clone(), logger.clone()),
384            js_string!("trace"),
385            0,
386        )
387        .function(
388            console_method(Self::warn, state.clone(), logger.clone()),
389            js_string!("warn"),
390            0,
391        )
392        .function(
393            console_method_mut(Self::count, state.clone(), logger.clone()),
394            js_string!("count"),
395            0,
396        )
397        .function(
398            console_method_mut(Self::count_reset, state.clone(), logger.clone()),
399            js_string!("countReset"),
400            0,
401        )
402        .function(
403            console_method_mut(Self::group, state.clone(), logger.clone()),
404            js_string!("group"),
405            0,
406        )
407        .function(
408            console_method_mut(Self::group_collapsed, state.clone(), logger.clone()),
409            js_string!("groupCollapsed"),
410            0,
411        )
412        .function(
413            console_method_mut(Self::group_end, state.clone(), logger.clone()),
414            js_string!("groupEnd"),
415            0,
416        )
417        .function(
418            console_method_mut(Self::time, state.clone(), logger.clone()),
419            js_string!("time"),
420            0,
421        )
422        .function(
423            console_method(Self::time_log, state.clone(), logger.clone()),
424            js_string!("timeLog"),
425            0,
426        )
427        .function(
428            console_method_mut(Self::time_end, state.clone(), logger.clone()),
429            js_string!("timeEnd"),
430            0,
431        )
432        .function(
433            console_method(Self::dir, state.clone(), logger.clone()),
434            js_string!("dir"),
435            0,
436        )
437        .function(
438            console_method(Self::dir, state, logger.clone()),
439            js_string!("dirxml"),
440            0,
441        )
442        .build()
443    }
444
445    /// Initializes the `console` built-in object.
446    pub fn init(context: &mut Context) -> JsObject {
447        Self::init_with_logger(DefaultLogger, context)
448    }
449
450    /// `console.assert(condition, ...data)`
451    ///
452    /// Prints a JavaScript value to the standard error if first argument evaluates to `false` or there
453    /// were no arguments.
454    ///
455    /// More information:
456    ///  - [MDN documentation][mdn]
457    ///  - [WHATWG `console` specification][spec]
458    ///
459    /// [spec]: https://console.spec.whatwg.org/#assert
460    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/assert
461    fn assert(
462        _: &JsValue,
463        args: &[JsValue],
464        console: &Self,
465        logger: &impl Logger,
466        context: &mut Context,
467    ) -> JsResult<JsValue> {
468        let assertion = args.first().is_some_and(JsValue::to_boolean);
469
470        if !assertion {
471            let mut args: Vec<JsValue> = args.iter().skip(1).cloned().collect();
472            let message = js_string!("Assertion failed");
473            if args.is_empty() {
474                args.push(JsValue::new(message));
475            } else if !args[0].is_string() {
476                args.insert(0, JsValue::new(message));
477            } else {
478                let value = JsString::from(args[0].display().to_string());
479                let concat = js_string!(message.as_str(), js_str!(": "), &value);
480                args[0] = JsValue::new(concat);
481            }
482
483            logger.error(formatter(&args, context)?, &console.state, context)?;
484        }
485
486        Ok(JsValue::undefined())
487    }
488
489    /// `console.clear()`
490    ///
491    /// Removes all groups and clears console if possible.
492    ///
493    /// More information:
494    ///  - [MDN documentation][mdn]
495    ///  - [WHATWG `console` specification][spec]
496    ///
497    /// [spec]: https://console.spec.whatwg.org/#clear
498    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/clear
499    #[allow(clippy::unnecessary_wraps)]
500    fn clear(
501        _: &JsValue,
502        _: &[JsValue],
503        console: &mut Self,
504        _: &impl Logger,
505        _: &mut Context,
506    ) -> JsResult<JsValue> {
507        console.state.groups.clear();
508        Ok(JsValue::undefined())
509    }
510
511    /// `console.debug(...data)`
512    ///
513    /// Prints a JavaScript values with "debug" logLevel.
514    ///
515    /// More information:
516    ///  - [MDN documentation][mdn]
517    ///  - [WHATWG `console` specification][spec]
518    ///
519    /// [spec]: https://console.spec.whatwg.org/#debug
520    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/debug
521    fn debug(
522        _: &JsValue,
523        args: &[JsValue],
524        console: &Self,
525        logger: &impl Logger,
526        context: &mut Context,
527    ) -> JsResult<JsValue> {
528        logger.debug(formatter(args, context)?, &console.state, context)?;
529        Ok(JsValue::undefined())
530    }
531
532    /// `console.error(...data)`
533    ///
534    /// Prints a JavaScript values with "error" logLevel.
535    ///
536    /// More information:
537    ///  - [MDN documentation][mdn]
538    ///  - [WHATWG `console` specification][spec]
539    ///
540    /// [spec]: https://console.spec.whatwg.org/#error
541    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/error
542    fn error(
543        _: &JsValue,
544        args: &[JsValue],
545        console: &Self,
546        logger: &impl Logger,
547        context: &mut Context,
548    ) -> JsResult<JsValue> {
549        logger.error(formatter(args, context)?, &console.state, context)?;
550        Ok(JsValue::undefined())
551    }
552
553    /// `console.info(...data)`
554    ///
555    /// Prints a JavaScript values with "info" logLevel.
556    ///
557    /// More information:
558    ///  - [MDN documentation][mdn]
559    ///  - [WHATWG `console` specification][spec]
560    ///
561    /// [spec]: https://console.spec.whatwg.org/#info
562    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/info
563    fn info(
564        _: &JsValue,
565        args: &[JsValue],
566        console: &Self,
567        logger: &impl Logger,
568        context: &mut Context,
569    ) -> JsResult<JsValue> {
570        logger.info(formatter(args, context)?, &console.state, context)?;
571        Ok(JsValue::undefined())
572    }
573
574    /// `console.log(...data)`
575    ///
576    /// Prints a JavaScript values with "log" logLevel.
577    ///
578    /// More information:
579    ///  - [MDN documentation][mdn]
580    ///  - [WHATWG `console` specification][spec]
581    ///
582    /// [spec]: https://console.spec.whatwg.org/#log
583    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/log
584    fn log(
585        _: &JsValue,
586        args: &[JsValue],
587        console: &Self,
588        logger: &impl Logger,
589        context: &mut Context,
590    ) -> JsResult<JsValue> {
591        logger.log(formatter(args, context)?, &console.state, context)?;
592        Ok(JsValue::undefined())
593    }
594
595    /// `console.trace(...data)`
596    ///
597    /// Prints a stack trace with "trace" logLevel, optionally labelled by data.
598    ///
599    /// More information:
600    ///  - [MDN documentation][mdn]
601    ///  - [WHATWG `console` specification][spec]
602    ///
603    /// [spec]: https://console.spec.whatwg.org/#trace
604    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/trace
605    fn trace(
606        _: &JsValue,
607        args: &[JsValue],
608        console: &Self,
609        logger: &impl Logger,
610        context: &mut Context,
611    ) -> JsResult<JsValue> {
612        Logger::trace(logger, formatter(args, context)?, &console.state, context)?;
613        Ok(JsValue::undefined())
614    }
615
616    /// `console.warn(...data)`
617    ///
618    /// Prints a JavaScript values with "warn" logLevel.
619    ///
620    /// More information:
621    ///  - [MDN documentation][mdn]
622    ///  - [WHATWG `console` specification][spec]
623    ///
624    /// [spec]: https://console.spec.whatwg.org/#warn
625    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/warn
626    fn warn(
627        _: &JsValue,
628        args: &[JsValue],
629        console: &Self,
630        logger: &impl Logger,
631        context: &mut Context,
632    ) -> JsResult<JsValue> {
633        logger.warn(formatter(args, context)?, &console.state, context)?;
634        Ok(JsValue::undefined())
635    }
636
637    /// `console.count(label)`
638    ///
639    /// Prints number of times the function was called with that particular label.
640    ///
641    /// More information:
642    ///  - [MDN documentation][mdn]
643    ///  - [WHATWG `console` specification][spec]
644    ///
645    /// [spec]: https://console.spec.whatwg.org/#count
646    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/count
647    fn count(
648        _: &JsValue,
649        args: &[JsValue],
650        console: &mut Self,
651        logger: &impl Logger,
652        context: &mut Context,
653    ) -> JsResult<JsValue> {
654        let label = match args.first() {
655            Some(value) => value.to_string(context)?,
656            None => "default".into(),
657        };
658
659        let msg = format!("count {}:", label.to_std_string_escaped());
660        let c = console.state.count_map.entry(label).or_insert(0);
661        *c += 1;
662
663        logger.info(format!("{msg} {c}"), &console.state, context)?;
664        Ok(JsValue::undefined())
665    }
666
667    /// `console.countReset(label)`
668    ///
669    /// Resets the counter for label.
670    ///
671    /// More information:
672    ///  - [MDN documentation][mdn]
673    ///  - [WHATWG `console` specification][spec]
674    ///
675    /// [spec]: https://console.spec.whatwg.org/#countreset
676    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/countReset
677    fn count_reset(
678        _: &JsValue,
679        args: &[JsValue],
680        console: &mut Self,
681        logger: &impl Logger,
682        context: &mut Context,
683    ) -> JsResult<JsValue> {
684        let label = match args.first() {
685            Some(value) => value.to_string(context)?,
686            None => "default".into(),
687        };
688
689        console.state.count_map.remove(&label);
690
691        logger.warn(
692            format!("countReset {}", label.to_std_string_escaped()),
693            &console.state,
694            context,
695        )?;
696
697        Ok(JsValue::undefined())
698    }
699
700    /// Returns current system time in ms.
701    fn system_time_in_ms() -> u128 {
702        let now = SystemTime::now();
703        now.duration_since(SystemTime::UNIX_EPOCH)
704            .expect("negative duration")
705            .as_millis()
706    }
707
708    /// `console.time(label)`
709    ///
710    /// Starts the timer for given label.
711    ///
712    /// More information:
713    ///  - [MDN documentation][mdn]
714    ///  - [WHATWG `console` specification][spec]
715    ///
716    /// [spec]: https://console.spec.whatwg.org/#time
717    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/time
718    fn time(
719        _: &JsValue,
720        args: &[JsValue],
721        console: &mut Self,
722        logger: &impl Logger,
723        context: &mut Context,
724    ) -> JsResult<JsValue> {
725        let label = match args.first() {
726            Some(value) => value.to_string(context)?,
727            None => "default".into(),
728        };
729
730        if let Entry::Vacant(e) = console.state.timer_map.entry(label.clone()) {
731            let time = Self::system_time_in_ms();
732            e.insert(time);
733        } else {
734            logger.warn(
735                format!("Timer '{}' already exist", label.to_std_string_escaped()),
736                &console.state,
737                context,
738            )?;
739        }
740
741        Ok(JsValue::undefined())
742    }
743
744    /// `console.timeLog(label, ...data)`
745    ///
746    /// Prints elapsed time for timer with given label.
747    ///
748    /// More information:
749    ///  - [MDN documentation][mdn]
750    ///  - [WHATWG `console` specification][spec]
751    ///
752    /// [spec]: https://console.spec.whatwg.org/#timelog
753    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/timeLog
754    fn time_log(
755        _: &JsValue,
756        args: &[JsValue],
757        console: &Self,
758        logger: &impl Logger,
759        context: &mut Context,
760    ) -> JsResult<JsValue> {
761        let label = match args.first() {
762            Some(value) => value.to_string(context)?,
763            None => "default".into(),
764        };
765
766        if let Some(t) = console.state.timer_map.get(&label) {
767            let time = Self::system_time_in_ms();
768            let mut concat = format!("{}: {} ms", label.to_std_string_escaped(), time - t);
769            for msg in args.iter().skip(1) {
770                concat = concat + " " + &msg.display().to_string();
771            }
772            logger.log(concat, &console.state, context)?;
773        } else {
774            logger.warn(
775                format!("Timer '{}' doesn't exist", label.to_std_string_escaped()),
776                &console.state,
777                context,
778            )?;
779        }
780
781        Ok(JsValue::undefined())
782    }
783
784    /// `console.timeEnd(label)`
785    ///
786    /// Removes the timer with given label.
787    ///
788    /// More information:
789    ///  - [MDN documentation][mdn]
790    ///  - [WHATWG `console` specification][spec]
791    ///
792    /// [spec]: https://console.spec.whatwg.org/#timeend
793    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/timeEnd
794    fn time_end(
795        _: &JsValue,
796        args: &[JsValue],
797        console: &mut Self,
798        logger: &impl Logger,
799        context: &mut Context,
800    ) -> JsResult<JsValue> {
801        let label = match args.first() {
802            Some(value) => value.to_string(context)?,
803            None => "default".into(),
804        };
805
806        if let Some(t) = console.state.timer_map.remove(&label) {
807            let time = Self::system_time_in_ms();
808            logger.info(
809                format!(
810                    "{}: {} ms - timer removed",
811                    label.to_std_string_escaped(),
812                    time - t
813                ),
814                &console.state,
815                context,
816            )?;
817        } else {
818            logger.warn(
819                format!("Timer '{}' doesn't exist", label.to_std_string_escaped()),
820                &console.state,
821                context,
822            )?;
823        }
824
825        Ok(JsValue::undefined())
826    }
827
828    /// `console.group(...data)`
829    ///
830    /// Adds new group with name from formatted data to stack.
831    ///
832    /// More information:
833    ///  - [MDN documentation][mdn]
834    ///  - [WHATWG `console` specification][spec]
835    ///
836    /// [spec]: https://console.spec.whatwg.org/#group
837    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/group
838    fn group(
839        _: &JsValue,
840        args: &[JsValue],
841        console: &mut Self,
842        logger: &impl Logger,
843        context: &mut Context,
844    ) -> JsResult<JsValue> {
845        let group_label = formatter(args, context)?;
846
847        logger.info(format!("group: {group_label}"), &console.state, context)?;
848        console.state.groups.push(group_label);
849
850        Ok(JsValue::undefined())
851    }
852
853    /// `console.groupCollapsed(...data)`
854    ///
855    /// Adds new group collapsed with name from formatted data to stack.
856    ///
857    /// More information:
858    ///  - [MDN documentation][mdn]
859    ///  - [WHATWG `console` specification][spec]
860    ///
861    /// [spec]: https://console.spec.whatwg.org/#groupcollapsed
862    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/groupcollapsed_static
863    fn group_collapsed(
864        _: &JsValue,
865        args: &[JsValue],
866        console: &mut Self,
867        logger: &impl Logger,
868        context: &mut Context,
869    ) -> JsResult<JsValue> {
870        Console::group(&JsValue::undefined(), args, console, logger, context)
871    }
872
873    /// `console.groupEnd(label)`
874    ///
875    /// Removes the last group from the stack.
876    ///
877    /// More information:
878    ///  - [MDN documentation][mdn]
879    ///  - [WHATWG `console` specification][spec]
880    ///
881    /// [spec]: https://console.spec.whatwg.org/#groupend
882    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/groupEnd
883    #[allow(clippy::unnecessary_wraps)]
884    fn group_end(
885        _: &JsValue,
886        _: &[JsValue],
887        console: &mut Self,
888        _: &impl Logger,
889        _: &mut Context,
890    ) -> JsResult<JsValue> {
891        console.state.groups.pop();
892
893        Ok(JsValue::undefined())
894    }
895
896    /// `console.dir(item, options)`
897    ///
898    /// Prints info about item
899    ///
900    /// More information:
901    ///  - [MDN documentation][mdn]
902    ///  - [WHATWG `console` specification][spec]
903    ///
904    /// [spec]: https://console.spec.whatwg.org/#dir
905    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/dir
906    #[allow(clippy::unnecessary_wraps)]
907    fn dir(
908        _: &JsValue,
909        args: &[JsValue],
910        console: &Self,
911        logger: &impl Logger,
912        context: &mut Context,
913    ) -> JsResult<JsValue> {
914        logger.info(
915            args.get_or_undefined(0).display_obj(true),
916            &console.state,
917            context,
918        )?;
919        Ok(JsValue::undefined())
920    }
921}