rquickjs_extra_console/
formatter.rs

1use std::fmt::Write;
2
3use rquickjs::class::Trace;
4use rquickjs::{Error, Result, Type, Value};
5
6#[derive(Default, Clone, Debug)]
7#[non_exhaustive]
8struct FormatArgs {
9    key: Option<bool>,
10}
11
12impl FormatArgs {
13    pub fn is_key(&self) -> bool {
14        self.key.unwrap_or(false)
15    }
16
17    pub fn with_key(self) -> Self {
18        Self {
19            key: Some(true),
20            ..self
21        }
22    }
23}
24
25/// A formatter for the [`Console`] object
26///
27/// This formatter is used to format values to be printed by the console object.
28///
29/// [`Console`]: crate::console::Console
30#[derive(Clone, Debug, Trace)]
31pub struct Formatter {
32    max_depth: usize,
33}
34
35impl Default for Formatter {
36    fn default() -> Self {
37        Self::builder().build()
38    }
39}
40
41impl Formatter {
42    pub fn builder() -> FormatterBuilder {
43        FormatterBuilder::default()
44    }
45
46    pub fn format(&self, out: &mut impl Write, value: Value<'_>) -> Result<()> {
47        self._format(out, value, FormatArgs::default(), 0)
48    }
49
50    /// A poor attempt at mimicking the node format
51    /// See https://github.com/nodejs/node/blob/363eca1033458b8c2808207e2e5fc88e0f4df655/lib/internal/util/inspect.js#L842
52    fn _format(
53        &self,
54        out: &mut impl Write,
55        value: Value<'_>,
56        args: FormatArgs,
57        depth: usize,
58    ) -> Result<()> {
59        match value.type_of() {
60            Type::String => {
61                write!(
62                    out,
63                    "{}",
64                    value
65                        .into_string()
66                        .ok_or(Error::new_from_js("value", "string"))?
67                        .to_string()?
68                )
69                .map_err(|_| Error::Unknown)?;
70            }
71            Type::Int => {
72                write!(
73                    out,
74                    "{}",
75                    value.as_int().ok_or(Error::new_from_js("value", "int"))?
76                )
77                .map_err(|_| Error::Unknown)?;
78            }
79            Type::Bool => {
80                write!(
81                    out,
82                    "{}",
83                    value.as_bool().ok_or(Error::new_from_js("value", "bool"))?
84                )
85                .map_err(|_| Error::Unknown)?;
86            }
87            Type::Float => {
88                write!(
89                    out,
90                    "{}",
91                    value
92                        .as_float()
93                        .ok_or(Error::new_from_js("value", "float"))?
94                )
95                .map_err(|_| Error::Unknown)?;
96            }
97            Type::BigInt => {
98                write!(
99                    out,
100                    "{}n",
101                    value
102                        .into_big_int()
103                        .ok_or(Error::new_from_js("value", "bigint"))?
104                        .to_i64()?
105                )
106                .map_err(|_| Error::Unknown)?;
107            }
108            Type::Array => {
109                let array = value
110                    .into_array()
111                    .ok_or(Error::new_from_js("value", "array"))?;
112                if depth > self.max_depth {
113                    write!(out, "[Array]").map_err(|_| Error::Unknown)?;
114                } else if args.is_key() {
115                    for (i, element) in array.iter().enumerate() {
116                        if i > 0 {
117                            write!(out, ",").map_err(|_| Error::Unknown)?;
118                        }
119                        self._format(out, element?, FormatArgs::default().with_key(), depth + 1)?;
120                    }
121                } else {
122                    write!(out, "[ ").map_err(|_| Error::Unknown)?;
123                    for (i, element) in array.iter().enumerate() {
124                        if i > 0 {
125                            write!(out, ", ").map_err(|_| Error::Unknown)?;
126                        }
127                        self._format(out, element?, FormatArgs::default(), depth + 1)?;
128                    }
129                    write!(out, " ]").map_err(|_| Error::Unknown)?;
130                }
131            }
132            Type::Object => {
133                if depth > self.max_depth {
134                    write!(out, "[Object]").map_err(|_| Error::Unknown)?;
135                } else if args.is_key() {
136                    write!(out, "[object Object]").map_err(|_| Error::Unknown)?;
137                } else {
138                    let object = value
139                        .into_object()
140                        .ok_or(Error::new_from_js("value", "object"))?;
141                    write!(out, "{{ ").map_err(|_| Error::Unknown)?;
142                    for prop in object.props() {
143                        let (key, val) = prop?;
144                        self._format(out, key, FormatArgs::default().with_key(), depth + 1)?;
145                        write!(out, ": ").map_err(|_| Error::Unknown)?;
146                        self._format(out, val, FormatArgs::default(), depth + 1)?;
147                    }
148                    write!(out, " }}").map_err(|_| Error::Unknown)?;
149                }
150            }
151            Type::Symbol => {
152                let symbol = value
153                    .as_symbol()
154                    .ok_or(Error::new_from_js("value", "symbol"))?;
155                let description = match symbol.description()?.as_string() {
156                    Some(description) => description.to_string()?,
157                    None => String::default(),
158                };
159                write!(out, "Symbol({description})").map_err(|_| Error::Unknown)?;
160            }
161            Type::Function => {
162                let function = value
163                    .as_function()
164                    .ok_or(Error::new_from_js("value", "function"))?
165                    .as_object()
166                    .ok_or(Error::new_from_js("function", "object"))?;
167                let name: Option<String> = function.get("name").ok().and_then(|n| {
168                    if n == "[object Object]" {
169                        None
170                    } else {
171                        Some(n)
172                    }
173                });
174                match name {
175                    Some(name) => write!(out, "[Function: {name}]").map_err(|_| Error::Unknown)?,
176                    None => write!(out, "[Function (anonymous)]").map_err(|_| Error::Unknown)?,
177                }
178            }
179            Type::Null => {
180                write!(out, "null",).map_err(|_| Error::Unknown)?;
181            }
182            Type::Undefined => {
183                write!(out, "undefined",).map_err(|_| Error::Unknown)?;
184            }
185            _ => {}
186        };
187
188        Ok(())
189    }
190}
191
192/// Builder for [`Formatter`]
193#[derive(Default, Clone, Debug)]
194#[non_exhaustive]
195pub struct FormatterBuilder {
196    max_depth: Option<usize>,
197}
198
199impl FormatterBuilder {
200    /// Set the maximum depth to format, defaults to 10.
201    ///
202    /// If the depth is reached, the formatter will not try to print
203    /// inner items and will print `[Array]` or `[Object]`.
204    pub fn max_depth(self, max_depth: usize) -> Self {
205        Self {
206            max_depth: Some(max_depth),
207            ..self
208        }
209    }
210
211    /// Build the formatter
212    pub fn build(self) -> Formatter {
213        Formatter {
214            max_depth: self.max_depth.unwrap_or(10),
215        }
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use rquickjs_extra_test::test_with;
222
223    use super::*;
224
225    type StdString = std::string::String;
226
227    #[test]
228    fn format_string() {
229        test_with(|ctx| {
230            let formatter = Formatter::default();
231            let value = ctx.eval("'test'").unwrap();
232            let mut out = StdString::default();
233            formatter.format(&mut out, value).unwrap();
234            assert_eq!("test", out);
235        })
236    }
237
238    #[test]
239    fn format_int() {
240        test_with(|ctx| {
241            let formatter = Formatter::default();
242            let value = ctx.eval("true").unwrap();
243            let mut out = StdString::default();
244            formatter.format(&mut out, value).unwrap();
245            assert_eq!("true", out);
246        })
247    }
248
249    #[test]
250    fn format_bool() {
251        test_with(|ctx| {
252            let formatter = Formatter::default();
253            let value = ctx.eval("'test'").unwrap();
254            let mut out = StdString::default();
255            formatter.format(&mut out, value).unwrap();
256            assert_eq!("test", out);
257        })
258    }
259
260    #[test]
261    fn format_float() {
262        test_with(|ctx| {
263            let formatter = Formatter::default();
264            let value = ctx.eval("1.5").unwrap();
265            let mut out = StdString::default();
266            formatter.format(&mut out, value).unwrap();
267            assert_eq!("1.5", out);
268        })
269    }
270
271    #[test]
272    fn format_bigint() {
273        test_with(|ctx| {
274            let formatter = Formatter::default();
275            let value = ctx.eval("BigInt('9007199254740991')").unwrap();
276            let mut out = StdString::default();
277            formatter.format(&mut out, value).unwrap();
278            assert_eq!("9007199254740991n", out);
279        })
280    }
281
282    #[test]
283    fn format_array() {
284        test_with(|ctx| {
285            let formatter = Formatter::default();
286            let value = ctx.eval("[1,2,3]").unwrap();
287            let mut out = StdString::default();
288            formatter.format(&mut out, value).unwrap();
289            assert_eq!("[ 1, 2, 3 ]", out);
290        })
291    }
292
293    #[test]
294    fn format_array_max_depth() {
295        test_with(|ctx| {
296            let formatter = Formatter::builder().max_depth(1).build();
297            let value = ctx.eval("[1,[2,[3]]]").unwrap();
298            let mut out = StdString::default();
299            formatter.format(&mut out, value).unwrap();
300            assert_eq!("[ 1, [ 2, [Array] ] ]", out);
301        })
302    }
303
304    #[test]
305    fn format_object() {
306        test_with(|ctx| {
307            let formatter = Formatter::default();
308            let value = ctx.eval("const a = {'a':1}; a").unwrap();
309            let mut out = StdString::default();
310            formatter.format(&mut out, value).unwrap();
311            assert_eq!("{ a: 1 }", out);
312        })
313    }
314
315    #[test]
316    fn format_object_complex() {
317        test_with(|ctx| {
318            let formatter = Formatter::default();
319            let value = ctx
320                .eval("const a = {[['a','b']]:{'c': [{1: 'd'}]}}; a")
321                .unwrap();
322            let mut out = StdString::default();
323            formatter.format(&mut out, value).unwrap();
324            assert_eq!("{ a,b: { c: [ { 1: d } ] } }", out);
325        })
326    }
327
328    #[test]
329    fn format_object_max_depth() {
330        test_with(|ctx| {
331            let formatter = Formatter::builder().max_depth(1).build();
332            let value = ctx.eval("const a = {1:{2:{3:4}}}; a").unwrap();
333            let mut out = StdString::default();
334            formatter.format(&mut out, value).unwrap();
335            assert_eq!("{ 1: { 2: [Object] } }", out);
336        })
337    }
338
339    #[test]
340    fn format_symbol() {
341        test_with(|ctx| {
342            let formatter = Formatter::default();
343            let value = ctx.eval("Symbol('a')").unwrap();
344            let mut out = StdString::default();
345            formatter.format(&mut out, value).unwrap();
346            assert_eq!("Symbol(a)", out);
347        })
348    }
349
350    #[test]
351    fn format_function() {
352        test_with(|ctx| {
353            let formatter = Formatter::default();
354            let value = ctx.eval("const myfunc = () => {}; myfunc").unwrap();
355            let mut out = StdString::default();
356            formatter.format(&mut out, value).unwrap();
357            assert_eq!("[Function: myfunc]", out);
358        })
359    }
360
361    #[test]
362    fn format_undefined() {
363        test_with(|ctx| {
364            let formatter = Formatter::default();
365            let value = ctx.eval("undefined").unwrap();
366            let mut out = StdString::default();
367            formatter.format(&mut out, value).unwrap();
368            assert_eq!("undefined", out);
369        })
370    }
371
372    #[test]
373    fn format_null() {
374        test_with(|ctx| {
375            let formatter = Formatter::default();
376            let value = ctx.eval("null").unwrap();
377            let mut out = StdString::default();
378            formatter.format(&mut out, value).unwrap();
379            assert_eq!("null", out);
380        })
381    }
382}