runtime/globals/console/
mod.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 */
6
7mod 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
95// TODO: Convert to Undefinable<String> as null is a valid label
96fn 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}