use crate::*;
pub(crate) fn format_file(path: &Path) -> Result<()> {
let source: String = std::fs::read_to_string(path)
.map_err(|e| anyhow!("Failed to read file {}: {}", path.display(), e))?;
let formatted: String = format_source(&source);
if formatted != source {
std::fs::write(path, &formatted)
.map_err(|e| anyhow!("Failed to write file {}: {}", path.display(), e))?;
log::info!("Formatted: {}", path.display());
}
Ok(())
}
pub(crate) fn format_dir(dir: &Path) -> Result<()> {
if !dir.is_dir() {
return Ok(());
}
let entries: Vec<std::fs::DirEntry> = std::fs::read_dir(dir)
.map_err(|e| anyhow!("Failed to read dir {}: {}", dir.display(), e))?
.filter_map(|entry| entry.ok())
.collect();
for entry in &entries {
let path: std::path::PathBuf = entry.path();
if path.is_dir() {
format_dir(&path)?;
} else if path.extension().is_some_and(|ext| ext == "rs")
&& let Err(error) = format_file(&path)
{
log::warn!("Failed to format {}: {}", path.display(), error);
}
}
Ok(())
}
pub(crate) fn format_source(source: &str) -> String {
let mut result: String = String::new();
let mut last_end: usize = 0;
for span in find_macro_spans(source) {
result.push_str(&source[last_end..span.start]);
let original: &str = &source[span.start..span.end];
let formatted: String = format_brace_block(original);
result.push_str(&formatted);
last_end = span.end;
}
result.push_str(&source[last_end..]);
result
}
pub(crate) fn find_macro_spans(source: &str) -> Vec<MacroSpan> {
let chars: Vec<char> = source.chars().collect();
let len: usize = chars.len();
let mut spans: Vec<MacroSpan> = Vec::new();
let mut i: usize = 0;
while i < len {
if chars[i] == '"' {
i = skip_string_literal(&chars, i);
} else if chars[i] == '\''
&& i + 2 < len
&& chars[i + 1] != '\''
&& chars[i + 1] != '\\'
&& is_char_literal(&chars, i)
{
i = skip_char_literal(&chars, i);
} else {
let matched: Option<usize> = try_match_macro_name(&chars, i, len);
if let Some(name_end) = matched {
let bang_pos: usize = skip_ws_and_comments(&chars, name_end, len);
if bang_pos < len && chars[bang_pos] == '!' {
let brace_pos: usize = skip_ws_and_comments(&chars, bang_pos + 1, len);
if brace_pos < len && chars[brace_pos] == '{' {
let close_brace: usize = find_matching_brace(&chars, brace_pos);
spans.push(MacroSpan {
start: brace_pos,
end: close_brace + 1,
});
i = close_brace + 1;
continue;
}
}
i = name_end;
} else {
i += 1;
}
}
}
spans
}
fn try_match_macro_name(chars: &[char], i: usize, len: usize) -> Option<usize> {
let names: &[&str] = &["html", "class", "css_vars", "var"];
for name in names {
let name_chars: Vec<char> = name.chars().collect();
let name_len: usize = name_chars.len();
if i + name_len > len {
continue;
}
let mut matched: bool = true;
for j in 0..name_len {
if chars[i + j] != name_chars[j] {
matched = false;
break;
}
}
if !matched {
continue;
}
if i + name_len < len && is_identifier_continue(chars[i + name_len]) {
continue;
}
if i > 0 && is_identifier_continue(chars[i - 1]) {
continue;
}
return Some(i + name_len);
}
None
}
fn skip_ws_and_comments(chars: &[char], mut i: usize, len: usize) -> usize {
while i < len {
if chars[i] == ' ' || chars[i] == '\t' || chars[i] == '\n' || chars[i] == '\r' {
i += 1;
} else if i + 1 < len && chars[i] == '/' && chars[i + 1] == '/' {
i = skip_line_comment(chars, i);
} else if i + 1 < len && chars[i] == '/' && chars[i + 1] == '*' {
i = skip_block_comment(chars, i);
} else {
break;
}
}
i
}
fn skip_string_literal(chars: &[char], start: usize) -> usize {
let len: usize = chars.len();
let mut i: usize = start + 1;
while i < len {
if chars[i] == '\\' {
i += 2;
} else if chars[i] == '"' {
return i + 1;
} else {
i += 1;
}
}
len
}
fn is_char_literal(chars: &[char], i: usize) -> bool {
let len: usize = chars.len();
if i == 0 {
return true;
}
let prev: char = chars[i - 1];
if prev.is_alphanumeric() || prev == '_' {
return false;
}
let mut j: usize = i + 1;
while j < len && chars[j] != '\'' {
if chars[j] == '\\' {
j += 1;
}
j += 1;
}
j < len
}
fn skip_char_literal(chars: &[char], start: usize) -> usize {
let len: usize = chars.len();
let mut i: usize = start + 1;
while i < len {
if chars[i] == '\\' {
i += 2;
} else if chars[i] == '\'' {
return i + 1;
} else {
i += 1;
}
}
len
}
fn skip_line_comment(chars: &[char], start: usize) -> usize {
let len: usize = chars.len();
let mut i: usize = start + 2;
while i < len && chars[i] != '\n' {
i += 1;
}
if i < len { i + 1 } else { len }
}
fn skip_block_comment(chars: &[char], start: usize) -> usize {
let len: usize = chars.len();
let mut i: usize = start + 2;
while i + 1 < len {
if chars[i] == '*' && chars[i + 1] == '/' {
return i + 2;
}
i += 1;
}
len
}
fn find_matching_brace(chars: &[char], open: usize) -> usize {
let len: usize = chars.len();
let mut depth: i32 = 1;
let mut i: usize = open + 1;
while i < len && depth > 0 {
match chars[i] {
'"' => {
i = skip_string_literal(chars, i);
continue;
}
'\'' => {
if is_char_literal(chars, i) {
i = skip_char_literal(chars, i);
continue;
}
i += 1;
continue;
}
'/' if i + 1 < len && chars[i + 1] == '/' => {
i = skip_line_comment(chars, i);
continue;
}
'/' if i + 1 < len && chars[i + 1] == '*' => {
i = skip_block_comment(chars, i);
continue;
}
'{' => {
depth += 1;
}
'}' => {
depth -= 1;
if depth == 0 {
return i;
}
}
_ => {}
}
i += 1;
}
len - 1
}
fn is_identifier_continue(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
fn is_identifier_start(c: char) -> bool {
c.is_alphabetic() || c == '_'
}
pub(crate) fn format_brace_block(block: &str) -> String {
let chars: Vec<char> = block.chars().collect();
let len: usize = chars.len();
let mut result: String = String::new();
let mut i: usize = 0;
while i < len {
if chars[i] == '"' {
let next: usize = skip_string_literal(&chars, i);
for c in &chars[i..next] {
result.push(*c);
}
i = next;
} else if chars[i] == '\'' && is_char_literal(&chars, i) {
let next: usize = skip_char_literal(&chars, i);
for c in &chars[i..next] {
result.push(*c);
}
i = next;
} else if i + 1 < len && chars[i] == '/' && chars[i + 1] == '/' {
let next: usize = skip_line_comment(&chars, i);
for c in &chars[i..next] {
result.push(*c);
}
i = next;
} else if i + 1 < len && chars[i] == '/' && chars[i + 1] == '*' {
let next: usize = skip_block_comment(&chars, i);
for c in &chars[i..next] {
result.push(*c);
}
i = next;
} else if chars[i] == '{' {
if !result.is_empty() {
let last: u8 = result.as_bytes()[result.len() - 1];
if last != b' ' && last != b'\t' && last != b'\n' && last != b'{' && last != b'(' {
result.push(' ');
}
}
result.push('{');
let mut j: usize = i + 1;
let mut found_newline: bool = false;
while j < len && (chars[j] == ' ' || chars[j] == '\t') {
j += 1;
}
if j < len && chars[j] == '\n' {
found_newline = true;
}
if !found_newline && j < len && chars[j] != '}' {
result.push(' ');
}
i = j;
} else if chars[i] == '}' {
let mut j: usize = result.len();
while j > 0 && (result.as_bytes()[j - 1] == b' ' || result.as_bytes()[j - 1] == b'\t') {
j -= 1;
}
if j > 0 && result.as_bytes()[j - 1] == b'\n' {
result.push('}');
} else {
result.truncate(j);
if j > 0 {
result.push(' ');
}
result.push('}');
}
i += 1;
} else if is_identifier_start(chars[i])
|| (chars[i] == 'r' && i + 1 < len && chars[i + 1] == '#')
{
let (ident, next) = consume_identifier(&chars, i, len);
let ws_end: usize = skip_horizontal_ws(&chars, next, len);
if ws_end < len
&& chars[ws_end] == ':'
&& (ws_end + 1 >= len || chars[ws_end + 1] != ':')
{
result.push_str(&ident);
result.push(':');
let after_colon: usize = skip_horizontal_ws(&chars, ws_end + 1, len);
if after_colon < len && chars[after_colon] != '\n' && chars[after_colon] != '\r' {
result.push(' ');
i = after_colon;
} else {
result.push(' ');
i = ws_end + 1;
}
} else {
result.push_str(&ident);
i = next;
}
} else {
result.push(chars[i]);
i += 1;
}
}
result
}
fn consume_identifier(chars: &[char], i: usize, len: usize) -> (String, usize) {
let mut result: String = String::new();
let mut j: usize = i;
if j + 1 < len && chars[j] == 'r' && chars[j + 1] == '#' {
result.push('r');
result.push('#');
j += 2;
}
while j < len && is_identifier_continue(chars[j]) {
result.push(chars[j]);
j += 1;
}
while j + 1 < len && chars[j] == '-' && is_identifier_start(chars[j + 1]) {
result.push('-');
j += 1;
if j + 1 < len && chars[j] == 'r' && chars[j + 1] == '#' {
result.push('r');
result.push('#');
j += 2;
}
while j < len && is_identifier_continue(chars[j]) {
result.push(chars[j]);
j += 1;
}
}
(result, j)
}
fn skip_horizontal_ws(chars: &[char], mut i: usize, len: usize) -> usize {
while i < len && (chars[i] == ' ' || chars[i] == '\t') {
i += 1;
}
i
}