Skip to main content

obeli_sk_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: use internals mode for richer inspection */
186                        'o' | 'O' => {
187                            let arg = data.get_or_undefined(arg_index);
188                            formatted.push_str(&arg.display().internals(true).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                        c => {
216                            formatted.push('%');
217                            formatted.push(c);
218                        }
219                    }
220                } else {
221                    formatted.push(c);
222                }
223            }
224
225            /* unformatted data */
226            for rest in data.iter().skip(arg_index) {
227                formatted.push(' ');
228                formatted.push_str(&to_string(rest, context));
229            }
230
231            Ok(formatted)
232        }
233    }
234}
235
236/// The current state of the console, passed to the logger backend.
237/// This should not be copied or cloned. References are only valid
238/// for the current logging call.
239#[derive(Debug, Default, Trace, Finalize)]
240pub struct ConsoleState {
241    /// The map of console counters, used in `console.count()`.
242    count_map: FxHashMap<JsString, u32>,
243
244    /// The map of console timers, used in `console.time`, `console.timeLog`
245    /// and `console.timeEnd`.
246    timer_map: FxHashMap<JsString, u128>,
247
248    /// The current list of groups. Groups should be indented, but some logging
249    /// libraries may want to use them in a different way.
250    groups: Vec<String>,
251}
252
253impl ConsoleState {
254    /// Returns the indentation level that should be applied to logging.
255    #[must_use]
256    pub fn indent(&self) -> usize {
257        2 * self.groups.len()
258    }
259
260    /// Returns the current list of groups.
261    #[must_use]
262    pub fn groups(&self) -> &Vec<String> {
263        &self.groups
264    }
265
266    /// Returns the count map.
267    #[must_use]
268    pub fn count_map(&self) -> &FxHashMap<JsString, u32> {
269        &self.count_map
270    }
271
272    /// Returns the timer map.
273    #[must_use]
274    pub fn timer_map(&self) -> &FxHashMap<JsString, u128> {
275        &self.timer_map
276    }
277}
278
279/// This is the internal console object state.
280#[derive(Debug, Default, Trace, Finalize, JsData)]
281pub struct Console {
282    state: ConsoleState,
283}
284
285impl Console {
286    /// Name of the built-in `console` property.
287    pub const NAME: JsString = js_string!("console");
288
289    /// Modify the context to include the `console` object.
290    ///
291    /// # Errors
292    /// This function will return an error if the property cannot be defined on the global object.
293    pub fn register_with_logger<L>(logger: L, context: &mut Context) -> JsResult<()>
294    where
295        L: Logger + 'static,
296    {
297        let console = Self::init_with_logger(logger, context);
298        context.register_global_property(
299            Self::NAME,
300            console,
301            Attribute::WRITABLE | Attribute::CONFIGURABLE,
302        )?;
303
304        Ok(())
305    }
306
307    /// Initializes the `console` with a special logger.
308    #[allow(clippy::too_many_lines)]
309    pub fn init_with_logger<L>(logger: L, context: &mut Context) -> JsObject
310    where
311        L: Logger + 'static,
312    {
313        fn console_method<L: Logger + 'static>(
314            f: fn(&JsValue, &[JsValue], &Console, &L, &mut Context) -> JsResult<JsValue>,
315            state: Rc<RefCell<Console>>,
316            logger: Rc<L>,
317        ) -> NativeFunction {
318            // SAFETY: `Console` doesn't contain types that need tracing.
319            unsafe {
320                NativeFunction::from_closure(move |this, args, context| {
321                    f(this, args, &state.borrow(), &logger, context)
322                })
323            }
324        }
325        fn console_method_mut<L: Logger + 'static>(
326            f: fn(&JsValue, &[JsValue], &mut Console, &L, &mut Context) -> JsResult<JsValue>,
327            state: Rc<RefCell<Console>>,
328            logger: Rc<L>,
329        ) -> NativeFunction {
330            // SAFETY: `Console` doesn't contain types that need tracing.
331            unsafe {
332                NativeFunction::from_closure(move |this, args, context| {
333                    f(this, args, &mut state.borrow_mut(), &logger, context)
334                })
335            }
336        }
337
338        let state = Rc::new(RefCell::new(Self::default()));
339        let logger = Rc::new(logger);
340
341        ObjectInitializer::with_native_data_and_proto(
342            Self::default(),
343            JsObject::with_object_proto(context.realm().intrinsics()),
344            context,
345        )
346        .property(
347            JsSymbol::to_string_tag(),
348            Self::NAME,
349            Attribute::CONFIGURABLE,
350        )
351        .function(
352            console_method(Self::assert, state.clone(), logger.clone()),
353            js_string!("assert"),
354            0,
355        )
356        .function(
357            console_method_mut(Self::clear, state.clone(), logger.clone()),
358            js_string!("clear"),
359            0,
360        )
361        .function(
362            console_method(Self::debug, state.clone(), logger.clone()),
363            js_string!("debug"),
364            0,
365        )
366        .function(
367            console_method(Self::error, state.clone(), logger.clone()),
368            js_string!("error"),
369            0,
370        )
371        .function(
372            console_method(Self::info, state.clone(), logger.clone()),
373            js_string!("info"),
374            0,
375        )
376        .function(
377            console_method(Self::log, state.clone(), logger.clone()),
378            js_string!("log"),
379            0,
380        )
381        .function(
382            console_method(Self::trace, state.clone(), logger.clone()),
383            js_string!("trace"),
384            0,
385        )
386        .function(
387            console_method(Self::warn, state.clone(), logger.clone()),
388            js_string!("warn"),
389            0,
390        )
391        .function(
392            console_method_mut(Self::count, state.clone(), logger.clone()),
393            js_string!("count"),
394            0,
395        )
396        .function(
397            console_method_mut(Self::count_reset, state.clone(), logger.clone()),
398            js_string!("countReset"),
399            0,
400        )
401        .function(
402            console_method_mut(Self::group, state.clone(), logger.clone()),
403            js_string!("group"),
404            0,
405        )
406        .function(
407            console_method_mut(Self::group_collapsed, state.clone(), logger.clone()),
408            js_string!("groupCollapsed"),
409            0,
410        )
411        .function(
412            console_method_mut(Self::group_end, state.clone(), logger.clone()),
413            js_string!("groupEnd"),
414            0,
415        )
416        .function(
417            console_method_mut(Self::time, state.clone(), logger.clone()),
418            js_string!("time"),
419            0,
420        )
421        .function(
422            console_method(Self::time_log, state.clone(), logger.clone()),
423            js_string!("timeLog"),
424            0,
425        )
426        .function(
427            console_method_mut(Self::time_end, state.clone(), logger.clone()),
428            js_string!("timeEnd"),
429            0,
430        )
431        .function(
432            console_method(Self::dir, state.clone(), logger.clone()),
433            js_string!("dir"),
434            0,
435        )
436        .function(
437            console_method(Self::dir, state, logger.clone()),
438            js_string!("dirxml"),
439            0,
440        )
441        .build()
442    }
443
444    /// Initializes the `console` built-in object.
445    pub fn init(context: &mut Context) -> JsObject {
446        Self::init_with_logger(DefaultLogger, context)
447    }
448
449    /// `console.assert(condition, ...data)`
450    ///
451    /// Prints a JavaScript value to the standard error if first argument evaluates to `false` or there
452    /// were no arguments.
453    ///
454    /// More information:
455    ///  - [MDN documentation][mdn]
456    ///  - [WHATWG `console` specification][spec]
457    ///
458    /// [spec]: https://console.spec.whatwg.org/#assert
459    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/assert
460    fn assert(
461        _: &JsValue,
462        args: &[JsValue],
463        console: &Self,
464        logger: &impl Logger,
465        context: &mut Context,
466    ) -> JsResult<JsValue> {
467        let assertion = args.first().is_some_and(JsValue::to_boolean);
468
469        if !assertion {
470            let mut args: Vec<JsValue> = args.iter().skip(1).cloned().collect();
471            let message = js_string!("Assertion failed");
472            if args.is_empty() {
473                args.push(JsValue::new(message));
474            } else if !args[0].is_string() {
475                args.insert(0, JsValue::new(message));
476            } else {
477                let value = JsString::from(args[0].display().to_string());
478                let concat = js_string!(message.as_str(), js_str!(": "), &value);
479                args[0] = JsValue::new(concat);
480            }
481
482            logger.error(formatter(&args, context)?, &console.state, context)?;
483        }
484
485        Ok(JsValue::undefined())
486    }
487
488    /// `console.clear()`
489    ///
490    /// Removes all groups and clears console if possible.
491    ///
492    /// More information:
493    ///  - [MDN documentation][mdn]
494    ///  - [WHATWG `console` specification][spec]
495    ///
496    /// [spec]: https://console.spec.whatwg.org/#clear
497    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/clear
498    #[allow(clippy::unnecessary_wraps)]
499    fn clear(
500        _: &JsValue,
501        _: &[JsValue],
502        console: &mut Self,
503        _: &impl Logger,
504        _: &mut Context,
505    ) -> JsResult<JsValue> {
506        console.state.groups.clear();
507        Ok(JsValue::undefined())
508    }
509
510    /// `console.debug(...data)`
511    ///
512    /// Prints a JavaScript values with "debug" logLevel.
513    ///
514    /// More information:
515    ///  - [MDN documentation][mdn]
516    ///  - [WHATWG `console` specification][spec]
517    ///
518    /// [spec]: https://console.spec.whatwg.org/#debug
519    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/debug
520    fn debug(
521        _: &JsValue,
522        args: &[JsValue],
523        console: &Self,
524        logger: &impl Logger,
525        context: &mut Context,
526    ) -> JsResult<JsValue> {
527        logger.debug(formatter(args, context)?, &console.state, context)?;
528        Ok(JsValue::undefined())
529    }
530
531    /// `console.error(...data)`
532    ///
533    /// Prints a JavaScript values with "error" logLevel.
534    ///
535    /// More information:
536    ///  - [MDN documentation][mdn]
537    ///  - [WHATWG `console` specification][spec]
538    ///
539    /// [spec]: https://console.spec.whatwg.org/#error
540    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/error
541    fn error(
542        _: &JsValue,
543        args: &[JsValue],
544        console: &Self,
545        logger: &impl Logger,
546        context: &mut Context,
547    ) -> JsResult<JsValue> {
548        logger.error(formatter(args, context)?, &console.state, context)?;
549        Ok(JsValue::undefined())
550    }
551
552    /// `console.info(...data)`
553    ///
554    /// Prints a JavaScript values with "info" logLevel.
555    ///
556    /// More information:
557    ///  - [MDN documentation][mdn]
558    ///  - [WHATWG `console` specification][spec]
559    ///
560    /// [spec]: https://console.spec.whatwg.org/#info
561    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/info
562    fn info(
563        _: &JsValue,
564        args: &[JsValue],
565        console: &Self,
566        logger: &impl Logger,
567        context: &mut Context,
568    ) -> JsResult<JsValue> {
569        logger.info(formatter(args, context)?, &console.state, context)?;
570        Ok(JsValue::undefined())
571    }
572
573    /// `console.log(...data)`
574    ///
575    /// Prints a JavaScript values with "log" logLevel.
576    ///
577    /// More information:
578    ///  - [MDN documentation][mdn]
579    ///  - [WHATWG `console` specification][spec]
580    ///
581    /// [spec]: https://console.spec.whatwg.org/#log
582    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/log
583    fn log(
584        _: &JsValue,
585        args: &[JsValue],
586        console: &Self,
587        logger: &impl Logger,
588        context: &mut Context,
589    ) -> JsResult<JsValue> {
590        logger.log(formatter(args, context)?, &console.state, context)?;
591        Ok(JsValue::undefined())
592    }
593
594    /// `console.trace(...data)`
595    ///
596    /// Prints a stack trace with "trace" logLevel, optionally labelled by data.
597    ///
598    /// More information:
599    ///  - [MDN documentation][mdn]
600    ///  - [WHATWG `console` specification][spec]
601    ///
602    /// [spec]: https://console.spec.whatwg.org/#trace
603    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/trace
604    fn trace(
605        _: &JsValue,
606        args: &[JsValue],
607        console: &Self,
608        logger: &impl Logger,
609        context: &mut Context,
610    ) -> JsResult<JsValue> {
611        Logger::trace(logger, formatter(args, context)?, &console.state, context)?;
612        Ok(JsValue::undefined())
613    }
614
615    /// `console.warn(...data)`
616    ///
617    /// Prints a JavaScript values with "warn" logLevel.
618    ///
619    /// More information:
620    ///  - [MDN documentation][mdn]
621    ///  - [WHATWG `console` specification][spec]
622    ///
623    /// [spec]: https://console.spec.whatwg.org/#warn
624    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/warn
625    fn warn(
626        _: &JsValue,
627        args: &[JsValue],
628        console: &Self,
629        logger: &impl Logger,
630        context: &mut Context,
631    ) -> JsResult<JsValue> {
632        logger.warn(formatter(args, context)?, &console.state, context)?;
633        Ok(JsValue::undefined())
634    }
635
636    /// `console.count(label)`
637    ///
638    /// Prints number of times the function was called with that particular label.
639    ///
640    /// More information:
641    ///  - [MDN documentation][mdn]
642    ///  - [WHATWG `console` specification][spec]
643    ///
644    /// [spec]: https://console.spec.whatwg.org/#count
645    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/count
646    fn count(
647        _: &JsValue,
648        args: &[JsValue],
649        console: &mut Self,
650        logger: &impl Logger,
651        context: &mut Context,
652    ) -> JsResult<JsValue> {
653        let label = match args.first() {
654            Some(value) => value.to_string(context)?,
655            None => "default".into(),
656        };
657
658        let msg = format!("count {}:", label.to_std_string_escaped());
659        let c = console.state.count_map.entry(label).or_insert(0);
660        *c += 1;
661
662        logger.info(format!("{msg} {c}"), &console.state, context)?;
663        Ok(JsValue::undefined())
664    }
665
666    /// `console.countReset(label)`
667    ///
668    /// Resets the counter for label.
669    ///
670    /// More information:
671    ///  - [MDN documentation][mdn]
672    ///  - [WHATWG `console` specification][spec]
673    ///
674    /// [spec]: https://console.spec.whatwg.org/#countreset
675    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/countReset
676    fn count_reset(
677        _: &JsValue,
678        args: &[JsValue],
679        console: &mut Self,
680        logger: &impl Logger,
681        context: &mut Context,
682    ) -> JsResult<JsValue> {
683        let label = match args.first() {
684            Some(value) => value.to_string(context)?,
685            None => "default".into(),
686        };
687
688        console.state.count_map.remove(&label);
689
690        logger.warn(
691            format!("countReset {}", label.to_std_string_escaped()),
692            &console.state,
693            context,
694        )?;
695
696        Ok(JsValue::undefined())
697    }
698
699    /// Returns current system time in ms.
700    fn system_time_in_ms() -> u128 {
701        let now = SystemTime::now();
702        now.duration_since(SystemTime::UNIX_EPOCH)
703            .expect("negative duration")
704            .as_millis()
705    }
706
707    /// `console.time(label)`
708    ///
709    /// Starts the timer for given label.
710    ///
711    /// More information:
712    ///  - [MDN documentation][mdn]
713    ///  - [WHATWG `console` specification][spec]
714    ///
715    /// [spec]: https://console.spec.whatwg.org/#time
716    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/time
717    fn time(
718        _: &JsValue,
719        args: &[JsValue],
720        console: &mut Self,
721        logger: &impl Logger,
722        context: &mut Context,
723    ) -> JsResult<JsValue> {
724        let label = match args.first() {
725            Some(value) => value.to_string(context)?,
726            None => "default".into(),
727        };
728
729        if let Entry::Vacant(e) = console.state.timer_map.entry(label.clone()) {
730            let time = Self::system_time_in_ms();
731            e.insert(time);
732        } else {
733            logger.warn(
734                format!("Timer '{}' already exist", label.to_std_string_escaped()),
735                &console.state,
736                context,
737            )?;
738        }
739
740        Ok(JsValue::undefined())
741    }
742
743    /// `console.timeLog(label, ...data)`
744    ///
745    /// Prints elapsed time for timer with given label.
746    ///
747    /// More information:
748    ///  - [MDN documentation][mdn]
749    ///  - [WHATWG `console` specification][spec]
750    ///
751    /// [spec]: https://console.spec.whatwg.org/#timelog
752    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/timeLog
753    fn time_log(
754        _: &JsValue,
755        args: &[JsValue],
756        console: &Self,
757        logger: &impl Logger,
758        context: &mut Context,
759    ) -> JsResult<JsValue> {
760        let label = match args.first() {
761            Some(value) => value.to_string(context)?,
762            None => "default".into(),
763        };
764
765        if let Some(t) = console.state.timer_map.get(&label) {
766            let time = Self::system_time_in_ms();
767            let mut concat = format!("{}: {} ms", label.to_std_string_escaped(), time - t);
768            for msg in args.iter().skip(1) {
769                concat = concat + " " + &msg.display().to_string();
770            }
771            logger.log(concat, &console.state, context)?;
772        } else {
773            logger.warn(
774                format!("Timer '{}' doesn't exist", label.to_std_string_escaped()),
775                &console.state,
776                context,
777            )?;
778        }
779
780        Ok(JsValue::undefined())
781    }
782
783    /// `console.timeEnd(label)`
784    ///
785    /// Removes the timer with given label.
786    ///
787    /// More information:
788    ///  - [MDN documentation][mdn]
789    ///  - [WHATWG `console` specification][spec]
790    ///
791    /// [spec]: https://console.spec.whatwg.org/#timeend
792    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/timeEnd
793    fn time_end(
794        _: &JsValue,
795        args: &[JsValue],
796        console: &mut Self,
797        logger: &impl Logger,
798        context: &mut Context,
799    ) -> JsResult<JsValue> {
800        let label = match args.first() {
801            Some(value) => value.to_string(context)?,
802            None => "default".into(),
803        };
804
805        if let Some(t) = console.state.timer_map.remove(&label) {
806            let time = Self::system_time_in_ms();
807            logger.info(
808                format!(
809                    "{}: {} ms - timer removed",
810                    label.to_std_string_escaped(),
811                    time - t
812                ),
813                &console.state,
814                context,
815            )?;
816        } else {
817            logger.warn(
818                format!("Timer '{}' doesn't exist", label.to_std_string_escaped()),
819                &console.state,
820                context,
821            )?;
822        }
823
824        Ok(JsValue::undefined())
825    }
826
827    /// `console.group(...data)`
828    ///
829    /// Adds new group with name from formatted data to stack.
830    ///
831    /// More information:
832    ///  - [MDN documentation][mdn]
833    ///  - [WHATWG `console` specification][spec]
834    ///
835    /// [spec]: https://console.spec.whatwg.org/#group
836    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/group
837    fn group(
838        _: &JsValue,
839        args: &[JsValue],
840        console: &mut Self,
841        logger: &impl Logger,
842        context: &mut Context,
843    ) -> JsResult<JsValue> {
844        let group_label = formatter(args, context)?;
845
846        logger.info(format!("group: {group_label}"), &console.state, context)?;
847        console.state.groups.push(group_label);
848
849        Ok(JsValue::undefined())
850    }
851
852    /// `console.groupCollapsed(...data)`
853    ///
854    /// Adds new group collapsed with name from formatted data to stack.
855    ///
856    /// More information:
857    ///  - [MDN documentation][mdn]
858    ///  - [WHATWG `console` specification][spec]
859    ///
860    /// [spec]: https://console.spec.whatwg.org/#groupcollapsed
861    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/groupcollapsed_static
862    fn group_collapsed(
863        _: &JsValue,
864        args: &[JsValue],
865        console: &mut Self,
866        logger: &impl Logger,
867        context: &mut Context,
868    ) -> JsResult<JsValue> {
869        Console::group(&JsValue::undefined(), args, console, logger, context)
870    }
871
872    /// `console.groupEnd(label)`
873    ///
874    /// Removes the last group from the stack.
875    ///
876    /// More information:
877    ///  - [MDN documentation][mdn]
878    ///  - [WHATWG `console` specification][spec]
879    ///
880    /// [spec]: https://console.spec.whatwg.org/#groupend
881    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/groupEnd
882    #[allow(clippy::unnecessary_wraps)]
883    fn group_end(
884        _: &JsValue,
885        _: &[JsValue],
886        console: &mut Self,
887        _: &impl Logger,
888        _: &mut Context,
889    ) -> JsResult<JsValue> {
890        console.state.groups.pop();
891
892        Ok(JsValue::undefined())
893    }
894
895    /// `console.dir(item, options)`
896    ///
897    /// Prints info about item
898    ///
899    /// More information:
900    ///  - [MDN documentation][mdn]
901    ///  - [WHATWG `console` specification][spec]
902    ///
903    /// [spec]: https://console.spec.whatwg.org/#dir
904    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/dir
905    #[allow(clippy::unnecessary_wraps)]
906    fn dir(
907        _: &JsValue,
908        args: &[JsValue],
909        console: &Self,
910        logger: &impl Logger,
911        context: &mut Context,
912    ) -> JsResult<JsValue> {
913        logger.info(
914            args.get_or_undefined(0).display_obj(true),
915            &console.state,
916            context,
917        )?;
918        Ok(JsValue::undefined())
919    }
920}