feat: 标准化占位符, 增加模板
This commit is contained in:
99
Cargo.lock
generated
99
Cargo.lock
generated
@@ -28,6 +28,15 @@ dependencies = [
|
|||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.18"
|
version = "0.6.18"
|
||||||
@@ -399,9 +408,11 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"toml",
|
||||||
"uuid",
|
"uuid",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"zip",
|
"zip",
|
||||||
@@ -1180,6 +1191,35 @@ dependencies = [
|
|||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.12.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.11"
|
version = "0.12.11"
|
||||||
@@ -1377,6 +1417,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -1643,6 +1692,47 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_write",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_write"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -2065,6 +2155,15 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "write16"
|
name = "write16"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ reqwest = { version = "0.12", features = ["json", "blocking"] }
|
|||||||
uuid = { version = "1.4", features = ["v4", "fast-rng", "macro-diagnostics"] }
|
uuid = { version = "1.4", features = ["v4", "fast-rng", "macro-diagnostics"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
toml = "0.8"
|
||||||
zip = "2.2.2"
|
zip = "2.2.2"
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
anyhow = "1.0.97"
|
anyhow = "1.0.97"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
|
regex = "1.10"
|
||||||
@@ -6,5 +6,9 @@
|
|||||||
|
|
||||||
|变量名|描述|
|
|变量名|描述|
|
||||||
|:----|:----|
|
|:----|:----|
|
||||||
|`__mod_name__`|项目名|
|
|`{{mod_name}}`|项目名|
|
||||||
|`__mod_name_lower__`|项目名小写驼峰|
|
|`{{mod_name_lower}}`|项目名小写驼峰|
|
||||||
|
|`{{behavior_pack_uuid}}`|行为包 UUID|
|
||||||
|
|`{{resource_pack_uuid}}`|资源包 UUID|
|
||||||
|
|`{{behavior_module_uuid}}`|行为包模块 UUID|
|
||||||
|
|`{{resource_module_uuid}}`|资源包模块 UUID|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
ProjectName = "Circus"
|
ProjectName = "{{mod_name}}"
|
||||||
|
|
||||||
ServerSystemName = "CircusServerSystem"
|
ServerSystemName = "{{mod_name}}ServerSystem"
|
||||||
ServerSystemPath = "circusScripts.modServer.serverSystem.CircusServerSystem"
|
ServerSystemPath = "{{mod_name_lower}}Scripts.modServer.serverSystem.{{mod_name}}ServerSystem"
|
||||||
|
|
||||||
ClientSystemName = "CircusClientSystem"
|
ClientSystemName = "{{mod_name}}ClientSystem"
|
||||||
ClientSystemPath = "circusScripts.modClient.clientSystem.CircusClientSystem"
|
ClientSystemPath = "{{mod_name_lower}}Scripts.modClient.clientSystem.{{mod_name}}ClientSystem"
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
{
|
{
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "data",
|
"type": "data",
|
||||||
"uuid": "{behavior_module_uuid}",
|
"uuid": "{{behavior_module_uuid}}",
|
||||||
"version": [
|
"version": [
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "behavior_pack",
|
"name": "behavior_pack",
|
||||||
"uuid": "{behavior_pack_uuid}",
|
"uuid": "{{behavior_pack_uuid}}",
|
||||||
"version": [
|
"version": [
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"name": "resource_pack",
|
"name": "resource_pack",
|
||||||
"uuid": "{resource_pack_uuid}",
|
"uuid": "{{resource_pack_uuid}}",
|
||||||
"version": [
|
"version": [
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
{
|
{
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "resources",
|
"type": "resources",
|
||||||
"uuid": "{resource_module_uuid}",
|
"uuid": "{{resource_module_uuid}}",
|
||||||
"version": [
|
"version": [
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
|
|||||||
28
examples/default/template.toml
Normal file
28
examples/default/template.toml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[template]
|
||||||
|
name = "default"
|
||||||
|
description = "默认 emod 项目模板"
|
||||||
|
|
||||||
|
[[renames]]
|
||||||
|
from = "behavior_pack/exampleScripts"
|
||||||
|
to = "behavior_pack/{{mod_name_lower}}Scripts"
|
||||||
|
|
||||||
|
[[renames]]
|
||||||
|
from = "behavior_pack"
|
||||||
|
to = "behavior_pack_{{behavior_pack_uuid_short}}"
|
||||||
|
|
||||||
|
[[renames]]
|
||||||
|
from = "resource_pack"
|
||||||
|
to = "resource_pack_{{resource_pack_uuid_short}}"
|
||||||
|
|
||||||
|
[variables]
|
||||||
|
mod_name = { required = true, description = "项目名称" }
|
||||||
|
mod_name_lower = { required = true, description = "项目名称(小写驼峰)" }
|
||||||
|
behavior_pack_uuid = { required = true, description = "行为包 UUID" }
|
||||||
|
resource_pack_uuid = { required = true, description = "资源包 UUID" }
|
||||||
|
behavior_module_uuid = { required = true, description = "行为包模块 UUID" }
|
||||||
|
resource_module_uuid = { required = true, description = "资源包模块 UUID" }
|
||||||
|
behavior_pack_uuid_short = { required = true, description = "行为包 UUID 前8位" }
|
||||||
|
resource_pack_uuid_short = { required = true, description = "资源包 UUID 前8位" }
|
||||||
|
|
||||||
|
[process]
|
||||||
|
file_extensions = ["json", "py", "lang", "txt"]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"pack_id": "{behavior_pack_uuid}",
|
"pack_id": "{{behavior_pack_uuid}}",
|
||||||
"type": "Addon",
|
"type": "Addon",
|
||||||
"version": [
|
"version": [
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"pack_id": "{resource_pack_uuid}",
|
"pack_id": "{{resource_pack_uuid}}",
|
||||||
"type": "Addon",
|
"type": "Addon",
|
||||||
"version": [
|
"version": [
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
entity::project::ProjectInfo,
|
entity::project::ProjectInfo,
|
||||||
|
template::TemplateEngine,
|
||||||
utils::{file, http::HttpClient},
|
utils::{file, http::HttpClient},
|
||||||
};
|
};
|
||||||
use std::{fs, path::PathBuf};
|
use std::{fs, path::PathBuf};
|
||||||
@@ -26,9 +27,9 @@ fn create_project(name: &str, target: Option<&str>, temp_dir: &PathBuf) -> Resul
|
|||||||
let local_dir = PathBuf::from(format!("./{}", name));
|
let local_dir = PathBuf::from(format!("./{}", name));
|
||||||
fs::create_dir(&local_dir)?;
|
fs::create_dir(&local_dir)?;
|
||||||
|
|
||||||
clone_and_copy_template(target, temp_dir, &local_dir)?;
|
let template_dir = clone_and_copy_template(target, temp_dir, &local_dir)?;
|
||||||
|
|
||||||
initialize_project(&local_dir, name)?;
|
initialize_project_with_template(&template_dir, &local_dir, name)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -57,7 +58,11 @@ fn check_example_exists(target: &str) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clone_and_copy_template(target: &str, temp_dir: &PathBuf, local_dir: &PathBuf) -> Result<()> {
|
fn clone_and_copy_template(
|
||||||
|
target: &str,
|
||||||
|
temp_dir: &PathBuf,
|
||||||
|
local_dir: &PathBuf,
|
||||||
|
) -> Result<PathBuf> {
|
||||||
let _ = fs::remove_dir_all(format!("{}/tmp", temp_dir.display()));
|
let _ = fs::remove_dir_all(format!("{}/tmp", temp_dir.display()));
|
||||||
|
|
||||||
let config = Config::load();
|
let config = Config::load();
|
||||||
@@ -67,10 +72,14 @@ fn clone_and_copy_template(target: &str, temp_dir: &PathBuf, local_dir: &PathBuf
|
|||||||
let target_dir = PathBuf::from(format!("{}/tmp/examples/{}", temp_dir.display(), target));
|
let target_dir = PathBuf::from(format!("{}/tmp/examples/{}", temp_dir.display(), target));
|
||||||
file::copy_folder(&target_dir, local_dir)?;
|
file::copy_folder(&target_dir, local_dir)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(target_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize_project(local_dir: &PathBuf, name: &str) -> Result<()> {
|
fn initialize_project_with_template(
|
||||||
|
template_dir: &PathBuf,
|
||||||
|
local_dir: &PathBuf,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<()> {
|
||||||
let lower_name = format!(
|
let lower_name = format!(
|
||||||
"{}{}",
|
"{}{}",
|
||||||
name.chars().next().unwrap().to_lowercase(),
|
name.chars().next().unwrap().to_lowercase(),
|
||||||
@@ -80,14 +89,41 @@ fn initialize_project(local_dir: &PathBuf, name: &str) -> Result<()> {
|
|||||||
println!("项目名称: {}", name);
|
println!("项目名称: {}", name);
|
||||||
println!("标识名称: {}", lower_name);
|
println!("标识名称: {}", lower_name);
|
||||||
|
|
||||||
let scripts_dir = local_dir.join(format!("behavior_pack/{}Scripts", lower_name));
|
|
||||||
fs::rename(local_dir.join("behavior_pack/exampleScripts"), &scripts_dir)?;
|
|
||||||
|
|
||||||
let project_info = generate_project_info(name, &lower_name);
|
let project_info = generate_project_info(name, &lower_name);
|
||||||
|
|
||||||
apply_project_info(local_dir, &scripts_dir, &project_info)?;
|
let mut engine = TemplateEngine::load(template_dir)?;
|
||||||
|
|
||||||
rename_pack_folders(local_dir, &project_info)?;
|
engine.set_variable("mod_name".to_string(), project_info.name.clone());
|
||||||
|
engine.set_variable(
|
||||||
|
"mod_name_lower".to_string(),
|
||||||
|
project_info.lower_name.clone(),
|
||||||
|
);
|
||||||
|
engine.set_variable(
|
||||||
|
"behavior_pack_uuid".to_string(),
|
||||||
|
project_info.behavior_pack_uuid.clone(),
|
||||||
|
);
|
||||||
|
engine.set_variable(
|
||||||
|
"resource_pack_uuid".to_string(),
|
||||||
|
project_info.resource_pack_uuid.clone(),
|
||||||
|
);
|
||||||
|
engine.set_variable(
|
||||||
|
"behavior_module_uuid".to_string(),
|
||||||
|
project_info.behavior_module_uuid.clone(),
|
||||||
|
);
|
||||||
|
engine.set_variable(
|
||||||
|
"resource_module_uuid".to_string(),
|
||||||
|
project_info.resource_module_uuid.clone(),
|
||||||
|
);
|
||||||
|
engine.set_variable(
|
||||||
|
"behavior_pack_uuid_short".to_string(),
|
||||||
|
project_info.behavior_pack_uuid.chars().take(8).collect(),
|
||||||
|
);
|
||||||
|
engine.set_variable(
|
||||||
|
"resource_pack_uuid_short".to_string(),
|
||||||
|
project_info.resource_pack_uuid.chars().take(8).collect(),
|
||||||
|
);
|
||||||
|
|
||||||
|
engine.process_directory(local_dir)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -102,82 +138,3 @@ fn generate_project_info(name: &str, lower_name: &str) -> ProjectInfo {
|
|||||||
resource_module_uuid: Uuid::new_v4().to_string(),
|
resource_module_uuid: Uuid::new_v4().to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_project_info(
|
|
||||||
local_dir: &PathBuf,
|
|
||||||
scripts_dir: &PathBuf,
|
|
||||||
info: &ProjectInfo,
|
|
||||||
) -> Result<()> {
|
|
||||||
let manifest_files = vec![
|
|
||||||
local_dir.join("world_behavior_packs.json"),
|
|
||||||
local_dir.join("world_resource_packs.json"),
|
|
||||||
local_dir.join("behavior_pack/pack_manifest.json"),
|
|
||||||
local_dir.join("resource_pack/pack_manifest.json"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for path in manifest_files {
|
|
||||||
apply_info_to_json(&path, info)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
process_python_files(scripts_dir, info)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_info_to_json(path: &PathBuf, info: &ProjectInfo) -> Result<()> {
|
|
||||||
println!(" - 修改文件: {}", path.display());
|
|
||||||
file::update_json_file(path, |json| {
|
|
||||||
let content = serde_json::to_string(json)?;
|
|
||||||
let updated = content
|
|
||||||
.replace("{behavior_pack_uuid}", &info.behavior_pack_uuid)
|
|
||||||
.replace("{resource_pack_uuid}", &info.resource_pack_uuid)
|
|
||||||
.replace("{behavior_module_uuid}", &info.behavior_module_uuid)
|
|
||||||
.replace("{resource_module_uuid}", &info.resource_module_uuid)
|
|
||||||
.replace("__mod_name__", &info.name)
|
|
||||||
.replace("__mod_name_lower__", &info.lower_name);
|
|
||||||
*json = serde_json::from_str(&updated)?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_python_files(dir: &PathBuf, info: &ProjectInfo) -> Result<()> {
|
|
||||||
if !dir.exists() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if dir.is_dir() {
|
|
||||||
for entry in fs::read_dir(dir)? {
|
|
||||||
let entry = entry?;
|
|
||||||
process_python_files(&entry.path(), info)?;
|
|
||||||
}
|
|
||||||
} else if dir.extension().and_then(|s| s.to_str()) == Some("py") {
|
|
||||||
apply_info_to_python(dir, info)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_info_to_python(path: &PathBuf, info: &ProjectInfo) -> Result<()> {
|
|
||||||
let content = fs::read_to_string(path)?;
|
|
||||||
let updated = content
|
|
||||||
.replace("__mod_name__", &info.name)
|
|
||||||
.replace("__mod_name_lower__", &info.lower_name);
|
|
||||||
fs::write(path, updated)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rename_pack_folders(local_dir: &PathBuf, info: &ProjectInfo) -> Result<()> {
|
|
||||||
let behavior_suffix: String = info.behavior_pack_uuid.chars().take(8).collect();
|
|
||||||
let resource_suffix: String = info.resource_pack_uuid.chars().take(8).collect();
|
|
||||||
|
|
||||||
fs::rename(
|
|
||||||
local_dir.join("behavior_pack"),
|
|
||||||
local_dir.join(format!("behavior_pack_{}", behavior_suffix)),
|
|
||||||
)?;
|
|
||||||
fs::rename(
|
|
||||||
local_dir.join("resource_pack"),
|
|
||||||
local_dir.join(format!("resource_pack_{}", resource_suffix)),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
10
src/error.rs
10
src/error.rs
@@ -11,8 +11,10 @@ pub enum CliError {
|
|||||||
Zip(zip::result::ZipError),
|
Zip(zip::result::ZipError),
|
||||||
Walkdir(walkdir::Error),
|
Walkdir(walkdir::Error),
|
||||||
Parse(ParseIntError),
|
Parse(ParseIntError),
|
||||||
|
Toml(toml::de::Error),
|
||||||
NotFound(String),
|
NotFound(String),
|
||||||
InvalidData(String),
|
InvalidData(String),
|
||||||
|
InvalidInput(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for CliError {
|
impl fmt::Display for CliError {
|
||||||
@@ -25,8 +27,10 @@ impl fmt::Display for CliError {
|
|||||||
CliError::Zip(e) => write!(f, "压缩错误: {}", e),
|
CliError::Zip(e) => write!(f, "压缩错误: {}", e),
|
||||||
CliError::Walkdir(e) => write!(f, "目录遍历错误: {}", e),
|
CliError::Walkdir(e) => write!(f, "目录遍历错误: {}", e),
|
||||||
CliError::Parse(e) => write!(f, "解析错误: {}", e),
|
CliError::Parse(e) => write!(f, "解析错误: {}", e),
|
||||||
|
CliError::Toml(e) => write!(f, "TOML解析错误: {}", e),
|
||||||
CliError::NotFound(msg) => write!(f, "未找到: {}", msg),
|
CliError::NotFound(msg) => write!(f, "未找到: {}", msg),
|
||||||
CliError::InvalidData(msg) => write!(f, "无效数据: {}", msg),
|
CliError::InvalidData(msg) => write!(f, "无效数据: {}", msg),
|
||||||
|
CliError::InvalidInput(msg) => write!(f, "无效输入: {}", msg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,4 +79,10 @@ impl From<ParseIntError> for CliError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<toml::de::Error> for CliError {
|
||||||
|
fn from(err: toml::de::Error) -> Self {
|
||||||
|
CliError::Toml(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, CliError>;
|
pub type Result<T> = std::result::Result<T, CliError>;
|
||||||
@@ -3,6 +3,7 @@ mod entity;
|
|||||||
mod utils;
|
mod utils;
|
||||||
mod error;
|
mod error;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod template;
|
||||||
|
|
||||||
use crate::commands::{Cli, Commands};
|
use crate::commands::{Cli, Commands};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|||||||
198
src/template.rs
Normal file
198
src/template.rs
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use regex::Regex;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct TemplateConfig {
|
||||||
|
pub template: TemplateInfo,
|
||||||
|
#[serde(default)]
|
||||||
|
pub renames: Vec<RenameRule>,
|
||||||
|
pub variables: HashMap<String, VariableConfig>,
|
||||||
|
pub process: ProcessConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct TemplateInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct RenameRule {
|
||||||
|
pub from: String,
|
||||||
|
pub to: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct VariableConfig {
|
||||||
|
pub required: bool,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct ProcessConfig {
|
||||||
|
pub file_extensions: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TemplateEngine {
|
||||||
|
config: TemplateConfig,
|
||||||
|
variables: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateEngine {
|
||||||
|
pub fn load(template_dir: &Path) -> crate::error::Result<Self> {
|
||||||
|
let config_path = template_dir.join("template.toml");
|
||||||
|
let content = fs::read_to_string(&config_path)?;
|
||||||
|
let config: TemplateConfig = toml::from_str(&content)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
config,
|
||||||
|
variables: HashMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_variable(&mut self, key: String, value: String) {
|
||||||
|
self.variables.insert(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_variables(&self) -> crate::error::Result<()> {
|
||||||
|
for (key, var_config) in &self.config.variables {
|
||||||
|
if var_config.required && !self.variables.contains_key(key) {
|
||||||
|
return Err(crate::error::CliError::InvalidInput(format!(
|
||||||
|
"缺少必需的变量: {} ({})",
|
||||||
|
key, var_config.description
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_directory(&self, dir: &Path) -> crate::error::Result<()> {
|
||||||
|
self.validate_variables()?;
|
||||||
|
|
||||||
|
self.replace_in_files(dir)?;
|
||||||
|
|
||||||
|
self.apply_renames(dir)?;
|
||||||
|
|
||||||
|
self.verify_no_placeholders(dir)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_in_files(&self, dir: &Path) -> crate::error::Result<()> {
|
||||||
|
let placeholder_regex = Regex::new(r"\{\{(\w+)\}\}").unwrap();
|
||||||
|
|
||||||
|
for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if !path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
|
||||||
|
if !self.config.process.file_extensions.contains(&ext.to_string()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = fs::read_to_string(path)?;
|
||||||
|
let mut updated = content.clone();
|
||||||
|
|
||||||
|
for cap in placeholder_regex.captures_iter(&content) {
|
||||||
|
let placeholder = &cap[0];
|
||||||
|
let var_name = &cap[1];
|
||||||
|
|
||||||
|
if let Some(value) = self.variables.get(var_name) {
|
||||||
|
updated = updated.replace(placeholder, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if updated != content {
|
||||||
|
fs::write(path, updated)?;
|
||||||
|
println!(" - 处理文件: {}", path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_renames(&self, dir: &Path) -> crate::error::Result<()> {
|
||||||
|
for rule in self.config.renames.iter().rev() {
|
||||||
|
let from_path = dir.join(&rule.from);
|
||||||
|
|
||||||
|
if !from_path.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let to_path_str = self.replace_placeholders(&rule.to);
|
||||||
|
let to_path = dir.join(&to_path_str);
|
||||||
|
|
||||||
|
if let Some(parent) = to_path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::rename(&from_path, &to_path)?;
|
||||||
|
println!(" - 重命名: {} -> {}", rule.from, to_path_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_placeholders(&self, text: &str) -> String {
|
||||||
|
let placeholder_regex = Regex::new(r"\{\{(\w+)\}\}").unwrap();
|
||||||
|
let mut result = text.to_string();
|
||||||
|
|
||||||
|
for cap in placeholder_regex.captures_iter(text) {
|
||||||
|
let placeholder = &cap[0];
|
||||||
|
let var_name = &cap[1];
|
||||||
|
|
||||||
|
if let Some(value) = self.variables.get(var_name) {
|
||||||
|
result = result.replace(placeholder, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_no_placeholders(&self, dir: &Path) -> crate::error::Result<()> {
|
||||||
|
let placeholder_regex = Regex::new(r"\{\{(\w+)\}\}").unwrap();
|
||||||
|
let mut found_placeholders = Vec::new();
|
||||||
|
|
||||||
|
for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if !path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
|
||||||
|
if !self.config.process.file_extensions.contains(&ext.to_string()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = fs::read_to_string(path)?;
|
||||||
|
|
||||||
|
for cap in placeholder_regex.captures_iter(&content) {
|
||||||
|
let var_name = &cap[1];
|
||||||
|
found_placeholders.push(format!("{}:{}", path.display(), var_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found_placeholders.is_empty() {
|
||||||
|
eprintln!("警告: 发现未替换的占位符:");
|
||||||
|
for placeholder in found_placeholders {
|
||||||
|
eprintln!(" - {}", placeholder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user