1mod error;
2mod types;
3
4use std::rc::Rc;
5
6use wasm_bindgen::{JsCast, JsValue};
7
8pub use error::PrettyFormatError;
9pub use types::{Config, Plugin, PrettyFormatOptions, Printer, Refs};
10
11use types::{Colors, Plugins};
12use web_sys::js_sys::{BigInt, Number, Object};
13
14fn print_number(val: &Number) -> String {
15 if Object::is(val, &JsValue::from_f64(-0.0)) {
16 "-0".into()
17 } else {
18 val.to_string(10)
19 .expect("Number should be formatted as string.")
20 .into()
21 }
22}
23
24fn print_big_int(val: &BigInt) -> String {
25 format!(
26 "{}n",
27 String::from(
28 val.to_string(10)
29 .expect("Number should be formatted as string.")
30 )
31 )
32}
33
34pub fn print_basic_value(
35 val: &JsValue,
36 _print_function_name: bool,
37 _escape_regex: bool,
38 escape_string: bool,
39) -> Option<String> {
40 if *val == JsValue::TRUE {
41 return Some("true".into());
42 }
43 if *val == JsValue::FALSE {
44 return Some("false".into());
45 }
46 if val.is_undefined() {
47 return Some("undefined".into());
48 }
49 if val.is_null() {
50 return Some("null".into());
51 }
52
53 let type_of = val.js_typeof();
54
55 if type_of == "number" {
56 return Some(print_number(val.unchecked_ref::<Number>()));
57 }
58 if type_of == "bigint" {
59 return Some(print_big_int(val.unchecked_ref::<BigInt>()));
60 }
61 if type_of == "string" {
62 if escape_string {
63 return Some(
64 val.as_string()
65 .expect("Value should be a string.")
66 .replace('"', "\\\"")
67 .replace('\\', "\\\\"),
68 );
69 }
70 return Some(format!(
71 "\"{}\"",
72 val.as_string().expect("Value should be a string.")
73 ));
74 }
75
76 todo!("print basic value {:?}", val)
77}
78
79pub fn print_complex_value(
80 val: &JsValue,
81 _config: &Config,
82 _indentation: String,
83 _depth: usize,
84 _refs: Refs,
85 _has_called_to_json: Option<bool>,
86) -> String {
87 todo!("print complex value {:?}", val)
88}
89
90fn print_plugin(
91 plugin: Rc<dyn Plugin>,
92 val: &JsValue,
93 config: &Config,
94 indentation: String,
95 depth: usize,
96 refs: Refs,
97) -> String {
98 plugin.serialize(val, config, indentation, depth, refs, &printer)
99}
100
101fn find_plugin(plugins: &Plugins, val: &JsValue) -> Option<Rc<dyn Plugin>> {
102 plugins.iter().find(|plugin| plugin.test(val)).cloned()
103}
104
105fn printer(
106 val: &JsValue,
107 config: &Config,
108 indentation: String,
109 depth: usize,
110 refs: Refs,
111 has_called_to_json: Option<bool>,
112) -> String {
113 if let Some(plugin) = find_plugin(&config.plugins, val) {
114 return print_plugin(plugin, val, config, indentation, depth, refs);
115 }
116
117 if let Some(basic_result) = print_basic_value(
118 val,
119 config.print_function_name,
120 config.escape_regex,
121 config.escape_string,
122 ) {
123 return basic_result;
124 }
125
126 print_complex_value(val, config, indentation, depth, refs, has_called_to_json)
127}
128
129fn validate_options(options: &PrettyFormatOptions) -> Result<(), PrettyFormatError> {
130 if options.min.is_some() && options.indent.is_some_and(|indent| indent != 0) {
131 Err(PrettyFormatError::Configuration(
132 "Options `min` and `indent` cannot be used togther.".into(),
133 ))
134 } else {
135 Ok(())
136 }
137}
138
139fn get_colors_highlight(options: &PrettyFormatOptions) -> Colors {
140 let theme = options.theme.clone().unwrap_or_default();
141
142 Colors {
143 comment: theme.comment,
144 content: theme.content,
145 prop: theme.prop,
146 tag: theme.tag,
147 value: theme.value,
148 }
149}
150
151fn get_colors_empty() -> Colors {
152 Colors::default()
153}
154
155fn get_print_function_name(options: &PrettyFormatOptions) -> bool {
156 options.print_function_name.unwrap_or(true)
157}
158
159fn get_escape_regex(options: &PrettyFormatOptions) -> bool {
160 options.escape_regex.unwrap_or(false)
161}
162
163fn get_escape_string(options: &PrettyFormatOptions) -> bool {
164 options.escape_string.unwrap_or(true)
165}
166
167fn get_config(options: PrettyFormatOptions) -> Config {
168 Config {
169 call_to_json: options.call_to_json.unwrap_or(true),
170 compare_keys: options.compare_keys.clone(),
171 colors: match options.highlight {
172 Some(true) => get_colors_highlight(&options),
173 _ => get_colors_empty(),
174 },
175 escape_regex: options.escape_regex.unwrap_or(false),
176 escape_string: options.escape_string.unwrap_or(true),
177 indent: match options.min {
178 Some(true) => "".into(),
179 _ => create_indent(options.indent.unwrap_or(2)),
180 },
181 max_depth: options.max_depth.unwrap_or(usize::MAX),
182 max_width: options.max_width.unwrap_or(usize::MAX),
183 min: options.min.unwrap_or(false),
184 plugins: options.plugins.unwrap_or_default(),
185 print_function_name: options.print_function_name.unwrap_or(true),
186 spacing_inner: match options.min {
187 Some(true) => " ",
188 _ => "\n",
189 }
190 .into(),
191 spacing_outer: match options.min {
192 Some(true) => "",
193 _ => "\n",
194 }
195 .into(),
196 }
197}
198
199fn create_indent(indent: usize) -> String {
200 " ".repeat(indent)
201}
202
203pub fn format(val: &JsValue, options: PrettyFormatOptions) -> Result<String, PrettyFormatError> {
204 validate_options(&options)?;
205
206 if let Some(plugins) = &options.plugins {
207 if let Some(plugin) = find_plugin(plugins, val) {
208 return Ok(print_plugin(
209 plugin,
210 val,
211 &get_config(options),
212 "".into(),
213 0,
214 vec![],
215 ));
216 }
217 }
218
219 let basic_result = print_basic_value(
220 val,
221 get_print_function_name(&options),
222 get_escape_regex(&options),
223 get_escape_string(&options),
224 );
225 if let Some(basic_result) = basic_result {
226 Ok(basic_result)
227 } else {
228 Ok(print_complex_value(
229 val,
230 &get_config(options),
231 "".into(),
232 0,
233 vec![],
234 None,
235 ))
236 }
237}