1use 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}