feat: 标准化占位符, 增加模板

This commit is contained in:
2025-11-29 23:38:03 +08:00
parent d647e84db0
commit 8da042ccd3
13 changed files with 402 additions and 103 deletions

View File

@@ -1,6 +1,7 @@
use crate::{
config::Config,
entity::project::ProjectInfo,
template::TemplateEngine,
utils::{file, http::HttpClient},
};
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));
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(())
}
@@ -57,7 +58,11 @@ fn check_example_exists(target: &str) -> Result<()> {
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 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));
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!(
"{}{}",
name.chars().next().unwrap().to_lowercase(),
@@ -80,14 +89,41 @@ fn initialize_project(local_dir: &PathBuf, name: &str) -> Result<()> {
println!("项目名称: {}", 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);
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(())
}
@@ -102,82 +138,3 @@ fn generate_project_info(name: &str, lower_name: &str) -> ProjectInfo {
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(())
}

View File

@@ -11,8 +11,10 @@ pub enum CliError {
Zip(zip::result::ZipError),
Walkdir(walkdir::Error),
Parse(ParseIntError),
Toml(toml::de::Error),
NotFound(String),
InvalidData(String),
InvalidInput(String),
}
impl fmt::Display for CliError {
@@ -25,8 +27,10 @@ impl fmt::Display for CliError {
CliError::Zip(e) => write!(f, "压缩错误: {}", e),
CliError::Walkdir(e) => write!(f, "目录遍历错误: {}", e),
CliError::Parse(e) => write!(f, "解析错误: {}", e),
CliError::Toml(e) => write!(f, "TOML解析错误: {}", e),
CliError::NotFound(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>;

View File

@@ -3,6 +3,7 @@ mod entity;
mod utils;
mod error;
mod config;
mod template;
use crate::commands::{Cli, Commands};
use clap::Parser;

198
src/template.rs Normal file
View 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(())
}
}