1#[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
32pub trait Logger: Trace {
34 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 fn debug(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
60 self.log(msg, state, context)
61 }
62
63 fn log(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()>;
68
69 fn info(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()>;
74
75 fn warn(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()>;
80
81 fn error(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()>;
86}
87
88#[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#[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
146fn 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 '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 '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 '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 's' => {
193 let arg = data.get_or_undefined(arg_index);
194
195 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 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#[derive(Debug, Default, Trace, Finalize)]
240pub struct ConsoleState {
241 count_map: FxHashMap<JsString, u32>,
243
244 timer_map: FxHashMap<JsString, u128>,
247
248 groups: Vec<String>,
251}
252
253impl ConsoleState {
254 #[must_use]
256 pub fn indent(&self) -> usize {
257 2 * self.groups.len()
258 }
259
260 #[must_use]
262 pub fn groups(&self) -> &Vec<String> {
263 &self.groups
264 }
265
266 #[must_use]
268 pub fn count_map(&self) -> &FxHashMap<JsString, u32> {
269 &self.count_map
270 }
271
272 #[must_use]
274 pub fn timer_map(&self) -> &FxHashMap<JsString, u128> {
275 &self.timer_map
276 }
277}
278
279#[derive(Debug, Default, Trace, Finalize, JsData)]
281pub struct Console {
282 state: ConsoleState,
283}
284
285impl Console {
286 pub const NAME: JsString = js_string!("console");
288
289 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 #[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 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 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 pub fn init(context: &mut Context) -> JsObject {
446 Self::init_with_logger(DefaultLogger, context)
447 }
448
449 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[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}