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