eformat
Version:
支持在多个目录下快速批量编辑和格式化 HTML 文件.
250 lines (227 loc) • 8.33 kB
JavaScript
/*
* @Author: 马文龙
* @Date: 2025-03-11 13:49:59
* @LastEditors: 马文龙
* @LastEditTime: 2025-05-23 11:15:32
* @FilePath: \eformat\src\format-html.js
* @Description:
*/
/**
* 使用方法 node format-html.js --manual / node format-html.js --file path/file.html
* --manual 选择目录或文件进行格式化
* --file 直接传入文件路径进行格式化 支持绝对路径和相对路径 './path/file.html || C:/path/file.html'
* 选择目录或文件进行格式化 已经加载在package.json中 如果项目报错就执行npm install
* 前置依赖包:
* npm install js-beautify
* npm install cheerio@1.0.0-rc.3
* npm install fast-glob
*/
const fs = require("fs");
const path = require("path");
const beautify = require("js-beautify").html;
const inquirer = require("inquirer");
const cheerio = require("cheerio");
// 新增目录选择函数
async function selectDirectory(startPath = __dirname) {
let currentPath = startPath;
const selectedFiles = [];
while (true) {
const dirContent = fs
.readdirSync(currentPath)
.map((item) => {
const fullPath = path.join(currentPath, item);
return {
name: fs.statSync(fullPath).isDirectory()
? `📁 ${item}/`
: `📄 ${item}`,
value: fullPath,
isDirectory: fs.statSync(fullPath).isDirectory(),
isSelectable:
!fs.statSync(fullPath).isDirectory() &&
[".htm", ".html"].includes(path.extname(fullPath).toLowerCase()),
};
})
.filter((item) => item.isDirectory || item.isSelectable);
const { chosen } = await inquirer.prompt([
{
type: "list",
name: "chosen",
message: `当前目录: ${path.relative(__dirname, currentPath) || "."}`,
choices: [
...dirContent,
new inquirer.Separator(),
{ name: "✅ 完成选择", value: "__done__" },
{ name: "↩️ 返回上级目录", value: "__up__" },
],
pageSize: 20,
},
]);
if (chosen === "__exit__") {
return null; // 返回null表示退出
}
if (chosen === "__up__") {
currentPath = path.dirname(currentPath);
} else if (fs.statSync(chosen).isDirectory()) {
currentPath = chosen;
} else {
console.log(`已选择: ${path.relative(__dirname, chosen)}`);
return chosen;
}
}
return selectedFiles;
}
/** 格式化方法 */
function formatHtml(filePath) {
console.log(`开始格式化文件: ${filePath}`);
if (!fs.existsSync(filePath)) {
console.error(`文件不存在: ${filePath}`);
throw new Error(`文件不存在: ${filePath}`);
}
let content = fs.readFileSync(filePath, "utf-8");
/** 保存PHP代码 */
const phpSegments = [];
/** 处理被注释的 */
content = content.replace(
/<!--\s*\?php\s*(.*?)\s*\?>\s*-->/gi,
(match, phpContent) => {
const actualPhp = `<?php ${phpContent} ?>`;
const placeholder = `<!--PHP_PLACEHOLDER_${phpSegments.length}-->`;
phpSegments.push(actualPhp);
return placeholder;
}
);
content = content.replace(/(^[ \t]*)<style>([\s\S]*?)<\/style>/gm, (match, indent, css) => {
// 计算额外缩进
const additionalIndent = indent + " ";
let indentLevel = 0;
const formattedCss = css
.split('\n')
.map(line => {
const trimmed = line.trim();
if (!trimmed) return null;
if (trimmed.startsWith("/*")) return additionalIndent + trimmed;
if (trimmed.startsWith("}")) indentLevel = Math.max(indentLevel - 1, 0);
const indented = " ".repeat(indentLevel) + trimmed;
if (trimmed.endsWith("{")) indentLevel++;
return " " + additionalIndent + indented;
})
.filter(Boolean)
.join('\n');
return `${additionalIndent}<style>\n${formattedCss}\n${indent}</style>`;
});
/** 处理正常的 */
content = content.replace(/(<\?(?:php|=)[\s\S]*?\?>)/gi, (match) => {
const placeholder = `<!--PHP_PLACEHOLDER_${phpSegments.length}-->`;
phpSegments.push(match);
return placeholder;
});
const $ = cheerio.load(content, { decodeEntities: false });
$("meta").each((index, element) => {
$(element).html($(element).html().replace(/\s+/g, " ").trim());
});
// $("style").each((index, element) => {
// let styleContent = $(element).html();
// // 按行处理保持原有行数
// // 获取style标签的缩进级别
// const styleIndent =
// $(element).prevAll().last().text().match(/^\s*/)[0] || "";
// // 额外添加两个空格的缩进
// const additionalIndent = styleIndent + " ";
// // 判断是否有换行
// let indentLevel = 0;
// styleContent = styleContent
// .split("\n")
// .map((line) => {
// const trimmedLine = line.trim();
// // 如果是空行,直接跳过
// if (trimmedLine === "") return null;
// // 如果是空行或注释,直接返回
// if (trimmedLine.startsWith("/*")) return additionalIndent + trimmedLine;
// // 如果是闭合的 `}`,减少缩进级别
// if (trimmedLine.startsWith("}")) indentLevel = Math.max(indentLevel - 1, 0);
// // 根据缩进级别调整行的缩进
// const indentedLine = " ".repeat(indentLevel) + trimmedLine;
// // 如果是以 `{` 结尾,增加缩进级别
// if (trimmedLine.endsWith("{")) indentLevel++;
// return additionalIndent + indentedLine;
// })
// .filter((line) => line !== null) // 过滤掉空行
// .join("\n");
// // 确保 <style> 标签内容换行
// const formattedStyleContent = `${styleIndent}<style>\n${styleContent}\n${styleIndent}</style>`;
// $(element).replaceWith(formattedStyleContent);
// });
// 格式化HTML
const formattedContent = beautify($.html(), {
indent_size: 2,
indent_char: " ",
max_preserve_newlines: 0,
preserve_newlines: false,
unformatted: ["meta", "style", "php"],
});
// 恢复 PHP 代码
let finalContent = formattedContent;
phpSegments.forEach((php, index) => {
const placeholder = `<!--PHP_PLACEHOLDER_${index}-->`;
finalContent = finalContent.replace(placeholder, php);
});
// 将格式化后的内容写回文件
fs.writeFileSync(filePath, finalContent, "utf-8");
console.log(`HTML文件 ${filePath} 已格式化`);
console.log(`格式化完成: ${path.basename(filePath)}`);
return true;
}
// 修改启动逻辑
if (process.argv.includes("--select") || process.argv.includes("--manual")) {
(async () => {
while (true) {
const file = await selectDirectory();
if (!file) break; // 用户选择退出时结束循环
try {
formatHtml(file);
console.log(`√ 成功格式化 ${path.basename(file)}`);
} catch (error) {
console.error(`× 格式化失败: ${error.message}`);
}
// 询问是否继续
const { continueSelect } = await inquirer.prompt([
{
type: "confirm",
name: "continueSelect",
message: "是否继续选择其他文件?",
default: true,
},
]);
if (!continueSelect) break;
}
})();
return;
}
// 新增支持通过 --file 参数直接传入文件路径
if (process.argv.includes("--file")) {
const fileIndex = process.argv.indexOf("--file") + 1;
const filePath = process.argv[fileIndex];
if (!filePath) {
console.error(
"× 请提供文件路径,例如:node format-html.js --file path/file.html"
);
process.exit(1);
}
try {
const absolutePath = path.resolve(filePath);
if (!fs.existsSync(absolutePath)) {
console.error(`× 文件不存在: ${absolutePath}`);
process.exit(1);
}
formatHtml(absolutePath);
console.log(`√ 成功格式化 ${path.basename(absolutePath)}`);
} catch (error) {
console.error(`× 格式化失败: ${error.message}`);
process.exit(1);
}
}
// ...existing code...
module.exports = {
formatHtml,
selectDirectory,
};