1mod format;
8mod table;
9
10use std::cell::{Cell, RefCell};
11use std::collections::hash_map::{Entry, HashMap};
12use std::time::Instant;
13
14use ascii_table::{Align, AsciiTable};
15use indent::indent_all_by;
16use indexmap::IndexSet;
17use ion::conversions::FromValue as _;
18use ion::flags::PropertyFlags;
19use ion::format::key::format_key;
20use ion::format::primitive::format_primitive;
21use ion::format::{Config as FormatConfig, format_value, indent_str};
22use ion::function::{Opt, Rest};
23use ion::{Context, Error, Object, OwnedKey, Result, Stack, Value, function_spec, js_fn};
24use mozjs::jsapi::JSFunctionSpec;
25
26use crate::cache::map::find_sourcemap;
27use crate::config::{Config, LogLevel};
28use crate::globals::console::format::{FormatArg, format_args, format_value_args};
29use crate::globals::console::table::{get_cells, sort_keys};
30
31const ANSI_CLEAR: &str = "\x1b[1;1H";
32const ANSI_CLEAR_SCREEN_DOWN: &str = "\x1b[0J";
33
34const DEFAULT_LABEL: &str = "default";
35
36thread_local! {
37 static COUNT_MAP: RefCell<HashMap<String, u32>> = RefCell::new(HashMap::new());
38 static TIMER_MAP: RefCell<HashMap<String, Instant>> = RefCell::new(HashMap::new());
39
40 static INDENTS: Cell<u16> = const { Cell::new(0) };
41}
42
43fn log_args(cx: &Context, args: &[Value], log_level: LogLevel) {
44 if log_level == LogLevel::None || args.is_empty() {
45 return;
46 }
47
48 if args.len() == 1 {
49 print_args(format_value_args(cx, args.iter()), log_level);
50 } else {
51 print_args(format_args(cx, args).into_iter(), log_level);
52 }
53}
54
55fn print_args<'cx>(args: impl Iterator<Item = FormatArg<'cx>>, log_level: LogLevel) {
56 if log_level == LogLevel::None {
57 return;
58 }
59
60 let mut first = true;
61
62 let mut prev_spaced = false;
63 for arg in args {
64 let spaced = arg.spaced();
65 let to_space = !first && (prev_spaced || spaced);
66 match log_level {
67 LogLevel::Info | LogLevel::Debug => {
68 if to_space {
69 print!(" ");
70 }
71 print!("{arg}");
72 }
73 LogLevel::Warn | LogLevel::Error => {
74 if to_space {
75 eprint!(" ");
76 }
77 eprint!("{arg}");
78 }
79 LogLevel::None => unreachable!(),
80 }
81 first = false;
82 prev_spaced = spaced;
83 }
84}
85
86fn print_indent(log_level: LogLevel) {
87 let indentation = usize::from(INDENTS.get());
88 match log_level {
89 LogLevel::Info | LogLevel::Debug => print!("{}", indent_str(indentation)),
90 LogLevel::Warn | LogLevel::Error => eprint!("{}", indent_str(indentation)),
91 LogLevel::None => {}
92 }
93}
94
95fn get_label(label: Option<String>) -> String {
97 if let Some(label) = label {
98 label
99 } else {
100 String::from(DEFAULT_LABEL)
101 }
102}
103
104#[js_fn]
105fn log(cx: &Context, Rest(values): Rest<Value>) {
106 if Config::global().log_level >= LogLevel::Info {
107 print_indent(LogLevel::Info);
108 log_args(cx, &values, LogLevel::Info);
109 println!();
110 }
111}
112
113#[js_fn]
114fn warn(cx: &Context, Rest(values): Rest<Value>) {
115 if Config::global().log_level >= LogLevel::Warn {
116 print_indent(LogLevel::Warn);
117 log_args(cx, &values, LogLevel::Warn);
118 println!();
119 }
120}
121
122#[js_fn]
123fn error(cx: &Context, Rest(values): Rest<Value>) {
124 if Config::global().log_level >= LogLevel::Error {
125 print_indent(LogLevel::Error);
126 log_args(cx, &values, LogLevel::Error);
127 println!();
128 }
129}
130
131#[js_fn]
132fn debug(cx: &Context, Rest(values): Rest<Value>) {
133 if Config::global().log_level == LogLevel::Debug {
134 print_indent(LogLevel::Debug);
135 log_args(cx, &values, LogLevel::Debug);
136 println!();
137 }
138}
139
140#[js_fn]
141fn assert(cx: &Context, Opt(assertion): Opt<bool>, Rest(values): Rest<Value>) {
142 if Config::global().log_level >= LogLevel::Error {
143 if let Some(assertion) = assertion {
144 if assertion {
145 return;
146 }
147
148 if values.is_empty() {
149 print_indent(LogLevel::Error);
150 eprintln!("Assertion Failed");
151 return;
152 }
153
154 if values[0].handle().is_string() {
155 print_indent(LogLevel::Error);
156 eprint!(
157 "Assertion Failed: {} ",
158 format_primitive(cx, FormatConfig::default(), &values[0])
159 );
160 log_args(cx, &values[2..], LogLevel::Error);
161 eprintln!();
162 return;
163 }
164
165 print_indent(LogLevel::Error);
166 eprint!("Assertion Failed: ");
167 log_args(cx, &values, LogLevel::Error);
168 println!();
169 } else {
170 eprintln!("Assertion Failed:");
171 }
172 }
173}
174
175#[js_fn]
176fn clear() {
177 INDENTS.set(0);
178
179 println!("{ANSI_CLEAR}");
180 println!("{ANSI_CLEAR_SCREEN_DOWN}");
181}
182
183#[js_fn]
184fn trace(cx: &Context, Rest(values): Rest<Value>) {
185 if Config::global().log_level == LogLevel::Debug {
186 print_indent(LogLevel::Debug);
187 print!("Trace: ");
188 log_args(cx, &values, LogLevel::Debug);
189 println!();
190
191 let mut stack = Stack::from_capture(cx);
192 let indents = ((INDENTS.get() + 1) * 2) as usize;
193
194 if let Some(stack) = &mut stack {
195 for record in &mut stack.records {
196 if let Some(sourcemap) = find_sourcemap(&record.location.file) {
197 record.transform_with_sourcemap(&sourcemap);
198 }
199 }
200
201 println!("{}", &indent_all_by(indents, stack.format()));
202 } else {
203 eprintln!("Current Stack could not be captured.");
204 }
205 }
206}
207
208#[js_fn]
209fn group(cx: &Context, Rest(values): Rest<Value>) {
210 INDENTS.set(INDENTS.get().min(u16::MAX - 1) + 1);
211
212 if Config::global().log_level >= LogLevel::Info {
213 log_args(cx, &values, LogLevel::Info);
214 println!();
215 }
216}
217
218#[js_fn]
219fn group_end() {
220 INDENTS.set(INDENTS.get().max(1) - 1);
221}
222
223#[js_fn]
224fn count(Opt(label): Opt<String>) {
225 let label = get_label(label);
226 COUNT_MAP.with_borrow_mut(|counts| {
227 let count = match counts.entry(label.clone()) {
228 Entry::Vacant(v) => *v.insert(1),
229 Entry::Occupied(mut o) => o.insert(o.get() + 1),
230 };
231 if Config::global().log_level >= LogLevel::Info {
232 print_indent(LogLevel::Info);
233 println!("{label}: {count}");
234 }
235 });
236}
237
238#[js_fn]
239fn count_reset(Opt(label): Opt<String>) {
240 let label = get_label(label);
241 COUNT_MAP.with_borrow_mut(|counts| match counts.get_mut(&label) {
242 Some(count) => {
243 *count = 0;
244 }
245 None => {
246 if Config::global().log_level >= LogLevel::Warn {
247 print_indent(LogLevel::Warn);
248 eprintln!("Count for {label} does not exist");
249 }
250 }
251 });
252}
253
254#[js_fn]
255fn time(Opt(label): Opt<String>) {
256 let label = get_label(label);
257 TIMER_MAP.with_borrow_mut(|timers| match timers.entry(label.clone()) {
258 Entry::Vacant(v) => {
259 v.insert(Instant::now());
260 }
261 Entry::Occupied(_) => {
262 if Config::global().log_level >= LogLevel::Warn {
263 print_indent(LogLevel::Warn);
264 eprintln!("Timer {label} already exists");
265 }
266 }
267 });
268}
269
270#[js_fn]
271fn time_log(cx: &Context, Opt(label): Opt<String>, Rest(values): Rest<Value>) {
272 let label = get_label(label);
273 TIMER_MAP.with_borrow(|timers| match timers.get(&label) {
274 Some(start) => {
275 if Config::global().log_level >= LogLevel::Info {
276 let duration = start.elapsed().as_millis();
277 print_indent(LogLevel::Info);
278 print!("{label}: {duration}ms ");
279 log_args(cx, &values, LogLevel::Info);
280 println!();
281 }
282 }
283 None => {
284 if Config::global().log_level >= LogLevel::Warn {
285 print_indent(LogLevel::Warn);
286 eprintln!("Timer {label} does not exist");
287 }
288 }
289 });
290}
291
292#[js_fn]
293fn time_end(Opt(label): Opt<String>) {
294 let label = get_label(label);
295 TIMER_MAP.with_borrow_mut(|timers| match timers.remove(&label) {
296 Some(start) => {
297 if Config::global().log_level >= LogLevel::Info {
298 let duration = start.elapsed().as_millis();
299 print_indent(LogLevel::Info);
300 print!("{label}: {duration}ms - Timer Ended");
301 println!();
302 }
303 }
304 None => {
305 if Config::global().log_level >= LogLevel::Warn {
306 print_indent(LogLevel::Warn);
307 eprintln!("Timer {label} does not exist");
308 }
309 }
310 });
311}
312
313#[js_fn]
314fn table(cx: &Context, data: Value, Opt(columns): Opt<Vec<String>>) -> Result<()> {
315 let indents = INDENTS.get();
316 if let Ok(object) = Object::from_value(cx, &data, true, ()) {
317 let rows = object.keys(cx, None).map(|key| key.to_owned_key(cx));
318 let mut has_values = false;
319
320 let (rows, columns) = if let Some(columns) = columns {
321 let mut keys = IndexSet::new();
322 for column in columns {
323 let key = match column.parse::<i32>() {
324 Ok(int) => OwnedKey::Int(int),
325 Err(_) => OwnedKey::String(column),
326 };
327 keys.insert(key);
328 }
329
330 (sort_keys(cx, rows)?, sort_keys(cx, keys.into_iter().map(Ok))?)
331 } else {
332 let rows: Vec<_> = rows.collect();
333 let mut keys = IndexSet::new();
334
335 for row in &rows {
336 let row = row.as_ref().map_err(Error::clone)?;
337 let value = object.get(cx, row)?.unwrap();
338 if let Ok(object) = Object::from_value(cx, &value, true, ()) {
339 let obj_keys = object.keys(cx, None).map(|key| key.to_owned_key(cx));
340 keys.reserve(obj_keys.len());
341 for key in obj_keys {
342 keys.insert(key?);
343 }
344 } else {
345 has_values = true;
346 }
347 }
348
349 (sort_keys(cx, rows)?, sort_keys(cx, keys.into_iter().map(Ok))?)
350 };
351
352 let mut table = AsciiTable::default();
353
354 table.column(0).set_header("Indices").set_align(Align::Center);
355 for (i, column) in columns.iter().enumerate() {
356 let key = format_key(cx, FormatConfig::default(), column);
357 table.column(i + 1).set_header(key.to_string()).set_align(Align::Center);
358 }
359 if has_values {
360 table.column(columns.len() + 1).set_header("Values").set_align(Align::Center);
361 }
362
363 let cells = get_cells(cx, &object, &rows, &columns, has_values);
364
365 println!("{}", indent_all_by((indents * 2) as usize, table.format(cells)));
366 } else if Config::global().log_level >= LogLevel::Info {
367 print_indent(LogLevel::Info);
368 println!(
369 "{}",
370 format_value(cx, FormatConfig::default().indentation(indents), &data)
371 );
372 }
373
374 Ok(())
375}
376
377const METHODS: &[JSFunctionSpec] = &[
378 function_spec!(log, 0),
379 function_spec!(log, c"info", 0),
380 function_spec!(log, c"dir", 0),
381 function_spec!(log, c"dirxml", 0),
382 function_spec!(warn, 0),
383 function_spec!(error, 0),
384 function_spec!(debug, 0),
385 function_spec!(assert, 0),
386 function_spec!(clear, 0),
387 function_spec!(trace, 0),
388 function_spec!(group, 0),
389 function_spec!(group, c"groupCollapsed", 0),
390 function_spec!(group_end, c"groupEnd", 0),
391 function_spec!(count, 1),
392 function_spec!(count_reset, c"countReset", 1),
393 function_spec!(time, 1),
394 function_spec!(time_log, c"timeLog", 1),
395 function_spec!(time_end, c"timeEnd", 1),
396 function_spec!(table, 1),
397 JSFunctionSpec::ZERO,
398];
399
400pub fn define(cx: &Context, global: &Object) -> bool {
401 let console = Object::new(cx);
402 (unsafe { console.define_methods(cx, METHODS) })
403 && global.define_as(cx, "console", &console, PropertyFlags::CONSTANT_ENUMERATED)
404}