use crate::cst::node::*;
pub struct CstFormatter {
indent_size: usize,
}
impl Default for CstFormatter {
fn default() -> Self {
Self { indent_size: 4 }
}
}
impl CstFormatter {
pub fn new() -> Self {
Self::default()
}
pub fn with_indent(indent_size: usize) -> Self {
Self { indent_size }
}
pub fn format(&self, root: &CstRoot) -> String {
let mut output = String::new();
for node in &root.nodes {
self.format_node(node, 0, &mut output);
}
if !output.ends_with('\n') {
output.push('\n');
}
output
}
fn format_node(&self, node: &CstNode, indent_level: usize, output: &mut String) {
match node {
CstNode::Trivia(trivia) => self.format_trivia(trivia, indent_level, output),
CstNode::Paragraph(para) => self.format_paragraph(para, indent_level, output),
CstNode::Command(cmd) => self.format_command(cmd, indent_level, output),
CstNode::SystemCall(call) => self.format_systemcall(call, indent_level, output),
CstNode::TextLine(text) => self.format_textline(text, indent_level, output),
CstNode::Block(block) => self.format_block(block, indent_level, output),
CstNode::EmbeddedCode(code) => self.format_embedded_code(code, indent_level, output),
CstNode::Attribute(attr) => self.format_attribute(attr, indent_level, output),
CstNode::Error { content, .. } => {
output.push_str(content);
output.push('\n');
}
}
}
fn format_trivia(&self, trivia: &CstTrivia, indent_level: usize, output: &mut String) {
match trivia {
CstTrivia::Whitespace { content, .. } => {
let newline_count = content.chars().filter(|&c| c == '\n').count();
if newline_count >= 2 {
output.push('\n');
}
}
CstTrivia::LineComment { content, .. } => {
self.indent(indent_level, output);
output.push_str("//");
output.push_str(content);
output.push('\n');
}
CstTrivia::BlockComment { content, .. } => {
let lines: Vec<&str> = content.lines().collect();
if lines.len() <= 1 {
self.indent(indent_level, output);
output.push_str("/*");
output.push_str(content);
output.push_str("*/");
output.push('\n');
} else {
let mut content_lines: Vec<&str> = Vec::new();
for line in &lines {
let trimmed = line.trim();
let stripped = if trimmed.starts_with("* ") {
&trimmed[2..]
} else if trimmed == "*" {
""
} else if trimmed.starts_with('*') {
&trimmed[1..]
} else {
trimmed
};
content_lines.push(stripped);
}
while content_lines.first().map(|s| s.is_empty()).unwrap_or(false) {
content_lines.remove(0);
}
while content_lines.last().map(|s| s.is_empty()).unwrap_or(false) {
content_lines.pop();
}
self.indent(indent_level, output);
output.push_str("/*\n");
for line in &content_lines {
self.indent(indent_level, output);
output.push_str(" *");
if !line.is_empty() {
output.push(' ');
output.push_str(line);
}
output.push('\n');
}
self.indent(indent_level, output);
output.push_str(" */");
output.push('\n');
}
}
}
}
fn format_paragraph(&self, para: &CstParagraph, indent_level: usize, output: &mut String) {
if !output.is_empty() && !output.ends_with("\n\n") {
output.push('\n');
}
output.push_str("::");
output.push_str(¶.name);
if !para.parameters.is_empty() {
output.push('(');
for (i, param) in para.parameters.iter().enumerate() {
if i > 0 {
output.push_str(", ");
}
self.format_parameter(param, output);
}
output.push(')');
}
output.push(' ');
self.format_block(¶.block, indent_level, output);
}
fn format_parameter(&self, param: &CstParameter, output: &mut String) {
output.push_str(¶m.name);
if let Some(ref default_value) = param.default_value {
output.push('=');
self.format_value(default_value, output);
}
}
fn format_block(&self, block: &CstBlock, indent_level: usize, output: &mut String) {
if indent_level > 0 {
self.indent(indent_level, output);
}
output.push_str("{\n");
for child in &block.children {
self.format_node(child, indent_level + 1, output);
}
self.indent(indent_level, output);
output.push_str("}\n");
}
fn format_attribute(&self, attr: &CstAttribute, indent_level: usize, output: &mut String) {
self.indent(indent_level, output);
output.push_str("#[");
output.push_str(&attr.keyword);
if let Some(condition) = &attr.condition {
output.push_str("(\"");
output.push_str(condition);
output.push_str("\")");
}
output.push_str("]\n");
}
fn format_command(&self, cmd: &CstCommand, indent_level: usize, output: &mut String) {
self.indent(indent_level, output);
output.push('@');
output.push_str(&cmd.command);
if !cmd.arguments.is_empty() {
match cmd.syntax {
CommandSyntax::Parenthesized { .. } => {
output.push('(');
for (i, arg) in cmd.arguments.iter().enumerate() {
if i > 0 {
output.push_str(", ");
}
self.format_argument(arg, output);
}
output.push(')');
}
CommandSyntax::SpaceSeparated => {
for arg in &cmd.arguments {
output.push(' ');
self.format_argument(arg, output);
}
}
}
}
output.push('\n');
}
fn format_systemcall(&self, call: &CstSystemCall, indent_level: usize, output: &mut String) {
self.indent(indent_level, output);
output.push('#');
output.push_str(&call.command);
if !call.arguments.is_empty() {
match call.syntax {
CommandSyntax::Parenthesized { .. } => {
output.push('(');
for (i, arg) in call.arguments.iter().enumerate() {
if i > 0 {
output.push_str(", ");
}
self.format_argument(arg, output);
}
output.push(')');
}
CommandSyntax::SpaceSeparated => {
for arg in &call.arguments {
output.push(' ');
self.format_argument(arg, output);
}
}
}
}
output.push('\n');
}
fn format_argument(&self, arg: &CstArgument, output: &mut String) {
output.push_str(&arg.name);
if let Some(ref value) = arg.value {
output.push('=');
self.format_value(value, output);
}
}
fn format_value(&self, value: &CstValue, output: &mut String) {
if matches!(value.kind, CstValueKind::Array) {
if let crate::format::RValue::Literal(lit) = &value.parsed {
output.push_str(&Self::format_literal_compact(lit));
return;
}
}
output.push_str(&value.raw);
}
fn format_literal_compact(lit: &crate::format::Literal) -> String {
use crate::format::Literal;
match lit {
Literal::Array(elements) => {
let parts: Vec<String> = elements
.iter()
.map(Self::format_literal_compact)
.collect();
format!("[{}]", parts.join(","))
}
Literal::String(s) => format!("\"{}\"", s),
other => other.to_string(),
}
}
fn format_textline(&self, text: &CstTextLine, indent_level: usize, output: &mut String) {
self.indent(indent_level, output);
if let Some(ref leading) = text.leading {
self.format_leading_text(leading, output);
output.push(' ');
}
if let Some(ref main_text) = text.text {
self.format_text(main_text, output);
}
if let Some(ref tailing) = text.tailing {
output.push(' ');
self.format_tailing_text(tailing, output);
}
output.push('\n');
}
fn format_leading_text(&self, leading: &CstLeadingText, output: &mut String) {
output.push('[');
match &leading.content {
CstLeadingTextContent::Text(s) => output.push_str(s),
CstLeadingTextContent::Template(tpl) => {
output.push('`');
self.format_template_literal(tpl, output);
output.push('`');
}
}
output.push(']');
}
fn format_text(&self, text: &CstText, output: &mut String) {
output.push_str(&text.raw);
}
fn format_template_literal(&self, tpl: &CstTemplateLiteral, output: &mut String) {
for part in &tpl.parts {
match part {
CstTemplatePart::Text { content, .. } => {
output.push_str(content);
}
CstTemplatePart::Value { variable, .. } => {
output.push_str("${");
output.push_str(&variable.chain.join("."));
output.push('}');
}
}
}
}
fn format_tailing_text(&self, tailing: &CstTailingText, output: &mut String) {
output.push('#');
output.push_str(&tailing.marker);
}
fn format_embedded_code(
&self,
code: &CstEmbeddedCode,
indent_level: usize,
output: &mut String,
) {
match code.syntax {
EmbeddedCodeSyntax::Brace => {
let trimmed_code = code.code.trim();
if trimmed_code.contains('\n') {
self.indent(indent_level, output);
output.push_str("@{\n");
let code_content = code
.code
.trim_end()
.trim_start_matches(|c: char| c == '\n' || c == '\r');
output.push_str(code_content);
output.push('\n');
self.indent(indent_level, output);
output.push_str("}\n");
} else {
self.indent(indent_level, output);
output.push_str("@{ ");
output.push_str(trimmed_code);
output.push_str(" }\n");
}
}
EmbeddedCodeSyntax::Hash => {
let trimmed_code = code.code.trim();
if trimmed_code.contains('\n') {
self.indent(indent_level, output);
output.push_str("##\n");
let code_content = code.code.trim_end();
output.push_str(code_content);
output.push('\n');
self.indent(indent_level, output);
output.push_str("##\n");
} else {
self.indent(indent_level, output);
output.push_str("## ");
output.push_str(trimmed_code);
output.push_str(" ##\n");
}
}
}
}
fn indent(&self, level: usize, output: &mut String) {
for _ in 0..(level * self.indent_size) {
output.push(' ');
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cst::parser::parse_tolerant;
#[test]
fn test_format_simple_command() {
let input = "@command(arg=1)";
let cst = parse_tolerant("test", input);
let formatter = CstFormatter::new();
let result = formatter.format(&cst);
assert!(result.contains("@command(arg=1)"));
}
#[test]
fn test_format_array_compact() {
let formatter = CstFormatter::new();
let cst = parse_tolerant("test", "@cmd x=[0,0]\n");
let result = formatter.format(&cst);
assert!(result.contains("@cmd x=[0,0]"), "got: {}", result);
let cst = parse_tolerant("test", "@cmd x=[ 0, 0 ]\n");
let result = formatter.format(&cst);
assert!(result.contains("@cmd x=[0,0]"), "got: {}", result);
let cst = parse_tolerant("test", "@cmd pts=[[1, 2], [3, 4]]\n");
let result = formatter.format(&cst);
assert!(result.contains("@cmd pts=[[1,2],[3,4]]"), "got: {}", result);
let cst = parse_tolerant("test", "@cmd tags=[\"a\", \"b\"]\n");
let result = formatter.format(&cst);
assert!(result.contains(r#"@cmd tags=["a","b"]"#), "got: {}", result);
let cst2 = parse_tolerant("test", &result);
let result2 = formatter.format(&cst2);
assert_eq!(result, result2, "Array formatting is not idempotent");
}
#[test]
fn test_format_paragraph() {
let input = r#"
::test {
@command(arg=1)
}
"#;
let cst = parse_tolerant("test", input);
let formatter = CstFormatter::new();
let result = formatter.format(&cst);
assert!(result.contains("::test {"));
assert!(result.contains(" @command(arg=1)"));
assert!(result.contains("}"));
}
#[test]
fn test_format_preserves_comments() {
let input = r#"
// 这是注释
::test {
/* 块注释 */
@command arg=1
}
"#;
let cst = parse_tolerant("test", input);
let formatter = CstFormatter::new();
let result = formatter.format(&cst);
assert!(result.contains("// 这是注释"));
assert!(result.contains("/* 块注释 */"));
}
#[test]
fn test_format_multiple_paragraphs() {
let input = r#"
::first {
@cmd1 arg=1
}
::second {
@cmd2 arg=2
}
"#;
let cst = parse_tolerant("test", input);
let formatter = CstFormatter::new();
let result = formatter.format(&cst);
assert!(result.contains("::first {"));
assert!(result.contains("::second {"));
assert!(result.contains("}\n\n::second"));
}
#[test]
fn test_format_text_line() {
let input = r#"
::test {
[speaker] "Hello, world!"
}
"#;
let cst = parse_tolerant("test", input);
let formatter = CstFormatter::new();
let result = formatter.format(&cst);
println!("Formatted result:\n{}", result);
assert!(result.contains("::test {"));
}
#[test]
fn test_format_system_call() {
let input = r#"
::test {
#goto(next)
}
"#;
let cst = parse_tolerant("test", input);
let formatter = CstFormatter::new();
let result = formatter.format(&cst);
assert!(result.contains("#goto(next)"));
}
#[test]
fn test_format_preserves_indent_before_comments() {
let input = r#"
::test {
// 这是一个注释
@command arg=1
/* 这是块注释 */
@another arg=2
}
"#;
let cst = parse_tolerant("test", input);
let formatter = CstFormatter::new();
let result = formatter.format(&cst);
println!("Formatted result:\n{}", result);
assert!(result.contains(" // 这是一个注释"));
assert!(result.contains(" /* 这是块注释 */"));
}
#[test]
fn test_format_no_extra_blank_lines() {
let input = r#"
::test {
@cmd1 arg=1
@cmd2 arg=2
@cmd3 arg=3
}
"#;
let cst = parse_tolerant("test", input);
let formatter = CstFormatter::new();
let result = formatter.format(&cst);
println!("Formatted result:\n{}", result);
assert!(!result.contains("@cmd1(arg=1)\n\n @cmd2"));
assert!(!result.contains("@cmd2(arg=2)\n\n @cmd3"));
}
#[test]
fn test_format_reduces_multiple_blank_lines() {
let input = r#"
::test {
@cmd1(arg=1)
@cmd2(arg=2)
}
"#;
let cst = parse_tolerant("test", input);
let formatter = CstFormatter::new();
let result = formatter.format(&cst);
println!("Formatted result:\n{}", result);
assert!(result.contains("@cmd1(arg=1)\n\n @cmd2"));
}
#[test]
fn test_format_preserves_command_syntax() {
let input1 = r#"
::test {
@command(arg=1, flag)
}
"#;
let cst1 = parse_tolerant("test", input1);
let formatter = CstFormatter::new();
let result1 = formatter.format(&cst1);
println!("Parenthesized syntax result:\n{}", result1);
assert!(result1.contains("@command(arg=1, flag)"));
let input2 = r#"
::test {
@command arg=1 flag
}
"#;
let cst2 = parse_tolerant("test", input2);
let result2 = formatter.format(&cst2);
println!("Space-separated syntax result:\n{}", result2);
assert!(result2.contains("@command arg=1 flag"));
}
#[test]
fn test_format_preserves_systemcall_syntax() {
let input1 = r#"
::test {
#goto(paragraph="main")
}
"#;
let cst1 = parse_tolerant("test", input1);
let formatter = CstFormatter::new();
let result1 = formatter.format(&cst1);
println!("Parenthesized systemcall result:\n{}", result1);
assert!(result1.contains("#goto(paragraph=\"main\")"));
let input2 = r#"
::test {
#goto paragraph="main"
}
"#;
let cst2 = parse_tolerant("test", input2);
let result2 = formatter.format(&cst2);
println!("Space-separated systemcall result:\n{}", result2);
assert!(result2.contains("#goto paragraph=\"main\""));
}
fn format_n_times(input: &str, n: usize) -> Vec<String> {
let formatter = CstFormatter::new();
let mut results = Vec::new();
let mut current = input.to_string();
for _ in 0..n {
let cst = parse_tolerant("test", ¤t);
current = formatter.format(&cst);
results.push(current.clone());
}
results
}
#[test]
fn test_format_hash_multiline_idempotent() {
let input = "::main {\n ##\n let x = 1;\n let y = 2;\n ##\n}\n";
let results = format_n_times(input, 5);
for (i, result) in results.iter().enumerate().skip(1) {
assert_eq!(
&results[0],
result,
"## 多行脚本格式化不幂等:第 1 次和第 {} 次结果不同\n第 1 次:\n{}\n第 {} 次:\n{}",
i + 1,
&results[0],
i + 1,
result
);
}
}
#[test]
fn test_format_hash_multiline_no_indent_idempotent() {
let input = "::main {\n##\nconst y = \"hello\";\nconsole.log(y);\n##\n}\n";
let results = format_n_times(input, 5);
for (i, result) in results.iter().enumerate().skip(1) {
assert_eq!(
&results[0],
result,
"## 无缩进脚本块格式化不幂等:第 1 次和第 {} 次结果不同",
i + 1
);
}
}
#[test]
fn test_format_block_comment_with_stars_idempotent() {
let input = "::main {\n /*\n * line 1\n * line 2\n */\n @cmd arg=1\n}\n";
let results = format_n_times(input, 5);
for (i, result) in results.iter().enumerate().skip(1) {
assert_eq!(
&results[0], result,
"带 * 的多行注释格式化不幂等:第 1 次和第 {} 次结果不同\n第 1 次:\n{}\n第 {} 次:\n{}",
i + 1, &results[0], i + 1, result
);
}
}
#[test]
fn test_format_block_comment_without_stars_idempotent() {
let input = "::main {\n /*\n line 1\n line 2\n */\n @cmd arg=1\n}\n";
let results = format_n_times(input, 5);
for (i, result) in results.iter().enumerate().skip(1) {
assert_eq!(
&results[0], result,
"不带 * 的多行注释格式化不幂等:第 1 次和第 {} 次结果不同\n第 1 次:\n{}\n第 {} 次:\n{}",
i + 1, &results[0], i + 1, result
);
}
}
#[test]
fn test_format_block_comment_inline_multiline_idempotent() {
let input = "::test {\n /* 多行注释\n 第二行 */\n @cmd arg=1\n}\n";
let results = format_n_times(input, 5);
for (i, result) in results.iter().enumerate().skip(1) {
assert_eq!(
&results[0],
result,
"内联多行注释格式化不幂等:第 1 次和第 {} 次结果不同\n第 1 次:\n{}\n第 {} 次:\n{}",
i + 1,
&results[0],
i + 1,
result
);
}
}
#[test]
fn test_format_block_comment_empty_lines_idempotent() {
let input = "::test {\n /*\n * line 1\n *\n * line 2\n */\n}\n";
let results = format_n_times(input, 5);
for (i, result) in results.iter().enumerate().skip(1) {
assert_eq!(
&results[0], result,
"含空行的多行注释格式化不幂等:第 1 次和第 {} 次结果不同\n第 1 次:\n{}\n第 {} 次:\n{}",
i + 1, &results[0], i + 1, result
);
}
assert!(results[0].contains(" *\n"), "多行注释中的空行应被保留");
}
#[test]
fn test_format_mixed_all_idempotent() {
let input = r#"::main {
/*
* 这是注释
* 第二行
*/
##
let x = 1;
let y = 2;
##
@cmd arg=1
}
"#;
let results = format_n_times(input, 5);
for (i, result) in results.iter().enumerate().skip(1) {
assert_eq!(
&results[0],
result,
"综合格式化不幂等:第 1 次和第 {} 次结果不同\n第 1 次:\n{}\n第 {} 次:\n{}",
i + 1,
&results[0],
i + 1,
result
);
}
}
#[test]
fn test_format_brace_multiline_idempotent() {
let input = "::code_test {\n @{\n const y = \"hello\";\n console.log(y);\n }\n}\n";
let results = format_n_times(input, 5);
for (i, result) in results.iter().enumerate().skip(1) {
assert_eq!(
&results[0],
result,
"@{{...}} 多行代码块格式化不幂等:第 1 次和第 {} 次结果不同\n第 1 次:\n{}\n第 {} 次:\n{}",
i + 1,
&results[0],
i + 1,
result
);
}
}
}