masm_formatter/
lib.rs

1use regex::Regex;
2use std::fs::File;
3use std::io::{self, BufRead, BufReader, Write};
4use std::path::Path;
5
6#[derive(Debug, PartialEq, Clone)]
7enum ConstructType {
8    Proc,
9    Export,
10    ExportLine,
11    Begin,
12    End,
13    While,
14    Repeat,
15    If,
16    Else,
17}
18
19impl ConstructType {
20    fn from_str(s: &str) -> Option<Self> {
21        match s {
22            "proc" => Some(Self::Proc),
23            "export" => Some(Self::Export),
24            "exportLine" => Some(Self::ExportLine),
25            "begin" => Some(Self::Begin),
26            "end" => Some(Self::End),
27            "while" => Some(Self::While),
28            "repeat" => Some(Self::Repeat),
29            "if" => Some(Self::If),
30            "else" => Some(Self::Else),
31            _ => None,
32        }
33    }
34}
35
36const INDENT: &str = "    ";
37
38fn is_comment(line: &str) -> bool {
39    line.trim_start().starts_with('#')
40}
41
42fn is_single_export_line(line: &str) -> bool {
43    let re = Regex::new(r"^export\.\w*(::\w+)+$").unwrap();
44    re.is_match(line)
45}
46
47pub fn format_code(code: &str) -> String {
48    let mut formatted_code = String::new();
49    let mut indentation_level = 0;
50    let mut construct_stack = Vec::new();
51    let mut last_line_was_empty = false;
52    let mut last_was_export_line = false;
53
54    let lines = code.lines().peekable();
55
56    for line in lines {
57        let trimmed_line = line.trim();
58        let first_word = trimmed_line.split('.').next();
59
60        if !trimmed_line.is_empty() {
61            if is_comment(trimmed_line) {
62                if last_was_export_line {
63                    formatted_code.push_str(trimmed_line);
64                } else {
65                    if let Some(prev_line) = formatted_code.lines().last() {
66                        let prev_indent_level =
67                            prev_line.chars().take_while(|&c| c == ' ').count() / 4;
68                        if prev_line.trim_start().starts_with("export") {
69                            formatted_code.push_str(&INDENT.repeat(prev_indent_level + 1));
70                        } else {
71                            formatted_code.push_str(&INDENT.repeat(indentation_level));
72                        }
73                    } else {
74                        formatted_code.push_str(&INDENT.repeat(indentation_level));
75                    }
76                    formatted_code.push_str(trimmed_line);
77                }
78                formatted_code.push('\n');
79                last_line_was_empty = false;
80                continue;
81            }
82
83            if is_single_export_line(trimmed_line) {
84                formatted_code.push_str(trimmed_line);
85                formatted_code.push('\n');
86                last_line_was_empty = false;
87                last_was_export_line = true;
88                continue;
89            }
90
91            last_was_export_line = false;
92
93            if let Some(word) = first_word {
94                if let Some(construct) = ConstructType::from_str(word) {
95                    match construct {
96                        ConstructType::End => {
97                            if let Some(last_construct) = construct_stack.pop() {
98                                if last_construct != ConstructType::End && indentation_level > 0 {
99                                    indentation_level -= 1;
100                                }
101                            }
102                        }
103                        ConstructType::Else => {
104                            if let Some(last_construct) = construct_stack.last() {
105                                if *last_construct == ConstructType::If && indentation_level > 0 {
106                                    indentation_level -= 1;
107                                }
108                            }
109                        }
110                        _ => {
111                            construct_stack.push(construct.clone());
112                        }
113                    }
114
115                    formatted_code.push_str(&INDENT.repeat(indentation_level));
116                    formatted_code.push_str(trimmed_line);
117                    formatted_code.push('\n');
118                    last_line_was_empty = false;
119
120                    match construct {
121                        ConstructType::Begin
122                        | ConstructType::If
123                        | ConstructType::Proc
124                        | ConstructType::Export
125                        | ConstructType::Repeat
126                        | ConstructType::While
127                        | ConstructType::Else => {
128                            indentation_level += 1;
129                        }
130                        _ => {}
131                    }
132
133                    continue;
134                }
135            }
136
137            formatted_code.push_str(&INDENT.repeat(indentation_level));
138            formatted_code.push_str(trimmed_line);
139            formatted_code.push('\n');
140            last_line_was_empty = false;
141        } else if !last_line_was_empty {
142            formatted_code.push('\n');
143            last_line_was_empty = true;
144        }
145    }
146
147    formatted_code
148}
149
150pub fn format_file(file_path: &Path) -> io::Result<()> {
151    let file = File::open(file_path)?;
152    let mut input_code = String::new();
153
154    let reader = BufReader::new(file);
155    for line in reader.lines() {
156        input_code.push_str(&line?);
157        input_code.push('\n');
158    }
159
160    let formatted_code = format_code(&input_code);
161
162    let mut file = File::create(file_path)?;
163    file.write_all(formatted_code.as_bytes())?;
164
165    Ok(())
166}