Skip to content

Commit

Permalink
Feat: 增加模板导入下载
Browse files Browse the repository at this point in the history
  • Loading branch information
pandaoh committed Nov 3, 2024
1 parent 4d76396 commit b269b53
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 47 deletions.
4 changes: 2 additions & 2 deletions bin/xcmd.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* @Author: HxB
* @Date: 2022-04-25 16:27:06
* @LastEditors: DoubleAm
* @LastEditTime: 2024-11-01 19:10:50
* @LastEditTime: 2024-11-03 16:44:49
* @Description: 命令处理文件
* @FilePath: \js-xcmd\bin\xcmd.js
*/
Expand Down Expand Up @@ -775,7 +775,7 @@ program
.command('add-umi-page [dir]')
.description('创建简单页面模板')
.action((dir) => {
downloadTpl('direct:http://cdn.biugle.cn/umi_page.zip', dir || '', ['PageCode', 'Author']);
downloadTpl('http://cdn.biugle.cn/umi_page.zip', dir || '', ['PageCode', 'Author']);
});

program.parse(process.argv);
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "js-xcmd",
"version": "1.5.13",
"version": "1.5.15",
"description": "XCmd library for node.js.",
"main": "main.js",
"bin": {
Expand Down Expand Up @@ -35,12 +35,13 @@
"@babel/plugin-proposal-decorators": "^7.25.9",
"@babel/preset-env": "^7.26.0",
"@babel/traverse": "^7.25.9",
"axios": "^1.7.7",
"commander": "^9.2.0",
"download-git-repo": "^3.0.2",
"fs-extra": "^11.2.0",
"glob": "^11.0.0",
"node-cmd": "^5.0.0",
"rimraf": "^5.0.5",
"unzipper": "^0.12.3",
"xlsx": "^0.18.5"
},
"time": "2717021815012024"
Expand Down
141 changes: 141 additions & 0 deletions utils/template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* 使用自定义模板渲染字符串内容
* 支持条件渲染、循环、嵌套变量、默认值以及简单占位符替换
*
* @param {string} content - 原始模板内容
* @param {object} replacements - 要替换的值
* @returns {string} - 渲染后的内容
*/
function renderTemplate(content, replacements) {
replacements = replacements || {};
if (!content) {
return '';
}

// 内部路径解析函数
const _resolvePath = (obj, path) => {
return path.split('.').reduce((acc, part) => {
if (acc && typeof acc === 'object' && part in acc) {
return acc[part];
}
return undefined; // 如果路径不存在,返回 undefined
}, obj);
};

// 内部方法:处理多余空行
const _trimTpl = (str) => {
// 用正则表达式将多个连续的空行替换为一个空行
return str.replace(/(\n\s*\n)+/g, '\n\n'); // 将多个空行替换为一个空行
};

// 循环渲染 - [[*arrayVarKey $item $index]]
content = content.replace(
/\[\[\s*\*\s*([\w.]+)\s+(\$\w+)(?:\s+(\$\w+))?\s*\]\]([\s\S]*?)\[\[\s*\/\s*\1\s*\]\]/g,
(match, arrayVarKey, itemVar, indexVar, innerContent) => {
const array = _resolvePath(replacements, arrayVarKey);
if (Array.isArray(array)) {
return array
.map((item, index) => {
const context = { ...replacements, [itemVar.slice(1)]: item };
if (indexVar) {
context[indexVar.slice(1)] = index; // 传递索引
}
return renderTemplate(innerContent, context); // 递归渲染
})
.join(''); // 使用空字符串连接渲染结果
}
return ''; // 如果不是数组则返回空字符串
}
);

// 存在变量的条件渲染 - [[#key]] ... [[/key]]
content = content.replace(
/\[\[\s*#\s*([\w.]+)\s*\]\]([\s\S]*?)\[\[\s*\/\s*\1\s*\]\]/g,
(match, key, innerContent) => {
return _resolvePath(replacements, key) ? innerContent : '';
}
);

// 不存在变量的条件渲染 - [[^key]] ... [[/key]]
content = content.replace(
/\[\[\s*\^\s*([\w.]+)\s*\]\]([\s\S]*?)\[\[\s*\/\s*\1\s*\]\]/g,
(match, key, innerContent) => {
return !_resolvePath(replacements, key) ? innerContent : '';
}
);

// 替换简单的占位符并支持默认值 - [[[key ?? defaultValue]]]
content = content.replace(/\[\[\[\s*([\w.]+)\s*(?:\?\?\s*([^\]]+))?\s*\]\]\]/g, (match, path, defaultValue) => {
const value = _resolvePath(replacements, path);
return `${value !== undefined ? value : defaultValue || ''}`.trim(); // 使用 .trim() 去除前后空白
});

// 处理空行和首尾空白
return _trimTpl(content);
}

const replacements = {
Config: {
PageTitle: 'Bex 的文章列表',
SubTitle: '模板渲染测试',
TestEmpty: undefined
},
Author: 'biugle',
articles: [
{ title: '第一篇文章', author: { name: '张三' }, date: '2024-11-01' },
{ title: '第二篇文章', author: { name: '李四' }, date: undefined }, // 模拟未发布的日期
{ title: '第三篇文章', author: {}, date: '2024-11-02' }, // 作者信息缺失
{} // 测试默认值与报错兼容
]
};
const htmlTemplate = `
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>[[[ Config.PageTitle ]]]-[[[ Config.TestEmpty ?? 1.0.0 ]]][[[ Config.TestEmpty.xxx ]]]</title>
</head>
<body>
<h1>[[[SubTitle]]]</h1>
<table border="1">
<thead>
<tr>
<th>标题</th>
<th>作者</th>
<th>发布日期</th>
</tr>
</thead>
<tbody>
[[*articles $article $index]]
<tr key="[[[index]]]">
<td>
[[[article.title ?? 空白标题]]]
</td>
<td>[[[ article.author.name ?? 未知作者 ]]]</td>
<td>
[[#article.date]]
[[[ article.date ]]]
[[/article.date]]
[[^article.date]]
日期未发布
[[/article.date]]
</td>
</tr>
[[/articles]]
</tbody>
</table>
<footer>
<p>版权所有-[[[ Author ]]]</p>
</footer>
</body>
</html>`;

// const renderedHtml = renderTemplate(htmlTemplate, replacements);
// console.log(renderedHtml);

module.exports = { renderTemplate };
133 changes: 90 additions & 43 deletions utils/tpl.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,83 @@
const download = require('download-git-repo');
const fs = require('fs');
const fsPromises = require('fs').promises;
const path = require('path');
const glob = require('glob');
const readline = require('readline');
const axios = require('axios');
const unzipper = require('unzipper');
const { renderTemplate } = require('./template');

/**
* 下载模板仓库并替换其中的占位符
* @param {string} repo - Git 仓库地址
* @param {string} dest - 下载目录
* @param {object} replacements - 用户输入的替换内容
* 下载文件
* @param {string} url - 文件的 URL
* @param {string} dest - 下载后保存的路径
*/
function downloadAndReplace(repo, dest, replacements) {
download(repo, dest, (err) => {
if (err) return console.error('模板下载失败:', err);
console.log(`模板已下载到 ${dest}`);
async function downloadFile(url, dest) {
const response = await axios.get(url, { responseType: 'stream' });
if (response.status !== 200) {
throw new Error(`下载失败,状态码: ${response.status}`);
}

// 获取下载目录中的所有文件路径并替换其中的占位符
const files = glob.sync(`${dest}/**/*`, { nodir: true });
files.forEach((file) => applyReplacements(file, replacements));
const writer = fs.createWriteStream(dest); // 使用 fs 创建写入流
response.data.pipe(writer);

console.log('模板替换已完成');
return new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
});
}

/**
* 解压 ZIP 文件
* @param {string} zipPath - ZIP 文件路径
* @param {string} extractPath - 解压目录
*/
async function unzipFile(zipPath, extractPath) {
return new Promise((resolve, reject) => {
fs.createReadStream(zipPath)
.pipe(unzipper.Extract({ path: extractPath }))
.on('close', resolve)
.on('error', reject);
});
}

/**
* 直接使用正则表达式替换文件中的占位符 [[[ ]]]
* 替换文件中的占位符
* @param {string} filePath - 文件路径
* @param {object} replacements - 要替换的值
*/
function applyReplacements(filePath, replacements) {
let content = fs.readFileSync(filePath, 'utf8');

// 遍历 replacements 对象,将 [[[key]]] 替换为对应的值
Object.keys(replacements).forEach((key) => {
const regex = new RegExp(`\\[\\[\\[${key}\\]\\]\\]`, 'g');
content = content.replace(regex, replacements[key]);
});
try {
let content = fs.readFileSync(filePath, 'utf8'); // 以 utf8 编码读取文件
const newContent = renderTemplate(content, replacements); // 使用替换值渲染新内容
fs.writeFileSync(filePath, newContent, 'utf8'); // 写入新内容
console.log(`文件 ${filePath} 替换成功`);
} catch (error) {
console.error(`处理文件 ${filePath} 时出错:`, error);
}
}

fs.writeFileSync(filePath, content, 'utf8');
/**
* 递归遍历目录,处理所有文件
* @param {string} dirPath - 目录路径
* @param {object} replacements - 要替换的值
*/
async function traverseDirectory(dirPath, replacements) {
const files = await fsPromises.readdir(dirPath); // 使用 Promise API 读取目录
for (const file of files) {
const filePath = path.join(dirPath, file);
if ((await fsPromises.stat(filePath)).isDirectory()) {
await traverseDirectory(filePath, replacements); // 递归调用
} else {
applyReplacements(filePath, replacements); // 处理文件
}
}
}

/**
* 提示用户输入以填充模板中的占位符
* @param {Array<string>} questions - 要收集的键名数组
*/
function promptUserInputs(questions) {
if (!Array.isArray(questions) || questions.length === 0) {
console.error('请提供一个有效的占位符名称数组');
return Promise.resolve({});
}

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
Expand Down Expand Up @@ -78,26 +106,45 @@ function promptUserInputs(questions) {
}

/**
* 主函数,用于运行模板下载和替换
* @param {string} gitRepo - Git 仓库地址
* 主函数,用于下载和替换模板
* @param {string} zipUrl - ZIP 文件 URL
* @param {string} downloadPath - 下载目录路径
* @param {Array<string>} options - 要收集的替换项
*/
async function downloadTpl(gitRepo, downloadPath, options) {
if (typeof gitRepo !== 'string' || typeof downloadPath !== 'string') {
console.error('请提供有效的 Git 仓库地址和下载路径');
return;
}
async function downloadTpl(zipUrl, downloadPath, options) {
let zipFilePath;

try {
const answers = await promptUserInputs(options);
if (!answers.PageCode) {
console.error('请提供 PageCode');
return;
}

const answers = await promptUserInputs(options);
if (!answers.PageCode) {
console.error('请提供 PageCode');
return;
downloadPath = downloadPath || `./${answers.PageCode}`;
const dest = path.resolve(downloadPath);
zipFilePath = path.join(dest, `template-${Date.now()}.zip`);

await fsPromises.mkdir(dest, { recursive: true }); // 使用 Promise API 创建目录

await downloadFile(zipUrl, zipFilePath);
console.log(`模板已下载到 ${zipFilePath}`);

await unzipFile(zipFilePath, dest);
console.log('模板解压完成');

await traverseDirectory(dest, answers); // 递归遍历目录
console.log('模板替换已完成');
} catch (error) {
console.error('操作失败:', error);
} finally {
if (zipFilePath && (await fsPromises.stat(zipFilePath).catch(() => false))) {
await fsPromises.unlink(zipFilePath); // 使用 Promise API 清理 ZIP 文件
}
}
downloadPath = downloadPath || `./${answers.PageCode}`;
const dest = path.resolve(downloadPath);
console.log({ answers, downloadPath });
downloadAndReplace(gitRepo, dest, answers);
}

module.exports = { downloadTpl };

// 调用示例
// downloadTpl('http://cdn.biugle.cn/umi_page.zip', '', ['PageCode', 'Author']);

0 comments on commit b269b53

Please sign in to comment.