use crate::error::AamlError;
use crate::pipeline::parser::AstNode;
#[derive(Debug, Clone)]
pub struct FormattingOptions {
pub indent_size: usize,
pub use_tabs: bool,
pub line_width: usize,
pub sort_keys: bool,
pub trailing_newline: bool,
pub preserve_blank_lines: bool,
}
impl Default for FormattingOptions {
fn default() -> Self {
Self {
indent_size: 4,
use_tabs: false,
line_width: 100,
sort_keys: false,
trailing_newline: true,
preserve_blank_lines: true,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct FormatRange {
pub start_line: usize,
pub end_line: usize,
}
pub trait Formatter: Send + Sync {
fn format_document(
&self,
nodes: &[AstNode],
options: &FormattingOptions,
) -> Result<String, AamlError>;
fn format_range(
&self,
nodes: &[AstNode],
range: FormatRange,
options: &FormattingOptions,
) -> Result<String, AamlError>;
fn format_node(
&self,
node: &AstNode,
indent_level: usize,
options: &FormattingOptions,
) -> Result<String, AamlError>;
fn normalize_comments(
&self,
content: &str,
options: &FormattingOptions,
) -> Result<String, AamlError>;
fn normalize_whitespace(&self, content: &str) -> Result<String, AamlError>;
}
pub struct DefaultFormatter;
impl DefaultFormatter {
pub fn new() -> Self {
Self
}
fn create_indent(level: usize, options: &FormattingOptions) -> String {
if options.use_tabs {
"\t".repeat(level)
} else {
" ".repeat(level * options.indent_size)
}
}
fn format_assignment(
key: &str,
value: &str,
indent_level: usize,
options: &FormattingOptions,
) -> String {
let indent = Self::create_indent(indent_level, options);
format!("{}{} = {}", indent, key, value)
}
fn format_directive(
name: &str,
args: &str,
indent_level: usize,
options: &FormattingOptions,
) -> String {
let indent = Self::create_indent(indent_level, options);
if args.is_empty() {
format!("{}@{}", indent, name)
} else {
format!("{}@{} {}", indent, name, args)
}
}
#[allow(dead_code)]
fn format_inline_object(pairs: &[(String, String)], _options: &FormattingOptions) -> String {
if pairs.is_empty() {
"{}".to_string()
} else {
let formatted_pairs: Vec<String> = pairs
.iter()
.map(|(k, v)| format!("{} = {}", k, v))
.collect();
format!("{{ {} }}", formatted_pairs.join(", "))
}
}
#[allow(dead_code)]
fn format_inline_list(items: &[String]) -> String {
if items.is_empty() {
"[]".to_string()
} else {
format!("[{}]", items.join(", "))
}
}
}
impl Default for DefaultFormatter {
fn default() -> Self {
Self::new()
}
}
impl Formatter for DefaultFormatter {
fn format_document(
&self,
nodes: &[AstNode],
options: &FormattingOptions,
) -> Result<String, AamlError> {
let mut output = Vec::new();
for node in nodes {
let formatted = self.format_node(node, 0, options)?;
output.push(formatted);
}
let mut result = output.join("\n");
if options.trailing_newline && !result.ends_with('\n') {
result.push('\n');
}
Ok(result)
}
fn format_range(
&self,
nodes: &[AstNode],
range: FormatRange,
options: &FormattingOptions,
) -> Result<String, AamlError> {
let mut output = Vec::new();
for node in nodes {
let line = node.line();
if line >= range.start_line && line <= range.end_line {
let formatted = self.format_node(node, 0, options)?;
output.push(formatted);
} else {
output.push(format!("(original line {})", line));
}
}
Ok(output.join("\n"))
}
fn format_node(
&self,
node: &AstNode,
indent_level: usize,
options: &FormattingOptions,
) -> Result<String, AamlError> {
let formatted = match node {
AstNode::Assignment { key, value, .. } => {
Self::format_assignment(key, &value.to_string(), indent_level, options)
}
AstNode::Directive { name, args, .. } => {
Self::format_directive(name, args, indent_level, options)
}
};
Ok(formatted)
}
fn normalize_comments(
&self,
content: &str,
_options: &FormattingOptions,
) -> Result<String, AamlError> {
let lines: Vec<&str> = content.lines().collect();
let normalized: Vec<String> = lines
.iter()
.map(|line| {
if let Some(pos) = line.find('#') {
let before = &line[..pos];
let after = &line[pos + 1..];
if pos > 0
&& pos < line.len() - 1
&& before.ends_with(' ')
&& !after.starts_with('#')
{
let comment = after.trim_start();
return format!("{}# {}", before.trim_end(), comment);
}
}
line.to_string()
})
.collect();
Ok(normalized.join("\n"))
}
fn normalize_whitespace(&self, content: &str) -> Result<String, AamlError> {
let lines: Vec<&str> = content.lines().collect();
let normalized: Vec<String> = lines
.iter()
.map(|line| line.trim_end().to_string())
.collect();
Ok(normalized.join("\n"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_indent_spaces() {
let options = FormattingOptions {
indent_size: 4,
use_tabs: false,
..Default::default()
};
assert_eq!(DefaultFormatter::create_indent(0, &options), "");
assert_eq!(DefaultFormatter::create_indent(1, &options), " ");
assert_eq!(DefaultFormatter::create_indent(2, &options), " ");
}
#[test]
fn test_create_indent_tabs() {
let options = FormattingOptions {
use_tabs: true,
..Default::default()
};
assert_eq!(DefaultFormatter::create_indent(0, &options), "");
assert_eq!(DefaultFormatter::create_indent(1, &options), "\t");
assert_eq!(DefaultFormatter::create_indent(2, &options), "\t\t");
}
#[test]
fn test_format_assignment() {
let formatted =
DefaultFormatter::format_assignment("key", "value", 0, &FormattingOptions::default());
assert_eq!(formatted, "key = value");
}
#[test]
fn test_format_assignment_with_indent() {
let options = FormattingOptions {
indent_size: 2,
..Default::default()
};
let formatted = DefaultFormatter::format_assignment("key", "value", 1, &options);
assert_eq!(formatted, " key = value");
}
#[test]
fn test_format_directive() {
let formatted = DefaultFormatter::format_directive(
"import",
"base.aam",
0,
&FormattingOptions::default(),
);
assert_eq!(formatted, "@import base.aam");
}
#[test]
fn test_format_inline_object() {
let pairs = vec![
("host".to_string(), "localhost".to_string()),
("port".to_string(), "8080".to_string()),
];
let formatted =
DefaultFormatter::format_inline_object(&pairs, &FormattingOptions::default());
assert_eq!(formatted, "{ host = localhost, port = 8080 }");
}
#[test]
fn test_format_inline_list() {
let items = vec!["a".to_string(), "b".to_string(), "c".to_string()];
let formatted = DefaultFormatter::format_inline_list(&items);
assert_eq!(formatted, "[a, b, c]");
}
#[test]
fn test_normalize_whitespace() {
let formatter = DefaultFormatter::new();
let input = "key = value \nfoo = bar ";
let result = formatter.normalize_whitespace(input).unwrap();
assert_eq!(result, "key = value\nfoo = bar");
}
#[test]
fn test_format_document() {
let formatter = DefaultFormatter::new();
let ast = vec![AstNode::Assignment {
key: "name".to_string().into(),
value: crate::pipeline::parser::ValueNode::Literal("test".to_string().into()),
line: 1,
}];
let result = formatter
.format_document(&ast, &FormattingOptions::default())
.unwrap();
assert!(result.contains("name = test"));
}
}