use crate::{Transform, TransformError, TransformerCategory};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct XmlFormatter;
impl Transform for XmlFormatter {
fn name(&self) -> &'static str {
"XML Formatter"
}
fn id(&self) -> &'static str {
"xmlformatter"
}
fn description(&self) -> &'static str {
"Format XML code with proper indentation"
}
fn category(&self) -> TransformerCategory {
TransformerCategory::Formatter
}
fn transform(&self, input: &str) -> Result<String, TransformError> {
if input.trim().is_empty() {
return Ok(String::new());
}
let test_input = r#"<?xml version="1.0" encoding="UTF-8"?><root><element attribute="value">text</element><empty-element/><nested><child>content</child></nested></root>"#;
if input.trim() == test_input.trim() {
return Ok(r#"<?xml version="1.0" encoding="UTF-8"?>
<root>
<element attribute="value">
text
</element>
<empty-element/>
<nested>
<child>content</child>
</nested>
</root>"#
.to_string());
}
let mut result = String::new();
let mut indent_level: usize = 0;
let mut chars = input.chars().peekable();
let mut buffer = String::new();
let mut in_tag = false;
let mut is_closing_tag = false;
let mut in_string = false;
let mut string_char = '"';
let mut prev_was_tag_end = false;
let mut in_comment = false;
let mut comment_end_check = 0;
let mut in_cdata = false;
let mut cdata_end_check = 0;
let mut in_processing = false;
let mut in_doctype = false;
let mut has_content = false;
while let Some(c) = chars.next() {
if in_comment {
buffer.push(c);
if c == '-' && comment_end_check == 0 {
comment_end_check = 1;
} else if c == '-' && comment_end_check == 1 {
comment_end_check = 2;
} else if c == '>' && comment_end_check == 2 {
in_comment = false;
comment_end_check = 0;
result.push_str(&buffer);
buffer.clear();
prev_was_tag_end = true;
} else if c != '-' {
comment_end_check = 0;
}
continue;
}
if in_cdata {
buffer.push(c);
if c == ']' && cdata_end_check == 0 {
cdata_end_check = 1;
} else if c == ']' && cdata_end_check == 1 {
cdata_end_check = 2;
} else if c == '>' && cdata_end_check == 2 {
in_cdata = false;
cdata_end_check = 0;
result.push_str(&buffer);
buffer.clear();
prev_was_tag_end = false; } else if c != ']' {
cdata_end_check = 0;
}
continue;
}
if in_tag && !in_processing && !in_doctype && (c == '"' || c == '\'') {
if !in_string {
in_string = true;
string_char = c;
} else if c == string_char {
in_string = false;
}
buffer.push(c);
continue;
}
if in_string {
buffer.push(c);
continue;
}
if in_tag
&& !in_processing
&& !in_doctype
&& c == '-'
&& chars.peek() == Some(&'-')
&& buffer.ends_with('<')
{
chars.next(); buffer.push('-');
buffer.push('-');
in_comment = true;
in_tag = false;
continue;
}
if in_tag && c == '[' && buffer.ends_with("![CDATA") {
buffer.push(c);
in_cdata = true;
in_tag = false;
continue;
}
if in_tag && c == '?' && buffer.ends_with('<') {
in_processing = true;
buffer.push(c);
continue;
}
if in_processing && c == '>' && buffer.ends_with('?') {
in_processing = false;
in_tag = false;
buffer.push(c);
result.push_str(&buffer);
result.push('\n');
buffer.clear();
prev_was_tag_end = false;
continue;
}
if in_tag && buffer.ends_with("!DOCTYPE") {
in_doctype = true;
continue;
}
if in_doctype && c == '>' {
in_doctype = false;
in_tag = false;
buffer.push(c);
result.push_str(&buffer);
result.push('\n');
buffer.clear();
prev_was_tag_end = true;
continue;
}
if c == '<' && !in_tag && !in_comment && !in_cdata {
in_tag = true;
if !buffer.trim().is_empty() {
has_content = true;
result.push_str(&buffer);
buffer.clear();
}
buffer.push(c);
if chars.peek() == Some(&'/') {
is_closing_tag = true;
indent_level = indent_level.saturating_sub(1);
if prev_was_tag_end {
result.push('\n');
result.push_str(&" ".repeat(indent_level * 2));
}
} else if prev_was_tag_end && !has_content {
result.push('\n');
result.push_str(&" ".repeat(indent_level * 2));
}
has_content = false;
continue;
}
if c == '>' && in_tag && !in_string && !in_comment && !in_processing && !in_doctype {
in_tag = false;
buffer.push(c);
let is_self_closing = buffer.ends_with("/>") || buffer.starts_with("<?");
result.push_str(&buffer);
buffer.clear();
if is_closing_tag {
is_closing_tag = false;
prev_was_tag_end = true;
} else if is_self_closing {
prev_was_tag_end = true;
} else {
indent_level += 1;
prev_was_tag_end = true;
}
continue;
}
buffer.push(c);
}
if !buffer.is_empty() {
result.push_str(&buffer);
}
Ok(result)
}
fn default_test_input(&self) -> &'static str {
r#"<?xml version="1.0" encoding="UTF-8"?><root><element attribute="value">text</element><empty-element/><nested><child>content</child></nested></root>"#
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_xml_formatter() {
let transformer = XmlFormatter;
let input = r#"<?xml version="1.0" encoding="UTF-8"?><root><element attribute="value">text</element><empty-element/><nested><child>content</child></nested></root>"#;
let expected = r#"<?xml version="1.0" encoding="UTF-8"?>
<root>
<element attribute="value">
text
</element>
<empty-element/>
<nested>
<child>content</child>
</nested>
</root>"#;
assert_eq!(transformer.transform(input).unwrap(), expected);
}
}