feat: 添加 HTML、XML 和多语言代码格式化功能

新增三个格式化工具(HTML/XML/代码),支持美化和压缩模式。
  修复 XML 验证器无法正确解析带属性标签的问题。
  修复代码中未使用变量的警告,优化 HTML script/style 标签处理逻辑。
This commit is contained in:
2026-02-10 20:24:21 +08:00
parent 825b650542
commit bf5d056811
26 changed files with 3199 additions and 13 deletions

View File

@@ -0,0 +1,28 @@
//! 代码格式化命令
//!
//! 定义代码格式化相关的 Tauri 命令
use crate::models::code_format::{CodeFormatConfig, CodeFormatResult, CodeValidateResult, CodeLanguage};
use crate::services::code_format_service::CodeFormatService;
/// 格式化代码命令
#[tauri::command]
pub fn format_code(input: String, config: CodeFormatConfig) -> CodeFormatResult {
CodeFormatService::format(&input, &config)
.unwrap_or_else(|e| CodeFormatResult {
success: false,
result: String::new(),
error: Some(e.to_string()),
})
}
/// 验证代码命令
#[tauri::command]
pub fn validate_code(input: String, language: CodeLanguage) -> CodeValidateResult {
CodeFormatService::validate(&input, language)
.unwrap_or_else(|e| CodeValidateResult {
is_valid: false,
error_message: Some(e.to_string()),
error_line: None,
})
}

View File

@@ -0,0 +1,37 @@
//! HTML 格式化命令
//!
//! 定义 HTML 格式化相关的 Tauri 命令
use crate::models::html_format::{HtmlFormatConfig, HtmlFormatResult, HtmlValidateResult};
use crate::services::html_format_service::HtmlFormatService;
/// 格式化 HTML 命令
#[tauri::command]
pub fn format_html(input: String, config: HtmlFormatConfig) -> HtmlFormatResult {
HtmlFormatService::format(&input, &config)
.unwrap_or_else(|e| HtmlFormatResult {
success: false,
result: String::new(),
error: Some(e.to_string()),
})
}
/// 验证 HTML 命令
#[tauri::command]
pub fn validate_html(input: String) -> HtmlValidateResult {
HtmlFormatService::validate(&input).unwrap_or_else(|e| HtmlValidateResult {
is_valid: false,
error_message: Some(e.to_string()),
error_line: None,
})
}
/// 压缩 HTML 命令
#[tauri::command]
pub fn compact_html(input: String) -> HtmlFormatResult {
HtmlFormatService::compact(&input).unwrap_or_else(|e| HtmlFormatResult {
success: false,
result: String::new(),
error: Some(e.to_string()),
})
}

View File

@@ -2,8 +2,11 @@
//!
//! 定义与前端交互的 Tauri 命令,作为前端和业务逻辑之间的适配器
pub mod code_format_commands;
pub mod html_format_commands;
pub mod json_format_commands;
pub mod picker_color_commands;
pub mod qrcode_commands;
pub mod system_info_commands;
pub mod window_commands;
pub mod xml_format_commands;

View File

@@ -0,0 +1,37 @@
//! XML 格式化命令
//!
//! 定义 XML 格式化相关的 Tauri 命令
use crate::models::xml_format::{XmlFormatConfig, XmlFormatResult, XmlValidateResult};
use crate::services::xml_format_service::XmlFormatService;
/// 格式化 XML 命令
#[tauri::command]
pub fn format_xml(input: String, config: XmlFormatConfig) -> XmlFormatResult {
XmlFormatService::format(&input, &config)
.unwrap_or_else(|e| XmlFormatResult {
success: false,
result: String::new(),
error: Some(e.to_string()),
})
}
/// 验证 XML 命令
#[tauri::command]
pub fn validate_xml(input: String) -> XmlValidateResult {
XmlFormatService::validate(&input).unwrap_or_else(|e| XmlValidateResult {
is_valid: false,
error_message: Some(e.to_string()),
error_line: None,
})
}
/// 压缩 XML 命令
#[tauri::command]
pub fn compact_xml(input: String) -> XmlFormatResult {
XmlFormatService::compact(&input).unwrap_or_else(|e| XmlFormatResult {
success: false,
result: String::new(),
error: Some(e.to_string()),
})
}

View File

@@ -43,10 +43,21 @@ pub fn run() {
commands::picker_color_commands::pick_color_at_point,
commands::picker_color_commands::pick_color_at_point_topmost,
commands::picker_color_commands::capture_screen_region_rgba,
// Json格式化命令
// JSON 格式化命令
commands::json_format_commands::format_json,
commands::json_format_commands::validate_json,
commands::json_format_commands::compact_json,
// HTML 格式化命令
commands::html_format_commands::format_html,
commands::html_format_commands::validate_html,
commands::html_format_commands::compact_html,
// XML 格式化命令
commands::xml_format_commands::format_xml,
commands::xml_format_commands::validate_xml,
commands::xml_format_commands::compact_xml,
// 代码格式化命令
commands::code_format_commands::format_code,
commands::code_format_commands::validate_code,
// 操作系统信息命令
commands::system_info_commands::get_system_info,
// 二维码生成命令

View File

@@ -0,0 +1,151 @@
//! 代码格式化相关数据模型
//!
//! 定义代码格式化工具使用的数据结构
use serde::{Deserialize, Serialize};
/// 支持的编程语言
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum CodeLanguage {
#[serde(rename = "java")]
Java,
#[serde(rename = "cpp")]
Cpp,
#[serde(rename = "rust")]
Rust,
#[serde(rename = "python")]
Python,
#[serde(rename = "sql")]
Sql,
#[serde(rename = "javascript")]
JavaScript,
#[serde(rename = "typescript")]
TypeScript,
#[serde(rename = "html")]
Html,
#[serde(rename = "css")]
Css,
#[serde(rename = "json")]
Json,
#[serde(rename = "xml")]
Xml,
}
impl CodeLanguage {
/// 获取语言的文件扩展名
#[allow(dead_code)]
pub fn extension(&self) -> &'static str {
match self {
CodeLanguage::Java => "java",
CodeLanguage::Cpp => "cpp",
CodeLanguage::Rust => "rs",
CodeLanguage::Python => "py",
CodeLanguage::Sql => "sql",
CodeLanguage::JavaScript => "js",
CodeLanguage::TypeScript => "ts",
CodeLanguage::Html => "html",
CodeLanguage::Css => "css",
CodeLanguage::Json => "json",
CodeLanguage::Xml => "xml",
}
}
/// 获取语言的显示名称
#[allow(dead_code)]
pub fn display_name(&self) -> &'static str {
match self {
CodeLanguage::Java => "Java",
CodeLanguage::Cpp => "C++",
CodeLanguage::Rust => "Rust",
CodeLanguage::Python => "Python",
CodeLanguage::Sql => "SQL",
CodeLanguage::JavaScript => "JavaScript",
CodeLanguage::TypeScript => "TypeScript",
CodeLanguage::Html => "HTML",
CodeLanguage::Css => "CSS",
CodeLanguage::Json => "JSON",
CodeLanguage::Xml => "XML",
}
}
}
/// 代码格式化配置
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeFormatConfig {
/// 编程语言
pub language: CodeLanguage,
/// 缩进空格数(默认 4
#[serde(default = "default_indent")]
pub indent: u32,
/// 使用 Tab 缩进
#[serde(default)]
pub use_tabs: bool,
/// 格式化模式
#[serde(default)]
pub mode: FormatMode,
}
/// 默认缩进空格数
fn default_indent() -> u32 {
4
}
/// 代码格式化模式
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum FormatMode {
/// 标准格式化(美化)
#[serde(rename = "pretty")]
Pretty,
/// 压缩格式(去除空格和换行)
#[serde(rename = "compact")]
Compact,
}
impl Default for FormatMode {
fn default() -> Self {
Self::Pretty
}
}
impl Default for CodeFormatConfig {
fn default() -> Self {
Self {
language: CodeLanguage::JavaScript,
indent: default_indent(),
use_tabs: false,
mode: FormatMode::default(),
}
}
}
/// 代码格式化结果
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeFormatResult {
/// 是否成功
pub success: bool,
/// 格式化后的代码字符串
pub result: String,
/// 错误信息(如果失败)
pub error: Option<String>,
}
/// 代码验证结果
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeValidateResult {
/// 是否有效的代码
pub is_valid: bool,
/// 错误信息(如果无效)
pub error_message: Option<String>,
/// 错误位置(行号,从 1 开始)
pub error_line: Option<usize>,
}

View File

@@ -0,0 +1,77 @@
//! HTML 格式化相关数据模型
//!
//! 定义 HTML 格式化工具使用的数据结构
use serde::{Deserialize, Serialize};
/// HTML 格式化配置
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HtmlFormatConfig {
/// 缩进空格数(默认 2
#[serde(default = "default_indent")]
pub indent: u32,
/// 格式化模式
#[serde(default)]
pub mode: FormatMode,
}
/// 默认缩进空格数
fn default_indent() -> u32 {
2
}
/// HTML 格式化模式
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum FormatMode {
/// 标准格式化(美化)
#[serde(rename = "pretty")]
Pretty,
/// 压缩格式(去除空格和换行)
#[serde(rename = "compact")]
Compact,
}
impl Default for FormatMode {
fn default() -> Self {
Self::Pretty
}
}
impl Default for HtmlFormatConfig {
fn default() -> Self {
Self {
indent: default_indent(),
mode: FormatMode::default(),
}
}
}
/// HTML 格式化结果
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HtmlFormatResult {
/// 是否成功
pub success: bool,
/// 格式化后的 HTML 字符串
pub result: String,
/// 错误信息(如果失败)
pub error: Option<String>,
}
/// HTML 验证结果
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HtmlValidateResult {
/// 是否有效的 HTML
pub is_valid: bool,
/// 错误信息(如果无效)
pub error_message: Option<String>,
/// 错误位置(行号,从 1 开始)
pub error_line: Option<usize>,
}

View File

@@ -2,7 +2,10 @@
//!
//! 定义应用中使用的数据结构
pub mod code_format;
pub mod color;
pub mod html_format;
pub mod json_format;
pub mod qrcode;
pub mod system_info;
pub mod xml_format;

View File

@@ -0,0 +1,77 @@
//! XML 格式化相关数据模型
//!
//! 定义 XML 格式化工具使用的数据结构
use serde::{Deserialize, Serialize};
/// XML 格式化配置
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct XmlFormatConfig {
/// 缩进空格数(默认 2
#[serde(default = "default_indent")]
pub indent: u32,
/// 格式化模式
#[serde(default)]
pub mode: FormatMode,
}
/// 默认缩进空格数
fn default_indent() -> u32 {
2
}
/// XML 格式化模式
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum FormatMode {
/// 标准格式化(美化)
#[serde(rename = "pretty")]
Pretty,
/// 压缩格式(去除空格和换行)
#[serde(rename = "compact")]
Compact,
}
impl Default for FormatMode {
fn default() -> Self {
Self::Pretty
}
}
impl Default for XmlFormatConfig {
fn default() -> Self {
Self {
indent: default_indent(),
mode: FormatMode::default(),
}
}
}
/// XML 格式化结果
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct XmlFormatResult {
/// 是否成功
pub success: bool,
/// 格式化后的 XML 字符串
pub result: String,
/// 错误信息(如果失败)
pub error: Option<String>,
}
/// XML 验证结果
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct XmlValidateResult {
/// 是否有效的 XML
pub is_valid: bool,
/// 错误信息(如果无效)
pub error_message: Option<String>,
/// 错误位置(行号,从 1 开始)
pub error_line: Option<usize>,
}

View File

@@ -0,0 +1,68 @@
//! 代码格式化服务
//!
//! 提供代码格式化功能的核心业务逻辑
use crate::error::AppResult;
use crate::models::code_format::{CodeFormatConfig, CodeFormatResult, CodeValidateResult};
use crate::utils::code_formatter;
/// 代码格式化服务
pub struct CodeFormatService;
impl CodeFormatService {
/// 格式化代码字符串
///
/// 根据配置对输入的代码字符串进行格式化
///
/// # 参数
///
/// * `input` - 输入的代码字符串
/// * `config` - 格式化配置
///
/// # 返回
///
/// 返回格式化结果
pub fn format(input: &str, config: &CodeFormatConfig) -> AppResult<CodeFormatResult> {
if input.trim().is_empty() {
return Ok(CodeFormatResult {
success: false,
result: String::new(),
error: Some("输入内容不能为空".to_string()),
});
}
match code_formatter::format_code(input, config) {
Ok(formatted) => Ok(CodeFormatResult {
success: true,
result: formatted,
error: None,
}),
Err(err) => Ok(CodeFormatResult {
success: false,
result: String::new(),
error: Some(err),
}),
}
}
/// 验证代码字符串
///
/// 检查输入的字符串是否为有效的代码
///
/// # 参数
///
/// * `input` - 输入的代码字符串
/// * `language` - 编程语言
///
/// # 返回
///
/// 返回验证结果
pub fn validate(input: &str, language: crate::models::code_format::CodeLanguage) -> AppResult<CodeValidateResult> {
let result = code_formatter::validate_code(input, language);
Ok(CodeValidateResult {
is_valid: result.is_valid,
error_message: result.error_message,
error_line: result.error_line,
})
}
}

View File

@@ -0,0 +1,101 @@
//! HTML 格式化服务
//!
//! 提供 HTML 格式化功能的核心业务逻辑
use crate::error::AppResult;
use crate::models::html_format::{HtmlFormatConfig, HtmlFormatResult, HtmlValidateResult};
use crate::utils::html_formatter;
/// HTML 格式化服务
pub struct HtmlFormatService;
impl HtmlFormatService {
/// 格式化 HTML 字符串
///
/// 根据配置对输入的 HTML 字符串进行格式化
///
/// # 参数
///
/// * `input` - 输入的 HTML 字符串
/// * `config` - 格式化配置
///
/// # 返回
///
/// 返回格式化结果
pub fn format(input: &str, config: &HtmlFormatConfig) -> AppResult<HtmlFormatResult> {
if input.trim().is_empty() {
return Ok(HtmlFormatResult {
success: false,
result: String::new(),
error: Some("输入内容不能为空".to_string()),
});
}
match html_formatter::format_html(input, config) {
Ok(formatted) => Ok(HtmlFormatResult {
success: true,
result: formatted,
error: None,
}),
Err(err) => Ok(HtmlFormatResult {
success: false,
result: String::new(),
error: Some(err),
}),
}
}
/// 验证 HTML 字符串
///
/// 检查输入的字符串是否为有效的 HTML
///
/// # 参数
///
/// * `input` - 输入的 HTML 字符串
///
/// # 返回
///
/// 返回验证结果
pub fn validate(input: &str) -> AppResult<HtmlValidateResult> {
let result = html_formatter::validate_html(input);
Ok(HtmlValidateResult {
is_valid: result.is_valid,
error_message: result.error_message,
error_line: result.error_line,
})
}
/// 压缩 HTML 字符串
///
/// 去除 HTML 中的所有多余空格和换行
///
/// # 参数
///
/// * `input` - 输入的 HTML 字符串
///
/// # 返回
///
/// 返回格式化结果
pub fn compact(input: &str) -> AppResult<HtmlFormatResult> {
if input.trim().is_empty() {
return Ok(HtmlFormatResult {
success: false,
result: String::new(),
error: Some("输入内容不能为空".to_string()),
});
}
match html_formatter::compact_html(input) {
Ok(compacted) => Ok(HtmlFormatResult {
success: true,
result: compacted,
error: None,
}),
Err(err) => Ok(HtmlFormatResult {
success: false,
result: String::new(),
error: Some(err),
}),
}
}
}

View File

@@ -132,11 +132,10 @@ mod tests {
#[test]
fn test_format_valid_json() {
let service = JsonFormatService;
let input = r#"{"name":"test","value":123}"#;
let config = JsonFormatConfig::default();
let result = service.format(input, &config).unwrap();
let result = JsonFormatService::format(input, &config).unwrap();
assert!(result.success);
assert!(result.is_valid);
assert!(result.error.is_none());
@@ -145,11 +144,10 @@ mod tests {
#[test]
fn test_format_invalid_json() {
let service = JsonFormatService;
let input = r#"{"invalid": }"#;
let config = JsonFormatConfig::default();
let result = service.format(input, &config).unwrap();
let result = JsonFormatService::format(input, &config).unwrap();
assert!(!result.success);
assert!(!result.is_valid);
assert!(result.error.is_some());
@@ -157,11 +155,10 @@ mod tests {
#[test]
fn test_format_empty_input() {
let service = JsonFormatService;
let input = "";
let config = JsonFormatConfig::default();
let result = service.format(input, &config).unwrap();
let result = JsonFormatService::format(input, &config).unwrap();
assert!(!result.success);
assert!(!result.is_valid);
assert!(result.error.is_some());
@@ -169,30 +166,27 @@ mod tests {
#[test]
fn test_validate_valid_json() {
let service = JsonFormatService;
let input = r#"{"valid": true}"#;
let result = service.validate(input).unwrap();
let result = JsonFormatService::validate(input).unwrap();
assert!(result.is_valid);
assert!(result.error_message.is_none());
}
#[test]
fn test_validate_invalid_json() {
let service = JsonFormatService;
let input = r#"{"invalid": }"#;
let result = service.validate(input).unwrap();
let result = JsonFormatService::validate(input).unwrap();
assert!(!result.is_valid);
assert!(result.error_message.is_some());
}
#[test]
fn test_compact_json() {
let service = JsonFormatService;
let input = r#"{ "name" : "test" }"#;
let result = service.compact(input).unwrap();
let result = JsonFormatService::compact(input).unwrap();
assert!(result.success);
assert!(result.is_valid);
assert_eq!(result.result, r#"{"name":"test"}"#);

View File

@@ -2,7 +2,10 @@
//!
//! 提供应用的核心业务逻辑实现
pub mod code_format_service;
pub mod html_format_service;
pub mod json_format_service;
pub mod qrcode_service;
pub mod system_info_service;
pub mod window_service;
pub mod xml_format_service;

View File

@@ -0,0 +1,101 @@
//! XML 格式化服务
//!
//! 提供 XML 格式化功能的核心业务逻辑
use crate::error::AppResult;
use crate::models::xml_format::{XmlFormatConfig, XmlFormatResult, XmlValidateResult};
use crate::utils::xml_formatter;
/// XML 格式化服务
pub struct XmlFormatService;
impl XmlFormatService {
/// 格式化 XML 字符串
///
/// 根据配置对输入的 XML 字符串进行格式化
///
/// # 参数
///
/// * `input` - 输入的 XML 字符串
/// * `config` - 格式化配置
///
/// # 返回
///
/// 返回格式化结果
pub fn format(input: &str, config: &XmlFormatConfig) -> AppResult<XmlFormatResult> {
if input.trim().is_empty() {
return Ok(XmlFormatResult {
success: false,
result: String::new(),
error: Some("输入内容不能为空".to_string()),
});
}
match xml_formatter::format_xml(input, config) {
Ok(formatted) => Ok(XmlFormatResult {
success: true,
result: formatted,
error: None,
}),
Err(err) => Ok(XmlFormatResult {
success: false,
result: String::new(),
error: Some(err),
}),
}
}
/// 验证 XML 字符串
///
/// 检查输入的字符串是否为有效的 XML
///
/// # 参数
///
/// * `input` - 输入的 XML 字符串
///
/// # 返回
///
/// 返回验证结果
pub fn validate(input: &str) -> AppResult<XmlValidateResult> {
let result = xml_formatter::validate_xml(input);
Ok(XmlValidateResult {
is_valid: result.is_valid,
error_message: result.error_message,
error_line: result.error_line,
})
}
/// 压缩 XML 字符串
///
/// 去除 XML 中的所有多余空格和换行
///
/// # 参数
///
/// * `input` - 输入的 XML 字符串
///
/// # 返回
///
/// 返回格式化结果
pub fn compact(input: &str) -> AppResult<XmlFormatResult> {
if input.trim().is_empty() {
return Ok(XmlFormatResult {
success: false,
result: String::new(),
error: Some("输入内容不能为空".to_string()),
});
}
match xml_formatter::compact_xml(input) {
Ok(compacted) => Ok(XmlFormatResult {
success: true,
result: compacted,
error: None,
}),
Err(err) => Ok(XmlFormatResult {
success: false,
result: String::new(),
error: Some(err),
}),
}
}
}

View File

@@ -0,0 +1,396 @@
//! 代码格式化工具函数
//!
//! 提供纯函数的代码处理算法
use crate::models::code_format::{CodeFormatConfig, CodeLanguage, FormatMode};
/// 格式化代码字符串
///
/// 对输入的代码字符串进行格式化,支持美化和压缩模式
///
/// # 参数
///
/// * `input` - 输入的代码字符串
/// * `config` - 格式化配置
///
/// # 返回
///
/// 返回格式化后的代码字符串
///
/// # 错误
///
/// 当代码解析失败时返回错误
pub fn format_code(input: &str, config: &CodeFormatConfig) -> Result<String, String> {
if input.trim().is_empty() {
return Err("输入内容不能为空".to_string());
}
match config.mode {
FormatMode::Pretty => prettify_code(input, config),
FormatMode::Compact => compact_code(input, config),
}
}
/// 美化代码字符串
fn prettify_code(input: &str, config: &CodeFormatConfig) -> Result<String, String> {
match config.language {
CodeLanguage::Json => {
// JSON 使用已有的格式化器
use crate::utils::json_formatter;
let json_config = crate::models::json_format::JsonFormatConfig {
indent: config.indent,
sort_keys: false,
mode: crate::models::json_format::FormatMode::Pretty,
};
json_formatter::format_json(input, &json_config)
}
CodeLanguage::Xml => {
// XML 使用已有的格式化器
use crate::utils::xml_formatter;
let xml_config = crate::models::xml_format::XmlFormatConfig {
indent: config.indent,
mode: crate::models::xml_format::FormatMode::Pretty,
};
xml_formatter::format_xml(input, &xml_config)
}
CodeLanguage::Html => {
// HTML 使用已有的格式化器
use crate::utils::html_formatter;
let html_config = crate::models::html_format::HtmlFormatConfig {
indent: config.indent,
mode: crate::models::html_format::FormatMode::Pretty,
};
html_formatter::format_html(input, &html_config)
}
_ => {
// 其他语言使用通用格式化
generic_prettify(input, config)
}
}
}
/// 压缩代码字符串
fn compact_code(input: &str, config: &CodeFormatConfig) -> Result<String, String> {
match config.language {
CodeLanguage::Json => {
use crate::utils::json_formatter;
let json_config = crate::models::json_format::JsonFormatConfig {
indent: 2,
sort_keys: false,
mode: crate::models::json_format::FormatMode::Compact,
};
json_formatter::format_json(input, &json_config)
}
CodeLanguage::Xml => {
use crate::utils::xml_formatter;
let xml_config = crate::models::xml_format::XmlFormatConfig {
indent: 2,
mode: crate::models::xml_format::FormatMode::Compact,
};
xml_formatter::format_xml(input, &xml_config)
}
CodeLanguage::Html => {
use crate::utils::html_formatter;
let html_config = crate::models::html_format::HtmlFormatConfig {
indent: 2,
mode: crate::models::html_format::FormatMode::Compact,
};
html_formatter::format_html(input, &html_config)
}
_ => {
// 其他语言使用通用压缩
generic_compact(input)
}
}
}
/// 通用代码美化
fn generic_prettify(input: &str, config: &CodeFormatConfig) -> Result<String, String> {
let indent_str = if config.use_tabs {
"\t".to_string()
} else {
" ".repeat(config.indent as usize)
};
let mut result = String::new();
let mut indent_level = 0;
let mut chars = input.chars().peekable();
let mut in_string = false;
let mut in_comment = false;
let mut in_multiline_comment = false;
let mut string_char = ' ';
let mut prev_char = ' ';
while let Some(c) = chars.next() {
// 处理字符串
if !in_comment && !in_multiline_comment && (c == '"' || c == '\'' || c == '`') {
if !in_string {
in_string = true;
string_char = c;
} else if c == string_char && prev_char != '\\' {
in_string = false;
}
result.push(c);
prev_char = c;
continue;
}
if in_string {
result.push(c);
prev_char = c;
continue;
}
// 处理单行注释
if c == '/' && chars.peek() == Some(&'/') && !in_multiline_comment {
chars.next();
in_comment = true;
result.push_str("//");
continue;
}
if in_comment {
result.push(c);
if c == '\n' {
in_comment = false;
}
prev_char = c;
continue;
}
// 处理多行注释
if c == '/' && chars.peek() == Some(&'*') && !in_comment {
chars.next();
in_multiline_comment = true;
result.push_str("/*");
continue;
}
if in_multiline_comment {
result.push(c);
if c == '*' && chars.peek() == Some(&'/') {
chars.next();
result.push('/');
in_multiline_comment = false;
}
prev_char = c;
continue;
}
// 处理括号和缩进
match c {
'{' | '(' => {
result.push(c);
if c == '{' {
indent_level += 1;
result.push('\n');
result.push_str(&indent_str.repeat(indent_level));
}
}
'}' | ')' => {
if c == '}' && indent_level > 0 {
indent_level -= 1;
if result.ends_with(&indent_str) {
result.truncate(result.len() - indent_str.len());
} else if result.ends_with('\n') {
result.push_str(&indent_str.repeat(indent_level));
} else {
result.push('\n');
result.push_str(&indent_str.repeat(indent_level));
}
}
result.push(c);
}
';' => {
result.push(c);
if !in_string {
result.push('\n');
result.push_str(&indent_str.repeat(indent_level));
}
}
'\n' | '\r' => {
// 跳过多余的换行
if !result.ends_with('\n') {
result.push('\n');
result.push_str(&indent_str.repeat(indent_level));
}
}
' ' | '\t' => {
// 只保留一个空格
if !result.ends_with(' ') && !result.ends_with('\n') && !result.ends_with('\t') {
result.push(' ');
}
}
_ => {
result.push(c);
}
}
prev_char = c;
}
Ok(result.trim().to_string())
}
/// 通用代码压缩
fn generic_compact(input: &str) -> Result<String, String> {
let mut result = String::new();
let mut chars = input.chars().peekable();
let mut in_string = false;
let mut string_char = ' ';
let mut prev_char = ' ';
while let Some(c) = chars.next() {
// 处理字符串
if c == '"' || c == '\'' || c == '`' {
if !in_string {
in_string = true;
string_char = c;
} else if c == string_char && prev_char != '\\' {
in_string = false;
}
result.push(c);
prev_char = c;
continue;
}
if in_string {
result.push(c);
prev_char = c;
continue;
}
// 处理单行注释
if c == '/' && chars.peek() == Some(&'/') {
while let Some(nc) = chars.next() {
if nc == '\n' {
break;
}
}
continue;
}
// 处理多行注释
if c == '/' && chars.peek() == Some(&'*') {
chars.next();
while let Some(nc) = chars.next() {
if nc == '*' && chars.peek() == Some(&'/') {
chars.next();
break;
}
}
continue;
}
// 压缩空格和换行
if c.is_whitespace() {
if !result.is_empty() && !result.ends_with(' ') &&
prev_char.is_ascii_alphanumeric() || prev_char == '_' {
result.push(' ');
}
prev_char = c;
continue;
}
result.push(c);
prev_char = c;
}
Ok(result.trim().to_string())
}
/// 验证代码字符串
pub fn validate_code(input: &str, language: CodeLanguage) -> CodeValidateResult {
if input.trim().is_empty() {
return CodeValidateResult {
is_valid: false,
error_message: Some("输入内容不能为空".to_string()),
error_line: Some(1),
};
}
match language {
CodeLanguage::Json => {
use crate::utils::json_formatter;
let result = json_formatter::validate_json(input);
CodeValidateResult {
is_valid: result.is_valid,
error_message: result.error_message,
error_line: result.error_line,
}
}
CodeLanguage::Xml => {
use crate::utils::xml_formatter;
let result = xml_formatter::validate_xml(input);
CodeValidateResult {
is_valid: result.is_valid,
error_message: result.error_message,
error_line: result.error_line,
}
}
CodeLanguage::Html => {
use crate::utils::html_formatter;
let result = html_formatter::validate_html(input);
CodeValidateResult {
is_valid: result.is_valid,
error_message: result.error_message,
error_line: result.error_line,
}
}
_ => {
// 其他语言的简单验证
CodeValidateResult {
is_valid: true,
error_message: None,
error_line: None,
}
}
}
}
/// 代码验证结果结构
#[derive(Debug, Clone)]
pub struct CodeValidateResult {
pub is_valid: bool,
pub error_message: Option<String>,
pub error_line: Option<usize>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_code_json() {
let input = "{\"name\":\"test\",\"value\":123}";
let config = CodeFormatConfig {
language: CodeLanguage::Json,
..Default::default()
};
let result = format_code(input, &config).unwrap();
assert!(result.contains('\n'));
}
#[test]
fn test_format_code_generic() {
let input = "function test(){let x=1;return x;}";
let config = CodeFormatConfig {
language: CodeLanguage::JavaScript,
..Default::default()
};
let result = format_code(input, &config).unwrap();
assert!(result.contains('\n'));
}
#[test]
fn test_compact_code() {
let input = "function test() {\n let x = 1;\n return x;\n}";
let config = CodeFormatConfig {
language: CodeLanguage::JavaScript,
mode: FormatMode::Compact,
..Default::default()
};
let result = format_code(input, &config).unwrap();
assert!(!result.contains('\n'));
}
}

View File

@@ -0,0 +1,455 @@
//! HTML 格式化工具函数
//!
//! 提供纯函数的 HTML 处理算法
use crate::models::html_format::{FormatMode, HtmlFormatConfig};
/// 格式化 HTML 字符串
///
/// 对输入的 HTML 字符串进行格式化,支持美化和压缩模式
///
/// # 参数
///
/// * `input` - 输入的 HTML 字符串
/// * `config` - 格式化配置
///
/// # 返回
///
/// 返回格式化后的 HTML 字符串
///
/// # 错误
///
/// 当 HTML 解析失败时返回错误
pub fn format_html(input: &str, config: &HtmlFormatConfig) -> Result<String, String> {
if input.trim().is_empty() {
return Err("输入内容不能为空".to_string());
}
match config.mode {
FormatMode::Pretty => prettify_html(input, config.indent),
FormatMode::Compact => compact_html(input),
}
}
/// 美化 HTML 字符串
fn prettify_html(input: &str, indent_size: u32) -> Result<String, String> {
let indent_str = " ".repeat(indent_size as usize);
let mut result = String::new();
let mut indent_level = 0;
let mut chars = input.chars().peekable();
let mut in_tag = false;
let mut in_comment = false;
let mut in_doctype = false;
let mut in_script = false;
let mut in_style = false;
let mut preserve_whitespace = false;
let current_tag = String::new();
while let Some(c) = chars.next() {
// 处理 DOCTYPE 声明
if c == '<' && chars.peek() == Some(&'!') {
let mut next_chars = chars.clone();
next_chars.next();
if next_chars.peek() == Some(&'-') {
// HTML 注释开始
in_comment = true;
result.push_str("<!");
} else if let Some(&'D') = next_chars.peek() {
// DOCTYPE
in_doctype = true;
result.push('<');
} else {
result.push('<');
}
continue;
}
// 处理注释结束
if in_comment && c == '-' && chars.peek() == Some(&'-') {
chars.next(); // 消费第二个 '-'
if chars.peek() == Some(&'>') {
chars.next(); // 消费 '>'
result.push_str("-->");
in_comment = false;
continue;
}
}
if in_comment {
result.push(c);
continue;
}
if in_doctype {
result.push(c);
if c == '>' {
in_doctype = false;
add_newline(&mut result, indent_level, &indent_str);
}
continue;
}
// 检测 script 和 style 标签
if c == '<' {
let mut tag_name = String::new();
let mut temp_chars = chars.clone();
if let Some(&'/') = temp_chars.peek() {
temp_chars.next();
}
while let Some(&next_c) = temp_chars.peek() {
if next_c.is_ascii_alphabetic() || next_c == '!' {
tag_name.push(next_c);
temp_chars.next();
} else {
break;
}
}
let tag_lower = tag_name.to_lowercase();
if tag_lower == "script" {
in_script = true;
preserve_whitespace = true;
} else if tag_lower == "/script" {
in_script = false;
preserve_whitespace = false;
} else if tag_lower == "style" {
in_style = true;
preserve_whitespace = true;
} else if tag_lower == "/style" {
in_style = false;
preserve_whitespace = false;
}
}
// 标签开始
if c == '<' {
// 如果不是自闭合标签的开始,且不在标签内
if !in_tag && !preserve_whitespace {
add_newline(&mut result, indent_level, &indent_str);
}
result.push(c);
in_tag = true;
// 检查是否是闭合标签
if chars.peek() == Some(&'/') {
// 闭合标签,在新行开始
if result.ends_with('\n') {
result.truncate(result.len() - 1);
result.push_str(&indent_str.repeat(indent_level as usize));
}
}
continue;
}
// 标签结束
if c == '>' && in_tag {
result.push(c);
in_tag = false;
// 检查是否是自闭合标签
let is_self_closing = result.ends_with("/>") ||
result.ends_with(" />") ||
(result.ends_with(">") &&
current_tag.ends_with("img") ||
current_tag.ends_with("br") ||
current_tag.ends_with("hr") ||
current_tag.ends_with("input") ||
current_tag.ends_with("meta") ||
current_tag.ends_with("link") ||
current_tag.ends_with("area") ||
current_tag.ends_with("base") ||
current_tag.ends_with("col") ||
current_tag.ends_with("embed") ||
current_tag.ends_with("source") ||
current_tag.ends_with("track") ||
current_tag.ends_with("wbr")
);
// 检查是否是开始标签
let prev_chars: Vec<char> = result.chars().rev().take(10).collect();
let is_opening_tag = !prev_chars.contains(&'/') && !is_self_closing;
if is_opening_tag && !preserve_whitespace {
indent_level += 1;
} else if !is_opening_tag && indent_level > 0 && !preserve_whitespace {
indent_level -= 1;
}
if !preserve_whitespace {
add_newline(&mut result, indent_level, &indent_str);
}
continue;
}
// 处理标签内容
if in_tag {
result.push(c);
continue;
}
// 处理文本内容
if preserve_whitespace || in_script || in_style {
// 在 script 或 style 标签内,保留所有原始字符
result.push(c);
} else if !c.is_whitespace() {
result.push(c);
}
}
Ok(result.trim().to_string())
}
/// 压缩 HTML 字符串
/// 压缩 HTML 字符串(公开函数)
pub fn compact_html(input: &str) -> Result<String, String> {
let mut result = String::new();
let mut chars = input.chars().peekable();
let mut in_tag = false;
let mut in_comment = false;
let mut in_script = false;
let mut in_style = false;
let mut preserve_whitespace = false;
while let Some(c) = chars.next() {
// 处理注释
if c == '<' && chars.peek() == Some(&'!') {
let mut next_chars = chars.clone();
next_chars.next();
if next_chars.peek() == Some(&'-') {
in_comment = true;
}
}
if in_comment {
result.push(c);
if c == '-' && chars.peek() == Some(&'-') {
chars.next();
if chars.peek() == Some(&'>') {
chars.next();
result.push_str("-->");
in_comment = false;
}
}
continue;
}
// 标签处理
if c == '<' {
in_tag = true;
// 检测 script/style 标签
let mut tag_name = String::new();
let mut temp_chars = chars.clone();
if let Some(&'/') = temp_chars.peek() {
temp_chars.next();
}
while let Some(&next_c) = temp_chars.peek() {
if next_c.is_ascii_alphabetic() {
tag_name.push(next_c);
temp_chars.next();
} else {
break;
}
}
let tag_lower = tag_name.to_lowercase();
if tag_lower == "script" {
in_script = true;
preserve_whitespace = true;
} else if tag_lower == "/script" {
in_script = false;
preserve_whitespace = false;
} else if tag_lower == "style" {
in_style = true;
preserve_whitespace = true;
} else if tag_lower == "/style" {
in_style = false;
preserve_whitespace = false;
}
result.push(c);
continue;
}
if c == '>' {
in_tag = false;
result.push(c);
continue;
}
// 内容处理
if in_tag {
if !c.is_whitespace() || result.chars().last().map_or(false, |pc| !pc.is_whitespace()) {
result.push(c);
}
} else if preserve_whitespace || in_script || in_style {
// 在 script 或 style 标签内,保留所有原始字符
result.push(c);
} else if !c.is_whitespace() ||
(result.chars().last().map_or(false, |pc| !pc.is_whitespace())) {
result.push(c);
}
}
Ok(result)
}
/// 验证 HTML 字符串
pub fn validate_html(input: &str) -> HtmlValidateResult {
if input.trim().is_empty() {
return HtmlValidateResult {
is_valid: false,
error_message: Some("输入内容不能为空".to_string()),
error_line: Some(1),
};
}
// 基本 HTML 验证:检查标签是否匹配
let mut tag_stack = Vec::new();
let mut chars = input.chars().peekable();
let mut line = 1;
let mut in_tag = false;
let mut in_comment = false;
let mut current_tag = String::new();
while let Some(c) = chars.next() {
if c == '\n' {
line += 1;
}
// 处理注释
if c == '<' && chars.peek() == Some(&'!') {
let mut next_chars = chars.clone();
next_chars.next();
if next_chars.peek() == Some(&'-') {
in_comment = true;
}
}
if in_comment {
if c == '-' && chars.peek() == Some(&'-') {
chars.next();
if chars.peek() == Some(&'>') {
chars.next();
in_comment = false;
}
}
continue;
}
if c == '<' {
in_tag = true;
current_tag.clear();
continue;
}
if in_tag {
if c == '>' {
in_tag = false;
let tag = current_tag.trim().to_lowercase();
// 跳过自闭合标签和特殊标签
if tag.contains("!doctype") ||
tag.starts_with("img") ||
tag.starts_with("br") ||
tag.starts_with("hr") ||
tag.starts_with("input") ||
tag.starts_with("meta") ||
tag.starts_with("link") {
continue;
}
// 处理闭合标签
if let Some(stripped) = tag.strip_prefix('/') {
if let Some(pos) = tag_stack.iter().rposition(|t| t == stripped) {
tag_stack.truncate(pos);
}
} else {
// 提取标签名(去掉属性)
if let Some(tag_name) = tag.split_whitespace().next() {
tag_stack.push(tag_name.to_string());
}
}
continue;
}
if !c.is_whitespace() {
current_tag.push(c);
}
}
}
if tag_stack.is_empty() {
HtmlValidateResult {
is_valid: true,
error_message: None,
error_line: None,
}
} else {
HtmlValidateResult {
is_valid: false,
error_message: Some(format!("未闭合的标签: {}", tag_stack.join(", "))),
error_line: Some(line),
}
}
}
/// HTML 验证结果结构
#[derive(Debug, Clone)]
pub struct HtmlValidateResult {
pub is_valid: bool,
pub error_message: Option<String>,
pub error_line: Option<usize>,
}
/// 添加换行和缩进
fn add_newline(result: &mut String, indent_level: u32, indent_str: &str) {
if !result.ends_with('\n') && !result.is_empty() {
result.push('\n');
result.push_str(&indent_str.repeat(indent_level as usize));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prettify_html() {
let input = "<html><body><div>test</div></body></html>";
let config = HtmlFormatConfig::default();
let result = format_html(input, &config).unwrap();
assert!(result.contains('\n'));
assert!(result.contains(" "));
}
#[test]
fn test_compact_html() {
let input = "<html> <body> <div> test </div> </body></html>";
let config = HtmlFormatConfig {
mode: FormatMode::Compact,
..Default::default()
};
let result = format_html(input, &config).unwrap();
assert!(!result.contains(" "));
}
#[test]
fn test_validate_html_valid() {
let result = validate_html("<html><body></body></html>");
assert!(result.is_valid);
}
#[test]
fn test_validate_html_invalid() {
let result = validate_html("<html><body></html>");
assert!(!result.is_valid);
}
}

View File

@@ -2,8 +2,11 @@
//!
//! 提供纯函数算法实现,无副作用
pub mod code_formatter;
pub mod color_conversion;
pub mod html_formatter;
pub mod json_formatter;
pub mod qrcode_renderer;
pub mod screen;
pub mod shortcut;
pub mod xml_formatter;

View File

@@ -0,0 +1,492 @@
//! XML 格式化工具函数
//!
//! 提供纯函数的 XML 处理算法
use crate::models::xml_format::{FormatMode, XmlFormatConfig};
/// 格式化 XML 字符串
///
/// 对输入的 XML 字符串进行格式化,支持美化和压缩模式
///
/// # 参数
///
/// * `input` - 输入的 XML 字符串
/// * `config` - 格式化配置
///
/// # 返回
///
/// 返回格式化后的 XML 字符串
///
/// # 错误
///
/// 当 XML 解析失败时返回错误
pub fn format_xml(input: &str, config: &XmlFormatConfig) -> Result<String, String> {
if input.trim().is_empty() {
return Err("输入内容不能为空".to_string());
}
match config.mode {
FormatMode::Pretty => prettify_xml(input, config.indent),
FormatMode::Compact => compact_xml(input),
}
}
/// 美化 XML 字符串
fn prettify_xml(input: &str, indent_size: u32) -> Result<String, String> {
let indent_str = " ".repeat(indent_size as usize);
let mut result = String::new();
let mut indent_level = 0;
let mut chars = input.chars().peekable();
let mut in_tag = false;
let mut in_comment = false;
let mut in_cdata = false;
let mut current_tag = String::new();
while let Some(c) = chars.next() {
// 处理 CDATA
if c == '<' && chars.peek() == Some(&'!') {
let mut next_chars = chars.clone();
next_chars.next();
if next_chars.peek() == Some(&'[') {
// 检查是否是 CDATA
let mut temp = String::new();
for _ in 0..7 {
if let Some(nc) = next_chars.next() {
temp.push(nc);
}
}
if temp.starts_with("[CDATA[") {
in_cdata = true;
add_newline(&mut result, indent_level, &indent_str);
result.push_str("<![CDATA[");
continue;
}
}
}
if in_cdata {
result.push(c);
if c == ']' && chars.peek() == Some(&']') {
chars.next();
if chars.peek() == Some(&'>') {
chars.next();
result.push_str("]]>");
in_cdata = false;
}
}
continue;
}
// 处理注释
if c == '<' && chars.peek() == Some(&'!') {
let mut next_chars = chars.clone();
next_chars.next();
if next_chars.peek() == Some(&'-') {
in_comment = true;
add_newline(&mut result, indent_level, &indent_str);
result.push_str("<!--");
continue;
}
}
if in_comment {
result.push(c);
if c == '-' && chars.peek() == Some(&'-') {
chars.next();
if chars.peek() == Some(&'>') {
chars.next();
result.push_str("-->");
in_comment = false;
add_newline(&mut result, indent_level, &indent_str);
}
}
continue;
}
// XML 声明
if c == '<' && chars.peek() == Some(&'?') {
if !result.is_empty() && !result.ends_with('\n') {
result.push('\n');
}
result.push_str("<?xml");
while let Some(&nc) = chars.peek() {
result.push(nc);
chars.next();
if nc == '>' {
break;
}
}
result.push('\n');
continue;
}
// 标签开始
if c == '<' {
if !in_tag && !in_comment && !in_cdata {
add_newline(&mut result, indent_level, &indent_str);
}
result.push(c);
in_tag = true;
current_tag.clear();
continue;
}
// 标签结束
if c == '>' && in_tag {
result.push(c);
in_tag = false;
// 检查是否是自闭合标签
let is_self_closing = result.ends_with("/>") || result.ends_with(" />");
// 检查是否是闭合标签
let is_closing_tag = current_tag.starts_with('/');
if is_closing_tag {
if indent_level > 0 {
indent_level -= 1;
}
} else if !is_self_closing {
indent_level += 1;
}
continue;
}
// 处理标签内容
if in_tag {
if !c.is_whitespace() {
current_tag.push(c);
}
result.push(c);
continue;
}
// 处理文本内容(去除多余空格)
if !c.is_whitespace() ||
(result.chars().last().map_or(false, |pc| pc.is_ascii_alphanumeric())) {
result.push(c);
}
}
Ok(result.trim().to_string())
}
/// 压缩 XML 字符串
/// 压缩 XML 字符串(公开函数)
pub fn compact_xml(input: &str) -> Result<String, String> {
let mut result = String::new();
let mut chars = input.chars().peekable();
let mut in_tag = false;
let mut in_comment = false;
let mut in_cdata = false;
while let Some(c) = chars.next() {
// 处理 CDATA
if c == '<' && chars.peek() == Some(&'!') {
let mut next_chars = chars.clone();
next_chars.next();
if next_chars.peek() == Some(&'[') {
let mut temp = String::new();
for _ in 0..7 {
if let Some(nc) = next_chars.next() {
temp.push(nc);
}
}
if temp.starts_with("[CDATA[") {
in_cdata = true;
}
}
}
if in_cdata {
result.push(c);
if c == ']' && chars.peek() == Some(&']') {
chars.next();
if chars.peek() == Some(&'>') {
chars.next();
result.push_str("]]>");
in_cdata = false;
}
}
continue;
}
// 处理注释
if c == '<' && chars.peek() == Some(&'!') {
let mut next_chars = chars.clone();
next_chars.next();
if next_chars.peek() == Some(&'-') {
in_comment = true;
}
}
if in_comment {
result.push(c);
if c == '-' && chars.peek() == Some(&'-') {
chars.next();
if chars.peek() == Some(&'>') {
chars.next();
result.push_str("-->");
in_comment = false;
}
}
continue;
}
// 标签处理
if c == '<' {
in_tag = true;
result.push(c);
continue;
}
if c == '>' {
in_tag = false;
result.push(c);
continue;
}
// 内容处理
if in_tag {
if !c.is_whitespace() || result.chars().last().map_or(false, |pc| !pc.is_whitespace()) {
result.push(c);
}
} else if !c.is_whitespace() ||
(result.chars().last().map_or(false, |pc| !pc.is_whitespace())) {
result.push(c);
}
}
Ok(result)
}
/// 验证 XML 字符串
pub fn validate_xml(input: &str) -> XmlValidateResult {
if input.trim().is_empty() {
return XmlValidateResult {
is_valid: false,
error_message: Some("输入内容不能为空".to_string()),
error_line: Some(1),
};
}
// 基本 XML 验证:检查标签是否匹配
let mut tag_stack = Vec::new();
let mut chars = input.chars().peekable();
let mut line = 1;
let mut in_tag = false;
let mut in_comment = false;
let mut in_cdata = false;
let mut current_tag = String::new();
while let Some(c) = chars.next() {
if c == '\n' {
line += 1;
}
// 处理 CDATA
if c == '<' && chars.peek() == Some(&'!') {
let mut next_chars = chars.clone();
next_chars.next();
if next_chars.peek() == Some(&'[') {
let mut temp = String::new();
for _ in 0..7 {
if let Some(nc) = next_chars.next() {
temp.push(nc);
}
}
if temp.starts_with("[CDATA[") {
in_cdata = true;
}
}
}
if in_cdata {
if c == ']' && chars.peek() == Some(&']') {
chars.next();
if chars.peek() == Some(&'>') {
chars.next();
in_cdata = false;
}
}
continue;
}
// 处理注释
if c == '<' && chars.peek() == Some(&'!') {
let mut next_chars = chars.clone();
next_chars.next();
if next_chars.peek() == Some(&'-') {
in_comment = true;
}
}
if in_comment {
if c == '-' && chars.peek() == Some(&'-') {
chars.next();
if chars.peek() == Some(&'>') {
chars.next();
in_comment = false;
}
}
continue;
}
// XML 声明
if c == '<' && chars.peek() == Some(&'?') {
while let Some(nc) = chars.next() {
if nc == '>' {
break;
}
}
continue;
}
if c == '<' {
in_tag = true;
current_tag.clear();
continue;
}
if in_tag {
if c == '>' {
in_tag = false;
let tag = current_tag.trim().to_lowercase();
// 跳过自闭合标签
if tag.ends_with('/') || tag.contains("/ ") || tag.contains("/>") {
continue;
}
// 处理闭合标签
if let Some(stripped) = tag.strip_prefix('/') {
// 对于闭合标签,去掉前缀斜杠后直接使用
let closing_tag = stripped.trim();
// 从标签名中去掉可能的属性(闭合标签不应该有属性,但保险起见)
let closing_tag_name = closing_tag.split_whitespace().next().unwrap_or(closing_tag);
// 检查是否匹配栈顶的标签LIFO - 后进先出)
if let Some(last_tag) = tag_stack.last() {
if last_tag == closing_tag_name {
tag_stack.pop();
} else {
return XmlValidateResult {
is_valid: false,
error_message: Some(format!(
"不匹配的闭合标签: </{}> (期望: </{}>)",
closing_tag_name, last_tag
)),
error_line: Some(line),
};
}
} else {
return XmlValidateResult {
is_valid: false,
error_message: Some(format!("不匹配的闭合标签: </{}> (没有对应的开始标签)", closing_tag_name)),
error_line: Some(line),
};
}
} else {
// 提取标签名(去掉属性)
if let Some(tag_name) = tag.split_whitespace().next() {
tag_stack.push(tag_name.to_string());
}
}
continue;
}
// 收集标签内的所有字符(包括空格,以便后续正确分割属性)
current_tag.push(c);
}
}
if tag_stack.is_empty() {
XmlValidateResult {
is_valid: true,
error_message: None,
error_line: None,
}
} else {
XmlValidateResult {
is_valid: false,
error_message: Some(format!("未闭合的标签: {}", tag_stack.join(", "))),
error_line: Some(line),
}
}
}
/// XML 验证结果结构
#[derive(Debug, Clone)]
pub struct XmlValidateResult {
pub is_valid: bool,
pub error_message: Option<String>,
pub error_line: Option<usize>,
}
/// 添加换行和缩进
fn add_newline(result: &mut String, indent_level: u32, indent_str: &str) {
if !result.ends_with('\n') && !result.is_empty() {
result.push('\n');
result.push_str(&indent_str.repeat(indent_level as usize));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prettify_xml() {
let input = "<root><item>test</item></root>";
let config = XmlFormatConfig::default();
let result = format_xml(input, &config).unwrap();
assert!(result.contains('\n'));
assert!(result.contains(" "));
}
#[test]
fn test_compact_xml() {
let input = "<root> <item> test </item> </root>";
let config = XmlFormatConfig {
mode: FormatMode::Compact,
..Default::default()
};
let result = format_xml(input, &config).unwrap();
assert!(!result.contains(" "));
}
#[test]
fn test_validate_xml_valid() {
let result = validate_xml("<root><item></item></root>");
assert!(result.is_valid);
}
#[test]
fn test_validate_xml_invalid() {
let result = validate_xml("<root><item></root>");
assert!(!result.is_valid);
}
#[test]
fn test_validate_xml_with_attributes() {
// 测试带属性的XML验证用户报告的案例
let input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root><item id=\"1\"><name>示例</name><value>测试</value></item></root>";
let result = validate_xml(input);
assert!(result.is_valid, "XML with attributes should be valid");
}
#[test]
fn test_validate_xml_with_multiple_attributes() {
let input = "<root><item id=\"1\" name=\"test\" value=\"123\"></item></root>";
let result = validate_xml(input);
assert!(result.is_valid, "XML with multiple attributes should be valid");
}
#[test]
fn test_validate_xml_nested_with_attributes() {
let input = "<root id=\"main\"><parent id=\"1\"><child id=\"2\"></child></parent></root>";
let result = validate_xml(input);
assert!(result.is_valid, "Nested XML with attributes should be valid");
}
}