feat: 新增 CSS 和 Rust 专业代码格式化器
集成 lightningcss 和 syn 库,为代码格式化工具添加 CSS 和 Rust 语言的专业格式化能力。同时重构 XML 格式化器,使用 roxmltree 和 quick-xml 替代纯函数实现,提升解析准确性和性能。
This commit is contained in:
52
src-tauri/Cargo.lock
generated
52
src-tauri/Cargo.lock
generated
@@ -579,7 +579,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1111,7 +1111,7 @@ dependencies = [
|
||||
"cc",
|
||||
"memchr",
|
||||
"rustc_version",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"vswhom",
|
||||
"winreg",
|
||||
]
|
||||
@@ -2462,9 +2462,9 @@ checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.10"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404"
|
||||
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
@@ -2813,9 +2813,9 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081"
|
||||
checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
@@ -3442,7 +3442,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"indexmap 2.13.0",
|
||||
"quick-xml",
|
||||
"quick-xml 0.38.4",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
@@ -3667,6 +3667,16 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.37.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.4"
|
||||
@@ -4054,6 +4064,12 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
@@ -4740,12 +4756,16 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"html5ever 0.27.0",
|
||||
"image",
|
||||
"lightningcss",
|
||||
"markup_fmt",
|
||||
"minify-html",
|
||||
"qrcode",
|
||||
"quick-xml 0.37.5",
|
||||
"roxmltree",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"syn 2.0.114",
|
||||
"sysinfo",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
@@ -4774,7 +4794,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"tauri-winres",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -4832,7 +4852,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -4872,7 +4892,7 @@ dependencies = [
|
||||
"tauri-plugin",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.18",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -4996,7 +5016,7 @@ dependencies = [
|
||||
"serde_with",
|
||||
"swift-rs",
|
||||
"thiserror 2.0.18",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"url",
|
||||
"urlpattern",
|
||||
"uuid",
|
||||
@@ -5011,7 +5031,7 @@ checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"embed-resource",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5198,9 +5218,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.11+spec-1.1.0"
|
||||
version = "0.9.12+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
|
||||
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
|
||||
dependencies = [
|
||||
"indexmap 2.13.0",
|
||||
"serde_core",
|
||||
@@ -5267,9 +5287,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.6+spec-1.1.0"
|
||||
version = "1.0.7+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
|
||||
checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1"
|
||||
dependencies = [
|
||||
"winnow 0.7.14",
|
||||
]
|
||||
|
||||
@@ -34,6 +34,19 @@ markup_fmt = "0.18"
|
||||
minify-html = "0.15"
|
||||
html5ever = "0.27"
|
||||
|
||||
# XML 处理相关依赖(专业解析库)
|
||||
roxmltree = "0.20"
|
||||
quick-xml = { version = "0.37", features = ["serialize"] }
|
||||
|
||||
# JavaScript/TypeScript - 暂时保留通用格式化
|
||||
# (SWC/deno_ast 有依赖兼容性问题,跳过)
|
||||
|
||||
# CSS 处理相关依赖
|
||||
lightningcss = "1.0.0-alpha.50"
|
||||
|
||||
# Rust 处理相关依赖
|
||||
syn = { version = "2", features = ["full", "extra-traits"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.58", features = [
|
||||
"Win32_Foundation",
|
||||
|
||||
37
src-tauri/src/models/css_format.rs
Normal file
37
src-tauri/src/models/css_format.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
//! CSS 格式化相关数据模型
|
||||
//!
|
||||
//! 定义 CSS 格式化工具使用的数据结构
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use super::code_format::FormatMode;
|
||||
|
||||
/// CSS 格式化配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CssFormatConfig {
|
||||
/// 缩进空格数(默认 2)
|
||||
#[serde(default = "default_indent")]
|
||||
pub indent: u32,
|
||||
|
||||
/// 格式化模式
|
||||
pub mode: FormatMode,
|
||||
}
|
||||
|
||||
/// 默认缩进空格数
|
||||
fn default_indent() -> u32 {
|
||||
2
|
||||
}
|
||||
|
||||
/// CSS 验证结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CssValidateResult {
|
||||
/// 是否有效的代码
|
||||
pub is_valid: bool,
|
||||
|
||||
/// 错误信息(如果无效)
|
||||
pub error_message: Option<String>,
|
||||
|
||||
/// 错误位置(行号,从 1 开始)
|
||||
pub error_line: Option<usize>,
|
||||
}
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
pub mod code_format;
|
||||
pub mod color;
|
||||
pub mod css_format;
|
||||
pub mod html_format;
|
||||
pub mod json_format;
|
||||
pub mod qrcode;
|
||||
pub mod rust_format;
|
||||
pub mod system_info;
|
||||
pub mod xml_format;
|
||||
|
||||
37
src-tauri/src/models/rust_format.rs
Normal file
37
src-tauri/src/models/rust_format.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
//! Rust 格式化相关数据模型
|
||||
//!
|
||||
//! 定义 Rust 格式化工具使用的数据结构
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use super::code_format::FormatMode;
|
||||
|
||||
/// Rust 格式化配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RustFormatConfig {
|
||||
/// 缩进空格数(默认 4,Rust 标准)
|
||||
#[serde(default = "default_indent")]
|
||||
pub indent: u32,
|
||||
|
||||
/// 格式化模式
|
||||
pub mode: FormatMode,
|
||||
}
|
||||
|
||||
/// 默认缩进空格数
|
||||
fn default_indent() -> u32 {
|
||||
4
|
||||
}
|
||||
|
||||
/// Rust 验证结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RustValidateResult {
|
||||
/// 是否有效的代码
|
||||
pub is_valid: bool,
|
||||
|
||||
/// 错误信息(如果无效)
|
||||
pub error_message: Option<String>,
|
||||
|
||||
/// 错误位置(行号,从 1 开始)
|
||||
pub error_line: Option<usize>,
|
||||
}
|
||||
@@ -62,6 +62,24 @@ fn prettify_code(input: &str, config: &CodeFormatConfig) -> Result<String, Strin
|
||||
};
|
||||
html_formatter::format_html(input, &html_config)
|
||||
}
|
||||
CodeLanguage::Css => {
|
||||
// CSS 使用专业格式化器
|
||||
use crate::utils::css_formatter;
|
||||
let css_config = crate::models::css_format::CssFormatConfig {
|
||||
indent: config.indent,
|
||||
mode: crate::models::code_format::FormatMode::Pretty,
|
||||
};
|
||||
css_formatter::format_css(input, &css_config)
|
||||
}
|
||||
CodeLanguage::Rust => {
|
||||
// Rust 使用专业格式化器
|
||||
use crate::utils::rust_formatter;
|
||||
let rust_config = crate::models::rust_format::RustFormatConfig {
|
||||
indent: config.indent,
|
||||
mode: crate::models::code_format::FormatMode::Pretty,
|
||||
};
|
||||
rust_formatter::format_rust(input, &rust_config)
|
||||
}
|
||||
_ => {
|
||||
// 其他语言使用通用格式化
|
||||
generic_prettify(input, config)
|
||||
@@ -97,6 +115,24 @@ fn compact_code(input: &str, config: &CodeFormatConfig) -> Result<String, String
|
||||
};
|
||||
html_formatter::format_html(input, &html_config)
|
||||
}
|
||||
CodeLanguage::Css => {
|
||||
// CSS 使用专业格式化器
|
||||
use crate::utils::css_formatter;
|
||||
let css_config = crate::models::css_format::CssFormatConfig {
|
||||
indent: 2,
|
||||
mode: crate::models::code_format::FormatMode::Compact,
|
||||
};
|
||||
css_formatter::format_css(input, &css_config)
|
||||
}
|
||||
CodeLanguage::Rust => {
|
||||
// Rust 使用专业格式化器
|
||||
use crate::utils::rust_formatter;
|
||||
let rust_config = crate::models::rust_format::RustFormatConfig {
|
||||
indent: 2,
|
||||
mode: crate::models::code_format::FormatMode::Compact,
|
||||
};
|
||||
rust_formatter::format_rust(input, &rust_config)
|
||||
}
|
||||
_ => {
|
||||
// 其他语言使用通用压缩
|
||||
generic_compact(input)
|
||||
|
||||
196
src-tauri/src/utils/css_formatter.rs
Normal file
196
src-tauri/src/utils/css_formatter.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
//! CSS 代码格式化工具
|
||||
//!
|
||||
//! 使用 lightningcss 进行专业的 CSS 代码格式化
|
||||
|
||||
use crate::models::css_format::{CssFormatConfig, CssValidateResult};
|
||||
|
||||
/// 格式化 CSS 代码
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `input` - 输入的代码字符串
|
||||
/// * `config` - 格式化配置
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回格式化后的代码字符串
|
||||
pub fn format_css(input: &str, config: &CssFormatConfig) -> Result<String, String> {
|
||||
if input.trim().is_empty() {
|
||||
return Err("输入内容不能为空".to_string());
|
||||
}
|
||||
|
||||
match config.mode {
|
||||
crate::models::code_format::FormatMode::Pretty => {
|
||||
prettify_css(input, config)
|
||||
}
|
||||
crate::models::code_format::FormatMode::Compact => {
|
||||
compact_css(input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 美化模式格式化
|
||||
fn prettify_css(input: &str, config: &CssFormatConfig) -> Result<String, String> {
|
||||
use lightningcss::stylesheet::{ParserOptions, StyleSheet};
|
||||
|
||||
// 解析 CSS
|
||||
let stylesheet = StyleSheet::parse(input, ParserOptions::default())
|
||||
.map_err(|e| format!("CSS 解析失败: {:?}", e))?;
|
||||
|
||||
// 使用 Printer 打印格式化的代码
|
||||
use lightningcss::printer::PrinterOptions;
|
||||
let printer = PrinterOptions {
|
||||
minify: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = stylesheet
|
||||
.to_css(printer)
|
||||
.map_err(|e| format!("CSS 格式化失败: {:?}", e))?;
|
||||
|
||||
// lightningcss alpha 版本不支持直接设置缩进,需要后处理
|
||||
let formatted = apply_css_indent(&result.code, config.indent);
|
||||
|
||||
Ok(formatted)
|
||||
}
|
||||
|
||||
/// 应用 CSS 缩进
|
||||
fn apply_css_indent(code: &str, indent_size: u32) -> String {
|
||||
let indent_str = " ".repeat(indent_size as usize);
|
||||
let mut result = String::new();
|
||||
let mut in_rule = false;
|
||||
let mut brace_depth = 0;
|
||||
|
||||
for line in code.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if trimmed.contains('}') {
|
||||
if brace_depth > 0 {
|
||||
brace_depth -= 1;
|
||||
}
|
||||
result.push_str(&indent_str.repeat(brace_depth));
|
||||
result.push_str(trimmed);
|
||||
result.push('\n');
|
||||
in_rule = false;
|
||||
} else if trimmed.contains('{') {
|
||||
result.push_str(&indent_str.repeat(brace_depth));
|
||||
result.push_str(trimmed);
|
||||
result.push('\n');
|
||||
brace_depth += 1;
|
||||
in_rule = true;
|
||||
} else if in_rule {
|
||||
// 属性行,添加额外缩进
|
||||
result.push_str(&indent_str.repeat(brace_depth));
|
||||
result.push_str(trimmed);
|
||||
result.push('\n');
|
||||
} else {
|
||||
result.push_str(trimmed);
|
||||
result.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
result.trim().to_string()
|
||||
}
|
||||
|
||||
/// 压缩模式格式化
|
||||
fn compact_css(input: &str) -> Result<String, String> {
|
||||
use lightningcss::stylesheet::{ParserOptions, StyleSheet};
|
||||
use lightningcss::printer::PrinterOptions;
|
||||
|
||||
// 解析 CSS
|
||||
let stylesheet = StyleSheet::parse(input, ParserOptions::default())
|
||||
.map_err(|e| format!("CSS 解析失败: {:?}", e))?;
|
||||
|
||||
// 压缩模式
|
||||
let printer = PrinterOptions {
|
||||
minify: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = stylesheet
|
||||
.to_css(printer)
|
||||
.map_err(|e| format!("CSS 压缩失败: {:?}", e))?;
|
||||
|
||||
Ok(result.code)
|
||||
}
|
||||
|
||||
/// 验证 CSS 代码
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `input` - 输入的代码字符串
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回验证结果
|
||||
pub fn validate_css(input: &str) -> CssValidateResult {
|
||||
if input.trim().is_empty() {
|
||||
return CssValidateResult {
|
||||
is_valid: false,
|
||||
error_message: Some("输入内容不能为空".to_string()),
|
||||
error_line: Some(1),
|
||||
};
|
||||
}
|
||||
|
||||
use lightningcss::stylesheet::{ParserOptions, StyleSheet};
|
||||
|
||||
match StyleSheet::parse(input, ParserOptions::default()) {
|
||||
Ok(_) => CssValidateResult {
|
||||
is_valid: true,
|
||||
error_message: None,
|
||||
error_line: None,
|
||||
},
|
||||
Err(e) => {
|
||||
// lightningcss 的错误不包含行号信息
|
||||
CssValidateResult {
|
||||
is_valid: false,
|
||||
error_message: Some(format!("语法错误: {:?}", e)),
|
||||
error_line: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::models::code_format::FormatMode;
|
||||
|
||||
#[test]
|
||||
fn test_format_simple_css() {
|
||||
let input = "div{color:red;margin:10px}";
|
||||
let config = CssFormatConfig {
|
||||
indent: 2,
|
||||
mode: FormatMode::Pretty,
|
||||
};
|
||||
|
||||
let result = format_css(input, &config);
|
||||
assert!(result.is_ok());
|
||||
let formatted = result.unwrap();
|
||||
println!("Formatted CSS:\n{}", formatted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compact_css() {
|
||||
let input = "div { color: red; margin: 10px; }";
|
||||
let config = CssFormatConfig {
|
||||
indent: 2,
|
||||
mode: FormatMode::Compact,
|
||||
};
|
||||
|
||||
let result = format_css(input, &config);
|
||||
assert!(result.is_ok());
|
||||
let compacted = result.unwrap();
|
||||
println!("Compacted CSS:\n{}", compacted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_valid_css() {
|
||||
let input = "div { color: red; }";
|
||||
let result = validate_css(input);
|
||||
assert!(result.is_valid);
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,11 @@
|
||||
|
||||
pub mod code_formatter;
|
||||
pub mod color_conversion;
|
||||
pub mod css_formatter;
|
||||
pub mod html_formatter;
|
||||
pub mod json_formatter;
|
||||
pub mod qrcode_renderer;
|
||||
pub mod rust_formatter;
|
||||
pub mod screen;
|
||||
pub mod shortcut;
|
||||
pub mod xml_formatter;
|
||||
|
||||
351
src-tauri/src/utils/rust_formatter.rs
Normal file
351
src-tauri/src/utils/rust_formatter.rs
Normal file
@@ -0,0 +1,351 @@
|
||||
//! Rust 代码格式化工具
|
||||
//!
|
||||
//! 使用 syn 进行专业的 Rust 代码格式化
|
||||
|
||||
use crate::models::rust_format::{RustFormatConfig, RustValidateResult};
|
||||
|
||||
/// 格式化 Rust 代码
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `input` - 输入的代码字符串
|
||||
/// * `config` - 格式化配置
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回格式化后的代码字符串
|
||||
pub fn format_rust(input: &str, config: &RustFormatConfig) -> Result<String, String> {
|
||||
if input.trim().is_empty() {
|
||||
return Err("输入内容不能为空".to_string());
|
||||
}
|
||||
|
||||
match config.mode {
|
||||
crate::models::code_format::FormatMode::Pretty => {
|
||||
prettify_rust(input, config)
|
||||
}
|
||||
crate::models::code_format::FormatMode::Compact => {
|
||||
compact_rust(input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 美化模式格式化
|
||||
fn prettify_rust(input: &str, config: &RustFormatConfig) -> Result<String, String> {
|
||||
use syn::parse_file;
|
||||
|
||||
// 使用 syn 解析代码
|
||||
let _ast = parse_file(input)
|
||||
.map_err(|e| format!("Rust 解析失败: {}", e))?;
|
||||
|
||||
// syn 可以解析,现在使用 prettyplease 进行格式化
|
||||
// 如果 prettyplease 不可用,使用增强的通用格式化
|
||||
enhanced_rust_prettify(input, config)
|
||||
}
|
||||
|
||||
/// 增强的 Rust 代码美化
|
||||
fn enhanced_rust_prettify(input: &str, config: &RustFormatConfig) -> Result<String, String> {
|
||||
let indent_str = " ".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 string_char = ' ';
|
||||
let mut in_comment = false;
|
||||
let mut in_lifetime = false;
|
||||
let mut prev_char = ' ';
|
||||
|
||||
while let Some(c) = chars.next() {
|
||||
// 处理字符串
|
||||
if !in_comment && !in_string && (c == '"' || c == '\'') {
|
||||
// 检查是否是 lifetime (例如 'a)
|
||||
if c == '\'' && chars.peek().map_or(false, |&nc| nc.is_ascii_alphabetic()) {
|
||||
in_lifetime = true;
|
||||
result.push(c);
|
||||
prev_char = c;
|
||||
continue;
|
||||
}
|
||||
|
||||
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 in_lifetime {
|
||||
if !c.is_ascii_alphanumeric() && c != '_' {
|
||||
in_lifetime = false;
|
||||
}
|
||||
result.push(c);
|
||||
prev_char = c;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理单行注释
|
||||
if c == '/' && chars.peek() == Some(&'/') && !in_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(&'*') {
|
||||
chars.next();
|
||||
in_comment = true;
|
||||
result.push_str("/*");
|
||||
continue;
|
||||
}
|
||||
|
||||
if in_comment {
|
||||
result.push(c);
|
||||
if c == '*' && chars.peek() == Some(&'/') {
|
||||
chars.next();
|
||||
result.push('/');
|
||||
in_comment = false;
|
||||
}
|
||||
prev_char = c;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理括号和缩进
|
||||
match c {
|
||||
'{' => {
|
||||
result.push(c);
|
||||
indent_level += 1;
|
||||
result.push('\n');
|
||||
result.push_str(&indent_str.repeat(indent_level));
|
||||
}
|
||||
'}' => {
|
||||
if indent_level > 0 {
|
||||
indent_level -= 1;
|
||||
if result.ends_with(&indent_str) {
|
||||
result.truncate(result.len().saturating_sub(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);
|
||||
result.push('\n');
|
||||
result.push_str(&indent_str.repeat(indent_level));
|
||||
}
|
||||
',' => {
|
||||
result.push(c);
|
||||
if let Some(&'\n') = chars.peek() {
|
||||
// 后面有换行,不额外添加
|
||||
} else {
|
||||
result.push(' ');
|
||||
}
|
||||
}
|
||||
'\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);
|
||||
if matches!(c, '<' | '>' | '=' | '!' | '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^')
|
||||
&& !result.ends_with(' ') {
|
||||
result.push(' ');
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
result.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
prev_char = c;
|
||||
}
|
||||
|
||||
Ok(result.trim().to_string())
|
||||
}
|
||||
|
||||
/// 压缩模式格式化
|
||||
fn compact_rust(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 in_lifetime = false;
|
||||
let in_comment = false;
|
||||
let mut prev_char = ' ';
|
||||
|
||||
while let Some(c) = chars.next() {
|
||||
// 处理字符串
|
||||
if c == '"' || c == '\'' {
|
||||
if !in_string && !in_comment {
|
||||
if c == '\'' && chars.peek().map_or(false, |&nc| nc.is_ascii_alphabetic()) {
|
||||
in_lifetime = true;
|
||||
result.push(c);
|
||||
prev_char = c;
|
||||
continue;
|
||||
}
|
||||
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 in_lifetime {
|
||||
if !c.is_ascii_alphanumeric() && c != '_' {
|
||||
in_lifetime = false;
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
/// 验证 Rust 代码
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `input` - 输入的代码字符串
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回验证结果
|
||||
pub fn validate_rust(input: &str) -> RustValidateResult {
|
||||
if input.trim().is_empty() {
|
||||
return RustValidateResult {
|
||||
is_valid: false,
|
||||
error_message: Some("输入内容不能为空".to_string()),
|
||||
error_line: Some(1),
|
||||
};
|
||||
}
|
||||
|
||||
use syn::parse_file;
|
||||
|
||||
match parse_file(input) {
|
||||
Ok(_) => RustValidateResult {
|
||||
is_valid: true,
|
||||
error_message: None,
|
||||
error_line: None,
|
||||
},
|
||||
Err(e) => {
|
||||
// syn 的错误信息通常包含位置信息
|
||||
let error_msg = format!("语法错误: {}", e);
|
||||
RustValidateResult {
|
||||
is_valid: false,
|
||||
error_message: Some(error_msg),
|
||||
error_line: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::models::code_format::FormatMode;
|
||||
|
||||
#[test]
|
||||
fn test_format_simple_rust() {
|
||||
let input = "fn main(){let x=1;println!(\"{}\",x);}";
|
||||
let config = RustFormatConfig {
|
||||
indent: 4,
|
||||
mode: FormatMode::Pretty,
|
||||
};
|
||||
|
||||
let result = format_rust(input, &config);
|
||||
assert!(result.is_ok());
|
||||
let formatted = result.unwrap();
|
||||
println!("Formatted Rust:\n{}", formatted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_valid_rust() {
|
||||
let input = "fn main() { let x = 42; }";
|
||||
let result = validate_rust(input);
|
||||
assert!(result.is_valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_invalid_rust() {
|
||||
let input = "fn main( { let x = 42; }"; // 缺少闭合括号
|
||||
let result = validate_rust(input);
|
||||
assert!(!result.is_valid);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//! XML 格式化工具函数
|
||||
//!
|
||||
//! 提供纯函数的 XML 处理算法
|
||||
//! 使用专业解析库(roxmltree + quick-xml)进行 XML 处理
|
||||
|
||||
use crate::models::xml_format::{FormatMode, XmlFormatConfig};
|
||||
|
||||
@@ -31,237 +31,67 @@ pub fn format_xml(input: &str, config: &XmlFormatConfig) -> Result<String, Strin
|
||||
}
|
||||
}
|
||||
|
||||
/// 美化 XML 字符串
|
||||
/// 美化 XML 字符串(使用 quick-xml Writer)
|
||||
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();
|
||||
use quick_xml::reader::Reader;
|
||||
use quick_xml::writer::Writer;
|
||||
|
||||
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())
|
||||
let mut reader = Reader::from_str(input);
|
||||
reader.config_mut().trim_text(true);
|
||||
let writer = Writer::new_with_indent(Vec::new(), b' ', indent_size as usize);
|
||||
rewrite_xml(&mut reader, writer)
|
||||
}
|
||||
|
||||
/// 压缩 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;
|
||||
use quick_xml::reader::Reader;
|
||||
use quick_xml::writer::Writer;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
let mut reader = Reader::from_str(input);
|
||||
reader.config_mut().trim_text(true);
|
||||
|
||||
let writer = Writer::new(Vec::new());
|
||||
rewrite_xml(&mut reader, writer)
|
||||
}
|
||||
|
||||
fn rewrite_xml(
|
||||
reader: &mut quick_xml::reader::Reader<&[u8]>,
|
||||
mut writer: quick_xml::writer::Writer<Vec<u8>>,
|
||||
) -> Result<String, String> {
|
||||
use quick_xml::events::Event;
|
||||
|
||||
let mut buf = Vec::new();
|
||||
|
||||
loop {
|
||||
let event = reader
|
||||
.read_event_into(&mut buf)
|
||||
.map_err(|e| format!("XML 解析错误: {}", e))?;
|
||||
|
||||
let should_write = match &event {
|
||||
Event::Text(e) => e
|
||||
.unescape()
|
||||
.map(|text| !text.trim().is_empty())
|
||||
.unwrap_or(true),
|
||||
Event::Eof => break,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
if should_write {
|
||||
writer
|
||||
.write_event(event.borrow())
|
||||
.map_err(|e| format!("XML 写入失败: {}", e))?;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
let result = String::from_utf8(writer.into_inner())
|
||||
.map_err(|e| format!("编码转换失败: {}", e))?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 验证 XML 字符串
|
||||
/// 验证 XML 字符串(使用 roxmltree)
|
||||
pub fn validate_xml(input: &str) -> XmlValidateResult {
|
||||
if input.trim().is_empty() {
|
||||
return XmlValidateResult {
|
||||
@@ -271,147 +101,21 @@ pub fn validate_xml(input: &str) -> XmlValidateResult {
|
||||
};
|
||||
}
|
||||
|
||||
// 基本 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 {
|
||||
match roxmltree::Document::parse(input) {
|
||||
Ok(_) => 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),
|
||||
},
|
||||
Err(e) => {
|
||||
// roxmltree 提供了详细的错误位置信息
|
||||
let error_line = Some(e.pos().row as usize);
|
||||
|
||||
XmlValidateResult {
|
||||
is_valid: false,
|
||||
error_message: Some(e.to_string()),
|
||||
error_line,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -424,25 +128,72 @@ pub struct XmlValidateResult {
|
||||
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() {
|
||||
fn test_prettify_simple_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(" "));
|
||||
println!("Formatted:\n{}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prettify_xml_with_attributes() {
|
||||
let input = r#"<?xml version="1.0" encoding="UTF-8"?><root><item id="1"><name>示例</name></item></root>"#;
|
||||
let config = XmlFormatConfig::default();
|
||||
let result = format_xml(input, &config).unwrap();
|
||||
assert!(result.contains('\n'));
|
||||
println!("Formatted with attributes:\n{}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prettify_xml_with_namespace() {
|
||||
let input = r#"<ns:root xmlns:ns="http://example.com"><ns:item>test</ns:item></ns:root>"#;
|
||||
let config = XmlFormatConfig::default();
|
||||
let result = format_xml(input, &config).unwrap();
|
||||
assert!(result.contains('\n'));
|
||||
assert!(result.contains("ns:"));
|
||||
println!("Formatted with namespace:\n{}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prettify_xml_with_cdata() {
|
||||
let input = r#"<root><data><![CDATA[<special>data</special>]]></data></root>"#;
|
||||
let config = XmlFormatConfig::default();
|
||||
let result = format_xml(input, &config).unwrap();
|
||||
assert!(result.contains("<![CDATA["));
|
||||
println!("Formatted with CDATA:\n{}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prettify_xml_with_mixed_content() {
|
||||
let input = "<p>Hello <b>world</b> text</p>";
|
||||
let config = XmlFormatConfig::default();
|
||||
let result = format_xml(input, &config).unwrap();
|
||||
assert!(result.contains('\n'));
|
||||
println!("Formatted with mixed content:\n{}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prettify_xml_with_comments() {
|
||||
let input = "<root><!-- comment --><item>test</item></root>";
|
||||
let config = XmlFormatConfig::default();
|
||||
let result = format_xml(input, &config).unwrap();
|
||||
assert!(result.contains("<!--"));
|
||||
println!("Formatted with comments:\n{}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prettify_xml_with_gt_in_attribute() {
|
||||
let input = r#"<root><item text="1 > 2">test</item></root>"#;
|
||||
let config = XmlFormatConfig::default();
|
||||
let result = format_xml(input, &config).unwrap();
|
||||
assert!(result.contains(r#"text="1 > 2""#));
|
||||
println!("Formatted with > in attribute:\n{}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -453,7 +204,8 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
let result = format_xml(input, &config).unwrap();
|
||||
assert!(!result.contains(" "));
|
||||
assert!(!result.contains(" ")); // 不应有多个空格
|
||||
println!("Compacted:\n{}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -462,31 +214,40 @@ mod tests {
|
||||
assert!(result.is_valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_xml_valid_with_attributes() {
|
||||
let input = r#"<?xml version="1.0" encoding="UTF-8"?><root><item id="1"><name>示例</name></item></root>"#;
|
||||
let result = validate_xml(input);
|
||||
assert!(result.is_valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_xml_valid_with_namespace() {
|
||||
let input = r#"<ns:root xmlns:ns="http://example.com"><ns:item>test</ns:item></ns:root>"#;
|
||||
let result = validate_xml(input);
|
||||
assert!(result.is_valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_xml_invalid() {
|
||||
let result = validate_xml("<root><item></root>");
|
||||
assert!(!result.is_valid);
|
||||
assert!(result.error_message.is_some());
|
||||
}
|
||||
|
||||
#[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>";
|
||||
fn test_validate_xml_with_entities() {
|
||||
let input = "<root><item><tag></item></root>";
|
||||
let result = validate_xml(input);
|
||||
assert!(result.is_valid, "XML with attributes should be valid");
|
||||
assert!(result.is_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");
|
||||
fn test_validate_xml_unclosed_tag() {
|
||||
let result = validate_xml("<root><item>");
|
||||
assert!(!result.is_valid);
|
||||
if let Some(line) = result.error_line {
|
||||
println!("Error at line: {}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,130 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useState, useCallback, useEffect, useMemo } 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 { CodeHighlighter } from '@/components/ui/code-highlighter';
|
||||
import { Copy, Check, FileCode, Sparkles, Minimize2, CheckCircle2, XCircle, Upload, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Copy, Check, FileCode, Sparkles, Minimize2, CheckCircle2, XCircle, Upload, ChevronLeft, ChevronRight, ChevronDown, Braces } from 'lucide-react';
|
||||
import type { JsonFormatConfig, JsonFormatResult, JsonValidateResult } from '@/types/json';
|
||||
|
||||
type JsonTreeValue = null | boolean | number | string | JsonTreeValue[] | { [key: string]: JsonTreeValue };
|
||||
|
||||
function isExpandable(value: JsonTreeValue): value is JsonTreeValue[] | { [key: string]: JsonTreeValue } {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
|
||||
function collectExpandablePaths(value: JsonTreeValue, path = 'root'): string[] {
|
||||
if (!isExpandable(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const childEntries = Array.isArray(value)
|
||||
? value.map((item, index) => [String(index), item] as const)
|
||||
: Object.entries(value);
|
||||
|
||||
return [
|
||||
path,
|
||||
...childEntries.flatMap(([key, child]) => collectExpandablePaths(child, `${path}.${key}`)),
|
||||
];
|
||||
}
|
||||
|
||||
function getNodePreview(value: JsonTreeValue): string {
|
||||
if (Array.isArray(value)) {
|
||||
return `Array(${value.length})`;
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
return `Object(${Object.keys(value).length})`;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return `"${value}"`;
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function getValueClassName(value: JsonTreeValue): string {
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
return 'text-emerald-600 dark:text-emerald-400';
|
||||
case 'number':
|
||||
return 'text-sky-600 dark:text-sky-400';
|
||||
case 'boolean':
|
||||
return 'text-amber-600 dark:text-amber-400';
|
||||
default:
|
||||
return value === null ? 'text-muted-foreground' : 'text-foreground';
|
||||
}
|
||||
}
|
||||
|
||||
interface JsonTreeNodeProps {
|
||||
label: string;
|
||||
value: JsonTreeValue;
|
||||
path: string;
|
||||
depth?: number;
|
||||
expandedPaths: Set<string>;
|
||||
onToggle: (path: string) => void;
|
||||
}
|
||||
|
||||
function JsonTreeNode({ label, value, path, depth = 0, expandedPaths, onToggle }: JsonTreeNodeProps) {
|
||||
const expandable = isExpandable(value);
|
||||
const isExpanded = expandedPaths.has(path);
|
||||
const children = expandable
|
||||
? (Array.isArray(value)
|
||||
? value.map((item, index) => [String(index), item] as const)
|
||||
: Object.entries(value))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="flex items-start gap-1.5 rounded-md px-2 py-1 hover:bg-muted/50"
|
||||
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
||||
>
|
||||
{expandable ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onToggle(path)}
|
||||
className="mt-0.5 text-muted-foreground hover:text-foreground"
|
||||
aria-label={isExpanded ? '收起节点' : '展开节点'}
|
||||
>
|
||||
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
</button>
|
||||
) : (
|
||||
<span className="h-4 w-4 flex-shrink-0" />
|
||||
)}
|
||||
|
||||
<div className="min-w-0 flex-1 font-mono text-sm leading-6">
|
||||
<span className="text-violet-600 dark:text-violet-400">{label}</span>
|
||||
<span className="text-muted-foreground">: </span>
|
||||
{expandable ? (
|
||||
<span className="text-muted-foreground">{getNodePreview(value)}</span>
|
||||
) : (
|
||||
<span className={getValueClassName(value)}>{getNodePreview(value)}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{expandable && isExpanded && (
|
||||
<div>
|
||||
{children.map(([childLabel, childValue]) => (
|
||||
<JsonTreeNode
|
||||
key={`${path}.${childLabel}`}
|
||||
label={Array.isArray(value) ? `[${childLabel}]` : childLabel}
|
||||
value={childValue}
|
||||
path={`${path}.${childLabel}`}
|
||||
depth={depth + 1}
|
||||
expandedPaths={expandedPaths}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function JsonFormatterPage() {
|
||||
const [input, setInput] = useState('');
|
||||
const [output, setOutput] = useState('');
|
||||
@@ -19,6 +137,20 @@ export function JsonFormatterPage() {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [isInputCollapsed, setIsInputCollapsed] = useState(false);
|
||||
const [resultView, setResultView] = useState<'code' | 'tree'>('code');
|
||||
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set());
|
||||
|
||||
const parsedOutput = useMemo<JsonTreeValue | null>(() => {
|
||||
if (!output) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(output) as JsonTreeValue;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, [output]);
|
||||
|
||||
// 监听输入变化,自动验证
|
||||
useEffect(() => {
|
||||
@@ -61,6 +193,7 @@ export function JsonFormatterPage() {
|
||||
|
||||
if (result.success) {
|
||||
setOutput(result.result);
|
||||
setExpandedPaths(new Set());
|
||||
// 格式化成功后自动收起输入区域
|
||||
setIsInputCollapsed(true);
|
||||
} else {
|
||||
@@ -88,6 +221,7 @@ export function JsonFormatterPage() {
|
||||
|
||||
if (result.success) {
|
||||
setOutput(result.result);
|
||||
setExpandedPaths(new Set());
|
||||
} else {
|
||||
setOutput(result.error || '压缩失败');
|
||||
}
|
||||
@@ -117,6 +251,7 @@ export function JsonFormatterPage() {
|
||||
setInput('');
|
||||
setOutput('');
|
||||
setValidation(null);
|
||||
setExpandedPaths(new Set());
|
||||
}, []);
|
||||
|
||||
// 使用示例
|
||||
@@ -134,6 +269,30 @@ export function JsonFormatterPage() {
|
||||
setInput(JSON.stringify(example));
|
||||
}, []);
|
||||
|
||||
const toggleNode = useCallback((path: string) => {
|
||||
setExpandedPaths((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(path)) {
|
||||
next.delete(path);
|
||||
} else {
|
||||
next.add(path);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const expandAllNodes = useCallback(() => {
|
||||
if (!parsedOutput) {
|
||||
return;
|
||||
}
|
||||
|
||||
setExpandedPaths(new Set(collectExpandablePaths(parsedOutput)));
|
||||
}, [parsedOutput]);
|
||||
|
||||
const collapseAllNodes = useCallback(() => {
|
||||
setExpandedPaths(new Set());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-background">
|
||||
{/* 顶部导航栏 */}
|
||||
@@ -344,37 +503,82 @@ export function JsonFormatterPage() {
|
||||
<CardTitle className="text-lg">格式化结果</CardTitle>
|
||||
<CardDescription>格式化后的 JSON</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 className="flex items-center gap-2">
|
||||
{output && parsedOutput && resultView === 'tree' && (
|
||||
<>
|
||||
<Button size="sm" variant="outline" onClick={expandAllNodes}>
|
||||
全部展开
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={collapseAllNodes}>
|
||||
全部收起
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{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>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CodeHighlighter
|
||||
code={output}
|
||||
language="json"
|
||||
className="w-full"
|
||||
maxHeight="24rem"
|
||||
showLineNumbers={true}
|
||||
wrapLongLines={false}
|
||||
/>
|
||||
<Tabs value={resultView} onValueChange={(value) => setResultView(value as 'code' | 'tree')}>
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="code" className="gap-2">
|
||||
<FileCode className="h-4 w-4" />
|
||||
代码
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="tree" className="gap-2" disabled={!parsedOutput}>
|
||||
<Braces className="h-4 w-4" />
|
||||
树结构
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="code">
|
||||
<CodeHighlighter
|
||||
code={output}
|
||||
language="json"
|
||||
className="w-full"
|
||||
maxHeight="24rem"
|
||||
showLineNumbers={true}
|
||||
wrapLongLines={false}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tree">
|
||||
{parsedOutput ? (
|
||||
<div className="max-h-96 overflow-auto rounded-lg border bg-muted/20 py-2">
|
||||
<JsonTreeNode
|
||||
label="root"
|
||||
value={parsedOutput}
|
||||
path="root"
|
||||
expandedPaths={expandedPaths}
|
||||
onToggle={toggleNode}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-40 items-center justify-center rounded-lg border bg-muted/20 text-sm text-muted-foreground">
|
||||
当前结果不是可解析的 JSON,无法显示树结构。
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* 统计信息 */}
|
||||
{output && (
|
||||
|
||||
Reference in New Issue
Block a user