use std::fmt::Write as _;
use rquickjs::function::This;
use rquickjs::{Function, Object, Value};
#[must_use]
pub fn strip_ansi(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' && chars.peek() == Some(&'[') {
chars.next();
for nc in chars.by_ref() {
if ('@'..='~').contains(&nc) {
break;
}
}
} else {
out.push(c);
}
}
out
}
pub const MAX_DEPTH: usize = 2;
pub const MAX_ARRAY_LENGTH: usize = 100;
pub const MAX_DIR_DEPTH: usize = 8;
const RESET: &str = "\x1b[0m";
const NUMBER: &str = "\x1b[33m";
const STRING: &str = "\x1b[32m";
const BOOLEAN: &str = "\x1b[33m";
const UNDEFINED: &str = "\x1b[90m";
const NULL: &str = "\x1b[1m";
const SYMBOL: &str = "\x1b[32m";
const DATE: &str = "\x1b[35m";
const REGEXP: &str = "\x1b[31m";
const SPECIAL: &str = "\x1b[36m";
#[derive(Clone, Copy)]
pub struct Inspector {
pub styled: bool,
max_depth: usize,
bare_top_string: bool,
}
impl Inspector {
pub fn new(styled: bool) -> Self {
Self {
styled,
max_depth: MAX_DEPTH,
bare_top_string: true,
}
}
pub fn with_depth(self, max_depth: usize) -> Self {
Self { max_depth, ..self }
}
pub fn quoted(self) -> Self {
Self {
bare_top_string: false,
..self
}
}
pub fn paint(self, out: &mut String, code: &str, text: &str) {
let text = strip_ansi(text);
if self.styled && !code.is_empty() {
out.push_str(code);
out.push_str(&text);
out.push_str(RESET);
} else {
out.push_str(&text);
}
}
pub fn punct(out: &mut String, text: &str) {
out.push_str(text);
}
pub fn printf(self, out: &mut String, fmt: &str, args: &[Value<'_>]) -> rquickjs::Result<usize> {
let mut consumed = 0usize;
let mut chars = fmt.chars().peekable();
let mut literal = String::new();
while let Some(c) = chars.next() {
if c != '%' {
literal.push(c);
continue;
}
let Some(&spec) = chars.peek() else {
literal.push('%');
break;
};
if spec == '%' {
chars.next();
literal.push('%');
continue;
}
if !matches!(spec, 's' | 'd' | 'i' | 'f' | 'j' | 'o' | 'O' | 'c') {
literal.push('%');
continue;
}
let Some(arg) = args.get(consumed) else {
literal.push('%');
continue;
};
chars.next();
consumed += 1;
out.push_str(&strip_ansi(&std::mem::take(&mut literal)));
match spec {
's' => {
if let Some(s) = arg.as_string() {
self.paint(out, "", &s.to_string()?);
} else {
Inspector::new(false).with_depth(0).value(out, arg, 0)?;
}
},
'd' | 'i' | 'f' => self.paint(out, NUMBER, &coerce_number(arg, spec)?),
'j' => {
let json = arg
.ctx()
.json_stringify(arg.clone())
.ok()
.flatten()
.and_then(|s| s.to_string().ok());
match json {
Some(text) => self.paint(out, "", &text),
None if arg.is_undefined() => self.paint(out, "", "undefined"),
None => self.paint(out, "", "[Circular]"),
}
},
'o' => self.quoted().with_depth(4).value(out, arg, 0)?,
'O' => self.quoted().with_depth(MAX_DEPTH).value(out, arg, 0)?,
_ => {},
}
}
out.push_str(&strip_ansi(&literal));
Ok(consumed + 1)
}
pub fn args(self, out: &mut String, args: &[Value<'_>]) -> rquickjs::Result<()> {
let mut start = 0usize;
if let Some(fmt) = args.first().and_then(rquickjs::Value::as_string) {
let fmt = fmt.to_string()?;
if fmt.contains('%') {
start = self.printf(out, &fmt, &args[1..])?;
}
}
for (i, v) in args.iter().enumerate().skip(start) {
if i > 0 || start > 0 {
out.push(' ');
}
self.value(out, v, 0)?;
}
Ok(())
}
#[allow(clippy::too_many_lines)]
pub fn value(self, out: &mut String, value: &Value<'_>, depth: usize) -> rquickjs::Result<()> {
use rquickjs::Type;
match value.type_of() {
Type::String => {
if let Some(s) = value.as_string() {
let s = s.to_string()?;
if depth == 0 && self.bare_top_string {
self.paint(out, "", &s);
} else {
self.paint(out, STRING, "e_js_string(&s));
}
}
},
Type::Int => self.paint(out, NUMBER, &value.as_int().unwrap_or_default().to_string()),
Type::Bool => self.paint(out, BOOLEAN, &value.as_bool().unwrap_or_default().to_string()),
Type::Float => self.paint(out, NUMBER, &value.as_float().unwrap_or_default().to_string()),
Type::BigInt => {
if let Some(b) = value.clone().into_big_int() {
self.paint(out, NUMBER, &format!("{}n", b.clone().to_i64()?));
}
},
Type::Array => {
let Some(array) = value.as_array() else { return Ok(()) };
if depth > self.max_depth {
self.paint(out, SPECIAL, "[Array]");
return Ok(());
}
if array.is_empty() {
Self::punct(out, "[]");
return Ok(());
}
Self::punct(out, "[ ");
let len = array.len();
for (i, element) in array.iter::<Value<'_>>().take(MAX_ARRAY_LENGTH).enumerate() {
if i > 0 {
Self::punct(out, ", ");
}
self.value(out, &element?, depth + 1)?;
}
if len > MAX_ARRAY_LENGTH {
let more = len - MAX_ARRAY_LENGTH;
let plural = if more == 1 { "item" } else { "items" };
Self::punct(out, &format!(", ... {more} more {plural}"));
}
Self::punct(out, " ]");
},
Type::Exception => {
if let Some(ex) = value.as_exception() {
let name = ex.get::<_, String>("name").unwrap_or_else(|_| "Error".to_string());
let mut rendered = name;
if let Some(message) = ex.message() {
rendered.push_str(": ");
rendered.push_str(&message);
}
if depth == 0 {
if let Some(stack) = ex.stack().filter(|s| !s.is_empty()) {
rendered.push('\n');
rendered.push_str(&stack);
}
}
self.paint(out, REGEXP, &rendered);
}
},
Type::Object => {
if depth > self.max_depth {
self.paint(out, SPECIAL, "[Object]");
return Ok(());
}
let Some(object) = value.as_object() else { return Ok(()) };
if self.special_object(out, object, depth)? {
return Ok(());
}
match constructor_name(object) {
Some(name) if name != "Object" => {
self.paint(out, "", &name);
Self::punct(out, " ");
},
None => {
self.paint(out, SPECIAL, "[Object: null prototype]");
Self::punct(out, " ");
},
Some(_) => {},
}
let mut wrote_any = false;
for (i, prop) in object.props::<String, Value<'_>>().enumerate() {
let (key, val) = prop?;
if i == 0 {
Self::punct(out, "{ ");
wrote_any = true;
} else {
Self::punct(out, ", ");
}
self.paint(out, "", &key);
Self::punct(out, ": ");
self.value(out, &val, depth + 1)?;
}
Self::punct(out, if wrote_any { " }" } else { "{}" });
},
Type::Symbol => {
if let Some(symbol) = value.as_symbol() {
let description = symbol
.description()?
.as_string()
.map(rquickjs::String::to_string)
.transpose()?
.unwrap_or_default();
self.paint(out, SYMBOL, &format!("Symbol({description})"));
}
},
Type::Function | Type::Constructor => {
let name = value
.as_object()
.and_then(|f| f.get::<_, String>("name").ok())
.filter(|n| !n.is_empty());
match name {
Some(name) => self.paint(out, SPECIAL, &format!("[Function: {name}]")),
None => self.paint(out, SPECIAL, "[Function (anonymous)]"),
}
},
Type::Promise => {
let Some(promise) = value.as_promise() else {
return Ok(());
};
Self::punct(out, "Promise { ");
match promise.state() {
rquickjs::promise::PromiseState::Pending => self.paint(out, SPECIAL, "<pending>"),
rquickjs::promise::PromiseState::Resolved => match promise.result::<Value<'_>>() {
Some(Ok(inner)) => self.value(out, &inner, depth + 1)?,
_ => self.paint(out, SPECIAL, "<pending>"),
},
rquickjs::promise::PromiseState::Rejected => {
self.paint(out, REGEXP, "<rejected>");
Self::punct(out, " ");
if let Some(Err(_)) = promise.result::<Value<'_>>() {
let reason = value.ctx().catch();
self.value(out, &reason, depth + 1)?;
}
},
}
Self::punct(out, " }");
},
Type::Null => self.paint(out, NULL, "null"),
Type::Undefined | Type::Uninitialized => self.paint(out, UNDEFINED, "undefined"),
_ => {},
}
Ok(())
}
pub fn special_object(self, out: &mut String, object: &Object<'_>, depth: usize) -> rquickjs::Result<bool> {
let ctor_name: String = object
.get::<_, Object<'_>>("constructor")
.and_then(|c| c.get::<_, String>("name"))
.unwrap_or_default();
match ctor_name.as_str() {
"Date" => {
let iso = object
.get::<_, Function<'_>>("toISOString")
.and_then(|f| f.call::<_, String>((This(object.clone()),)));
match iso {
Ok(s) => self.paint(out, DATE, &s),
Err(_) => self.paint(out, DATE, "Invalid Date"),
}
Ok(true)
},
"RegExp" => {
let source: String = object.get("source").unwrap_or_default();
let flags: String = object.get("flags").unwrap_or_default();
self.paint(out, REGEXP, &format!("/{source}/{flags}"));
Ok(true)
},
kind @ ("WeakMap" | "WeakSet") => {
Self::punct(out, kind);
Self::punct(out, " { ");
self.paint(out, SPECIAL, "<items unknown>");
Self::punct(out, " }");
Ok(true)
},
kind @ ("Int8Array" | "Uint8Array" | "Uint8ClampedArray" | "Int16Array" | "Uint16Array" | "Int32Array"
| "Uint32Array" | "Float32Array" | "Float64Array" | "BigInt64Array" | "BigUint64Array") => {
let len: usize = object.get("length").unwrap_or_default();
Self::punct(out, &format!("{kind}({len})"));
if len == 0 {
Self::punct(out, " []");
return Ok(true);
}
Self::punct(out, " [ ");
for i in 0..len.min(MAX_ARRAY_LENGTH) {
if i > 0 {
Self::punct(out, ", ");
}
let element: Value<'_> = object.get(i as u32)?;
self.value(out, &element, depth + 1)?;
}
if len > MAX_ARRAY_LENGTH {
let more = len - MAX_ARRAY_LENGTH;
let plural = if more == 1 { "item" } else { "items" };
Self::punct(out, &format!(", ... {more} more {plural}"));
}
Self::punct(out, " ]");
Ok(true)
},
"ArrayBuffer" | "SharedArrayBuffer" => {
let len: usize = object.get("byteLength").unwrap_or_default();
Self::punct(out, &format!("{ctor_name} {{ byteLength: "));
self.paint(out, NUMBER, &len.to_string());
Self::punct(out, " }");
Ok(true)
},
kind @ ("Map" | "Set") => {
let size: usize = object.get("size").unwrap_or_default();
Self::punct(out, &format!("{kind}({size})"));
if size == 0 {
Self::punct(out, " {}");
return Ok(true);
}
if depth > self.max_depth {
return Ok(true);
}
let entries: rquickjs::Result<Function<'_>> = object.get("entries");
let values: rquickjs::Result<Function<'_>> = object.get("values");
let iter_fn = if kind == "Map" { entries } else { values };
let Ok(iter_fn) = iter_fn else { return Ok(true) };
let iterator: Object<'_> = iter_fn.call((This(object.clone()),))?;
let next_fn: Function<'_> = iterator.get("next")?;
Self::punct(out, " { ");
let mut first = true;
loop {
let step: Object<'_> = next_fn.call((This(iterator.clone()),))?;
if step.get::<_, bool>("done").unwrap_or(true) {
break;
}
if !first {
Self::punct(out, ", ");
}
first = false;
let entry: Value<'_> = step.get("value")?;
if kind == "Map" {
let Some(pair) = entry.as_array() else { continue };
self.value(out, &pair.get::<Value<'_>>(0)?, depth + 1)?;
Self::punct(out, " => ");
self.value(out, &pair.get::<Value<'_>>(1)?, depth + 1)?;
} else {
self.value(out, &entry, depth + 1)?;
}
}
Self::punct(out, " }");
Ok(true)
},
_ => Ok(false),
}
}
}
fn constructor_name(object: &Object<'_>) -> Option<String> {
let prototype = object.get::<_, Value<'_>>("__proto__").ok()?;
if prototype.is_null() || prototype.is_undefined() {
return None;
}
object
.get::<_, Object<'_>>("constructor")
.and_then(|c| c.get::<_, String>("name"))
.ok()
.filter(|n| !n.is_empty())
}
fn quote_js_string(text: &str) -> String {
let quote = if !text.contains('\'') {
'\''
} else if !text.contains('"') {
'"'
} else {
'`'
};
let mut out = String::with_capacity(text.len() + 2);
out.push(quote);
for c in text.chars() {
match c {
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c == quote => {
out.push('\\');
out.push(c);
},
c if (c as u32) < 0x20 => {
let _ = write!(out, "\\x{:02x}", c as u32);
},
c => out.push(c),
}
}
out.push(quote);
out
}
fn coerce_number(arg: &Value<'_>, spec: char) -> rquickjs::Result<String> {
use rquickjs::Type;
match arg.type_of() {
Type::BigInt => {
if let Some(b) = arg.clone().into_big_int() {
return Ok(format!("{}n", b.to_i64()?));
}
return Ok("NaN".to_string());
},
Type::Symbol => return Ok("NaN".to_string()),
_ => {},
}
let global = match spec {
'i' => "parseInt",
'f' => "parseFloat",
_ => "Number",
};
let Ok(convert) = arg.ctx().globals().get::<_, Function<'_>>(global) else {
return Ok("NaN".to_string());
};
let converted: f64 = convert.call((arg.clone(),)).unwrap_or(f64::NAN);
if converted.is_nan() {
return Ok("NaN".to_string());
}
Ok(converted.to_string())
}