UNPKG

eformat

Version:

支持在多个目录下快速批量编辑和格式化 HTML 文件.

243 lines (220 loc) 8.05 kB
/* * @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) { 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} 已格式化`); } // 修改启动逻辑 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, };