From b269b53ee01e5a8f09d9d9395e3b11357b6671fb Mon Sep 17 00:00:00 2001 From: leo Date: Sun, 3 Nov 2024 17:53:17 +0800 Subject: [PATCH] =?UTF-8?q?Feat:=20=E5=A2=9E=E5=8A=A0=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/xcmd.js | 4 +- package.json | 5 +- utils/template.js | 141 ++++++++++++++++++++++++++++++++++++++++++++++ utils/tpl.js | 133 +++++++++++++++++++++++++++++-------------- 4 files changed, 236 insertions(+), 47 deletions(-) create mode 100644 utils/template.js diff --git a/bin/xcmd.js b/bin/xcmd.js index 64d891c..9f81309 100644 --- a/bin/xcmd.js +++ b/bin/xcmd.js @@ -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 */ @@ -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); diff --git a/package.json b/package.json index bf69fe3..bac2e2c 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -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" diff --git a/utils/template.js b/utils/template.js new file mode 100644 index 0000000..90e08f1 --- /dev/null +++ b/utils/template.js @@ -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 = ` + + + + + + [[[ Config.PageTitle ]]]-[[[ Config.TestEmpty ?? 1.0.0 ]]][[[ Config.TestEmpty.xxx ]]] + + + +

[[[SubTitle]]]

+ + + + + + + + + + + [[*articles $article $index]] + + + + + + [[/articles]] + +
标题作者发布日期
+ [[[article.title ?? 空白标题]]] + [[[ article.author.name ?? 未知作者 ]]] + [[#article.date]] + [[[ article.date ]]] + [[/article.date]] + [[^article.date]] + 日期未发布 + [[/article.date]] +
+ + + + +`; + +// const renderedHtml = renderTemplate(htmlTemplate, replacements); +// console.log(renderedHtml); + +module.exports = { renderTemplate }; diff --git a/utils/tpl.js b/utils/tpl.js index 9fadbb2..cb635d1 100644 --- a/utils/tpl.js +++ b/utils/tpl.js @@ -1,43 +1,76 @@ -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); // 处理文件 + } + } } /** @@ -45,11 +78,6 @@ function applyReplacements(filePath, replacements) { * @param {Array} 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 @@ -78,26 +106,45 @@ function promptUserInputs(questions) { } /** - * 主函数,用于运行模板下载和替换 - * @param {string} gitRepo - Git 仓库地址 + * 主函数,用于下载和替换模板 + * @param {string} zipUrl - ZIP 文件 URL * @param {string} downloadPath - 下载目录路径 * @param {Array} 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']);