runtime/globals/console/
format.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
7use std::fmt;
8use std::fmt::{Display, Formatter, Write as _};
9
10use ion::conversions::FromValue as _;
11use ion::format::{Config as FormatConfig, ValueDisplay, format_value};
12use ion::{BigInt, Context, Local, Result, Value};
13use mozjs::conversions::ConversionBehavior;
14
15use crate::config::{Config, LogLevel};
16use crate::globals::console::INDENTS;
17
18pub(crate) enum FormatArg<'cx> {
19	String(String),
20	Value { value: ValueDisplay<'cx>, spaced: bool },
21}
22
23impl FormatArg<'_> {
24	pub(crate) fn spaced(&self) -> bool {
25		match self {
26			FormatArg::String(_) => false,
27			FormatArg::Value { spaced, .. } => *spaced,
28		}
29	}
30}
31
32impl Display for FormatArg<'_> {
33	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
34		match self {
35			FormatArg::String(string) => string.fmt(f),
36			FormatArg::Value { value, .. } => value.fmt(f),
37		}
38	}
39}
40
41pub(crate) fn format_args<'cx>(cx: &'cx Context, args: &'cx [Value<'cx>]) -> Vec<FormatArg<'cx>> {
42	fn inner<'cx>(cx: &'cx Context, args: &'cx [Value<'cx>]) -> Result<Vec<FormatArg<'cx>>> {
43		if args.len() <= 1 || !args[0].get().is_string() {
44			return Ok(format_value_args(cx, args.iter()).collect());
45		}
46
47		let format = String::from_value(cx, &args[0], true, ())?;
48
49		if format.is_empty() {
50			return Ok(format_value_args(cx, args.iter()).collect());
51		}
52
53		let mut outputs = Vec::new();
54		let mut output = String::with_capacity(format.len());
55
56		let mut args = args.iter().skip(1).peekable();
57		let mut index = 0;
58
59		for (base, _) in format.match_indices('%') {
60			if base < index {
61				continue;
62			}
63
64			output.push_str(&format[index..base]);
65			index = base + 1;
66
67			match get_ascii_at(&format, index) {
68				next @ (Some(b'%') | None) => {
69					if next.is_some() || index == format.len() {
70						output.push('%');
71					}
72					index += 1;
73				}
74				Some(b'0'..=b'9' | b'.' | b'd' | b'i' | b'f') => {
75					let arg = args.next().unwrap();
76					format_number_arg(cx, arg, &format, &mut index, &mut output)?;
77				}
78				Some(b's') => {
79					let arg = args.next().unwrap();
80					index += 1;
81
82					output.push_str(&String::from_value(cx, arg, false, ())?);
83				}
84				Some(b'o' | b'O') => {
85					let arg = args.next().unwrap();
86					index += 1;
87
88					outputs.push(FormatArg::String(output));
89					output = String::with_capacity(format.len() - index);
90
91					outputs.push(FormatArg::Value {
92						value: format_value(cx, FormatConfig::default().indentation(INDENTS.get()), arg),
93						spaced: false,
94					});
95				}
96				Some(b'c') => {
97					index += 1;
98				}
99				Some(b) => {
100					output.push('%');
101					output.push(char::from(b));
102					index += 1;
103				}
104			}
105
106			if args.peek().is_none() {
107				break;
108			}
109		}
110
111		output.push_str(&format[index..]);
112		outputs.push(FormatArg::String(output));
113		outputs.extend(format_value_args(cx, args));
114		Ok(outputs)
115	}
116
117	inner(cx, args).unwrap_or_else(|error| {
118		if Config::global().log_level >= LogLevel::Warn {
119			eprintln!("{}", error.format());
120		}
121		Vec::new()
122	})
123}
124
125pub(crate) fn format_value_args<'cx>(
126	cx: &'cx Context, args: impl Iterator<Item = &'cx Value<'cx>>,
127) -> impl Iterator<Item = FormatArg<'cx>> {
128	args.map(|arg| FormatArg::Value {
129		value: format_value(cx, FormatConfig::default().indentation(INDENTS.get()), arg),
130		spaced: true,
131	})
132}
133
134pub(crate) fn format_number_arg<'cx>(
135	cx: &'cx Context, arg: &Value<'cx>, format: &str, index: &mut usize, output: &mut String,
136) -> Result<()> {
137	let (w_len, width) = parse_maximum(&format[*index..]).unzip();
138	*index += w_len.unwrap_or(0);
139	let (p_len, precision) = get_ascii_at(format, *index)
140		.filter(|b| *b == b'.')
141		.and_then(|_| parse_maximum(&format[*index + 1..]))
142		.unzip();
143	*index += p_len.map_or(0, |len| len + 1);
144
145	match get_ascii_at(format, *index) {
146		Some(b'd' | b'i') => {
147			if arg.get().is_symbol() {
148				output.push_str("NaN");
149			} else if arg.get().is_bigint() {
150				let bigint = BigInt::from(unsafe { Local::from_marked(&arg.get().to_bigint()) });
151				output.push_str(&bigint.to_string(cx, 10).unwrap().to_owned(cx)?);
152			} else {
153				write_printf(
154					output,
155					width,
156					precision,
157					i32::from_value(cx, arg, false, ConversionBehavior::Default)?,
158				)?;
159			}
160			*index += 1;
161		}
162		Some(b'f') => {
163			if arg.get().is_symbol() {
164				output.push_str("NaN");
165			} else {
166				write_printf(output, width, precision, f64::from_value(cx, arg, false, ())?)?;
167			}
168			*index += 1;
169		}
170		_ => output.push_str(&format[(*index - 1)..*index]),
171	}
172
173	Ok(())
174}
175fn get_ascii_at(str: &str, index: usize) -> Option<u8> {
176	str.as_bytes().get(index).copied().filter(u8::is_ascii)
177}
178
179fn parse_maximum(str: &str) -> Option<(usize, usize)> {
180	if str.is_empty() || !str.as_bytes()[0].is_ascii_digit() {
181		return None;
182	}
183
184	let end = str.bytes().position(|b| !b.is_ascii_digit()).unwrap_or(str.len());
185	Some((end, str[..end].parse().unwrap()))
186}
187
188fn write_printf<D: Display>(
189	output: &mut String, width: Option<usize>, precision: Option<usize>, display: D,
190) -> fmt::Result {
191	match (width, precision) {
192		(Some(width), Some(precision)) => write!(output, "{display:width$.precision$}"),
193		(Some(width), None) => write!(output, "{display:width$}"),
194		(None, Some(precision)) => write!(output, "{display:.precision$}"),
195		(None, None) => write!(output, "{display}"),
196	}
197}