use super::tokenizer::JavaScriptTokenizer;
use crate::core::parser;
use crate::core::tokens::Token;
pub(crate) fn format_javascript(content: &str) -> String {
let tokenizer = JavaScriptTokenizer::new();
let tokens = match parser::parse(content, &tokenizer) {
Ok(tokens) => tokens,
Err(_) => return content.to_string(), };
let formatted = format_tokens(&tokens);
let normalized_content = normalize_whitespace(content);
let normalized_formatted = normalize_whitespace(&formatted);
if normalized_content == normalized_formatted {
return content.to_string();
}
formatted
}
fn normalize_whitespace(s: &str) -> String {
let mut result = String::new();
let mut last_was_whitespace = false;
for c in s.chars() {
if c.is_whitespace() {
if !last_was_whitespace {
result.push(' ');
last_was_whitespace = true;
}
} else {
result.push(c);
last_was_whitespace = false;
}
}
result.trim().to_string()
}
fn format_tokens(tokens: &[Token]) -> String {
let mut result = String::new();
let mut indent_level = 0;
let mut at_line_start = true;
for (i, token) in tokens.iter().enumerate() {
match token {
Token::OpenBrace => {
if !at_line_start && !result.ends_with(' ') {
result.push(' ');
}
result.push('{');
indent_level += 1;
result.push('\n');
at_line_start = true;
}
Token::CloseBrace => {
if !at_line_start {
result.push('\n');
}
if indent_level > 0 {
indent_level -= 1;
}
for _ in 0..indent_level {
result.push_str(" ");
}
result.push('}');
let next_token = tokens.get(i + 1);
if !matches!(
next_token,
Some(Token::Semicolon) | Some(Token::Comma) | Some(Token::CloseParen)
) {
result.push('\n');
at_line_start = true;
} else {
at_line_start = false;
}
}
Token::OpenParen => {
result.push('(');
at_line_start = false;
}
Token::CloseParen => {
result.push(')');
at_line_start = false;
}
Token::OpenBracket => {
result.push('[');
at_line_start = false;
}
Token::CloseBracket => {
result.push(']');
at_line_start = false;
}
Token::Semicolon => {
result.push(';');
result.push('\n');
at_line_start = true;
}
Token::Colon => {
result.push(':');
result.push(' '); at_line_start = false;
}
Token::Comma => {
result.push(',');
result.push(' '); at_line_start = false;
}
Token::Dot => {
result.push('.');
at_line_start = false;
}
Token::Operator(op) => {
let prev_token = if i > 0 { tokens.get(i - 1) } else { None };
let is_unary = matches!(
prev_token,
Some(Token::OpenParen)
| Some(Token::OpenBrace)
| Some(Token::OpenBracket)
| Some(Token::Comma)
| Some(Token::Semicolon)
| Some(Token::Colon)
| Some(Token::Operator(_))
| None
);
if !is_unary && !at_line_start {
result.push(' ');
}
result.push_str(op);
let next_token = tokens.get(i + 1);
if !matches!(
next_token,
Some(Token::Semicolon) | Some(Token::Comma) | Some(Token::CloseParen)
) {
result.push(' ');
}
at_line_start = false;
}
Token::Keyword(keyword) => {
if at_line_start {
for _ in 0..indent_level {
result.push_str(" ");
}
}
result.push_str(keyword);
let next_token = tokens.get(i + 1);
if !matches!(
next_token,
Some(Token::Semicolon) | Some(Token::Comma) | Some(Token::Dot)
) {
result.push(' ');
}
at_line_start = false;
}
Token::Identifier(ident) => {
if at_line_start {
for _ in 0..indent_level {
result.push_str(" ");
}
}
result.push_str(ident);
at_line_start = false;
}
Token::StringLiteral(s) => {
result.push('"');
result.push_str(s);
result.push('"');
at_line_start = false;
}
Token::NumberLiteral(n) => {
result.push_str(n);
at_line_start = false;
}
Token::Comment(c) => {
if at_line_start {
for _ in 0..indent_level {
result.push_str(" ");
}
} else {
result.push(' '); }
result.push_str("//");
result.push_str(c);
at_line_start = false;
}
Token::Whitespace(_ws) => {
}
Token::Newline => {
result.push('\n');
at_line_start = true;
}
Token::Other(c) => {
result.push(*c);
at_line_start = false;
}
}
}
if !result.ends_with('\n') {
result.push('\n');
}
result
}