quickjs_runtime/features/
console.rs

1//! the console feature enables the script to use various cansole.log variants
2//! see also: [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console)
3//! the following methods are available
4//! * console.log()
5//! * console.info()
6//! * console.error()
7//! * console.warning()
8//! * console.trace()
9//!
10//! The methods use rust's log crate to output messages. e.g. console.info() uses the log::info!() macro
11//! so the console messages should appear in the log you initialized from rust
12//!
13//! All methods accept a single message string and optional substitution values
14//!
15//! e.g.
16//! ```javascript
17//! console.log('Oh dear %s totaly failed %i times because of a %.4f variance in the space time continuum', 'some guy', 12, 2.46)
18//! ```
19//! will output 'Oh dear some guy totaly failed 12 times because of a 2.4600 variance in the space time continuum'
20//!
21//! The string substitution you can use are
22//! * %o or %O Outputs a JavaScript object (serialized)
23//! * %d or %i Outputs an integer. Number formatting is supported, for example  console.log("Foo %.2d", 1.1) will output the number as two significant figures with a leading 0: Foo 01
24//! * %s Outputs a string (will attempt to call .toString() on objects, use %o to output a serialized JSON string)
25//! * %f Outputs a floating-point value. Formatting is supported, for example  console.log("Foo %.2f", 1.1) will output the number to 2 decimal places: Foo 1.10
26//! # Example
27//! ```rust
28//! use quickjs_runtime::builder::QuickJsRuntimeBuilder;
29//! use log::LevelFilter;
30//! use quickjs_runtime::jsutils::Script;
31//! simple_logging::log_to_file("console_test.log", LevelFilter::max())
32//!             .ok()
33//!             .expect("could not init logger");
34//! let rt = QuickJsRuntimeBuilder::new().build();
35//! rt.eval_sync(None, Script::new(
36//! "console.es",
37//! "console.log('the %s %s %s jumped over %i fences with a accuracy of %.2f', 'quick', 'brown', 'fox', 32, 0.512);"
38//! )).expect("script failed");
39//! ```
40//!
41//! which will result in a log entry like
42//! ```[00:00:00.012] (7f44e7d24700) INFO   the quick brown fox jumped over 32 fences with a accuracy of 0.51```
43
44use crate::jsutils::{JsError, JsValueType};
45use crate::quickjs_utils;
46use crate::quickjs_utils::functions::call_to_string;
47use crate::quickjs_utils::json::stringify;
48use crate::quickjs_utils::{functions, json, parse_args, primitives};
49use crate::quickjsrealmadapter::QuickJsRealmAdapter;
50use crate::quickjsruntimeadapter::QuickJsRuntimeAdapter;
51use crate::quickjsvalueadapter::QuickJsValueAdapter;
52use crate::reflection::Proxy;
53use libquickjs_sys as q;
54use log::LevelFilter;
55use std::str::FromStr;
56
57pub fn init(q_js_rt: &QuickJsRuntimeAdapter) -> Result<(), JsError> {
58    q_js_rt.add_context_init_hook(|_q_js_rt, q_ctx| init_ctx(q_ctx))
59}
60
61pub(crate) fn init_ctx(q_ctx: &QuickJsRealmAdapter) -> Result<(), JsError> {
62    Proxy::new()
63        .name("console")
64        .static_native_method("log", Some(console_log))
65        .static_native_method("trace", Some(console_trace))
66        .static_native_method("info", Some(console_info))
67        .static_native_method("warn", Some(console_warn))
68        .static_native_method("error", Some(console_error))
69        //.static_native_method("assert", Some(console_assert)) // todo
70        .static_native_method("debug", Some(console_debug))
71        .install(q_ctx, true)
72        .map(|_| {})
73}
74
75#[allow(clippy::or_fun_call)]
76unsafe fn parse_field_value(
77    ctx: *mut q::JSContext,
78    field: &str,
79    value: &QuickJsValueAdapter,
80) -> String {
81    // format ints
82    // only support ,2 / .3 to declare the number of digits to display, e.g. $.3i turns 3 to 003
83
84    // format floats
85    // only support ,2 / .3 to declare the number of decimals to display, e.g. $.3f turns 3.1 to 3.100
86
87    if field.eq(&"%.0f".to_string()) {
88        return parse_field_value(ctx, "%i", value);
89    }
90
91    if field.ends_with('d') || field.ends_with('i') {
92        let mut i_val: String = call_to_string(ctx, value).unwrap_or_default();
93
94        // remove chars behind .
95        if let Some(i) = i_val.find('.') {
96            let _ = i_val.split_off(i);
97        }
98
99        if let Some(dot_in_field_idx) = field.find('.') {
100            let mut m_field = field.to_string();
101            // get part behind dot
102            let mut num_decimals_str = m_field.split_off(dot_in_field_idx + 1);
103            // remove d or i at end
104            let _ = num_decimals_str.split_off(num_decimals_str.len() - 1);
105            // see if we have a number
106            if !num_decimals_str.is_empty() {
107                let ct_res = usize::from_str(num_decimals_str.as_str());
108                // check if we can parse the number to a usize
109                if let Ok(ct) = ct_res {
110                    // and if so, make i_val longer
111                    while i_val.len() < ct {
112                        i_val = format!("0{i_val}");
113                    }
114                }
115            }
116        }
117
118        return i_val;
119    } else if field.ends_with('f') {
120        let mut f_val: String = call_to_string(ctx, value).unwrap_or_default();
121
122        if let Some(dot_in_field_idx) = field.find('.') {
123            let mut m_field = field.to_string();
124            // get part behind dot
125            let mut num_decimals_str = m_field.split_off(dot_in_field_idx + 1);
126            // remove d or i at end
127            let _ = num_decimals_str.split_off(num_decimals_str.len() - 1);
128            // see if we have a number
129            if !num_decimals_str.is_empty() {
130                let ct_res = usize::from_str(num_decimals_str.as_str());
131                // check if we can parse the number to a usize
132                if let Ok(ct) = ct_res {
133                    // and if so, make i_val longer
134                    if ct > 0 {
135                        if !f_val.contains('.') {
136                            f_val.push('.');
137                        }
138
139                        let dot_idx = f_val.find('.').unwrap();
140
141                        while f_val.len() - dot_idx <= ct {
142                            f_val.push('0');
143                        }
144                        if f_val.len() - dot_idx > ct {
145                            let _ = f_val.split_off(dot_idx + ct + 1);
146                        }
147                    }
148                }
149            }
150            return f_val;
151        }
152    } else if field.ends_with('o') || field.ends_with('O') {
153        let json_str_res = json::stringify(ctx, value, None);
154        let json = match json_str_res {
155            Ok(json_str) => {
156                if json_str.is_undefined() {
157                    // if undefined is passed tp json.stringify it returns undefined, else always a string
158                    "undefined".to_string()
159                } else {
160                    primitives::to_string(ctx, &json_str).unwrap_or_default()
161                }
162            }
163            Err(_e) => "".to_string(),
164        };
165        return json;
166    }
167    call_to_string(ctx, value).unwrap_or_default()
168}
169
170unsafe fn stringify_log_obj(ctx: *mut q::JSContext, arg: &QuickJsValueAdapter) -> String {
171    match stringify(ctx, arg, None) {
172        Ok(r) => match primitives::to_string(ctx, &r) {
173            Ok(s) => s,
174            Err(e) => format!("Error: {e}"),
175        },
176        Err(e) => format!("Error: {e}"),
177    }
178}
179
180#[allow(clippy::or_fun_call)]
181unsafe fn parse_line(ctx: *mut q::JSContext, args: Vec<QuickJsValueAdapter>) -> String {
182    let mut output = String::new();
183
184    output.push_str("JS_REALM:");
185    QuickJsRealmAdapter::with_context(ctx, |realm| {
186        output.push('[');
187        output.push_str(realm.id.as_str());
188        output.push_str("][");
189        if let Ok(script_or_module_name) = quickjs_utils::get_script_or_module_name_q(realm) {
190            output.push_str(script_or_module_name.as_str());
191        }
192        output.push_str("]: ");
193    });
194
195    if args.is_empty() {
196        return output;
197    }
198
199    let message = match &args[0].get_js_type() {
200        JsValueType::Object => stringify_log_obj(ctx, &args[0]),
201        JsValueType::Function => stringify_log_obj(ctx, &args[0]),
202        JsValueType::Array => stringify_log_obj(ctx, &args[0]),
203        _ => functions::call_to_string(ctx, &args[0]).unwrap_or_default(),
204    };
205
206    let mut field_code = String::new();
207    let mut in_field = false;
208
209    let mut x = 1;
210
211    let mut filled = 1;
212
213    if args[0].is_string() {
214        for chr in message.chars() {
215            if in_field {
216                field_code.push(chr);
217                if chr.eq(&'s') || chr.eq(&'d') || chr.eq(&'f') || chr.eq(&'o') || chr.eq(&'i') {
218                    // end field
219
220                    if x < args.len() {
221                        output.push_str(
222                            parse_field_value(ctx, field_code.as_str(), &args[x]).as_str(),
223                        );
224                        x += 1;
225                        filled += 1;
226                    }
227
228                    in_field = false;
229                    field_code = String::new();
230                }
231            } else if chr.eq(&'%') {
232                in_field = true;
233            } else {
234                output.push(chr);
235            }
236        }
237    } else {
238        output.push_str(message.as_str());
239    }
240
241    for arg in args.iter().skip(filled) {
242        // add args which we're not filled in str
243        output.push(' ');
244        let tail_arg = match arg.get_js_type() {
245            JsValueType::Object => stringify_log_obj(ctx, arg),
246            JsValueType::Function => stringify_log_obj(ctx, arg),
247            JsValueType::Array => stringify_log_obj(ctx, arg),
248            _ => call_to_string(ctx, arg).unwrap_or_default(),
249        };
250        output.push_str(tail_arg.as_str());
251    }
252
253    output
254}
255
256unsafe extern "C" fn console_log(
257    ctx: *mut q::JSContext,
258    _this_val: q::JSValue,
259    argc: ::std::os::raw::c_int,
260    argv: *mut q::JSValue,
261) -> q::JSValue {
262    if log::max_level() >= LevelFilter::Info {
263        let args = parse_args(ctx, argc, argv);
264        log::info!("{}", parse_line(ctx, args));
265    }
266    quickjs_utils::new_null()
267}
268
269unsafe extern "C" fn console_trace(
270    ctx: *mut q::JSContext,
271    _this_val: q::JSValue,
272    argc: ::std::os::raw::c_int,
273    argv: *mut q::JSValue,
274) -> q::JSValue {
275    if log::max_level() >= LevelFilter::Trace {
276        let args = parse_args(ctx, argc, argv);
277        log::trace!("{}", parse_line(ctx, args));
278    }
279    quickjs_utils::new_null()
280}
281
282unsafe extern "C" fn console_debug(
283    ctx: *mut q::JSContext,
284    _this_val: q::JSValue,
285    argc: ::std::os::raw::c_int,
286    argv: *mut q::JSValue,
287) -> q::JSValue {
288    if log::max_level() >= LevelFilter::Debug {
289        let args = parse_args(ctx, argc, argv);
290        log::debug!("{}", parse_line(ctx, args));
291    }
292    quickjs_utils::new_null()
293}
294
295unsafe extern "C" fn console_info(
296    ctx: *mut q::JSContext,
297    _this_val: q::JSValue,
298    argc: ::std::os::raw::c_int,
299    argv: *mut q::JSValue,
300) -> q::JSValue {
301    if log::max_level() >= LevelFilter::Info {
302        let args = parse_args(ctx, argc, argv);
303        log::info!("{}", parse_line(ctx, args));
304    }
305    quickjs_utils::new_null()
306}
307
308unsafe extern "C" fn console_warn(
309    ctx: *mut q::JSContext,
310    _this_val: q::JSValue,
311    argc: ::std::os::raw::c_int,
312    argv: *mut q::JSValue,
313) -> q::JSValue {
314    if log::max_level() >= LevelFilter::Warn {
315        let args = parse_args(ctx, argc, argv);
316        log::warn!("{}", parse_line(ctx, args));
317    }
318    quickjs_utils::new_null()
319}
320
321unsafe extern "C" fn console_error(
322    ctx: *mut q::JSContext,
323    _this_val: q::JSValue,
324    argc: ::std::os::raw::c_int,
325    argv: *mut q::JSValue,
326) -> q::JSValue {
327    if log::max_level() >= LevelFilter::Error {
328        let args = parse_args(ctx, argc, argv);
329        log::error!("{}", parse_line(ctx, args));
330    }
331    quickjs_utils::new_null()
332}
333
334#[cfg(test)]
335pub mod tests {
336    use crate::builder::QuickJsRuntimeBuilder;
337    use crate::jsutils::Script;
338    use std::thread;
339    use std::time::Duration;
340
341    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
342    pub async fn test_console() {
343        eprintln!("> test_console");
344        /*
345                let loglevel = log::LevelFilter::Info;
346
347                tracing_log::LogTracer::builder()
348                    //.ignore_crate("swc_ecma_codegen")
349                    //.ignore_crate("swc_ecma_transforms_base")
350                    .with_max_level(loglevel)
351                    .init()
352                    .expect("could not init LogTracer");
353
354                // Graylog address
355                let address = format!("{}:{}", "192.168.10.43", 12201);
356
357                // Start tracing
358                let mut conn_handle = tracing_gelf::Logger::builder()
359                    .init_udp(address)
360                    .expect("could not init udp con for logger");
361
362                // Spawn background task
363                // Any futures executor can be used
364
365                println!("> init");
366                tokio::runtime::Handle::current().spawn(async move {
367                    //
368                    conn_handle.connect().await
369                    //
370                });
371                println!("< init");
372                log::error!("Logger initialized");
373
374                tracing::error!("via tracing");
375        */
376        // Send log using a macro defined in the create log
377
378        log::info!("> test_console");
379        let rt = QuickJsRuntimeBuilder::new().build();
380        rt.eval_sync(
381            None,
382            Script::new(
383                "test_console.es",
384                "console.log('one %s', 'two', 3);\
385            console.error('two %s %s', 'two', 3);\
386            console.error('date:', new Date());\
387            console.error('err:', new Error('testpoof'));\
388            console.error('array:', [1, 2, true, {a: 1}]);\
389            console.error('obj: %o', {a: 1});\
390            console.error({obj: true}, {obj: false});",
391            ),
392        )
393        .expect("test_console.es failed");
394        log::info!("< test_console");
395
396        thread::sleep(Duration::from_secs(1));
397    }
398}