diff --git a/package.json b/package.json index 3de0b11d7..bbc88dc6e 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "type-check": "tsc --noEmit --isolatedDeclarations", "new-post": "node scripts/new-post.js", "clean": "node scripts/clean-unused-images.js", + "del-space": "node scripts/del-space.js", "format": "biome format --write ./src", "lint": "biome check --write ./src" }, diff --git a/scripts/del-space.js b/scripts/del-space.js new file mode 100644 index 000000000..fb09c7195 --- /dev/null +++ b/scripts/del-space.js @@ -0,0 +1,312 @@ +#!/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) + 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 标签 + const htmlImageRegex = /]+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 { + // 相对路径 + absolutePath = path.resolve(markdownDir, decodedPath); + } + + 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; +} + +/** + * 检查路径中是否包含除了扩展名点之外的其他点(仅检查文件名部分) + */ +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); +}); diff --git a/src/content/assets/images/Mermaid Chart - Create complex, visual diagrams with text.-2026-01-09-031619.png b/src/content/assets/images/MermaidChart-Createcomplexvisualdiagramswithtext-2026-01-09-031619.png similarity index 100% rename from src/content/assets/images/Mermaid Chart - Create complex, visual diagrams with text.-2026-01-09-031619.png rename to src/content/assets/images/MermaidChart-Createcomplexvisualdiagramswithtext-2026-01-09-031619.png diff --git a/src/content/assets/images/msedge_HwStEUKYVx 1.gif b/src/content/assets/images/msedge_HwStEUKYVx1.gif similarity index 100% rename from src/content/assets/images/msedge_HwStEUKYVx 1.gif rename to src/content/assets/images/msedge_HwStEUKYVx1.gif diff --git a/src/content/assets/images/屏幕截图 2025-08-09 213908.png b/src/content/assets/images/屏幕截图2025-08-09213908.png similarity index 100% rename from src/content/assets/images/屏幕截图 2025-08-09 213908.png rename to src/content/assets/images/屏幕截图2025-08-09213908.png diff --git a/src/content/posts/WPwebplayer.md b/src/content/posts/WPwebplayer.md index aa1d26d8a..3ef70303f 100644 --- a/src/content/posts/WPwebplayer.md +++ b/src/content/posts/WPwebplayer.md @@ -2,7 +2,7 @@ title: 【开源】WPwebplayer——简洁通用的网页播放器 published: 2025-08-09 description: '一款简单,简洁,轻量,通用的开源网站音乐播放器' -image: '../assets/images/屏幕截图 2025-08-09 213908.png' +image: '../assets/images/屏幕截图2025-08-09213908.png' tags: [Web] category: '记录' draft: false diff --git a/src/content/posts/shorter-url.md b/src/content/posts/shorter-url.md index 7c9ca960a..b746b62af 100644 --- a/src/content/posts/shorter-url.md +++ b/src/content/posts/shorter-url.md @@ -31,5 +31,5 @@ Cloudflare Page/Worker的重定向文件提供了基于文件的重定向功能 # 服务架构图 -![](../assets/images/Mermaid%20Chart%20-%20Create%20complex,%20visual%20diagrams%20with%20text.-2026-01-09-031619.png) +![](../assets/images/MermaidChart-Createcomplexvisualdiagramswithtext-2026-01-09-031619.png) diff --git a/src/content/posts/uptimeflare.md b/src/content/posts/uptimeflare.md index 0dd47e656..7356ca097 100644 --- a/src/content/posts/uptimeflare.md +++ b/src/content/posts/uptimeflare.md @@ -46,7 +46,7 @@ lang: "" 首先,我们先尝试将其部署到Cloudflare 创建一个Cloudflare API Token **编辑Workers** 和 **D1** -![](../assets/images/msedge_HwStEUKYVx%201.gif) +![](../assets/images/msedge_HwStEUKYVx1.gif) 接下来将该Token绑定到你的Github仓库 ![](../assets/images/uptimeflare-4.png)