1#![warn(missing_docs)]
2
3use nargo_ir::{JsExpr, JsStmt, TemplateNodeIR, Trivia};
9use nargo_parser::Parser;
10use nargo_types::{Error, NargoValue, Result};
11use oak_core::source::{SourceBuffer, ToSource};
12use oak_json::{parse, JsonValue};
13use serde::{Deserialize, Serialize};
14use std::sync::Arc;
15
16#[derive(Debug, Clone, Serialize, Deserialize, Default)]
20pub struct FormatterConfig {
21 #[serde(alias = "indentSize", default = "default_indent_size")]
23 pub indent_size: usize,
24 #[serde(alias = "indentType", default = "default_indent_type")]
26 pub indent_type: IndentType,
27 #[serde(alias = "lineWidth", default = "default_line_width")]
29 pub line_width: usize,
30 #[serde(alias = "singleQuote", default = "default_single_quote")]
32 pub single_quote: bool,
33 #[serde(default = "default_semi")]
35 pub semi: bool,
36 #[serde(alias = "trailingComma", default = "default_trailing_comma")]
38 pub trailing_comma: TrailingComma,
39 #[serde(alias = "attrSpacing", default = "default_attr_spacing")]
41 pub attr_spacing: bool,
42 #[serde(alias = "selfClosingSpace", default = "default_self_closing_space")]
44 pub self_closing_space: bool,
45 #[serde(alias = "tagSpacing", default = "default_tag_spacing")]
47 pub tag_spacing: bool,
48 #[serde(alias = "arrowParens", default = "default_arrow_parens")]
50 pub arrow_parens: bool,
51 #[serde(alias = "objectSpacing", default = "default_object_spacing")]
53 pub object_spacing: bool,
54 #[serde(alias = "arraySpacing", default = "default_array_spacing")]
56 pub array_spacing: bool,
57 #[serde(alias = "functionSpacing", default = "default_function_spacing")]
59 pub function_spacing: bool,
60 #[serde(alias = "blockSpacing", default = "default_block_spacing")]
62 pub block_spacing: bool,
63}
64
65fn default_indent_size() -> usize {
67 2
68}
69
70fn default_indent_type() -> IndentType {
72 IndentType::Spaces
73}
74
75fn default_line_width() -> usize {
77 80
78}
79
80fn default_single_quote() -> bool {
82 true
83}
84
85fn default_semi() -> bool {
87 false
88}
89
90fn default_trailing_comma() -> TrailingComma {
92 TrailingComma::Es5
93}
94
95fn default_attr_spacing() -> bool {
97 true
98}
99
100fn default_self_closing_space() -> bool {
102 true
103}
104
105fn default_tag_spacing() -> bool {
107 true
108}
109
110fn default_arrow_parens() -> bool {
112 true
113}
114
115fn default_object_spacing() -> bool {
117 true
118}
119
120fn default_array_spacing() -> bool {
122 true
123}
124
125fn default_function_spacing() -> bool {
127 true
128}
129
130fn default_block_spacing() -> bool {
132 true
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
137pub enum IndentType {
138 #[serde(alias = "spaces")]
140 #[default]
141 Spaces,
142 #[serde(alias = "tabs")]
144 Tabs,
145}
146
147#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
149pub enum TrailingComma {
150 #[serde(alias = "none")]
152 None,
153 #[serde(alias = "es5")]
155 #[default]
156 Es5,
157 #[serde(alias = "all")]
159 All,
160}
161
162pub struct NargoFormatter {
166 parser: Parser<'static>,
168 registry: Arc<nargo_parser::ParserRegistry>,
170 config: FormatterConfig,
172}
173
174impl NargoFormatter {
175 pub fn new() -> Self {
180 Self::with_config(FormatterConfig::default())
181 }
182
183 pub fn with_config(config: FormatterConfig) -> Self {
191 let mut registry = nargo_parser::ParserRegistry::new();
192
193 registry.register_template_parser("vue", std::sync::Arc::new(nargo_parser::OakVueTemplateParser));
195
196 registry.register_script_parser("ts", std::sync::Arc::new(nargo_parser::OakTypeScriptParser));
198 registry.register_script_parser("js", std::sync::Arc::new(nargo_parser::OakTypeScriptParser));
199
200 registry.register_style_parser("css", std::sync::Arc::new(nargo_parser::OakCssParser));
202
203 let registry = std::sync::Arc::new(registry);
207 let parser = Parser::new("format".to_string(), "", registry.clone());
208 Self { parser, registry, config }
209 }
210
211 pub fn load_config_from_file(file_path: &str) -> Result<FormatterConfig> {
219 let content = std::fs::read_to_string(file_path)?;
220 let config: FormatterConfig = serde_json::from_str(&content).map_err(|e| nargo_types::Error::external_error("serde_json".to_string(), e.to_string(), nargo_types::Span::default()))?;
221 Ok(config)
222 }
223
224 pub fn save_config_to_file(config: &FormatterConfig, file_path: &str) -> Result<()> {
233 let content = serde_json::to_string_pretty(config).map_err(|e| nargo_types::Error::external_error("serde_json".to_string(), e.to_string(), nargo_types::Span::default()))?;
234 std::fs::write(file_path, content)?;
235 Ok(())
236 }
237
238 pub async fn format(&self, source: &str) -> Result<String> {
246 let mut parser = Parser::new("format".to_string(), source, self.registry.clone());
255 let ir = parser.parse_all()?;
256 let mut output = String::new();
257
258 if let Some(template) = &ir.template {
259 output.push_str("<template>\n");
260 for node in &template.nodes {
261 let formatted = self.format_template_node(node, 1);
262 if !formatted.is_empty() {
263 output.push_str(&formatted);
264 }
265 }
266 output.push_str("</template>\n\n");
267 }
268
269 if let Some(script) = &ir.script {
270 output.push_str("<script>\n");
271 for stmt in &script.body {
272 let formatted = self.format_js_stmt(stmt, 1);
273 let trimmed = formatted.trim();
274 if !trimmed.is_empty() && trimmed != ";" {
275 output.push_str(&formatted);
276 output.push('\n');
277 }
278 }
279 output.push_str("</script>\n\n");
280 }
281
282 for style in &ir.styles {
283 output.push_str("<style");
284 if style.lang != "css" {
285 output.push_str(&format!(" lang=\"{}\"", style.lang));
286 }
287 if style.scoped {
288 output.push_str(" scoped");
289 }
290 output.push_str(">");
291 output.push_str(&style.code);
292 if !style.code.ends_with('\n') {
293 output.push('\n');
294 }
295 output.push_str("</style>\n\n");
296 }
297
298 for block in &ir.custom_blocks {
299 output.push_str(&format!("<{}", block.name));
300 for (k, v) in &block.attributes {
301 output.push_str(&format!(" {}=\"{}\"", k, v));
302 }
303 output.push_str(">");
304 output.push_str(&block.content);
305 if !block.content.ends_with('\n') {
306 output.push('\n');
307 }
308 output.push_str(&format!("</{}>\n\n", block.name));
309 }
310
311 Ok(output.trim().to_string() + "\n")
312 }
313
314 fn get_indent(&self, indent: usize) -> String {
322 match self.config.indent_type {
323 IndentType::Spaces => " ".repeat(self.config.indent_size * indent),
324 IndentType::Tabs => "\t".repeat(indent),
325 }
326 }
327
328 fn format_template_node(&self, node: &TemplateNodeIR, indent: usize) -> String {
337 let indent_str = self.get_indent(indent);
338 match node {
339 TemplateNodeIR::Text(text, _, _) => {
340 let trimmed = text.trim();
341 if trimmed.is_empty() {
342 String::new()
343 }
344 else {
345 format!("{}{}\n", indent_str, trimmed)
346 }
347 }
348 TemplateNodeIR::Element(el) => {
349 let trivia_str = self.format_trivia(&el.trivia, indent);
350 let mut s = format!("{}{}<{}", trivia_str, indent_str, el.tag);
351
352 for attr in &el.attributes {
353 let name = if attr.is_directive {
354 if attr.name.starts_with("v-") {
355 attr.name.clone()
356 }
357 else {
358 format!("v-{}", attr.name)
359 }
360 }
361 else if attr.is_dynamic {
362 if attr.name.starts_with(':') {
363 attr.name.clone()
364 }
365 else {
366 format!(":{}", attr.name)
367 }
368 }
369 else {
370 attr.name.clone()
371 };
372
373 if let Some(val) = &attr.value {
374 let spacing = if self.config.attr_spacing { " " } else { "" };
375 s.push_str(&format!(" {}{}=\"{}\"", name, spacing, val));
376 }
377 else if let Some(val_ast) = &attr.value_ast {
378 let spacing = if self.config.attr_spacing { " " } else { "" };
379 s.push_str(&format!(" {}{}=\"{}\"", name, spacing, self.format_js_expr(val_ast)));
380 }
381 else {
382 s.push_str(&format!(" {}", name));
383 }
384 }
385
386 if el.children.is_empty() {
387 let spacing = if self.config.self_closing_space { " " } else { "" };
388 s.push_str(&format!("{}/>", spacing));
389 }
390 else {
391 s.push_str(">");
392 for child in &el.children {
393 s.push_str(&self.format_template_node(child, indent + 1));
394 }
395 s.push_str(&format!("{}</{}>\n", indent_str, el.tag));
396 }
397 s
398 }
399 TemplateNodeIR::Interpolation(expr) => {
400 format!("{}{{{{{}}}}}\n", indent_str, self.format_js_expr(&expr.ast.as_ref().unwrap()))
401 }
402 TemplateNodeIR::Comment(comment, _, _) => {
403 format!("{}<!--{}-->\n", indent_str, comment)
404 }
405 _ => String::new(),
406 }
407 }
408
409 fn format_js_stmt(&self, stmt: &JsStmt, indent: usize) -> String {
418 let indent_str = self.get_indent(indent);
419 let trivia = stmt.trivia();
420 let trivia_str = self.format_trivia(trivia, indent);
421
422 let stmt_core = match stmt {
423 JsStmt::Expr(expr, _, _) => {
424 let expr_str = self.format_js_expr(expr);
425 if self.config.semi {
426 format!("{};\n", expr_str)
427 }
428 else {
429 format!("{}\n", expr_str)
430 }
431 }
432 JsStmt::Import { source, specifiers, .. } => {
433 let quote = if self.config.single_quote { "'" } else { "\"" };
434 format!("import {{ {} }} from {}{}{};\n", specifiers.join(", "), quote, source, quote)
435 }
436 JsStmt::Export { declaration, .. } => {
437 let decl_str = self.format_js_stmt(declaration, 0);
438 if decl_str.starts_with("export") {
439 decl_str
440 }
441 else {
442 format!("export {}", decl_str)
443 }
444 }
445 JsStmt::ExportAll { source, .. } => {
446 let quote = if self.config.single_quote { "'" } else { "\"" };
447 format!("export * from {}{}{};\n", quote, source, quote)
448 }
449 JsStmt::ExportNamed { source, specifiers, .. } => {
450 if let Some(src) = source {
451 let quote = if self.config.single_quote { "'" } else { "\"" };
452 format!("export {{ {} }} from {}{}{};\n", specifiers.join(", "), quote, src, quote)
453 }
454 else {
455 format!("export {{ {} }}{}\n", specifiers.join(", "), if self.config.semi { ";" } else { "" })
456 }
457 }
458 JsStmt::VariableDecl { kind, id, init, .. } => {
459 if let Some(init_expr) = init {
460 format!("{} {} = {}{}\n", kind, id, self.format_js_expr(init_expr), if self.config.semi { ";" } else { "" })
461 }
462 else {
463 format!("{} {}{}\n", kind, id, if self.config.semi { ";" } else { "" })
464 }
465 }
466 JsStmt::FunctionDecl { id, params, body, .. } => {
467 let mut s = format!("function {}({}) {{\n", id, params.join(", "));
468 for sub_stmt in body {
469 s.push_str(&self.format_js_stmt(sub_stmt, indent + 1));
470 }
471 s.push_str(&format!("{}}}\n", indent_str));
472 s
473 }
474 _ => String::new(),
475 };
476
477 format!("{}{}{}", trivia_str, indent_str, stmt_core)
478 }
479
480 fn format_js_expr(&self, expr: &JsExpr) -> String {
488 let trivia = expr.trivia();
489 let mut s = String::new();
490 for comment in &trivia.leading_comments {
491 s.push_str(&format!("//{}\n", comment.content));
492 }
493
494 let expr_core = match expr {
495 JsExpr::Literal(val, _, _) => self.format_nargo_value(val),
496 JsExpr::Identifier(id, _, _) => id.clone(),
497 JsExpr::Unary { op, argument, .. } => {
498 format!("{}{}", op, self.format_js_expr(argument))
499 }
500 JsExpr::Binary { left, op, right, .. } => {
501 format!("{} {} {}", self.format_js_expr(left), op, self.format_js_expr(right))
502 }
503 JsExpr::Call { callee, args, .. } => {
504 let args_str: Vec<_> = args.iter().map(|a| self.format_js_expr(a)).collect();
505 let spacing = if self.config.function_spacing { " " } else { "" };
506 format!("{}{}({}{}{})", self.format_js_expr(callee), spacing, spacing, args_str.join(", "), spacing)
507 }
508 JsExpr::Member { object, property, computed, .. } => {
509 if *computed {
510 format!("{}[{}]", self.format_js_expr(object), self.format_js_expr(property))
511 }
512 else {
513 if let JsExpr::Identifier(ref prop, _, _) = **property {
514 format!("{}.{}", self.format_js_expr(object), prop)
515 }
516 else {
517 format!("{}[{}]", self.format_js_expr(object), self.format_js_expr(property))
518 }
519 }
520 }
521 JsExpr::Object(props, _, _) => {
522 let mut props_vec: Vec<_> = props.iter().collect();
523 props_vec.sort_by(|a, b| a.0.cmp(b.0));
524 let props_str: Vec<_> = props_vec.iter().map(|(k, v)| format!("{}: {}", k, self.format_js_expr(v))).collect();
525 let spacing = if self.config.object_spacing { " " } else { "" };
526 format!("{{{}{}{}}}", spacing, props_str.join(", "), spacing)
527 }
528 JsExpr::Array(elements, _, _) => {
529 let elems_str: Vec<_> = elements.iter().map(|e| self.format_js_expr(e)).collect();
530 let spacing = if self.config.array_spacing { " " } else { "" };
531 format!("[{}]", elems_str.join(", "))
532 }
533 JsExpr::ArrowFunction { params, body, .. } => {
534 if self.config.arrow_parens || params.len() != 1 {
535 format!("({}) => {}", params.join(", "), self.format_js_expr(body))
536 }
537 else {
538 format!("{} => {}", params[0], self.format_js_expr(body))
539 }
540 }
541 JsExpr::Conditional { test, consequent, alternate, .. } => {
542 format!("{} ? {} : {}", self.format_js_expr(test), self.format_js_expr(consequent), self.format_js_expr(alternate))
543 }
544 JsExpr::TemplateLiteral { quasis, expressions, .. } => {
545 let mut s = "`".to_string();
546 for i in 0..quasis.len() {
547 s.push_str(&quasis[i]);
548 if i < expressions.len() {
549 s.push_str(&format!("${{{}}}", self.format_js_expr(&expressions[i])));
550 }
551 }
552 s.push('`');
553 s
554 }
555 _ => String::new(),
556 };
557
558 s.push_str(&expr_core);
559 s
560 }
561
562 fn format_trivia(&self, trivia: &Trivia, indent: usize) -> String {
571 let indent_str = self.get_indent(indent);
572 let mut s = String::new();
573
574 for comment in &trivia.leading_comments {
575 s.push_str(&format!("{}//{}\n", indent_str, comment.content));
576 }
577
578 s
579 }
580
581 fn format_nargo_value(&self, val: &NargoValue) -> String {
589 match val {
590 NargoValue::Null => "null".to_string(),
591 NargoValue::Bool(b) => b.to_string(),
592 NargoValue::Number(n) => n.to_string(),
593 NargoValue::String(s) => {
594 if self.config.single_quote {
595 format!("'{}'", s)
596 }
597 else {
598 format!("\"{}\"", s)
599 }
600 }
601 NargoValue::Array(arr) => {
602 let items: Vec<_> = arr.iter().map(|v| self.format_nargo_value(v)).collect();
603 format!("[{}]", items.join(", "))
604 }
605 NargoValue::Object(obj) => {
606 let items: Vec<_> = obj.iter().map(|(k, v)| format!("{}: {}", k, self.format_nargo_value(v))).collect();
607 format!("{{ {} }}", items.join(", "))
608 }
609 NargoValue::Signal(s) => format!("${}", s),
610 NargoValue::Raw(r) => r.clone(),
611 _ => format!("{:?}", val),
612 }
613 }
614}