use std::io::{self, Cursor, IsTerminal, Read};
use std::path::PathBuf;
use anyhow::Result;
use quick_xml::events::{BytesText, Event};
use quick_xml::{Reader, Writer};
pub struct Formatter {
pub compress: bool,
pub indent: usize,
pub keep_comments: bool,
pub eof_newline: bool,
}
impl Formatter {
pub fn format_file(self, file_path: PathBuf) -> Result<String> {
let content = std::fs::read_to_string(file_path)?;
self.format_xml(&content)
}
pub fn format_stdin(self) -> Result<String> {
if io::stdin().is_terminal() {
anyhow::bail!("No input provided. Use -f <file> or pipe XML data.");
}
let mut content = String::new();
io::stdin().read_to_string(&mut content)?;
self.format_xml(&content)
}
pub fn format_xml(&self, xml_content: &str) -> Result<String> {
let cursor = Cursor::new(Vec::new());
let mut reader = Reader::from_str(xml_content);
reader.config_mut().trim_text(false);
let mut writer = if self.compress {
Writer::new(cursor)
} else {
Writer::new_with_indent(cursor, b' ', self.indent)
};
loop {
match reader.read_event()? {
Event::Text(ref e) => {
let text_content = reader.decoder().decode(&e)?.to_string();
let filtered_lines: Vec<&str> = text_content
.lines()
.filter(|line| !line.trim().is_empty())
.collect();
if !filtered_lines.is_empty() {
let filtered_text = filtered_lines.join("\n");
writer.write_event(Event::Text(BytesText::new(&filtered_text)))?;
}
}
Event::Comment(e) => {
if self.keep_comments {
writer.write_event(Event::Comment(e))?;
}
}
Event::Eof => break,
event => writer.write_event(event)?,
}
}
let mut result = writer.into_inner().into_inner();
if self.eof_newline && !result.ends_with(&['\n' as u8]) {
result.push('\n' as u8);
}
Ok(String::from_utf8(result)?)
}
}
impl Default for Formatter {
fn default() -> Self {
Self {
compress: false,
indent: 2,
keep_comments: false,
eof_newline: false,
}
}
}