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().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 => {
217 formatted.push('%');
218 formatted.push(c);
219 }
220 }
221 } else {
222 formatted.push(c);
223 }
224 }
225
226 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#[derive(Debug, Default, Trace, Finalize)]
241pub struct ConsoleState {
242 count_map: FxHashMap<JsString, u32>,
244
245 timer_map: FxHashMap<JsString, u128>,
248
249 groups: Vec<String>,
252}
253
254impl ConsoleState {
255 #[must_use]
257 pub fn indent(&self) -> usize {
258 2 * self.groups.len()
259 }
260
261 #[must_use]
263 pub fn groups(&self) -> &Vec<String> {
264 &self.groups
265 }
266
267 #[must_use]
269 pub fn count_map(&self) -> &FxHashMap<JsString, u32> {
270 &self.count_map
271 }
272
273 #[must_use]
275 pub fn timer_map(&self) -> &FxHashMap<JsString, u128> {
276 &self.timer_map
277 }
278}
279
280#[derive(Debug, Default, Trace, Finalize, JsData)]
282pub struct Console {
283 state: ConsoleState,
284}
285
286impl Console {
287 pub const NAME: JsString = js_string!("console");
289
290 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 #[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 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 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 pub fn init(context: &mut Context) -> JsObject {
447 Self::init_with_logger(DefaultLogger, context)
448 }
449
450 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[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}