#![allow(clippy::print_stdout)]
#[cfg(test)]
mod tests;
use crate::{
builtins::{
function::make_builtin_fn,
object::InternalState,
value::{display_obj, RcString, ResultValue, Value},
},
exec::Interpreter,
BoaProfiler,
};
use rustc_hash::FxHashMap;
use std::time::SystemTime;
#[derive(Debug, Default)]
pub struct ConsoleState {
count_map: FxHashMap<RcString, u32>,
timer_map: FxHashMap<RcString, u128>,
groups: Vec<String>,
}
impl InternalState for ConsoleState {}
#[derive(Debug)]
pub enum LogMessage {
Log(String),
Info(String),
Warn(String),
Error(String),
}
fn get_arg_at_index<'a, T>(args: &'a [Value], index: usize) -> Option<T>
where
T: From<&'a Value> + Default,
{
args.get(index).map(|s| T::from(s))
}
pub fn logger(msg: LogMessage, console_state: &ConsoleState) {
let indent = 2 * console_state.groups.len();
match msg {
LogMessage::Error(msg) => {
eprintln!("{:>width$}", msg, width = indent);
}
LogMessage::Log(msg) | LogMessage::Info(msg) | LogMessage::Warn(msg) => {
println!("{:>width$}", msg, width = indent);
}
}
}
pub fn formatter(data: &[Value], ctx: &mut Interpreter) -> Result<String, Value> {
let target = ctx.to_string(&data.get(0).cloned().unwrap_or_default())?;
match data.len() {
0 => Ok(String::new()),
1 => Ok(target.to_string()),
_ => {
let mut formatted = String::new();
let mut arg_index = 1;
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 = get_arg_at_index::<i32>(data, arg_index).unwrap_or_default();
formatted.push_str(&format!("{}", arg));
arg_index += 1;
}
'f' => {
let arg = get_arg_at_index::<f64>(data, arg_index).unwrap_or_default();
formatted.push_str(&format!("{number:.prec$}", number = arg, prec = 6));
arg_index += 1
}
'o' | 'O' => {
let arg = data.get(arg_index).cloned().unwrap_or_default();
formatted.push_str(&format!("{}", arg));
arg_index += 1
}
's' => {
let arg =
ctx.to_string(&data.get(arg_index).cloned().unwrap_or_default())?;
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_str(&format!(" {}", rest))
}
Ok(formatted)
}
}
}
pub fn assert(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let assertion = get_arg_at_index::<bool>(args, 0).unwrap_or_default();
if !assertion {
let mut args: Vec<Value> = args.iter().skip(1).cloned().collect();
let message = "Assertion failed".to_string();
if args.is_empty() {
args.push(Value::from(message));
} else if !args[0].is_string() {
args.insert(0, Value::from(message));
} else {
let concat = format!("{}: {}", message, args[0]);
args[0] = Value::from(concat);
}
this.with_internal_state_ref::<_, Result<(), Value>, _>(|state| {
logger(LogMessage::Error(formatter(&args, ctx)?), state);
Ok(())
})?;
}
Ok(Value::undefined())
}
pub fn clear(this: &Value, _: &[Value], _: &mut Interpreter) -> ResultValue {
this.with_internal_state_mut(|state: &mut ConsoleState| {
state.groups.clear();
});
Ok(Value::undefined())
}
pub fn debug(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
this.with_internal_state_ref::<_, Result<(), Value>, _>(|state| {
logger(LogMessage::Log(formatter(args, ctx)?), state);
Ok(())
})?;
Ok(Value::undefined())
}
pub fn error(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
this.with_internal_state_ref::<_, Result<(), Value>, _>(|state| {
logger(LogMessage::Error(formatter(args, ctx)?), state);
Ok(())
})?;
Ok(Value::undefined())
}
pub fn info(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
this.with_internal_state_ref::<_, Result<(), Value>, _>(|state| {
logger(LogMessage::Info(formatter(args, ctx)?), state);
Ok(())
})?;
Ok(Value::undefined())
}
pub fn log(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
this.with_internal_state_ref::<_, Result<(), Value>, _>(|state| {
logger(LogMessage::Log(formatter(args, ctx)?), state);
Ok(())
})?;
Ok(Value::undefined())
}
pub fn trace(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
if !args.is_empty() {
this.with_internal_state_ref::<_, Result<(), Value>, _>(|state| {
logger(LogMessage::Log(formatter(args, ctx)?), state);
Ok(())
})?;
this.with_internal_state_ref(|state| {
logger(
LogMessage::Log("Not implemented: <stack trace>".to_string()),
state,
)
});
}
Ok(Value::undefined())
}
pub fn warn(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
this.with_internal_state_ref::<_, Result<(), Value>, _>(|state| {
logger(LogMessage::Warn(formatter(args, ctx)?), state);
Ok(())
})?;
Ok(Value::undefined())
}
pub fn count(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let label = match args.get(0) {
Some(value) => ctx.to_string(value)?,
None => "default".into(),
};
this.with_internal_state_mut(|state: &mut ConsoleState| {
let msg = format!("count {}:", &label);
let c = state.count_map.entry(label).or_insert(0);
*c += 1;
logger(LogMessage::Info(format!("{} {}", msg, c)), state);
});
Ok(Value::undefined())
}
pub fn count_reset(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let label = match args.get(0) {
Some(value) => ctx.to_string(value)?,
None => "default".into(),
};
this.with_internal_state_mut(|state: &mut ConsoleState| {
state.count_map.remove(&label);
logger(LogMessage::Warn(format!("countReset {}", label)), state);
});
Ok(Value::undefined())
}
fn system_time_in_ms() -> u128 {
let now = SystemTime::now();
now.duration_since(SystemTime::UNIX_EPOCH)
.expect("negative duration")
.as_millis()
}
pub fn time(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let label = match args.get(0) {
Some(value) => ctx.to_string(value)?,
None => "default".into(),
};
this.with_internal_state_mut(|state: &mut ConsoleState| {
if state.timer_map.get(&label).is_some() {
logger(
LogMessage::Warn(format!("Timer '{}' already exist", label)),
state,
);
} else {
let time = system_time_in_ms();
state.timer_map.insert(label, time);
}
});
Ok(Value::undefined())
}
pub fn time_log(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let label = match args.get(0) {
Some(value) => ctx.to_string(value)?,
None => "default".into(),
};
this.with_internal_state_mut(|state: &mut ConsoleState| {
if let Some(t) = state.timer_map.get(&label) {
let time = system_time_in_ms();
let mut concat = format!("{}: {} ms", label, time - t);
for msg in args.iter().skip(1) {
concat = concat + " " + &msg.to_string();
}
logger(LogMessage::Log(concat), state);
} else {
logger(
LogMessage::Warn(format!("Timer '{}' doesn't exist", label)),
state,
);
}
});
Ok(Value::undefined())
}
pub fn time_end(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let label = match args.get(0) {
Some(value) => ctx.to_string(value)?,
None => "default".into(),
};
this.with_internal_state_mut(|state: &mut ConsoleState| {
if let Some(t) = state.timer_map.remove(label.as_str()) {
let time = system_time_in_ms();
logger(
LogMessage::Info(format!("{}: {} ms - timer removed", label, time - t)),
state,
);
} else {
logger(
LogMessage::Warn(format!("Timer '{}' doesn't exist", label)),
state,
);
}
});
Ok(Value::undefined())
}
pub fn group(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let group_label = formatter(args, ctx)?;
this.with_internal_state_mut(|state: &mut ConsoleState| {
logger(LogMessage::Info(format!("group: {}", &group_label)), state);
state.groups.push(group_label);
});
Ok(Value::undefined())
}
pub fn group_end(this: &Value, _: &[Value], _: &mut Interpreter) -> ResultValue {
this.with_internal_state_mut(|state: &mut ConsoleState| {
state.groups.pop();
});
Ok(Value::undefined())
}
pub fn dir(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
this.with_internal_state_mut(|state: &mut ConsoleState| {
let undefined = Value::undefined();
logger(
LogMessage::Info(display_obj(args.get(0).unwrap_or(&undefined), true)),
state,
);
});
Ok(Value::undefined())
}
pub fn create(global: &Value) -> Value {
let console = Value::new_object(Some(global));
make_builtin_fn(assert, "assert", &console, 0);
make_builtin_fn(clear, "clear", &console, 0);
make_builtin_fn(debug, "debug", &console, 0);
make_builtin_fn(error, "error", &console, 0);
make_builtin_fn(info, "info", &console, 0);
make_builtin_fn(log, "log", &console, 0);
make_builtin_fn(trace, "trace", &console, 0);
make_builtin_fn(warn, "warn", &console, 0);
make_builtin_fn(error, "exception", &console, 0);
make_builtin_fn(count, "count", &console, 0);
make_builtin_fn(count_reset, "countReset", &console, 0);
make_builtin_fn(group, "group", &console, 0);
make_builtin_fn(group, "groupCollapsed", &console, 0);
make_builtin_fn(group_end, "groupEnd", &console, 0);
make_builtin_fn(time, "time", &console, 0);
make_builtin_fn(time_log, "timeLog", &console, 0);
make_builtin_fn(time_end, "timeEnd", &console, 0);
make_builtin_fn(dir, "dir", &console, 0);
make_builtin_fn(dir, "dirxml", &console, 0);
console.set_internal_state(ConsoleState::default());
console
}
#[inline]
pub fn init(global: &Value) -> (&str, Value) {
let _timer = BoaProfiler::global().start_event("console", "init");
("console", create(global))
}