#[cfg(test)]
pub(crate) mod tests;
use boa_engine::JsVariant;
use boa_engine::property::Attribute;
use boa_engine::{
Context, JsArgs, JsData, JsError, JsResult, JsString, JsSymbol, js_str, js_string,
native_function::NativeFunction,
object::{JsObject, ObjectInitializer},
value::{JsValue, Numeric},
};
use boa_gc::{Finalize, Trace};
use rustc_hash::FxHashMap;
use std::{
cell::RefCell, collections::hash_map::Entry, fmt::Write as _, io::Write, rc::Rc,
time::SystemTime,
};
pub trait Logger: Trace {
fn trace(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
self.log(msg, state, context)?;
let stack_trace_dump = context
.stack_trace()
.map(|frame| frame.code_block().name())
.map(JsString::to_std_string_escaped)
.collect::<Vec<_>>();
for frame in stack_trace_dump {
self.log(frame, state, context)?;
}
Ok(())
}
fn debug(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
self.log(msg, state, context)
}
fn log(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()>;
fn info(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()>;
fn warn(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()>;
fn error(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()>;
}
#[derive(Debug, Trace, Finalize)]
pub struct DefaultLogger;
impl Logger for DefaultLogger {
#[inline]
fn log(&self, msg: String, state: &ConsoleState, _context: &mut Context) -> JsResult<()> {
let indent = state.indent();
writeln!(std::io::stdout(), "{msg:>indent$}").map_err(JsError::from_rust)
}
#[inline]
fn info(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
self.log(msg, state, context)
}
#[inline]
fn warn(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
self.log(msg, state, context)
}
#[inline]
fn error(&self, msg: String, state: &ConsoleState, _context: &mut Context) -> JsResult<()> {
let indent = state.indent();
writeln!(std::io::stderr(), "{msg:>indent$}").map_err(JsError::from_rust)
}
}
#[derive(Debug, Trace, Finalize)]
pub struct NullLogger;
impl Logger for NullLogger {
#[inline]
fn log(&self, _: String, _: &ConsoleState, _: &mut Context) -> JsResult<()> {
Ok(())
}
#[inline]
fn info(&self, _: String, _: &ConsoleState, _: &mut Context) -> JsResult<()> {
Ok(())
}
#[inline]
fn warn(&self, _: String, _: &ConsoleState, _: &mut Context) -> JsResult<()> {
Ok(())
}
#[inline]
fn error(&self, _: String, _: &ConsoleState, _: &mut Context) -> JsResult<()> {
Ok(())
}
}
fn formatter(data: &[JsValue], context: &mut Context) -> JsResult<String> {
fn to_string(value: &JsValue, _context: &mut Context) -> String {
match value.variant() {
JsVariant::String(s) => s.to_std_string_escaped(),
_ => value.display().to_string(),
}
}
match data {
[] => Ok(String::new()),
[val] => Ok(to_string(val, context)),
data => {
let mut formatted = String::new();
let mut arg_index = 1;
let target = data
.get_or_undefined(0)
.to_string(context)?
.to_std_string_escaped();
let mut chars = target.chars();
while let Some(c) = chars.next() {
if c == '%' {
let fmt = chars.next().unwrap_or('%');
match fmt {
'd' | 'i' => {
let arg = match data.get_or_undefined(arg_index).to_numeric(context)? {
Numeric::Number(r) => (r.floor() + 0.0).to_string(),
Numeric::BigInt(int) => int.to_string(),
};
formatted.push_str(&arg);
arg_index += 1;
}
'f' => {
let arg = data.get_or_undefined(arg_index).to_number(context)?;
let _ = write!(formatted, "{arg:.6}");
arg_index += 1;
}
'o' | 'O' => {
let arg = data.get_or_undefined(arg_index);
formatted.push_str(&arg.display().to_string());
arg_index += 1;
}
's' => {
let arg = data.get_or_undefined(arg_index);
let mut written = false;
if let Some(obj) = arg.as_object()
&& let Ok(to_string) = obj.get(js_string!("toString"), context)
&& let Some(to_string_fn) = to_string.as_function()
{
let arg =
to_string_fn.call(arg, &[], context)?.to_string(context)?;
formatted.push_str(&arg.to_std_string_escaped());
written = true;
}
if !written {
let arg = arg.to_string(context)?.to_std_string_escaped();
formatted.push_str(&arg);
}
arg_index += 1;
}
'%' => formatted.push('%'),
c => {
formatted.push('%');
formatted.push(c);
}
}
} else {
formatted.push(c);
}
}
for rest in data.iter().skip(arg_index) {
formatted.push(' ');
formatted.push_str(&to_string(rest, context));
}
Ok(formatted)
}
}
}
#[derive(Debug, Default, Trace, Finalize)]
pub struct ConsoleState {
count_map: FxHashMap<JsString, u32>,
timer_map: FxHashMap<JsString, u128>,
groups: Vec<String>,
}
impl ConsoleState {
#[must_use]
pub fn indent(&self) -> usize {
2 * self.groups.len()
}
#[must_use]
pub fn groups(&self) -> &Vec<String> {
&self.groups
}
#[must_use]
pub fn count_map(&self) -> &FxHashMap<JsString, u32> {
&self.count_map
}
#[must_use]
pub fn timer_map(&self) -> &FxHashMap<JsString, u128> {
&self.timer_map
}
}
#[derive(Debug, Default, Trace, Finalize, JsData)]
pub struct Console {
state: ConsoleState,
}
impl Console {
pub const NAME: JsString = js_string!("console");
pub fn register_with_logger<L>(logger: L, context: &mut Context) -> JsResult<()>
where
L: Logger + 'static,
{
let console = Self::init_with_logger(logger, context);
context.register_global_property(
Self::NAME,
console,
Attribute::WRITABLE | Attribute::CONFIGURABLE,
)?;
Ok(())
}
#[allow(clippy::too_many_lines)]
pub fn init_with_logger<L>(logger: L, context: &mut Context) -> JsObject
where
L: Logger + 'static,
{
fn console_method<L: Logger + 'static>(
f: fn(&JsValue, &[JsValue], &Console, &L, &mut Context) -> JsResult<JsValue>,
state: Rc<RefCell<Console>>,
logger: Rc<L>,
) -> NativeFunction {
unsafe {
NativeFunction::from_closure(move |this, args, context| {
f(this, args, &state.borrow(), &logger, context)
})
}
}
fn console_method_mut<L: Logger + 'static>(
f: fn(&JsValue, &[JsValue], &mut Console, &L, &mut Context) -> JsResult<JsValue>,
state: Rc<RefCell<Console>>,
logger: Rc<L>,
) -> NativeFunction {
unsafe {
NativeFunction::from_closure(move |this, args, context| {
f(this, args, &mut state.borrow_mut(), &logger, context)
})
}
}
let state = Rc::new(RefCell::new(Self::default()));
let logger = Rc::new(logger);
ObjectInitializer::with_native_data_and_proto(
Self::default(),
JsObject::with_object_proto(context.realm().intrinsics()),
context,
)
.property(
JsSymbol::to_string_tag(),
Self::NAME,
Attribute::CONFIGURABLE,
)
.function(
console_method(Self::assert, state.clone(), logger.clone()),
js_string!("assert"),
0,
)
.function(
console_method_mut(Self::clear, state.clone(), logger.clone()),
js_string!("clear"),
0,
)
.function(
console_method(Self::debug, state.clone(), logger.clone()),
js_string!("debug"),
0,
)
.function(
console_method(Self::error, state.clone(), logger.clone()),
js_string!("error"),
0,
)
.function(
console_method(Self::info, state.clone(), logger.clone()),
js_string!("info"),
0,
)
.function(
console_method(Self::log, state.clone(), logger.clone()),
js_string!("log"),
0,
)
.function(
console_method(Self::trace, state.clone(), logger.clone()),
js_string!("trace"),
0,
)
.function(
console_method(Self::warn, state.clone(), logger.clone()),
js_string!("warn"),
0,
)
.function(
console_method_mut(Self::count, state.clone(), logger.clone()),
js_string!("count"),
0,
)
.function(
console_method_mut(Self::count_reset, state.clone(), logger.clone()),
js_string!("countReset"),
0,
)
.function(
console_method_mut(Self::group, state.clone(), logger.clone()),
js_string!("group"),
0,
)
.function(
console_method_mut(Self::group_collapsed, state.clone(), logger.clone()),
js_string!("groupCollapsed"),
0,
)
.function(
console_method_mut(Self::group_end, state.clone(), logger.clone()),
js_string!("groupEnd"),
0,
)
.function(
console_method_mut(Self::time, state.clone(), logger.clone()),
js_string!("time"),
0,
)
.function(
console_method(Self::time_log, state.clone(), logger.clone()),
js_string!("timeLog"),
0,
)
.function(
console_method_mut(Self::time_end, state.clone(), logger.clone()),
js_string!("timeEnd"),
0,
)
.function(
console_method(Self::dir, state.clone(), logger.clone()),
js_string!("dir"),
0,
)
.function(
console_method(Self::dir, state, logger.clone()),
js_string!("dirxml"),
0,
)
.build()
}
pub fn init(context: &mut Context) -> JsObject {
Self::init_with_logger(DefaultLogger, context)
}
fn assert(
_: &JsValue,
args: &[JsValue],
console: &Self,
logger: &impl Logger,
context: &mut Context,
) -> JsResult<JsValue> {
let assertion = args.first().is_some_and(JsValue::to_boolean);
if !assertion {
let mut args: Vec<JsValue> = args.iter().skip(1).cloned().collect();
let message = js_string!("Assertion failed");
if args.is_empty() {
args.push(JsValue::new(message));
} else if !args[0].is_string() {
args.insert(0, JsValue::new(message));
} else {
let value = JsString::from(args[0].display().to_string());
let concat = js_string!(message.as_str(), js_str!(": "), &value);
args[0] = JsValue::new(concat);
}
logger.error(formatter(&args, context)?, &console.state, context)?;
}
Ok(JsValue::undefined())
}
#[allow(clippy::unnecessary_wraps)]
fn clear(
_: &JsValue,
_: &[JsValue],
console: &mut Self,
_: &impl Logger,
_: &mut Context,
) -> JsResult<JsValue> {
console.state.groups.clear();
Ok(JsValue::undefined())
}
fn debug(
_: &JsValue,
args: &[JsValue],
console: &Self,
logger: &impl Logger,
context: &mut Context,
) -> JsResult<JsValue> {
logger.debug(formatter(args, context)?, &console.state, context)?;
Ok(JsValue::undefined())
}
fn error(
_: &JsValue,
args: &[JsValue],
console: &Self,
logger: &impl Logger,
context: &mut Context,
) -> JsResult<JsValue> {
logger.error(formatter(args, context)?, &console.state, context)?;
Ok(JsValue::undefined())
}
fn info(
_: &JsValue,
args: &[JsValue],
console: &Self,
logger: &impl Logger,
context: &mut Context,
) -> JsResult<JsValue> {
logger.info(formatter(args, context)?, &console.state, context)?;
Ok(JsValue::undefined())
}
fn log(
_: &JsValue,
args: &[JsValue],
console: &Self,
logger: &impl Logger,
context: &mut Context,
) -> JsResult<JsValue> {
logger.log(formatter(args, context)?, &console.state, context)?;
Ok(JsValue::undefined())
}
fn trace(
_: &JsValue,
args: &[JsValue],
console: &Self,
logger: &impl Logger,
context: &mut Context,
) -> JsResult<JsValue> {
Logger::trace(logger, formatter(args, context)?, &console.state, context)?;
Ok(JsValue::undefined())
}
fn warn(
_: &JsValue,
args: &[JsValue],
console: &Self,
logger: &impl Logger,
context: &mut Context,
) -> JsResult<JsValue> {
logger.warn(formatter(args, context)?, &console.state, context)?;
Ok(JsValue::undefined())
}
fn count(
_: &JsValue,
args: &[JsValue],
console: &mut Self,
logger: &impl Logger,
context: &mut Context,
) -> JsResult<JsValue> {
let label = match args.first() {
Some(value) => value.to_string(context)?,
None => "default".into(),
};
let msg = format!("count {}:", label.to_std_string_escaped());
let c = console.state.count_map.entry(label).or_insert(0);
*c += 1;
logger.info(format!("{msg} {c}"), &console.state, context)?;
Ok(JsValue::undefined())
}
fn count_reset(
_: &JsValue,
args: &[JsValue],
console: &mut Self,
logger: &impl Logger,
context: &mut Context,
) -> JsResult<JsValue> {
let label = match args.first() {
Some(value) => value.to_string(context)?,
None => "default".into(),
};
console.state.count_map.remove(&label);
logger.warn(
format!("countReset {}", label.to_std_string_escaped()),
&console.state,
context,
)?;
Ok(JsValue::undefined())
}
fn system_time_in_ms() -> u128 {
let now = SystemTime::now();
now.duration_since(SystemTime::UNIX_EPOCH)
.expect("negative duration")
.as_millis()
}
fn time(
_: &JsValue,
args: &[JsValue],
console: &mut Self,
logger: &impl Logger,
context: &mut Context,
) -> JsResult<JsValue> {
let label = match args.first() {
Some(value) => value.to_string(context)?,
None => "default".into(),
};
if let Entry::Vacant(e) = console.state.timer_map.entry(label.clone()) {
let time = Self::system_time_in_ms();
e.insert(time);
} else {
logger.warn(
format!("Timer '{}' already exist", label.to_std_string_escaped()),
&console.state,
context,
)?;
}
Ok(JsValue::undefined())
}
fn time_log(
_: &JsValue,
args: &[JsValue],
console: &Self,
logger: &impl Logger,
context: &mut Context,
) -> JsResult<JsValue> {
let label = match args.first() {
Some(value) => value.to_string(context)?,
None => "default".into(),
};
if let Some(t) = console.state.timer_map.get(&label) {
let time = Self::system_time_in_ms();
let mut concat = format!("{}: {} ms", label.to_std_string_escaped(), time - t);
for msg in args.iter().skip(1) {
concat = concat + " " + &msg.display().to_string();
}
logger.log(concat, &console.state, context)?;
} else {
logger.warn(
format!("Timer '{}' doesn't exist", label.to_std_string_escaped()),
&console.state,
context,
)?;
}
Ok(JsValue::undefined())
}
fn time_end(
_: &JsValue,
args: &[JsValue],
console: &mut Self,
logger: &impl Logger,
context: &mut Context,
) -> JsResult<JsValue> {
let label = match args.first() {
Some(value) => value.to_string(context)?,
None => "default".into(),
};
if let Some(t) = console.state.timer_map.remove(&label) {
let time = Self::system_time_in_ms();
logger.info(
format!(
"{}: {} ms - timer removed",
label.to_std_string_escaped(),
time - t
),
&console.state,
context,
)?;
} else {
logger.warn(
format!("Timer '{}' doesn't exist", label.to_std_string_escaped()),
&console.state,
context,
)?;
}
Ok(JsValue::undefined())
}
fn group(
_: &JsValue,
args: &[JsValue],
console: &mut Self,
logger: &impl Logger,
context: &mut Context,
) -> JsResult<JsValue> {
let group_label = formatter(args, context)?;
logger.info(format!("group: {group_label}"), &console.state, context)?;
console.state.groups.push(group_label);
Ok(JsValue::undefined())
}
fn group_collapsed(
_: &JsValue,
args: &[JsValue],
console: &mut Self,
logger: &impl Logger,
context: &mut Context,
) -> JsResult<JsValue> {
Console::group(&JsValue::undefined(), args, console, logger, context)
}
#[allow(clippy::unnecessary_wraps)]
fn group_end(
_: &JsValue,
_: &[JsValue],
console: &mut Self,
_: &impl Logger,
_: &mut Context,
) -> JsResult<JsValue> {
console.state.groups.pop();
Ok(JsValue::undefined())
}
#[allow(clippy::unnecessary_wraps)]
fn dir(
_: &JsValue,
args: &[JsValue],
console: &Self,
logger: &impl Logger,
context: &mut Context,
) -> JsResult<JsValue> {
logger.info(
args.get_or_undefined(0).display_obj(true),
&console.state,
context,
)?;
Ok(JsValue::undefined())
}
}