1use std::fmt::Write as _;
12
13use rquickjs::function::This;
14use rquickjs::{Function, Object, Value};
15
16#[must_use]
22pub fn strip_ansi(input: &str) -> String {
23 let mut out = String::with_capacity(input.len());
24 let mut chars = input.chars().peekable();
25 while let Some(c) = chars.next() {
26 if c == '\x1b' && chars.peek() == Some(&'[') {
27 chars.next();
28 for nc in chars.by_ref() {
29 if ('@'..='~').contains(&nc) {
30 break;
31 }
32 }
33 } else {
34 out.push(c);
35 }
36 }
37 out
38}
39
40
41pub const MAX_DEPTH: usize = 2;
44
45pub const MAX_ARRAY_LENGTH: usize = 100;
48
49pub const MAX_DIR_DEPTH: usize = 8;
53
54const RESET: &str = "\x1b[0m";
55const NUMBER: &str = "\x1b[33m";
57const STRING: &str = "\x1b[32m";
58const BOOLEAN: &str = "\x1b[33m";
59const UNDEFINED: &str = "\x1b[90m";
60const NULL: &str = "\x1b[1m";
61const SYMBOL: &str = "\x1b[32m";
62const DATE: &str = "\x1b[35m";
63const REGEXP: &str = "\x1b[31m";
64const SPECIAL: &str = "\x1b[36m";
65
66#[derive(Clone, Copy)]
68pub struct Inspector {
69 pub styled: bool,
71 max_depth: usize,
72 bare_top_string: bool,
76}
77
78impl Inspector {
79 pub fn new(styled: bool) -> Self {
80 Self {
81 styled,
82 max_depth: MAX_DEPTH,
83 bare_top_string: true,
84 }
85 }
86
87 pub fn with_depth(self, max_depth: usize) -> Self {
88 Self { max_depth, ..self }
89 }
90
91 pub fn quoted(self) -> Self {
93 Self {
94 bare_top_string: false,
95 ..self
96 }
97 }
98
99 pub fn paint(self, out: &mut String, code: &str, text: &str) {
102 let text = strip_ansi(text);
103 if self.styled && !code.is_empty() {
106 out.push_str(code);
107 out.push_str(&text);
108 out.push_str(RESET);
109 } else {
110 out.push_str(&text);
111 }
112 }
113
114 pub fn punct(out: &mut String, text: &str) {
116 out.push_str(text);
117 }
118
119 pub fn printf(self, out: &mut String, fmt: &str, args: &[Value<'_>]) -> rquickjs::Result<usize> {
125 let mut consumed = 0usize;
126 let mut chars = fmt.chars().peekable();
127 let mut literal = String::new();
128 while let Some(c) = chars.next() {
129 if c != '%' {
130 literal.push(c);
131 continue;
132 }
133 let Some(&spec) = chars.peek() else {
134 literal.push('%');
135 break;
136 };
137 if spec == '%' {
138 chars.next();
139 literal.push('%');
140 continue;
141 }
142 if !matches!(spec, 's' | 'd' | 'i' | 'f' | 'j' | 'o' | 'O' | 'c') {
143 literal.push('%');
144 continue;
145 }
146 let Some(arg) = args.get(consumed) else {
147 literal.push('%');
149 continue;
150 };
151 chars.next();
152 consumed += 1;
153 out.push_str(&strip_ansi(&std::mem::take(&mut literal)));
156 match spec {
157 's' => {
158 if let Some(s) = arg.as_string() {
159 self.paint(out, "", &s.to_string()?);
160 } else {
161 Inspector::new(false).with_depth(0).value(out, arg, 0)?;
163 }
164 },
165 'd' | 'i' | 'f' => self.paint(out, NUMBER, &coerce_number(arg, spec)?),
168 'j' => {
169 let json = arg
172 .ctx()
173 .json_stringify(arg.clone())
174 .ok()
175 .flatten()
176 .and_then(|s| s.to_string().ok());
177 match json {
178 Some(text) => self.paint(out, "", &text),
179 None if arg.is_undefined() => self.paint(out, "", "undefined"),
180 None => self.paint(out, "", "[Circular]"),
181 }
182 },
183 'o' => self.quoted().with_depth(4).value(out, arg, 0)?,
185 'O' => self.quoted().with_depth(MAX_DEPTH).value(out, arg, 0)?,
186 _ => {},
189 }
190 }
191 out.push_str(&strip_ansi(&literal));
192 Ok(consumed + 1)
193 }
194
195 pub fn args(self, out: &mut String, args: &[Value<'_>]) -> rquickjs::Result<()> {
198 let mut start = 0usize;
199 if let Some(fmt) = args.first().and_then(rquickjs::Value::as_string) {
200 let fmt = fmt.to_string()?;
201 if fmt.contains('%') {
202 start = self.printf(out, &fmt, &args[1..])?;
203 }
204 }
205 for (i, v) in args.iter().enumerate().skip(start) {
206 if i > 0 || start > 0 {
207 out.push(' ');
208 }
209 self.value(out, v, 0)?;
210 }
211 Ok(())
212 }
213
214 #[allow(clippy::too_many_lines)]
222 pub fn value(self, out: &mut String, value: &Value<'_>, depth: usize) -> rquickjs::Result<()> {
223 use rquickjs::Type;
224
225 match value.type_of() {
226 Type::String => {
227 if let Some(s) = value.as_string() {
228 let s = s.to_string()?;
229 if depth == 0 && self.bare_top_string {
230 self.paint(out, "", &s);
231 } else {
232 self.paint(out, STRING, "e_js_string(&s));
235 }
236 }
237 },
238 Type::Int => self.paint(out, NUMBER, &value.as_int().unwrap_or_default().to_string()),
239 Type::Bool => self.paint(out, BOOLEAN, &value.as_bool().unwrap_or_default().to_string()),
240 Type::Float => self.paint(out, NUMBER, &value.as_float().unwrap_or_default().to_string()),
241 Type::BigInt => {
242 if let Some(b) = value.clone().into_big_int() {
243 self.paint(out, NUMBER, &format!("{}n", b.clone().to_i64()?));
244 }
245 },
246 Type::Array => {
247 let Some(array) = value.as_array() else { return Ok(()) };
248 if depth > self.max_depth {
249 self.paint(out, SPECIAL, "[Array]");
250 return Ok(());
251 }
252 if array.is_empty() {
253 Self::punct(out, "[]");
254 return Ok(());
255 }
256 Self::punct(out, "[ ");
257 let len = array.len();
258 for (i, element) in array.iter::<Value<'_>>().take(MAX_ARRAY_LENGTH).enumerate() {
259 if i > 0 {
260 Self::punct(out, ", ");
261 }
262 self.value(out, &element?, depth + 1)?;
263 }
264 if len > MAX_ARRAY_LENGTH {
266 let more = len - MAX_ARRAY_LENGTH;
267 let plural = if more == 1 { "item" } else { "items" };
268 Self::punct(out, &format!(", ... {more} more {plural}"));
269 }
270 Self::punct(out, " ]");
271 },
272 Type::Exception => {
273 if let Some(ex) = value.as_exception() {
274 let name = ex.get::<_, String>("name").unwrap_or_else(|_| "Error".to_string());
275 let mut rendered = name;
276 if let Some(message) = ex.message() {
277 rendered.push_str(": ");
278 rendered.push_str(&message);
279 }
280 if depth == 0 {
283 if let Some(stack) = ex.stack().filter(|s| !s.is_empty()) {
284 rendered.push('\n');
285 rendered.push_str(&stack);
286 }
287 }
288 self.paint(out, REGEXP, &rendered);
289 }
290 },
291 Type::Object => {
292 if depth > self.max_depth {
293 self.paint(out, SPECIAL, "[Object]");
294 return Ok(());
295 }
296 let Some(object) = value.as_object() else { return Ok(()) };
297 if self.special_object(out, object, depth)? {
298 return Ok(());
299 }
300 match constructor_name(object) {
304 Some(name) if name != "Object" => {
305 self.paint(out, "", &name);
306 Self::punct(out, " ");
307 },
308 None => {
309 self.paint(out, SPECIAL, "[Object: null prototype]");
310 Self::punct(out, " ");
311 },
312 Some(_) => {},
313 }
314 let mut wrote_any = false;
315 for (i, prop) in object.props::<String, Value<'_>>().enumerate() {
316 let (key, val) = prop?;
317 if i == 0 {
318 Self::punct(out, "{ ");
319 wrote_any = true;
320 } else {
321 Self::punct(out, ", ");
322 }
323 self.paint(out, "", &key);
324 Self::punct(out, ": ");
325 self.value(out, &val, depth + 1)?;
326 }
327 Self::punct(out, if wrote_any { " }" } else { "{}" });
328 },
329 Type::Symbol => {
330 if let Some(symbol) = value.as_symbol() {
331 let description = symbol
332 .description()?
333 .as_string()
334 .map(rquickjs::String::to_string)
335 .transpose()?
336 .unwrap_or_default();
337 self.paint(out, SYMBOL, &format!("Symbol({description})"));
338 }
339 },
340 Type::Function | Type::Constructor => {
341 let name = value
342 .as_object()
343 .and_then(|f| f.get::<_, String>("name").ok())
344 .filter(|n| !n.is_empty());
345 match name {
346 Some(name) => self.paint(out, SPECIAL, &format!("[Function: {name}]")),
347 None => self.paint(out, SPECIAL, "[Function (anonymous)]"),
348 }
349 },
350 Type::Promise => {
354 let Some(promise) = value.as_promise() else {
355 return Ok(());
356 };
357 Self::punct(out, "Promise { ");
358 match promise.state() {
359 rquickjs::promise::PromiseState::Pending => self.paint(out, SPECIAL, "<pending>"),
360 rquickjs::promise::PromiseState::Resolved => match promise.result::<Value<'_>>() {
361 Some(Ok(inner)) => self.value(out, &inner, depth + 1)?,
362 _ => self.paint(out, SPECIAL, "<pending>"),
363 },
364 rquickjs::promise::PromiseState::Rejected => {
365 self.paint(out, REGEXP, "<rejected>");
366 Self::punct(out, " ");
367 if let Some(Err(_)) = promise.result::<Value<'_>>() {
371 let reason = value.ctx().catch();
372 self.value(out, &reason, depth + 1)?;
373 }
374 },
375 }
376 Self::punct(out, " }");
377 },
378 Type::Null => self.paint(out, NULL, "null"),
379 Type::Undefined | Type::Uninitialized => self.paint(out, UNDEFINED, "undefined"),
380 _ => {},
381 }
382 Ok(())
383 }
384
385 pub fn special_object(self, out: &mut String, object: &Object<'_>, depth: usize) -> rquickjs::Result<bool> {
392 let ctor_name: String = object
393 .get::<_, Object<'_>>("constructor")
394 .and_then(|c| c.get::<_, String>("name"))
395 .unwrap_or_default();
396 match ctor_name.as_str() {
397 "Date" => {
398 let iso = object
400 .get::<_, Function<'_>>("toISOString")
401 .and_then(|f| f.call::<_, String>((This(object.clone()),)));
402 match iso {
403 Ok(s) => self.paint(out, DATE, &s),
404 Err(_) => self.paint(out, DATE, "Invalid Date"),
405 }
406 Ok(true)
407 },
408 "RegExp" => {
409 let source: String = object.get("source").unwrap_or_default();
410 let flags: String = object.get("flags").unwrap_or_default();
411 self.paint(out, REGEXP, &format!("/{source}/{flags}"));
412 Ok(true)
413 },
414 kind @ ("WeakMap" | "WeakSet") => {
415 Self::punct(out, kind);
416 Self::punct(out, " { ");
417 self.paint(out, SPECIAL, "<items unknown>");
418 Self::punct(out, " }");
419 Ok(true)
420 },
421 kind @ ("Int8Array" | "Uint8Array" | "Uint8ClampedArray" | "Int16Array" | "Uint16Array" | "Int32Array"
422 | "Uint32Array" | "Float32Array" | "Float64Array" | "BigInt64Array" | "BigUint64Array") => {
423 let len: usize = object.get("length").unwrap_or_default();
424 Self::punct(out, &format!("{kind}({len})"));
425 if len == 0 {
426 Self::punct(out, " []");
427 return Ok(true);
428 }
429 Self::punct(out, " [ ");
430 for i in 0..len.min(MAX_ARRAY_LENGTH) {
431 if i > 0 {
432 Self::punct(out, ", ");
433 }
434 let element: Value<'_> = object.get(i as u32)?;
435 self.value(out, &element, depth + 1)?;
436 }
437 if len > MAX_ARRAY_LENGTH {
438 let more = len - MAX_ARRAY_LENGTH;
439 let plural = if more == 1 { "item" } else { "items" };
440 Self::punct(out, &format!(", ... {more} more {plural}"));
441 }
442 Self::punct(out, " ]");
443 Ok(true)
444 },
445 "ArrayBuffer" | "SharedArrayBuffer" => {
446 let len: usize = object.get("byteLength").unwrap_or_default();
447 Self::punct(out, &format!("{ctor_name} {{ byteLength: "));
448 self.paint(out, NUMBER, &len.to_string());
449 Self::punct(out, " }");
450 Ok(true)
451 },
452 kind @ ("Map" | "Set") => {
453 let size: usize = object.get("size").unwrap_or_default();
454 Self::punct(out, &format!("{kind}({size})"));
455 if size == 0 {
456 Self::punct(out, " {}");
457 return Ok(true);
458 }
459 if depth > self.max_depth {
460 return Ok(true);
461 }
462 let entries: rquickjs::Result<Function<'_>> = object.get("entries");
464 let values: rquickjs::Result<Function<'_>> = object.get("values");
465 let iter_fn = if kind == "Map" { entries } else { values };
466 let Ok(iter_fn) = iter_fn else { return Ok(true) };
467 let iterator: Object<'_> = iter_fn.call((This(object.clone()),))?;
468 let next_fn: Function<'_> = iterator.get("next")?;
469 Self::punct(out, " { ");
470 let mut first = true;
471 loop {
472 let step: Object<'_> = next_fn.call((This(iterator.clone()),))?;
473 if step.get::<_, bool>("done").unwrap_or(true) {
474 break;
475 }
476 if !first {
477 Self::punct(out, ", ");
478 }
479 first = false;
480 let entry: Value<'_> = step.get("value")?;
481 if kind == "Map" {
482 let Some(pair) = entry.as_array() else { continue };
483 self.value(out, &pair.get::<Value<'_>>(0)?, depth + 1)?;
484 Self::punct(out, " => ");
485 self.value(out, &pair.get::<Value<'_>>(1)?, depth + 1)?;
486 } else {
487 self.value(out, &entry, depth + 1)?;
488 }
489 }
490 Self::punct(out, " }");
491 Ok(true)
492 },
493 _ => Ok(false),
494 }
495 }
496}
497
498fn constructor_name(object: &Object<'_>) -> Option<String> {
502 let prototype = object.get::<_, Value<'_>>("__proto__").ok()?;
503 if prototype.is_null() || prototype.is_undefined() {
504 return None;
505 }
506 object
507 .get::<_, Object<'_>>("constructor")
508 .and_then(|c| c.get::<_, String>("name"))
509 .ok()
510 .filter(|n| !n.is_empty())
511}
512
513fn quote_js_string(text: &str) -> String {
518 let quote = if !text.contains('\'') {
519 '\''
520 } else if !text.contains('"') {
521 '"'
522 } else {
523 '`'
524 };
525 let mut out = String::with_capacity(text.len() + 2);
526 out.push(quote);
527 for c in text.chars() {
528 match c {
529 '\\' => out.push_str("\\\\"),
530 '\n' => out.push_str("\\n"),
531 '\r' => out.push_str("\\r"),
532 '\t' => out.push_str("\\t"),
533 c if c == quote => {
534 out.push('\\');
535 out.push(c);
536 },
537 c if (c as u32) < 0x20 => {
538 let _ = write!(out, "\\x{:02x}", c as u32);
541 },
542 c => out.push(c),
543 }
544 }
545 out.push(quote);
546 out
547}
548
549fn coerce_number(arg: &Value<'_>, spec: char) -> rquickjs::Result<String> {
554 use rquickjs::Type;
555
556 match arg.type_of() {
557 Type::BigInt => {
558 if let Some(b) = arg.clone().into_big_int() {
559 return Ok(format!("{}n", b.to_i64()?));
560 }
561 return Ok("NaN".to_string());
562 },
563 Type::Symbol => return Ok("NaN".to_string()),
564 _ => {},
565 }
566 let global = match spec {
567 'i' => "parseInt",
568 'f' => "parseFloat",
569 _ => "Number",
570 };
571 let Ok(convert) = arg.ctx().globals().get::<_, Function<'_>>(global) else {
572 return Ok("NaN".to_string());
573 };
574 let converted: f64 = convert.call((arg.clone(),)).unwrap_or(f64::NAN);
575 if converted.is_nan() {
576 return Ok("NaN".to_string());
577 }
578 Ok(converted.to_string())
579}