feat: 添加 HTML、XML 和多语言代码格式化功能
新增三个格式化工具(HTML/XML/代码),支持美化和压缩模式。 修复 XML 验证器无法正确解析带属性标签的问题。 修复代码中未使用变量的警告,优化 HTML script/style 标签处理逻辑。
This commit is contained in:
28
src-tauri/src/commands/code_format_commands.rs
Normal file
28
src-tauri/src/commands/code_format_commands.rs
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
37
src-tauri/src/commands/html_format_commands.rs
Normal file
37
src-tauri/src/commands/html_format_commands.rs
Normal 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()),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
37
src-tauri/src/commands/xml_format_commands.rs
Normal file
37
src-tauri/src/commands/xml_format_commands.rs
Normal 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()),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
// 二维码生成命令
|
// 二维码生成命令
|
||||||
|
|||||||
151
src-tauri/src/models/code_format.rs
Normal file
151
src-tauri/src/models/code_format.rs
Normal 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>,
|
||||||
|
}
|
||||||
77
src-tauri/src/models/html_format.rs
Normal file
77
src-tauri/src/models/html_format.rs
Normal 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>,
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
77
src-tauri/src/models/xml_format.rs
Normal file
77
src-tauri/src/models/xml_format.rs
Normal 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>,
|
||||||
|
}
|
||||||
68
src-tauri/src/services/code_format_service.rs
Normal file
68
src-tauri/src/services/code_format_service.rs
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src-tauri/src/services/html_format_service.rs
Normal file
101
src-tauri/src/services/html_format_service.rs
Normal 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),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"}"#);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
101
src-tauri/src/services/xml_format_service.rs
Normal file
101
src-tauri/src/services/xml_format_service.rs
Normal 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),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
396
src-tauri/src/utils/code_formatter.rs
Normal file
396
src-tauri/src/utils/code_formatter.rs
Normal 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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
455
src-tauri/src/utils/html_formatter.rs
Normal file
455
src-tauri/src/utils/html_formatter.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
492
src-tauri/src/utils/xml_formatter.rs
Normal file
492
src-tauri/src/utils/xml_formatter.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 />} />
|
||||||
|
|||||||
318
src/components/features/CodeFormatter/CodeFormatterPage.tsx
Normal file
318
src/components/features/CodeFormatter/CodeFormatterPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
361
src/components/features/HtmlFormatter/HtmlFormatterPage.tsx
Normal file
361
src/components/features/HtmlFormatter/HtmlFormatterPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
288
src/components/features/XmlFormatter/XmlFormatterPage.tsx
Normal file
288
src/components/features/XmlFormatter/XmlFormatterPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
62
src/types/code.ts
Normal 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
42
src/types/html.ts
Normal 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
42
src/types/xml.ts
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user