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 命令,作为前端和业务逻辑之间的适配器 //! 定义与前端交互的 Tauri 命令,作为前端和业务逻辑之间的适配器
pub mod code_format_commands;
pub mod html_format_commands;
pub mod json_format_commands; pub mod json_format_commands;
pub mod picker_color_commands; pub mod picker_color_commands;
pub mod qrcode_commands; pub mod qrcode_commands;
pub mod system_info_commands; pub mod system_info_commands;
pub mod window_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,
commands::picker_color_commands::pick_color_at_point_topmost, commands::picker_color_commands::pick_color_at_point_topmost,
commands::picker_color_commands::capture_screen_region_rgba, commands::picker_color_commands::capture_screen_region_rgba,
// Json格式化命令 // JSON 格式化命令
commands::json_format_commands::format_json, commands::json_format_commands::format_json,
commands::json_format_commands::validate_json, commands::json_format_commands::validate_json,
commands::json_format_commands::compact_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, 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 color;
pub mod html_format;
pub mod json_format; pub mod json_format;
pub mod qrcode; pub mod qrcode;
pub mod system_info; 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] #[test]
fn test_format_valid_json() { fn test_format_valid_json() {
let service = JsonFormatService;
let input = r#"{"name":"test","value":123}"#; let input = r#"{"name":"test","value":123}"#;
let config = JsonFormatConfig::default(); let config = JsonFormatConfig::default();
let result = service.format(input, &config).unwrap(); let result = JsonFormatService::format(input, &config).unwrap();
assert!(result.success); assert!(result.success);
assert!(result.is_valid); assert!(result.is_valid);
assert!(result.error.is_none()); assert!(result.error.is_none());
@@ -145,11 +144,10 @@ mod tests {
#[test] #[test]
fn test_format_invalid_json() { fn test_format_invalid_json() {
let service = JsonFormatService;
let input = r#"{"invalid": }"#; let input = r#"{"invalid": }"#;
let config = JsonFormatConfig::default(); let config = JsonFormatConfig::default();
let result = service.format(input, &config).unwrap(); let result = JsonFormatService::format(input, &config).unwrap();
assert!(!result.success); assert!(!result.success);
assert!(!result.is_valid); assert!(!result.is_valid);
assert!(result.error.is_some()); assert!(result.error.is_some());
@@ -157,11 +155,10 @@ mod tests {
#[test] #[test]
fn test_format_empty_input() { fn test_format_empty_input() {
let service = JsonFormatService;
let input = ""; let input = "";
let config = JsonFormatConfig::default(); let config = JsonFormatConfig::default();
let result = service.format(input, &config).unwrap(); let result = JsonFormatService::format(input, &config).unwrap();
assert!(!result.success); assert!(!result.success);
assert!(!result.is_valid); assert!(!result.is_valid);
assert!(result.error.is_some()); assert!(result.error.is_some());
@@ -169,30 +166,27 @@ mod tests {
#[test] #[test]
fn test_validate_valid_json() { fn test_validate_valid_json() {
let service = JsonFormatService;
let input = r#"{"valid": true}"#; let input = r#"{"valid": true}"#;
let result = service.validate(input).unwrap(); let result = JsonFormatService::validate(input).unwrap();
assert!(result.is_valid); assert!(result.is_valid);
assert!(result.error_message.is_none()); assert!(result.error_message.is_none());
} }
#[test] #[test]
fn test_validate_invalid_json() { fn test_validate_invalid_json() {
let service = JsonFormatService;
let input = r#"{"invalid": }"#; let input = r#"{"invalid": }"#;
let result = service.validate(input).unwrap(); let result = JsonFormatService::validate(input).unwrap();
assert!(!result.is_valid); assert!(!result.is_valid);
assert!(result.error_message.is_some()); assert!(result.error_message.is_some());
} }
#[test] #[test]
fn test_compact_json() { fn test_compact_json() {
let service = JsonFormatService;
let input = r#"{ "name" : "test" }"#; let input = r#"{ "name" : "test" }"#;
let result = service.compact(input).unwrap(); let result = JsonFormatService::compact(input).unwrap();
assert!(result.success); assert!(result.success);
assert!(result.is_valid); assert!(result.is_valid);
assert_eq!(result.result, r#"{"name":"test"}"#); 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 json_format_service;
pub mod qrcode_service; pub mod qrcode_service;
pub mod system_info_service; pub mod system_info_service;
pub mod window_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 color_conversion;
pub mod html_formatter;
pub mod json_formatter; pub mod json_formatter;
pub mod qrcode_renderer; pub mod qrcode_renderer;
pub mod screen; pub mod screen;
pub mod shortcut; 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");
}
}

View File

@@ -6,6 +6,9 @@ import Settings from "@/pages/Settings";
import { CommandPalette } from "@/components/command-palette/CommandPalette"; import { CommandPalette } from "@/components/command-palette/CommandPalette";
import { ColorPickerPage } from "@/components/features/ColorPicker/ColorPickerPage"; import { ColorPickerPage } from "@/components/features/ColorPicker/ColorPickerPage";
import { JsonFormatterPage } from "@/components/features/JsonFormatter/JsonFormatterPage"; 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 { SystemInfoPage } from "@/components/features/SystemInfo/SystemInfoPage";
import { QrCodeGeneratorPage } from "@/components/features/QrCodeGenerator/QrCodeGeneratorPage"; import { QrCodeGeneratorPage } from "@/components/features/QrCodeGenerator/QrCodeGeneratorPage";
@@ -23,6 +26,9 @@ function App() {
<Route path="/settings" element={<Settings />} /> <Route path="/settings" element={<Settings />} />
<Route path="/feature/color-picker" element={<ColorPickerPage />} /> <Route path="/feature/color-picker" element={<ColorPickerPage />} />
<Route path="/feature/json-formatter" element={<JsonFormatterPage />} /> <Route path="/feature/json-formatter" element={<JsonFormatterPage />} />
<Route path="/feature/html-formatter" element={<HtmlFormatterPage />} />
<Route path="/feature/xml-formatter" element={<XmlFormatterPage />} />
<Route path="/feature/code-formatter" element={<CodeFormatterPage />} />
<Route path="/feature/system-info" element={<SystemInfoPage />} /> <Route path="/feature/system-info" element={<SystemInfoPage />} />
<Route path="/feature/qr-generator" element={<QrCodeGeneratorPage />} /> <Route path="/feature/qr-generator" element={<QrCodeGeneratorPage />} />
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />

View File

@@ -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<CodeValidateResult | null>(null);
const [config, setConfig] = useState<CodeFormatConfig>({
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<CodeValidateResult>('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<CodeFormatResult>('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<CodeLanguage, string> = {
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: '<div><span>test</span></div>',
css: '.test{color:red;font-size:14px}',
json: '{"name":"test","value":123}',
xml: '<root><item>test</item></root>',
};
setInput(examples[config.language] || examples.javascript);
}, [config.language]);
return (
<div className="flex flex-col h-screen bg-background">
<header className="border-b bg-background/95 backdrop-blur flex-shrink-0">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center gap-3">
<Button variant="ghost" size="sm" onClick={() => window.history.back()}>
</Button>
<div className="flex items-center gap-2">
<Code className="w-6 h-6 text-primary" />
<h1 className="text-xl font-bold"></h1>
</div>
</div>
</div>
</header>
<main className="flex-1 container mx-auto px-4 py-6 overflow-y-auto">
<div className="max-w-6xl mx-auto space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex flex-wrap items-center gap-4">
<label className="text-sm font-medium">:</label>
<div className="flex flex-wrap gap-1">
{LANGUAGES.map((lang) => (
<Button
key={lang.value}
size="sm"
variant={config.language === lang.value ? 'default' : 'outline'}
onClick={() => setConfig({ ...config, language: lang.value })}
>
{lang.label}
</Button>
))}
</div>
</div>
<div className="flex flex-wrap items-center gap-6">
<div className="flex items-center gap-3">
<label className="text-sm font-medium">:</label>
<div className="flex gap-1">
{[2, 4, 8].map((spaces) => (
<Button
key={spaces}
size="sm"
variant={config.indent === spaces ? 'default' : 'outline'}
onClick={() => setConfig({ ...config, indent: spaces })}
>
{spaces}
</Button>
))}
</div>
</div>
<div className="flex items-center gap-3">
<label className="text-sm font-medium">使 Tab:</label>
<Button
size="sm"
variant={config.useTabs ? 'default' : 'outline'}
onClick={() => setConfig({ ...config, useTabs: !config.useTabs })}
>
{config.useTabs ? '开启' : '关闭'}
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg"></CardTitle>
<CardDescription></CardDescription>
</div>
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={loadExample}>
<Upload className="w-4 h-4 mr-1" />
</Button>
{input && (
<Button size="sm" variant="ghost" onClick={clearInput}>
</Button>
)}
</div>
</div>
</CardHeader>
<CardContent>
<div className="relative">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
className="w-full h-96 p-4 font-mono text-sm bg-muted rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-primary"
placeholder={`在此输入 ${LANGUAGES.find(l => l.value === config.language)?.label || '代码'}...`}
spellCheck={false}
/>
{validation && (
<div className="absolute top-2 right-2">
{validation.isValid ? (
<Badge variant="default" className="gap-1 bg-green-500 hover:bg-green-600">
<CheckCircle2 className="w-3 h-3" />
</Badge>
) : (
<Badge variant="destructive" className="gap-1">
<XCircle className="w-3 h-3" />
</Badge>
)}
</div>
)}
</div>
{validation && !validation.isValid && validation.errorMessage && (
<div className="mt-3 p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<p className="text-sm text-destructive font-medium">{validation.errorMessage}</p>
</div>
)}
<div className="flex gap-2 mt-4">
<Button
onClick={formatCode}
disabled={!input.trim() || isProcessing}
className="flex-1 gap-2"
>
{isProcessing ? (
<>
<Sparkles className="w-4 h-4 animate-spin" />
...
</>
) : (
<>
<Sparkles className="w-4 h-4" />
</>
)}
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg"></CardTitle>
<CardDescription></CardDescription>
</div>
{output && (
<Button size="sm" variant="outline" onClick={copyToClipboard} className="gap-2">
{copied ? (
<>
<Check className="w-4 h-4" />
</>
) : (
<>
<Copy className="w-4 h-4" />
</>
)}
</Button>
)}
</div>
</CardHeader>
<CardContent>
<pre className="w-full h-96 p-4 font-mono text-sm bg-muted rounded-lg overflow-auto">
{output || <span className="text-muted-foreground">...</span>}
</pre>
{output && (
<div className="flex gap-4 mt-4 text-sm text-muted-foreground">
<span>: {output.length}</span>
<span>: {output.split('\n').length}</span>
</div>
)}
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>使</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>1. </p>
<p>2. </p>
<p>3. 使 Tab</p>
<p>4. "格式化"</p>
<p>5. "复制"</p>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,361 @@
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, FileCode, Sparkles, Minimize2, CheckCircle2, XCircle, Upload } from 'lucide-react';
import type { HtmlFormatConfig, HtmlFormatResult, HtmlValidateResult } from '@/types/html';
export function HtmlFormatterPage() {
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const [validation, setValidation] = useState<HtmlValidateResult | null>(null);
const [config, setConfig] = useState<HtmlFormatConfig>({
indent: 2,
mode: 'pretty',
});
const [copied, setCopied] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
// 监听输入变化,自动验证
useEffect(() => {
if (input.trim()) {
validateHtml();
} else {
setValidation(null);
}
}, [input]);
// 验证 HTML
const validateHtml = useCallback(async () => {
if (!input.trim()) {
setValidation(null);
return;
}
try {
const result = await invoke<HtmlValidateResult>('validate_html', {
input,
});
setValidation(result);
} catch (error) {
console.error('验证失败:', error);
}
}, [input]);
// 格式化 HTML
const formatHtml = useCallback(async () => {
if (!input.trim()) {
return;
}
setIsProcessing(true);
try {
const result = await invoke<HtmlFormatResult>('format_html', {
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]);
// 压缩 HTML
const compactHtml = useCallback(async () => {
if (!input.trim()) {
return;
}
setIsProcessing(true);
try {
const result = await invoke<HtmlFormatResult>('compact_html', {
input,
});
if (result.success) {
setOutput(result.result);
} else {
setOutput(result.error || '压缩失败');
}
} catch (error) {
console.error('压缩失败:', error);
setOutput('错误: ' + String(error));
} finally {
setIsProcessing(false);
}
}, [input]);
// 复制到剪贴板
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 example = `<!DOCTYPE html>
<html><head><title>示例</title></head><body><div class="container"><h1>欢迎</h1><p>这是一个示例。</p></div></body></html>`;
setInput(example);
}, []);
return (
<div className="flex flex-col h-screen bg-background">
{/* 顶部导航栏 */}
<header className="border-b bg-background/95 backdrop-blur flex-shrink-0">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center gap-3">
<Button variant="ghost" size="sm" onClick={() => window.history.back()}>
</Button>
<div className="flex items-center gap-2">
<FileCode className="w-6 h-6 text-primary" />
<h1 className="text-xl font-bold">HTML </h1>
</div>
</div>
</div>
</header>
{/* 主内容区 */}
<main className="flex-1 container mx-auto px-4 py-6 overflow-y-auto">
<div className="max-w-6xl mx-auto space-y-6">
{/* 配置选项 */}
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription> HTML </CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap items-center gap-6">
{/* 缩进空格数 */}
<div className="flex items-center gap-3">
<label className="text-sm font-medium">:</label>
<div className="flex gap-1">
{[2, 4].map((spaces) => (
<Button
key={spaces}
size="sm"
variant={config.indent === spaces ? 'default' : 'outline'}
onClick={() => setConfig({ ...config, indent: spaces })}
>
{spaces}
</Button>
))}
</div>
</div>
{/* 格式化模式 */}
<div className="flex items-center gap-3">
<label className="text-sm font-medium">:</label>
<div className="flex gap-1">
{(['pretty', 'compact'] as const).map((mode) => (
<Button
key={mode}
size="sm"
variant={config.mode === mode ? 'default' : 'outline'}
onClick={() => setConfig({ ...config, mode })}
>
{mode === 'pretty' ? '美化' : '压缩'}
</Button>
))}
</div>
</div>
</div>
</CardContent>
</Card>
{/* 输入输出区域 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 输入区域 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg"> HTML</CardTitle>
<CardDescription> HTML </CardDescription>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={loadExample}
>
<Upload className="w-4 h-4 mr-1" />
</Button>
{input && (
<Button
size="sm"
variant="ghost"
onClick={clearInput}
>
</Button>
)}
</div>
</div>
</CardHeader>
<CardContent>
<div className="relative">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
className="w-full h-96 p-4 font-mono text-sm bg-muted rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="在此输入 HTML..."
spellCheck={false}
/>
{/* 验证状态指示器 */}
{validation && (
<div className="absolute top-2 right-2">
{validation.isValid ? (
<Badge variant="default" className="gap-1 bg-green-500 hover:bg-green-600">
<CheckCircle2 className="w-3 h-3" />
</Badge>
) : (
<Badge variant="destructive" className="gap-1">
<XCircle className="w-3 h-3" />
</Badge>
)}
</div>
)}
</div>
{/* 错误信息 */}
{validation && !validation.isValid && validation.errorMessage && (
<div className="mt-3 p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<p className="text-sm text-destructive font-medium">
{validation.errorMessage}
</p>
{validation.errorLine && (
<p className="text-xs text-destructive/80 mt-1">
位置: {validation.errorLine}
</p>
)}
</div>
)}
{/* 操作按钮 */}
<div className="flex gap-2 mt-4">
<Button
onClick={formatHtml}
disabled={!input.trim() || isProcessing}
className="flex-1 gap-2"
>
{isProcessing ? (
<>
<Sparkles className="w-4 h-4 animate-spin" />
...
</>
) : (
<>
<Sparkles className="w-4 h-4" />
</>
)}
</Button>
<Button
onClick={compactHtml}
variant="outline"
disabled={!input.trim() || isProcessing}
>
<Minimize2 className="w-4 h-4 mr-1" />
</Button>
</div>
</CardContent>
</Card>
{/* 输出区域 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg"></CardTitle>
<CardDescription> HTML</CardDescription>
</div>
{output && (
<Button
size="sm"
variant="outline"
onClick={copyToClipboard}
className="gap-2"
>
{copied ? (
<>
<Check className="w-4 h-4" />
</>
) : (
<>
<Copy className="w-4 h-4" />
</>
)}
</Button>
)}
</div>
</CardHeader>
<CardContent>
<div className="relative">
<pre className="w-full h-96 p-4 font-mono text-sm bg-muted rounded-lg overflow-auto">
{output || (
<span className="text-muted-foreground">
...
</span>
)}
</pre>
</div>
{/* 统计信息 */}
{output && (
<div className="flex gap-4 mt-4 text-sm text-muted-foreground">
<span>: {output.length}</span>
<span>: {output.split('\n').length}</span>
</div>
)}
</CardContent>
</Card>
</div>
{/* 使用说明 */}
<Card>
<CardHeader>
<CardTitle>使</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>1. HTML </p>
<p>2. HTML </p>
<p>3. </p>
<p>4. "格式化" HTML"压缩"</p>
<p>5. "复制"</p>
</CardContent>
</Card>
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,288 @@
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, FileCode, Sparkles, Minimize2, CheckCircle2, XCircle, Upload } from 'lucide-react';
import type { XmlFormatConfig, XmlFormatResult, XmlValidateResult } from '@/types/xml';
export function XmlFormatterPage() {
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const [validation, setValidation] = useState<XmlValidateResult | null>(null);
const [config, setConfig] = useState<XmlFormatConfig>({
indent: 2,
mode: 'pretty',
});
const [copied, setCopied] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
useEffect(() => {
if (input.trim()) {
validateXml();
} else {
setValidation(null);
}
}, [input]);
const validateXml = useCallback(async () => {
if (!input.trim()) {
setValidation(null);
return;
}
try {
const result = await invoke<XmlValidateResult>('validate_xml', { input });
setValidation(result);
} catch (error) {
console.error('验证失败:', error);
}
}, [input]);
const formatXml = useCallback(async () => {
if (!input.trim()) return;
setIsProcessing(true);
try {
const result = await invoke<XmlFormatResult>('format_xml', { 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 compactXml = useCallback(async () => {
if (!input.trim()) return;
setIsProcessing(true);
try {
const result = await invoke<XmlFormatResult>('compact_xml', { input });
if (result.success) {
setOutput(result.result);
} else {
setOutput(result.error || '压缩失败');
}
} catch (error) {
console.error('压缩失败:', error);
setOutput('错误: ' + String(error));
} finally {
setIsProcessing(false);
}
}, [input]);
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 example = '<?xml version="1.0" encoding="UTF-8"?><root><item id="1"><name>示例</name><value>测试</value></item></root>';
setInput(example);
}, []);
return (
<div className="flex flex-col h-screen bg-background">
<header className="border-b bg-background/95 backdrop-blur flex-shrink-0">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center gap-3">
<Button variant="ghost" size="sm" onClick={() => window.history.back()}>
</Button>
<div className="flex items-center gap-2">
<FileCode className="w-6 h-6 text-primary" />
<h1 className="text-xl font-bold">XML </h1>
</div>
</div>
</div>
</header>
<main className="flex-1 container mx-auto px-4 py-6 overflow-y-auto">
<div className="max-w-6xl mx-auto space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription> XML </CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap items-center gap-6">
<div className="flex items-center gap-3">
<label className="text-sm font-medium">:</label>
<div className="flex gap-1">
{[2, 4].map((spaces) => (
<Button
key={spaces}
size="sm"
variant={config.indent === spaces ? 'default' : 'outline'}
onClick={() => setConfig({ ...config, indent: spaces })}
>
{spaces}
</Button>
))}
</div>
</div>
<div className="flex items-center gap-3">
<label className="text-sm font-medium">:</label>
<div className="flex gap-1">
{(['pretty', 'compact'] as const).map((mode) => (
<Button
key={mode}
size="sm"
variant={config.mode === mode ? 'default' : 'outline'}
onClick={() => setConfig({ ...config, mode })}
>
{mode === 'pretty' ? '美化' : '压缩'}
</Button>
))}
</div>
</div>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg"> XML</CardTitle>
<CardDescription> XML </CardDescription>
</div>
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={loadExample}>
<Upload className="w-4 h-4 mr-1" />
</Button>
{input && (
<Button size="sm" variant="ghost" onClick={clearInput}>
</Button>
)}
</div>
</div>
</CardHeader>
<CardContent>
<div className="relative">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
className="w-full h-96 p-4 font-mono text-sm bg-muted rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="在此输入 XML..."
spellCheck={false}
/>
{validation && (
<div className="absolute top-2 right-2">
{validation.isValid ? (
<Badge variant="default" className="gap-1 bg-green-500 hover:bg-green-600">
<CheckCircle2 className="w-3 h-3" />
</Badge>
) : (
<Badge variant="destructive" className="gap-1">
<XCircle className="w-3 h-3" />
</Badge>
)}
</div>
)}
</div>
{validation && !validation.isValid && validation.errorMessage && (
<div className="mt-3 p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<p className="text-sm text-destructive font-medium">{validation.errorMessage}</p>
</div>
)}
<div className="flex gap-2 mt-4">
<Button onClick={formatXml} disabled={!input.trim() || isProcessing} className="flex-1 gap-2">
{isProcessing ? (
<>
<Sparkles className="w-4 h-4 animate-spin" />
...
</>
) : (
<>
<Sparkles className="w-4 h-4" />
</>
)}
</Button>
<Button onClick={compactXml} variant="outline" disabled={!input.trim() || isProcessing}>
<Minimize2 className="w-4 h-4 mr-1" />
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg"></CardTitle>
<CardDescription> XML</CardDescription>
</div>
{output && (
<Button size="sm" variant="outline" onClick={copyToClipboard} className="gap-2">
{copied ? (
<>
<Check className="w-4 h-4" />
</>
) : (
<>
<Copy className="w-4 h-4" />
</>
)}
</Button>
)}
</div>
</CardHeader>
<CardContent>
<pre className="w-full h-96 p-4 font-mono text-sm bg-muted rounded-lg overflow-auto">
{output || <span className="text-muted-foreground">...</span>}
</pre>
{output && (
<div className="flex gap-4 mt-4 text-sm text-muted-foreground">
<span>: {output.length}</span>
<span>: {output.split('\n').length}</span>
</div>
)}
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>使</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>1. XML </p>
<p>2. XML </p>
<p>3. </p>
<p>4. "格式化" XML"压缩"</p>
<p>5. "复制"</p>
</CardContent>
</Card>
</div>
</main>
</div>
);
}

View File

@@ -147,6 +147,36 @@ export const featuresData: Feature[] = [
tags: ['json', '格式化', '验证', 'format', 'validate'], tags: ['json', '格式化', '验证', 'format', 'validate'],
implemented: true, implemented: true,
}, },
{
id: 'html-formatter',
name: 'HTML 格式化',
description: '格式化和验证 HTML 代码',
icon: 'FileCode',
category: 'dev',
route: '/feature/html-formatter',
tags: ['html', '格式化', '验证', 'format', 'validate', '美化'],
implemented: true,
},
{
id: 'xml-formatter',
name: 'XML 格式化',
description: '格式化和验证 XML 数据',
icon: 'FileText',
category: 'dev',
route: '/feature/xml-formatter',
tags: ['xml', '格式化', '验证', 'format', 'validate', '美化'],
implemented: true,
},
{
id: 'code-formatter',
name: '代码格式化',
description: '支持多种编程语言的代码格式化',
icon: 'Code',
category: 'dev',
route: '/feature/code-formatter',
tags: ['代码', '格式化', 'java', 'cpp', 'rust', 'python', 'sql', 'javascript', 'typescript'],
implemented: true,
},
{ {
id: 'base64-tool', id: 'base64-tool',
name: 'Base64 编解码', name: 'Base64 编解码',

62
src/types/code.ts Normal file
View File

@@ -0,0 +1,62 @@
/**
* 代码格式化相关类型定义
*/
/**
* 支持的编程语言
*/
export type CodeLanguage =
| 'java'
| 'cpp'
| 'rust'
| 'python'
| 'sql'
| 'javascript'
| 'typescript'
| 'html'
| 'css'
| 'json'
| 'xml';
/**
* 代码格式化配置
*/
export interface CodeFormatConfig {
/** 编程语言 */
language: CodeLanguage;
/** 缩进空格数(默认 4 */
indent?: number;
/** 使用 Tab 缩进 */
useTabs?: boolean;
/** 格式化模式 */
mode?: FormatMode;
}
/**
* 代码格式化模式
*/
export type FormatMode = 'pretty' | 'compact';
/**
* 代码格式化结果
*/
export interface CodeFormatResult {
/** 是否成功 */
success: boolean;
/** 格式化后的代码字符串 */
result: string;
/** 错误信息(如果失败) */
error?: string;
}
/**
* 代码验证结果
*/
export interface CodeValidateResult {
/** 是否有效的代码 */
isValid: boolean;
/** 错误信息(如果无效) */
errorMessage?: string;
/** 错误位置(行号,从 1 开始) */
errorLine?: number;
}

42
src/types/html.ts Normal file
View File

@@ -0,0 +1,42 @@
/**
* HTML 格式化相关类型定义
*/
/**
* HTML 格式化配置
*/
export interface HtmlFormatConfig {
/** 缩进空格数(默认 2 */
indent?: number;
/** 格式化模式 */
mode?: FormatMode;
}
/**
* HTML 格式化模式
*/
export type FormatMode = 'pretty' | 'compact';
/**
* HTML 格式化结果
*/
export interface HtmlFormatResult {
/** 是否成功 */
success: boolean;
/** 格式化后的 HTML 字符串 */
result: string;
/** 错误信息(如果失败) */
error?: string;
}
/**
* HTML 验证结果
*/
export interface HtmlValidateResult {
/** 是否有效的 HTML */
isValid: boolean;
/** 错误信息(如果无效) */
errorMessage?: string;
/** 错误位置(行号,从 1 开始) */
errorLine?: number;
}

42
src/types/xml.ts Normal file
View File

@@ -0,0 +1,42 @@
/**
* XML 格式化相关类型定义
*/
/**
* XML 格式化配置
*/
export interface XmlFormatConfig {
/** 缩进空格数(默认 2 */
indent?: number;
/** 格式化模式 */
mode?: FormatMode;
}
/**
* XML 格式化模式
*/
export type FormatMode = 'pretty' | 'compact';
/**
* XML 格式化结果
*/
export interface XmlFormatResult {
/** 是否成功 */
success: boolean;
/** 格式化后的 XML 字符串 */
result: string;
/** 错误信息(如果失败) */
error?: string;
}
/**
* XML 验证结果
*/
export interface XmlValidateResult {
/** 是否有效的 XML */
isValid: boolean;
/** 错误信息(如果无效) */
errorMessage?: string;
/** 错误位置(行号,从 1 开始) */
errorLine?: number;
}