From bf5d0568113a97e42b54cd77529afa735ecda267 Mon Sep 17 00:00:00 2001 From: shenjianZ Date: Tue, 10 Feb 2026 20:24:21 +0800 Subject: [PATCH] =?UTF-8?q?=20=20feat:=20=E6=B7=BB=E5=8A=A0=20HTML?= =?UTF-8?q?=E3=80=81XML=20=E5=92=8C=E5=A4=9A=E8=AF=AD=E8=A8=80=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=A0=BC=E5=BC=8F=E5=8C=96=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增三个格式化工具(HTML/XML/代码),支持美化和压缩模式。 修复 XML 验证器无法正确解析带属性标签的问题。 修复代码中未使用变量的警告,优化 HTML script/style 标签处理逻辑。 --- .../src/commands/code_format_commands.rs | 28 + .../src/commands/html_format_commands.rs | 37 ++ src-tauri/src/commands/mod.rs | 3 + src-tauri/src/commands/xml_format_commands.rs | 37 ++ src-tauri/src/lib.rs | 13 +- src-tauri/src/models/code_format.rs | 151 ++++++ src-tauri/src/models/html_format.rs | 77 +++ src-tauri/src/models/mod.rs | 3 + src-tauri/src/models/xml_format.rs | 77 +++ src-tauri/src/services/code_format_service.rs | 68 +++ src-tauri/src/services/html_format_service.rs | 101 ++++ src-tauri/src/services/json_format_service.rs | 18 +- src-tauri/src/services/mod.rs | 3 + src-tauri/src/services/xml_format_service.rs | 101 ++++ src-tauri/src/utils/code_formatter.rs | 396 ++++++++++++++ src-tauri/src/utils/html_formatter.rs | 455 ++++++++++++++++ src-tauri/src/utils/mod.rs | 3 + src-tauri/src/utils/xml_formatter.rs | 492 ++++++++++++++++++ src/App.tsx | 6 + .../CodeFormatter/CodeFormatterPage.tsx | 318 +++++++++++ .../HtmlFormatter/HtmlFormatterPage.tsx | 361 +++++++++++++ .../XmlFormatter/XmlFormatterPage.tsx | 288 ++++++++++ src/features/data.ts | 30 ++ src/types/code.ts | 62 +++ src/types/html.ts | 42 ++ src/types/xml.ts | 42 ++ 26 files changed, 3199 insertions(+), 13 deletions(-) create mode 100644 src-tauri/src/commands/code_format_commands.rs create mode 100644 src-tauri/src/commands/html_format_commands.rs create mode 100644 src-tauri/src/commands/xml_format_commands.rs create mode 100644 src-tauri/src/models/code_format.rs create mode 100644 src-tauri/src/models/html_format.rs create mode 100644 src-tauri/src/models/xml_format.rs create mode 100644 src-tauri/src/services/code_format_service.rs create mode 100644 src-tauri/src/services/html_format_service.rs create mode 100644 src-tauri/src/services/xml_format_service.rs create mode 100644 src-tauri/src/utils/code_formatter.rs create mode 100644 src-tauri/src/utils/html_formatter.rs create mode 100644 src-tauri/src/utils/xml_formatter.rs create mode 100644 src/components/features/CodeFormatter/CodeFormatterPage.tsx create mode 100644 src/components/features/HtmlFormatter/HtmlFormatterPage.tsx create mode 100644 src/components/features/XmlFormatter/XmlFormatterPage.tsx create mode 100644 src/types/code.ts create mode 100644 src/types/html.ts create mode 100644 src/types/xml.ts diff --git a/src-tauri/src/commands/code_format_commands.rs b/src-tauri/src/commands/code_format_commands.rs new file mode 100644 index 0000000..e7850db --- /dev/null +++ b/src-tauri/src/commands/code_format_commands.rs @@ -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, + }) +} diff --git a/src-tauri/src/commands/html_format_commands.rs b/src-tauri/src/commands/html_format_commands.rs new file mode 100644 index 0000000..c21a32a --- /dev/null +++ b/src-tauri/src/commands/html_format_commands.rs @@ -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()), + }) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 25a17dd..046be22 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -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; diff --git a/src-tauri/src/commands/xml_format_commands.rs b/src-tauri/src/commands/xml_format_commands.rs new file mode 100644 index 0000000..76dcac5 --- /dev/null +++ b/src-tauri/src/commands/xml_format_commands.rs @@ -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()), + }) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 390052d..1714c49 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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, // 二维码生成命令 diff --git a/src-tauri/src/models/code_format.rs b/src-tauri/src/models/code_format.rs new file mode 100644 index 0000000..51b4b76 --- /dev/null +++ b/src-tauri/src/models/code_format.rs @@ -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, +} + +/// 代码验证结果 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeValidateResult { + /// 是否有效的代码 + pub is_valid: bool, + + /// 错误信息(如果无效) + pub error_message: Option, + + /// 错误位置(行号,从 1 开始) + pub error_line: Option, +} diff --git a/src-tauri/src/models/html_format.rs b/src-tauri/src/models/html_format.rs new file mode 100644 index 0000000..70be05f --- /dev/null +++ b/src-tauri/src/models/html_format.rs @@ -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, +} + +/// HTML 验证结果 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HtmlValidateResult { + /// 是否有效的 HTML + pub is_valid: bool, + + /// 错误信息(如果无效) + pub error_message: Option, + + /// 错误位置(行号,从 1 开始) + pub error_line: Option, +} diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index d88bf93..0b33cb9 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -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; diff --git a/src-tauri/src/models/xml_format.rs b/src-tauri/src/models/xml_format.rs new file mode 100644 index 0000000..67b2730 --- /dev/null +++ b/src-tauri/src/models/xml_format.rs @@ -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, +} + +/// XML 验证结果 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct XmlValidateResult { + /// 是否有效的 XML + pub is_valid: bool, + + /// 错误信息(如果无效) + pub error_message: Option, + + /// 错误位置(行号,从 1 开始) + pub error_line: Option, +} diff --git a/src-tauri/src/services/code_format_service.rs b/src-tauri/src/services/code_format_service.rs new file mode 100644 index 0000000..e61d504 --- /dev/null +++ b/src-tauri/src/services/code_format_service.rs @@ -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 { + 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 { + 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, + }) + } +} diff --git a/src-tauri/src/services/html_format_service.rs b/src-tauri/src/services/html_format_service.rs new file mode 100644 index 0000000..4ba6026 --- /dev/null +++ b/src-tauri/src/services/html_format_service.rs @@ -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 { + 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 { + 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 { + 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), + }), + } + } +} diff --git a/src-tauri/src/services/json_format_service.rs b/src-tauri/src/services/json_format_service.rs index 15d1caa..72af898 100644 --- a/src-tauri/src/services/json_format_service.rs +++ b/src-tauri/src/services/json_format_service.rs @@ -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"}"#); diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index a39d1c7..c929d3b 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -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; diff --git a/src-tauri/src/services/xml_format_service.rs b/src-tauri/src/services/xml_format_service.rs new file mode 100644 index 0000000..2b72314 --- /dev/null +++ b/src-tauri/src/services/xml_format_service.rs @@ -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 { + 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 { + 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 { + 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), + }), + } + } +} diff --git a/src-tauri/src/utils/code_formatter.rs b/src-tauri/src/utils/code_formatter.rs new file mode 100644 index 0000000..4d685f5 --- /dev/null +++ b/src-tauri/src/utils/code_formatter.rs @@ -0,0 +1,396 @@ +//! 代码格式化工具函数 +//! +//! 提供纯函数的代码处理算法 + +use crate::models::code_format::{CodeFormatConfig, CodeLanguage, FormatMode}; + +/// 格式化代码字符串 +/// +/// 对输入的代码字符串进行格式化,支持美化和压缩模式 +/// +/// # 参数 +/// +/// * `input` - 输入的代码字符串 +/// * `config` - 格式化配置 +/// +/// # 返回 +/// +/// 返回格式化后的代码字符串 +/// +/// # 错误 +/// +/// 当代码解析失败时返回错误 +pub fn format_code(input: &str, config: &CodeFormatConfig) -> Result { + 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 { + 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 { + 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 { + 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 { + 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, + pub error_line: Option, +} + +#[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')); + } +} diff --git a/src-tauri/src/utils/html_formatter.rs b/src-tauri/src/utils/html_formatter.rs new file mode 100644 index 0000000..4358302 --- /dev/null +++ b/src-tauri/src/utils/html_formatter.rs @@ -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 { + 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 { + 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("') { + 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 = 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 { + 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, + pub error_line: Option, +} + +/// 添加换行和缩进 +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 = "
test
"; + 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 = "
test
"; + 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(""); + assert!(result.is_valid); + } + + #[test] + fn test_validate_html_invalid() { + let result = validate_html(""); + assert!(!result.is_valid); + } +} diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index 1221f93..74d366b 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -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; diff --git a/src-tauri/src/utils/xml_formatter.rs b/src-tauri/src/utils/xml_formatter.rs new file mode 100644 index 0000000..c544bee --- /dev/null +++ b/src-tauri/src/utils/xml_formatter.rs @@ -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 { + 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 { + 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("') { + 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(""); + 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("' { + 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 { + 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, + pub error_line: Option, +} + +/// 添加换行和缩进 +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 = "test"; + 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 = " test "; + 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(""); + assert!(result.is_valid); + } + + #[test] + fn test_validate_xml_invalid() { + let result = validate_xml(""); + assert!(!result.is_valid); + } + + #[test] + fn test_validate_xml_with_attributes() { + // 测试带属性的XML验证(用户报告的案例) + let input = "示例测试"; + 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 = ""; + 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 = ""; + let result = validate_xml(input); + assert!(result.is_valid, "Nested XML with attributes should be valid"); + } +} diff --git a/src/App.tsx b/src/App.tsx index 51cbda4..8c714a4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,9 @@ import Settings from "@/pages/Settings"; import { CommandPalette } from "@/components/command-palette/CommandPalette"; import { ColorPickerPage } from "@/components/features/ColorPicker/ColorPickerPage"; import { JsonFormatterPage } from "@/components/features/JsonFormatter/JsonFormatterPage"; +import { HtmlFormatterPage } from "@/components/features/HtmlFormatter/HtmlFormatterPage"; +import { XmlFormatterPage } from "@/components/features/XmlFormatter/XmlFormatterPage"; +import { CodeFormatterPage } from "@/components/features/CodeFormatter/CodeFormatterPage"; import { SystemInfoPage } from "@/components/features/SystemInfo/SystemInfoPage"; import { QrCodeGeneratorPage } from "@/components/features/QrCodeGenerator/QrCodeGeneratorPage"; @@ -23,6 +26,9 @@ function App() { } /> } /> } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/src/components/features/CodeFormatter/CodeFormatterPage.tsx b/src/components/features/CodeFormatter/CodeFormatterPage.tsx new file mode 100644 index 0000000..53dc0ec --- /dev/null +++ b/src/components/features/CodeFormatter/CodeFormatterPage.tsx @@ -0,0 +1,318 @@ +import { useState, useCallback, useEffect } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Copy, Check, Code, Sparkles, CheckCircle2, XCircle, Upload } from 'lucide-react'; +import type { CodeFormatConfig, CodeFormatResult, CodeValidateResult, CodeLanguage } from '@/types/code'; + +const LANGUAGES: { value: CodeLanguage; label: string }[] = [ + { value: 'java', label: 'Java' }, + { value: 'cpp', label: 'C++' }, + { value: 'rust', label: 'Rust' }, + { value: 'python', label: 'Python' }, + { value: 'sql', label: 'SQL' }, + { value: 'javascript', label: 'JavaScript' }, + { value: 'typescript', label: 'TypeScript' }, + { value: 'html', label: 'HTML' }, + { value: 'css', label: 'CSS' }, + { value: 'json', label: 'JSON' }, + { value: 'xml', label: 'XML' }, +]; + +export function CodeFormatterPage() { + const [input, setInput] = useState(''); + const [output, setOutput] = useState(''); + const [validation, setValidation] = useState(null); + const [config, setConfig] = useState({ + language: 'javascript', + indent: 4, + useTabs: false, + mode: 'pretty', + }); + const [copied, setCopied] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + + useEffect(() => { + if (input.trim()) { + validateCode(); + } else { + setValidation(null); + } + }, [input, config.language]); + + const validateCode = useCallback(async () => { + if (!input.trim()) { + setValidation(null); + return; + } + + try { + const result = await invoke('validate_code', { + input, + language: config.language, + }); + setValidation(result); + } catch (error) { + console.error('验证失败:', error); + } + }, [input, config.language]); + + const formatCode = useCallback(async () => { + if (!input.trim()) return; + + setIsProcessing(true); + try { + const result = await invoke('format_code', { + input, + config, + }); + if (result.success) { + setOutput(result.result); + } else { + setOutput(result.error || '格式化失败'); + } + } catch (error) { + console.error('格式化失败:', error); + setOutput('错误: ' + String(error)); + } finally { + setIsProcessing(false); + } + }, [input, config]); + + const copyToClipboard = useCallback(async () => { + if (!output) return; + try { + await navigator.clipboard.writeText(output); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (error) { + console.error('复制失败:', error); + } + }, [output]); + + const clearInput = useCallback(() => { + setInput(''); + setOutput(''); + setValidation(null); + }, []); + + const loadExample = useCallback(() => { + const examples: Record = { + javascript: 'function test(){const x=1;return x*2;}', + typescript: 'function test():number{const x:number=1;return x*2;}', + java: 'public class Test{public int test(){int x=1;return x*2;}}', + cpp: 'int test(){int x=1;return x*2;}', + rust: 'fn test()->i32{let x=1;x*2}', + python: 'def test():\n\tx=1\n\treturn x*2', + sql: 'SELECT*FROM users WHERE id=1', + html: '
test
', + css: '.test{color:red;font-size:14px}', + json: '{"name":"test","value":123}', + xml: 'test', + }; + setInput(examples[config.language] || examples.javascript); + }, [config.language]); + + return ( +
+
+
+
+ +
+ +

代码格式化工具

+
+
+
+
+ +
+
+ + + 配置选项 + 自定义代码格式化行为 + + +
+
+ +
+ {LANGUAGES.map((lang) => ( + + ))} +
+
+ +
+
+ +
+ {[2, 4, 8].map((spaces) => ( + + ))} +
+
+ +
+ + +
+
+
+
+
+ +
+ + +
+
+ 输入代码 + 粘贴或输入代码 +
+
+ + {input && ( + + )} +
+
+
+ +
+