use std::{error::Error, fmt}; #[cfg(not(target_arch = "wasm32"))] use tree_sitter::{Node, Parser, TreeCursor}; use wasm_bindgen::prelude::*; const INDENT: usize = 4; #[derive(Debug, Clone, PartialEq, Eq)] pub struct FormatError { message: String, } impl FormatError { fn new(message: impl Into) -> Self { Self { message: message.into(), } } pub fn message(&self) -> &str { &self.message } } impl fmt::Display for FormatError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.message) } } impl Error for FormatError {} pub fn format_source(source: &str) -> Result { decodal::parse_source(source) .map_err(|diagnostic| FormatError::new(format!("parse error: {}", diagnostic.message)))?; format_source_impl(source) } #[cfg(not(target_arch = "wasm32"))] fn format_source_impl(source: &str) -> Result { let mut parser = Parser::new(); parser .set_language(&tree_sitter_decodal::language()) .map_err(|error| { FormatError::new(format!("failed to load tree-sitter grammar: {error}")) })?; let tree = parser .parse(source, None) .ok_or_else(|| FormatError::new("tree-sitter failed to parse source"))?; let root = tree.root_node(); if root.has_error() { return Err(FormatError::new( "parse error: source contains tree-sitter errors", )); } let mut formatter = Formatter::new(source); let mut out = formatter.format_source_file(root); if !out.ends_with('\n') { out.push('\n'); } Ok(out) } #[wasm_bindgen(js_name = formatSource)] pub fn format_source_json(source: &str) -> String { match format_source(source) { Ok(formatted) => serde_json::json!({ "ok": true, "source": formatted }).to_string(), Err(error) => serde_json::json!({ "ok": false, "error": error.message() }).to_string(), } } #[cfg(target_arch = "wasm32")] fn format_source_impl(source: &str) -> Result { Ok(LexicalFormatter::new(source).format()) } #[cfg(target_arch = "wasm32")] struct LexicalFormatter<'a> { chars: core::iter::Peekable>, out: String, indent: usize, line_start: bool, } #[cfg(target_arch = "wasm32")] impl<'a> LexicalFormatter<'a> { fn new(source: &'a str) -> Self { Self { chars: source.chars().peekable(), out: String::new(), indent: 0, line_start: true, } } fn format(mut self) -> String { while let Some(ch) = self.chars.next() { match ch { ' ' | '\t' | '\r' | '\n' => self.consume_whitespace(ch), '#' => self.write_comment(), '"' => self.write_string(), '{' => self.open_block('{'), '[' => self.open_block('['), '}' => self.close_block('}'), ']' => self.close_block(']'), ';' => self.end_statement(), ',' => self.write_comma(), '.' => self.write_compact('.'), ':' => self.write_spaced_operator(":"), '(' => self.write_compact('('), ')' => self.write_compact(')'), '+' | '-' | '*' | '/' | '=' | '!' | '&' | '|' | '>' | '<' => { self.write_operator(ch) } _ => self.write_word(ch), } } self.trim_trailing_spaces(); if !self.out.ends_with('\n') { self.out.push('\n'); } self.out } fn consume_whitespace(&mut self, ch: char) { if ch == '\n' && self.out.ends_with('\n') && !self.out.ends_with("\n\n") { self.out.push('\n'); self.line_start = true; } } fn write_comment(&mut self) { if self.line_start { self.write_indent(); } else { self.ensure_space(); } self.out.push('#'); for ch in self.chars.by_ref() { if ch == '\n' { break; } self.out.push(ch); } self.trim_trailing_spaces(); self.out.push('\n'); self.line_start = true; } fn write_string(&mut self) { self.write_indent_if_needed(); self.ensure_word_boundary(); self.out.push('"'); let mut escaped = false; for ch in self.chars.by_ref() { self.out.push(ch); if escaped { escaped = false; } else if ch == '\\' { escaped = true; } else if ch == '"' { break; } } } fn open_block(&mut self, ch: char) { self.trim_trailing_spaces(); self.out.push(' '); self.out.push(ch); self.out.push('\n'); self.indent += INDENT; self.line_start = true; } fn close_block(&mut self, ch: char) { self.trim_trailing_spaces(); if !self.out.ends_with('\n') { self.out.push('\n'); } self.indent = self.indent.saturating_sub(INDENT); self.write_indent(); self.out.push(ch); self.line_start = false; } fn end_statement(&mut self) { self.trim_trailing_spaces(); self.out.push(';'); self.out.push('\n'); self.line_start = true; } fn write_comma(&mut self) { self.trim_trailing_spaces(); self.out.push(','); self.out.push(' '); self.line_start = false; } fn write_operator(&mut self, ch: char) { let mut op = String::new(); op.push(ch); if let Some(next) = self.chars.peek().copied() { let two_char = matches!( (ch, next), ('=', '=') | ('!', '=') | ('>', '=') | ('<', '=') | ('+', '+') | ('/', '/') | ('&', '&') | ('|', '|') | ('=', '>') ); if two_char { op.push(next); self.chars.next(); } } self.write_spaced_operator(&op); } fn write_spaced_operator(&mut self, op: &str) { self.trim_trailing_spaces(); self.ensure_space(); self.out.push_str(op); self.out.push(' '); self.line_start = false; } fn write_compact(&mut self, ch: char) { self.trim_trailing_spaces(); self.write_indent_if_needed(); self.out.push(ch); self.line_start = false; } fn write_word(&mut self, first: char) { self.write_indent_if_needed(); self.ensure_word_boundary(); self.out.push(first); while let Some(next) = self.chars.peek().copied() { if next.is_alphanumeric() || next == '_' { self.out.push(next); self.chars.next(); } else { break; } } self.line_start = false; } fn write_indent_if_needed(&mut self) { if self.line_start { self.write_indent(); self.line_start = false; } } fn write_indent(&mut self) { for _ in 0..self.indent { self.out.push(' '); } } fn ensure_word_boundary(&mut self) { let needs_space = self .out .chars() .last() .is_some_and(|ch| ch.is_alphanumeric() || matches!(ch, '_' | '"')); if needs_space { self.out.push(' '); } } fn ensure_space(&mut self) { let needs_space = self .out .chars() .last() .is_some_and(|ch| !matches!(ch, ' ' | '\n' | '.' | '(' | '[')); if needs_space { self.out.push(' '); } } fn trim_trailing_spaces(&mut self) { while self.out.ends_with(' ') || self.out.ends_with('\t') { self.out.pop(); } } } #[cfg(not(target_arch = "wasm32"))] struct Formatter<'a> { source: &'a str, } #[cfg(not(target_arch = "wasm32"))] impl<'a> Formatter<'a> { fn new(source: &'a str) -> Self { Self { source } } fn format_source_file(&mut self, node: Node<'a>) -> String { let mut out = String::new(); let children = named_children(node); self.write_statement_list(&mut out, &children, 0); out } fn write_statement_list(&mut self, out: &mut String, children: &[Node<'a>], indent: usize) { let mut pending_comments = Vec::new(); let mut pending_blank = false; let mut previous_end = 0; let mut previous_statement_end_row = None; for &child in children { let is_trailing_comment = child.kind() == "comment" && previous_statement_end_row == Some(child.start_position().row); if previous_statement_end_row.is_some() && !is_trailing_comment { if !out.ends_with('\n') { out.push('\n'); } previous_statement_end_row = None; } if previous_end > 0 && has_blank_line(self.slice(previous_end, child.start_byte())) { pending_blank = true; } if child.kind() == "comment" { if is_trailing_comment { if !out.ends_with(' ') { out.push(' '); } out.push_str(self.raw_trimmed(child)); out.push('\n'); previous_statement_end_row = None; } else { pending_comments.push(child); } previous_end = child.end_byte(); continue; } if pending_blank && !out.is_empty() && !out.ends_with("\n\n") { out.push('\n'); } pending_blank = false; for comment in pending_comments.drain(..) { write_indent(out, indent); out.push_str(self.raw_trimmed(comment)); out.push('\n'); } write_indent(out, indent); self.write_statement(out, child, indent); previous_statement_end_row = Some(child.end_position().row); previous_end = child.end_byte(); } if previous_statement_end_row.is_some() && !out.ends_with('\n') { out.push('\n'); } if pending_blank && !out.is_empty() && !out.ends_with("\n\n") { out.push('\n'); } for comment in pending_comments { write_indent(out, indent); out.push_str(self.raw_trimmed(comment)); out.push('\n'); } } fn write_statement(&mut self, out: &mut String, node: Node<'a>, indent: usize) { match node.kind() { "field_definition" => { self.write_field_definition(out, node, indent); out.push(';'); } _ => { self.write_expr(out, node, indent, 0); out.push(';'); } } } fn write_field_definition(&mut self, out: &mut String, node: Node<'a>, indent: usize) { if let Some(path) = node.child_by_field_name("path") { self.write_field_path(out, path); } else { out.push_str(self.raw_trimmed(node)); return; } out.push_str(" = "); if let Some(value) = node.child_by_field_name("value") { self.write_expr(out, value, indent, 0); } } fn write_field_path(&mut self, out: &mut String, node: Node<'a>) { let identifiers: Vec<_> = named_children(node) .into_iter() .filter(|child| child.kind() == "identifier") .collect(); if identifiers.is_empty() { out.push_str(self.raw_trimmed(node)); return; } for (index, identifier) in identifiers.into_iter().enumerate() { if index > 0 { out.push('.'); } out.push_str(self.raw_trimmed(identifier)); } } fn write_expr(&mut self, out: &mut String, node: Node<'a>, indent: usize, parent_prec: u8) { if expression_contains_comment(node) && !matches!( node.kind(), "object" | "array" | "let_expression" | "match_expression" ) { out.push_str(self.raw_trimmed(node)); return; } let prec = self.precedence(node); let parenthesize = prec < parent_prec; if parenthesize { out.push('('); } match node.kind() { "literal" => { if let Some(child) = named_children(node).first().copied() { out.push_str(self.raw_trimmed(child)); } else { out.push_str(self.raw_trimmed(node)); } } "identifier" | "string" | "integer" | "float" | "boolean" | "regex_literal" => { out.push_str(self.raw_trimmed(node)); } "comparison_constraint" => self.write_comparison_constraint(out, node, indent), "object" => self.write_object(out, node, indent), "array" => self.write_array(out, node, indent), "let_expression" => self.write_let(out, node, indent), "function_expression" => self.write_function(out, node, indent), "match_expression" => self.write_match(out, node, indent), "import_expression" => self.write_import(out, node), "parenthesized_expression" => { out.push('('); if let Some(expr) = named_children(node) .into_iter() .find(|child| child.kind() != "comment") { self.write_expr(out, expr, indent, 0); } out.push(')'); } "call_expression" => self.write_call(out, node, indent), "path_expression" => self.write_path(out, node, indent), "unary_expression" => self.write_unary(out, node, indent), "binary_expression" | "default_expression" => { self.write_binary_like(out, node, indent, prec) } _ => out.push_str(self.raw_trimmed(node)), } if parenthesize { out.push(')'); } } fn write_object(&mut self, out: &mut String, node: Node<'a>, indent: usize) { let children = named_children(node); if children.is_empty() { out.push_str("{}"); return; } out.push('{'); out.push('\n'); self.write_statement_list(out, &children, indent + INDENT); write_indent(out, indent); out.push('}'); } fn write_array(&mut self, out: &mut String, node: Node<'a>, indent: usize) { let children = named_children(node); if children.is_empty() { out.push_str("[]"); return; } let has_comment = children.iter().any(|child| child.kind() == "comment"); let inline = !has_comment && children.iter().all(|child| is_inline_expr(*child)); if inline { out.push('['); for (index, child) in children.into_iter().enumerate() { if index > 0 { out.push_str(", "); } self.write_expr(out, child, indent, 0); } out.push(']'); return; } out.push('['); out.push('\n'); let mut pending_comments = Vec::new(); for child in children { if child.kind() == "comment" { pending_comments.push(child); continue; } for comment in pending_comments.drain(..) { write_indent(out, indent + INDENT); out.push_str(self.raw_trimmed(comment)); out.push('\n'); } write_indent(out, indent + INDENT); self.write_expr(out, child, indent + INDENT, 0); out.push_str(",\n"); } for comment in pending_comments { write_indent(out, indent + INDENT); out.push_str(self.raw_trimmed(comment)); out.push('\n'); } write_indent(out, indent); out.push(']'); } fn write_let(&mut self, out: &mut String, node: Node<'a>, indent: usize) { let body = node.child_by_field_name("body"); out.push_str("let"); out.push('\n'); let children: Vec<_> = named_children(node) .into_iter() .filter(|child| Some(*child) != body) .collect(); self.write_statement_list(out, &children, indent + INDENT); write_indent(out, indent); out.push_str("in"); if let Some(body) = body { out.push('\n'); write_indent(out, indent + INDENT); self.write_expr(out, body, indent + INDENT, 0); } } fn write_function(&mut self, out: &mut String, node: Node<'a>, indent: usize) { let body = node.child_by_field_name("body"); let params: Vec<_> = named_children(node) .into_iter() .filter(|child| child.kind() == "parameter") .collect(); out.push('('); for (index, param) in params.into_iter().enumerate() { if index > 0 { out.push_str(", "); } self.write_parameter(out, param, indent); } out.push_str(") =>"); if let Some(body) = body { if is_inline_expr(body) { out.push(' '); self.write_expr(out, body, indent, 0); } else { out.push('\n'); write_indent(out, indent + INDENT); self.write_expr(out, body, indent + INDENT, 0); } } } fn write_parameter(&mut self, out: &mut String, node: Node<'a>, indent: usize) { if let Some(name) = node.child_by_field_name("name") { out.push_str(self.raw_trimmed(name)); } if let Some(constraint) = node.child_by_field_name("constraint") { out.push_str(": "); self.write_expr(out, constraint, indent, 0); } } fn write_match(&mut self, out: &mut String, node: Node<'a>, indent: usize) { out.push_str("match "); if let Some(scrutinee) = node.child_by_field_name("scrutinee") { self.write_expr(out, scrutinee, indent, 0); } out.push_str(" {"); let children: Vec<_> = named_children(node) .into_iter() .filter(|child| child.kind() == "match_arm" || child.kind() == "comment") .collect(); if !children.is_empty() { out.push('\n'); self.write_match_arms(out, &children, indent + INDENT); write_indent(out, indent); } out.push('}'); } fn write_match_arms(&mut self, out: &mut String, children: &[Node<'a>], indent: usize) { let mut pending_comments = Vec::new(); for &child in children { if child.kind() == "comment" { pending_comments.push(child); continue; } for comment in pending_comments.drain(..) { write_indent(out, indent); out.push_str(self.raw_trimmed(comment)); out.push('\n'); } write_indent(out, indent); if let Some(pattern) = child.child_by_field_name("pattern") { self.write_expr(out, pattern, indent, 0); } out.push_str(": "); if let Some(body) = child.child_by_field_name("body") { self.write_expr(out, body, indent, 0); } out.push_str(";\n"); } for comment in pending_comments { write_indent(out, indent); out.push_str(self.raw_trimmed(comment)); out.push('\n'); } } fn write_import(&mut self, out: &mut String, node: Node<'a>) { out.push_str("import "); if let Some(specifier) = node.child_by_field_name("specifier") { out.push_str(self.raw_trimmed(specifier)); } } fn write_call(&mut self, out: &mut String, node: Node<'a>, indent: usize) { let callee = node.child_by_field_name("function"); if let Some(callee) = callee { self.write_expr(out, callee, indent, self.precedence(node)); } out.push('('); let args: Vec<_> = named_children(node) .into_iter() .filter(|child| Some(*child) != callee && child.kind() != "comment") .collect(); for (index, arg) in args.into_iter().enumerate() { if index > 0 { out.push_str(", "); } self.write_expr(out, arg, indent, 0); } out.push(')'); } fn write_path(&mut self, out: &mut String, node: Node<'a>, indent: usize) { if let Some(object) = node.child_by_field_name("object") { self.write_expr(out, object, indent, self.precedence(node)); } out.push('.'); if let Some(field) = node.child_by_field_name("field") { out.push_str(self.raw_trimmed(field)); } } fn write_unary(&mut self, out: &mut String, node: Node<'a>, indent: usize) { if let Some(operand) = node.child_by_field_name("operand") { let operator = self.slice(node.start_byte(), operand.start_byte()).trim(); out.push_str(operator); self.write_expr(out, operand, indent, self.precedence(node)); } else { out.push_str(self.raw_trimmed(node)); } } fn write_comparison_constraint(&mut self, out: &mut String, node: Node<'a>, indent: usize) { if let Some(value) = node.child_by_field_name("value") { let operator = self.slice(node.start_byte(), value.start_byte()).trim(); out.push_str(operator); out.push(' '); self.write_expr(out, value, indent, self.precedence(node)); } else { out.push_str(self.raw_trimmed(node)); } } fn write_binary_like(&mut self, out: &mut String, node: Node<'a>, indent: usize, prec: u8) { let left = node .child_by_field_name("left") .or_else(|| node.child_by_field_name("base")); let right = node .child_by_field_name("right") .or_else(|| node.child_by_field_name("fallback")); let (Some(left), Some(right)) = (left, right) else { out.push_str(self.raw_trimmed(node)); return; }; self.write_expr(out, left, indent, prec); out.push(' '); if node.kind() == "default_expression" { out.push_str("default"); } else { out.push_str(self.slice(left.end_byte(), right.start_byte()).trim()); } out.push(' '); self.write_expr(out, right, indent, prec + 1); } fn precedence(&self, node: Node<'a>) -> u8 { match node.kind() { "default_expression" => 1, "binary_expression" => self.binary_precedence(node), "unary_expression" | "comparison_constraint" => 17, "call_expression" | "path_expression" => 19, _ => 20, } } fn binary_precedence(&self, node: Node<'a>) -> u8 { let Some(left) = node.child_by_field_name("left") else { return 10; }; let Some(right) = node.child_by_field_name("right") else { return 10; }; match self.slice(left.end_byte(), right.start_byte()).trim() { "//" => 3, "&" => 5, "||" => 7, "&&" => 9, "==" | "!=" | ">" | ">=" | "<" | "<=" => 11, "++" => 12, "+" | "-" => 13, "*" | "/" => 15, _ => 10, } } fn raw_trimmed(&self, node: Node<'a>) -> &'a str { self.slice(node.start_byte(), node.end_byte()).trim() } fn slice(&self, start: usize, end: usize) -> &'a str { &self.source[start..end] } } #[cfg(not(target_arch = "wasm32"))] fn named_children<'a>(node: Node<'a>) -> Vec> { let mut cursor: TreeCursor<'a> = node.walk(); node.named_children(&mut cursor).collect() } #[cfg(not(target_arch = "wasm32"))] fn expression_contains_comment(node: Node<'_>) -> bool { if node.kind() == "comment" { return true; } named_children(node) .into_iter() .any(expression_contains_comment) } #[cfg(not(target_arch = "wasm32"))] fn is_inline_expr(node: Node<'_>) -> bool { match node.kind() { "literal" | "identifier" | "string" | "integer" | "float" | "boolean" | "regex_literal" | "comparison_constraint" | "import_expression" => true, "path_expression" | "call_expression" | "unary_expression" | "binary_expression" | "default_expression" | "parenthesized_expression" => { !expression_contains_comment(node) && named_children(node).into_iter().all(is_inline_expr) } "array" => { !expression_contains_comment(node) && named_children(node).into_iter().all(is_inline_expr) } _ => false, } } #[cfg(not(target_arch = "wasm32"))] fn has_blank_line(text: &str) -> bool { text.bytes().filter(|byte| *byte == b'\n').count() >= 2 } #[cfg(not(target_arch = "wasm32"))] fn write_indent(out: &mut String, indent: usize) { for _ in 0..indent { out.push(' '); } } #[cfg(test)] mod tests { use super::*; #[test] fn formats_fields_and_preserves_comments() { let source = "Server={\n# host\nhost=String default \"localhost\"; # trailing\n\nport=Int&>0;\n};"; let formatted = format_source(source).unwrap(); assert_eq!( formatted, "Server = {\n # host\n host = String default \"localhost\"; # trailing\n\n port = Int & > 0;\n};\n" ); } #[test] fn formats_arrays_and_match_comments() { let source = "result=match x{# c\n_: [1,{a=2;}];};"; let formatted = format_source(source).unwrap(); assert_eq!( formatted, "result = match x {\n # c\n _: [\n 1,\n {\n a = 2;\n },\n ];\n};\n" ); } #[test] fn wasm_export_returns_json() { let output = format_source_json("value={a=1;};"); assert!(output.contains("\"ok\":true")); assert!(output.contains("value = {\\n a = 1;\\n};\\n")); } }