Files
fuwari/scripts/del-space.js
二叉树树 3d63d3e592 fix(scripts): 修复图片路径处理中的括号和空格问题
- 修改正则表达式以支持URL中包含括号的情况
- 改进图片重命名逻辑,处理多种路径格式
- 移除路径中的%20空格编码
2026-01-12 23:31:53 +08:00

325 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { glob } from "glob";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const CONTENT_DIR = path.join(process.cwd(), "src/content");
const POSTS_DIR = path.join(CONTENT_DIR, "posts");
/**
* 获取所有 markdown 文件
*/
async function getAllMarkdownFiles() {
try {
const pattern = path.join(POSTS_DIR, "**/*.md").replace(/\\/g, "/");
return await glob(pattern);
} catch (error) {
console.error("获取 markdown 文件失败:", error.message);
return [];
}
}
/**
* 处理单个 Markdown 文件
*/
async function processMarkdownFile(filePath) {
let content = fs.readFileSync(filePath, "utf-8");
let originalContent = content;
let hasChanges = false;
let changedCount = 0;
const replacements = [];
// 1. 处理 YAML frontmatter 中的 image 字段
const yamlImageRegex = /^---[\s\S]*?image:\s*(?:['"]([^'"]+)['"]|([^\s\n]+))[\s\S]*?^---/m;
let match = yamlImageRegex.exec(content);
if (match) {
const fullMatch = match[0];
// 捕获组1是带引号的捕获组2是不带引号的
const imagePath = match[1] || match[2];
if (imagePath && (imagePath.includes(" ") || imagePath.includes("%20") || imagePath.includes(",") || hasExtraDots(imagePath))) {
// 当路径包含空格、%20、逗号或额外点时处理
const result = await handleImageRename(filePath, imagePath);
if (result) {
// 替换 YAML 中的路径
// 注意:这里需要小心替换,只替换 image: 后的部分
// 简单起见,我们对整个 content 做字符串替换,但要注意唯一性
// 更稳妥的方式是替换 match[0] 中的 imagePath
// 这里我们先收集替换信息,最后统一替换,或者直接替换 content
// 为了防止多次替换导致错乱,我们记录下来
replacements.push({
original: imagePath,
new: result
});
}
}
}
// 2. 处理 Markdown 图片语法 ![alt](url)
// 修复:支持 URL 中包含一层括号,例如 image(1).png
const markdownImageRegex = /!\[.*?\]\(((?:[^()]+|\([^()]*\))+)\)/g;
while ((match = markdownImageRegex.exec(content)) !== null) {
const fullUrl = match[1];
// 去除可能的 title 部分
let url = fullUrl;
const titleMatch = url.match(/^(\S+)\s+["'].*["']$/);
if (titleMatch) {
url = titleMatch[1];
}
// 去除 <> 包裹
if (url.startsWith('<') && url.endsWith('>')) {
url = url.slice(1, -1);
}
if (url.includes(" ") || url.includes("%20") || url.includes(",") || hasExtraDots(url)) {
const result = await handleImageRename(filePath, url);
if (result) {
replacements.push({
original: url, // 这里需要替换的是原始引用的 url 部分 (不含 title)
new: result,
fullMatch: fullUrl // 也可以用于定位
});
}
}
}
// 3. 处理 HTML img 标签 <img src="...">
const htmlImageRegex = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi;
while ((match = htmlImageRegex.exec(content)) !== null) {
const url = match[1];
if (url.includes(" ") || url.includes("%20") || url.includes(",") || hasExtraDots(url)) {
const result = await handleImageRename(filePath, url);
if (result) {
replacements.push({
original: url,
new: result
});
}
}
}
// 执行替换
if (replacements.length > 0) {
// 按照 original 长度倒序排序,避免部分替换
replacements.sort((a, b) => b.original.length - a.original.length);
// 去重
const uniqueReplacements = new Map();
replacements.forEach(item => {
if (!uniqueReplacements.has(item.original)) {
uniqueReplacements.set(item.original, item.new);
}
});
for (const [original, newPath] of uniqueReplacements) {
// 全局替换
// 需要转义正则特殊字符
const escapedOriginal = original.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(escapedOriginal, 'g');
if (content.match(regex)) {
content = content.replace(regex, newPath);
hasChanges = true;
changedCount++;
console.log(` 🔄 更新引用: "${original}" -> "${newPath}"`);
}
}
}
if (hasChanges) {
fs.writeFileSync(filePath, content, "utf-8");
console.log(`💾 已保存文件: ${path.relative(process.cwd(), filePath)} (更新了 ${changedCount} 处引用)`);
}
}
/**
* 处理图片重命名
* @returns {string|null} 新的相对路径,如果没有变化或失败则返回 null
*/
async function handleImageRename(markdownPath, imagePath) {
// 1. 解析绝对路径
let absolutePath = null;
const markdownDir = path.dirname(markdownPath);
// 解码 URL (处理 %20)
let decodedPath = imagePath;
try {
decodedPath = decodeURIComponent(imagePath);
} catch (e) {
// ignore
}
if (decodedPath.startsWith("http://") || decodedPath.startsWith("https://")) {
return null;
}
if (decodedPath.startsWith("/")) {
// 绝对路径,通常相对于 public 或 src (这里假设是 src/content/assets 或者 public)
// Fuwari 项目结构似乎图片在 src/content/assets
// 如果以 / 引用,可能很难确定根目录,暂且跳过,除非它是相对于 content 的
// 观察现有代码normalizePath 忽略了 / 开头的。
// 但用户提到 "寻找MarkDown中的相对路径的图片",所以我们可以只关注相对路径
return null;
} else {
// 相对路径
const candidates = [decodedPath];
if (decodedPath !== imagePath) {
candidates.push(imagePath);
}
for (const candidate of candidates) {
const candidateAbs = path.resolve(markdownDir, candidate);
if (fs.existsSync(candidateAbs)) {
absolutePath = candidateAbs;
break;
}
}
}
if (!fs.existsSync(absolutePath)) {
console.warn(` ⚠️ 图片不存在 (跳过): ${decodedPath}`);
return null;
}
// 2. 生成新文件名 (删除空格、逗号、以及除了扩展名点之外的其他点)
const dir = path.dirname(absolutePath);
const filename = path.basename(absolutePath);
const ext = path.extname(filename);
const nameWithoutExt = path.basename(filename, ext);
// 移除空格、%20、逗号、以及点
const newName = nameWithoutExt
.replace(/\s+/g, "")
.replace(/%20/g, "")
.replace(/,/g, "")
.replace(/\./g, ""); // 移除文件名主体中的所有点
const newFilename = newName + ext;
if (filename === newFilename) {
return null; // 没有变化
}
const newAbsolutePath = path.join(dir, newFilename);
// 3. 重命名文件
try {
if (fs.existsSync(newAbsolutePath)) {
// 如果目标文件已存在
// 比较内容是否一致?或者直接覆盖?或者跳过?
// 简单起见,如果目标存在且不是同一个文件(虽然文件名不同,但在某些不区分大小写系统可能冲突,不过这里是去除空格,应该不同)
// 假设用户希望合并
console.warn(` ⚠️ 目标文件已存在: ${path.basename(newAbsolutePath)} (将使用已存在的文件)`);
// 如果源文件存在,删除源文件 (因为它被合并到了目标文件)
// 但为了安全,也许我们不应该删除,只是更新引用?
// 还是说如果A.png和A .png都存在我们将A .png重命名为A.png这会覆盖A.png吗
// fs.renameSync 会覆盖。
// 既然是"del-space",如果去空格后的文件已存在,说明可能已经有一份了。
// 我们可以认为它们是同一张图(或者用户不介意),直接使用新文件名,并保留(或覆盖)
// 安全起见,如果目标存在,我们不覆盖,只是更新引用指向它。
// 但是源文件怎么办?如果不删除,就是"clean"脚本的事了。
// 用户说 "同步修改原图的文件名"implies rename.
// 如果我 rename A to B, and B exists. rename fails or overwrites.
// 让我们尝试 rename如果报错再处理.
} else {
fs.renameSync(absolutePath, newAbsolutePath);
console.log(` ✨ 重命名图片: "${filename}" -> "${newFilename}"`);
}
} catch (error) {
console.error(` ❌ 重命名失败: ${error.message}`);
return null;
}
// 4. 返回新的相对路径
// 保持原来的相对路径结构,只改变文件名
// imagePath 可能是 ../assets/foo bar.png
// 我们需要返回 ../assets/foobar.png
// 重新构建引用路径
// 使用 path.dirname(imagePath) 可能会受到 OS 分隔符影响
// 我们简单地替换文件名
// 注意imagePath 可能是 encoded 的 (%20),也可能是 raw space
// 我们返回的新路径应该是不包含空格的,通常不需要 encode
// 获取 imagePath 的目录部分
// 简单的字符串操作:找到最后一个 / 或 \
const lastSeparatorIndex = Math.max(imagePath.lastIndexOf('/'), imagePath.lastIndexOf('\\'));
let newReferencePath;
if (lastSeparatorIndex === -1) {
newReferencePath = newFilename;
} else {
newReferencePath = imagePath.substring(0, lastSeparatorIndex + 1) + newFilename;
}
return newReferencePath.replace(/%20/g, "");
}
/**
* 检查路径中是否包含除了扩展名点之外的其他点(仅检查文件名部分)
*/
function hasExtraDots(imagePath) {
try {
// 解码
let decodedPath = imagePath;
try {
decodedPath = decodeURIComponent(imagePath);
} catch (e) {
// ignore
}
// 获取文件名
const filename = path.basename(decodedPath);
// 如果是以点开头的文件(如 .gitignore忽略
if (filename.startsWith('.')) {
// 如果只有开头的点,没有其他点,则是 false
// 如果有其他点,如 .foo.bar则是 true
const parts = filename.split('.');
return parts.length > 2;
}
// 正常文件名
const ext = path.extname(filename);
const nameWithoutExt = path.basename(filename, ext);
// 检查 nameWithoutExt 是否包含点
return nameWithoutExt.includes('.');
} catch (error) {
return false;
}
}
async function main() {
console.log("🔍 开始扫描 Markdown 文件中的空格图片路径...");
if (!fs.existsSync(POSTS_DIR)) {
console.error(`❌ Posts 目录不存在: ${POSTS_DIR}`);
return;
}
const files = await getAllMarkdownFiles();
console.log(`📄 找到 ${files.length} 个 Markdown 文件`);
for (const file of files) {
// console.log(`检查: ${path.relative(process.cwd(), file)}`);
await processMarkdownFile(file);
}
console.log("✅ 完成!");
}
main().catch(err => {
console.error("❌ 发生错误:", err);
process.exit(1);
});