This commit is contained in:
246
scripts/clean-unused-images.js
Normal file
246
scripts/clean-unused-images.js
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { glob } from 'glob';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
/**
|
||||
* 清理未使用的图片资源脚本
|
||||
* 扫描 src/content/posts 下的所有 markdown 文件,
|
||||
* 查找 src/content/assets 中未被引用的图片并删除
|
||||
*/
|
||||
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'src/content');
|
||||
const POSTS_DIR = path.join(CONTENT_DIR, 'posts');
|
||||
const ASSETS_DIR = path.join(CONTENT_DIR, 'assets');
|
||||
|
||||
// 支持的图片格式
|
||||
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.avif'];
|
||||
|
||||
/**
|
||||
* 获取所有 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 [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有图片文件
|
||||
*/
|
||||
async function getAllImageFiles() {
|
||||
try {
|
||||
const extensions = IMAGE_EXTENSIONS.join(',');
|
||||
const pattern = path.join(ASSETS_DIR, `**/*{${extensions}}`).replace(/\\/g, '/');
|
||||
return await glob(pattern);
|
||||
} catch (error) {
|
||||
console.error('获取图片文件失败:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 markdown 内容中提取图片引用
|
||||
*/
|
||||
function extractImageReferences(content) {
|
||||
const references = new Set();
|
||||
|
||||
// 匹配 YAML frontmatter 中的 image 字段(支持带引号和不带引号的值)
|
||||
const yamlImageRegex = /^---[\s\S]*?image:\s*(?:['"]([^'"]+)['"]|([^\s\n]+))[\s\S]*?^---/m;
|
||||
let match = yamlImageRegex.exec(content);
|
||||
if (match) {
|
||||
// match[1] 是带引号的值,match[2] 是不带引号的值
|
||||
references.add(match[1] || match[2]);
|
||||
}
|
||||
|
||||
// 匹配 markdown 图片语法: 
|
||||
const markdownImageRegex = /!\[.*?\]\(([^)]+)\)/g;
|
||||
while ((match = markdownImageRegex.exec(content)) !== null) {
|
||||
references.add(match[1]);
|
||||
}
|
||||
|
||||
// 匹配 HTML img 标签: <img src="path">
|
||||
const htmlImageRegex = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi;
|
||||
while ((match = htmlImageRegex.exec(content)) !== null) {
|
||||
references.add(match[1]);
|
||||
}
|
||||
|
||||
// 匹配 Astro Image 组件引用
|
||||
const astroImageRegex = /import\s+.*?\s+from\s+["']([^"']+\.(jpg|jpeg|png|gif|webp|svg|avif))["']/gi;
|
||||
while ((match = astroImageRegex.exec(content)) !== null) {
|
||||
references.add(match[1]);
|
||||
}
|
||||
|
||||
return Array.from(references);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化路径,处理相对路径和绝对路径
|
||||
*/
|
||||
function normalizePath(imagePath, markdownFilePath) {
|
||||
// 跳过外部 URL
|
||||
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 跳过以 / 开头的绝对路径(通常指向 public 目录)
|
||||
if (imagePath.startsWith('/')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理相对路径
|
||||
if (imagePath.startsWith('./') || imagePath.startsWith('../')) {
|
||||
const markdownDir = path.dirname(markdownFilePath);
|
||||
return path.resolve(markdownDir, imagePath);
|
||||
}
|
||||
|
||||
// 处理直接的文件名或相对路径
|
||||
const markdownDir = path.dirname(markdownFilePath);
|
||||
return path.resolve(markdownDir, imagePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数
|
||||
*/
|
||||
async function cleanUnusedImages() {
|
||||
console.log('🔍 开始扫描未使用的图片资源...');
|
||||
|
||||
// 检查目录是否存在
|
||||
if (!fs.existsSync(POSTS_DIR)) {
|
||||
console.error(`❌ Posts 目录不存在: ${POSTS_DIR}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(ASSETS_DIR)) {
|
||||
console.log(`ℹ️ Assets 目录不存在: ${ASSETS_DIR}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取所有文件
|
||||
const markdownFiles = await getAllMarkdownFiles();
|
||||
const imageFiles = await getAllImageFiles();
|
||||
|
||||
console.log(`📄 找到 ${markdownFiles.length} 个 markdown 文件`);
|
||||
console.log(`🖼️ 找到 ${imageFiles.length} 个图片文件`);
|
||||
|
||||
if (imageFiles.length === 0) {
|
||||
console.log('✅ 没有找到图片文件,无需清理');
|
||||
return;
|
||||
}
|
||||
|
||||
// 收集所有被引用的图片
|
||||
const referencedImages = new Set();
|
||||
|
||||
for (const mdFile of markdownFiles) {
|
||||
try {
|
||||
const content = fs.readFileSync(mdFile, 'utf-8');
|
||||
const references = extractImageReferences(content);
|
||||
|
||||
for (const ref of references) {
|
||||
const normalizedPath = normalizePath(ref, mdFile);
|
||||
if (normalizedPath) {
|
||||
const resolvedPath = path.resolve(normalizedPath);
|
||||
referencedImages.add(resolvedPath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ 读取文件失败: ${mdFile} - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🔗 找到 ${referencedImages.size} 个被引用的图片`);
|
||||
|
||||
// 找出未被引用的图片
|
||||
const unusedImages = [];
|
||||
|
||||
for (const imageFile of imageFiles) {
|
||||
const resolvedImagePath = path.resolve(imageFile);
|
||||
const isReferenced = referencedImages.has(resolvedImagePath);
|
||||
|
||||
if (!isReferenced) {
|
||||
unusedImages.push(imageFile);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🗑️ 找到 ${unusedImages.length} 个未使用的图片`);
|
||||
|
||||
if (unusedImages.length === 0) {
|
||||
console.log('✅ 所有图片都在使用中,无需清理');
|
||||
return;
|
||||
}
|
||||
|
||||
// 删除未使用的图片
|
||||
let deletedCount = 0;
|
||||
|
||||
for (const unusedImage of unusedImages) {
|
||||
try {
|
||||
fs.unlinkSync(unusedImage);
|
||||
console.log(`🗑️ 已删除: ${path.relative(process.cwd(), unusedImage)}`);
|
||||
deletedCount++;
|
||||
} catch (error) {
|
||||
console.error(`❌ 删除失败: ${unusedImage} - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理空目录
|
||||
try {
|
||||
cleanEmptyDirectories(ASSETS_DIR);
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ 清理空目录时出错: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log(`\n✅ 清理完成!删除了 ${deletedCount} 个未使用的图片文件`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归清理空目录
|
||||
*/
|
||||
function cleanEmptyDirectories(dir) {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
|
||||
const files = fs.readdirSync(dir);
|
||||
|
||||
if (files.length === 0) {
|
||||
fs.rmdirSync(dir);
|
||||
console.log(`🗑️ 已删除空目录: ${path.relative(process.cwd(), dir)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
if (fs.statSync(filePath).isDirectory()) {
|
||||
cleanEmptyDirectories(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// 再次检查目录是否为空
|
||||
const remainingFiles = fs.readdirSync(dir);
|
||||
if (remainingFiles.length === 0) {
|
||||
fs.rmdirSync(dir);
|
||||
console.log(`🗑️ 已删除空目录: ${path.relative(process.cwd(), dir)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行脚本
|
||||
// 检查是否直接运行此脚本
|
||||
const scriptPath = fileURLToPath(import.meta.url);
|
||||
const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(scriptPath);
|
||||
|
||||
if (isMainModule) {
|
||||
cleanUnusedImages().catch(error => {
|
||||
console.error('❌ 脚本执行失败:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export { cleanUnusedImages };
|
||||
74
scripts/convert_friends.py
Normal file
74
scripts/convert_friends.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import re
|
||||
import json
|
||||
import os
|
||||
|
||||
def extract_friend_data(html_content):
|
||||
"""从HTML内容中提取友链数据"""
|
||||
# 匹配友链卡片的正则表达式
|
||||
pattern = r'<a href="([^"]+)"[^>]*class="friend-card">\s*' \
|
||||
r'<div class="flex items-center gap-2">\s*' \
|
||||
r'<img src="([^"]+)"[^>]*>\s*' \
|
||||
r'<div class="font-bold[^"]*">([^<]+)</div>\s*' \
|
||||
r'</div>\s*' \
|
||||
r'<div class="text-sm[^"]*">([^<]+)</div>'
|
||||
|
||||
friends = []
|
||||
for match in re.finditer(pattern, html_content, re.DOTALL):
|
||||
url, avatar, name, description = match.groups()
|
||||
friend = {
|
||||
"name": name.strip(),
|
||||
"avatar": avatar.strip(),
|
||||
"description": description.strip(),
|
||||
"url": url.strip()
|
||||
}
|
||||
friends.append(friend)
|
||||
return friends
|
||||
|
||||
def read_friends_astro():
|
||||
"""读取friends.astro文件内容"""
|
||||
file_path = os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
||||
'src', 'pages', 'friends.astro')
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
def read_existing_friends_json():
|
||||
"""读取现有的friends.json文件内容"""
|
||||
file_path = os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
||||
'src', 'data', 'friends.json')
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return {"friends": []}
|
||||
|
||||
def write_friends_json(friends_data):
|
||||
"""将友链数据写入friends.json文件"""
|
||||
file_path = os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
||||
'src', 'data', 'friends.json')
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(friends_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
def main():
|
||||
# 读取friends.astro内容
|
||||
astro_content = read_friends_astro()
|
||||
|
||||
# 提取友链数据
|
||||
new_friends = extract_friend_data(astro_content)
|
||||
|
||||
# 读取现有的friends.json
|
||||
existing_data = read_existing_friends_json()
|
||||
|
||||
# 将新的友链数据添加到现有数据中
|
||||
# 使用URL作为唯一标识符,避免重复
|
||||
existing_urls = {friend["url"] for friend in existing_data["friends"]}
|
||||
for friend in new_friends:
|
||||
if friend["url"] not in existing_urls:
|
||||
existing_data["friends"].append(friend)
|
||||
existing_urls.add(friend["url"])
|
||||
|
||||
# 写入更新后的数据
|
||||
write_friends_json(existing_data)
|
||||
print(f"成功提取并添加了 {len(new_friends)} 个友链数据")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
81
scripts/generate-gallery-index.js
Normal file
81
scripts/generate-gallery-index.js
Normal file
@@ -0,0 +1,81 @@
|
||||
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 rootDir = path.resolve(__dirname, '..');
|
||||
|
||||
const IMAGE_DIR = path.join(rootDir, 'public', 'api', 'i');
|
||||
const OUTPUT_FILE = path.join(IMAGE_DIR, 'images.json');
|
||||
|
||||
// 支持的图片扩展名
|
||||
const IMAGE_EXTENSIONS = new Set([
|
||||
'.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.ico', '.avif'
|
||||
]);
|
||||
|
||||
function parseImagePath(relativePath) {
|
||||
// 路径格式: public/api/i/YYYY/MM/DD/filename.ext
|
||||
const match = relativePath.match(/(\d{4})\/(\d{1,2})\/(\d{1,2})\/([^/]+)\.(.+)$/);
|
||||
if (!match) return null;
|
||||
|
||||
const [, year, month, day, filename, ext] = match;
|
||||
|
||||
return {
|
||||
url: `/api/i/${year}/${month}/${day}/${filename}.${ext}`,
|
||||
filename: `${filename}.${ext}`,
|
||||
year,
|
||||
month: month.padStart(2, '0'),
|
||||
day: day.padStart(2, '0'),
|
||||
date: `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`
|
||||
};
|
||||
}
|
||||
|
||||
async function generateGalleryIndex() {
|
||||
console.log('正在扫描图片目录...');
|
||||
|
||||
// 扫描所有图片文件
|
||||
const pattern = path.join(IMAGE_DIR, '**/*.*').replace(/\\/g, '/');
|
||||
const files = await glob(pattern, {
|
||||
ignore: [
|
||||
'**/images.json', // 忽略索引文件本身
|
||||
'**/cache/**', // 忽略缓存目录
|
||||
'**/gallery-meow/**', // 忽略其他目录
|
||||
'**/favicon.ico', // 忽略 favicon
|
||||
'**/index.html' // 忽略 index.html
|
||||
]
|
||||
});
|
||||
|
||||
console.log(`找到 ${files.length} 个文件`);
|
||||
|
||||
// 解析并过滤图片
|
||||
const images = [];
|
||||
for (const file of files) {
|
||||
// 获取相对路径
|
||||
const relativePath = path.relative(IMAGE_DIR, file).replace(/\\/g, '/');
|
||||
|
||||
// 检查文件扩展名
|
||||
const ext = path.extname(file).toLowerCase();
|
||||
if (!IMAGE_EXTENSIONS.has(ext)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析路径信息
|
||||
const parsed = parseImagePath(relativePath);
|
||||
if (parsed) {
|
||||
images.push(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
// 按日期倒序排序
|
||||
images.sort((a, b) => b.date.localeCompare(a.date));
|
||||
|
||||
console.log(`共找到 ${images.length} 张图片`);
|
||||
|
||||
// 写入 JSON 文件
|
||||
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(images, null, 2), 'utf-8');
|
||||
console.log(`已生成索引文件: ${OUTPUT_FILE}`);
|
||||
}
|
||||
|
||||
generateGalleryIndex().catch(console.error);
|
||||
61
scripts/new-post.js
Normal file
61
scripts/new-post.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/* This is a script to create a new post markdown file with front-matter */
|
||||
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
|
||||
function getDate() {
|
||||
const today = new Date()
|
||||
const year = today.getFullYear()
|
||||
const month = String(today.getMonth() + 1).padStart(2, "0")
|
||||
const day = String(today.getDate()).padStart(2, "0")
|
||||
const hours = String(today.getHours()).padStart(2, "0")
|
||||
const minutes = String(today.getMinutes()).padStart(2, "0")
|
||||
const seconds = String(today.getSeconds()).padStart(2, "0")
|
||||
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error(`Error: No filename argument provided
|
||||
Usage: npm run new-post -- <filename>`)
|
||||
process.exit(1) // Terminate the script and return error code 1
|
||||
}
|
||||
|
||||
let fileName = args[0]
|
||||
|
||||
// Add .md extension if not present
|
||||
const fileExtensionRegex = /\.(md|mdx)$/i
|
||||
if (!fileExtensionRegex.test(fileName)) {
|
||||
fileName += ".md"
|
||||
}
|
||||
|
||||
const targetDir = "./src/content/posts/"
|
||||
const fullPath = path.join(targetDir, fileName)
|
||||
|
||||
if (fs.existsSync(fullPath)) {
|
||||
console.error(`Error: File ${fullPath} already exists `)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// recursive mode creates multi-level directories
|
||||
const dirPath = path.dirname(fullPath)
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true })
|
||||
}
|
||||
|
||||
const content = `---
|
||||
title: ${args[0]}
|
||||
published: ${getDate()}
|
||||
description: ''
|
||||
image: ''
|
||||
|
||||
draft: false
|
||||
lang: ''
|
||||
---
|
||||
`
|
||||
|
||||
fs.writeFileSync(path.join(targetDir, fileName), content)
|
||||
|
||||
console.log(`Post ${fullPath} created`)
|
||||
78
scripts/test-category-logic.js
Normal file
78
scripts/test-category-logic.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const CATEGORY_SEPARATOR = " > ";
|
||||
|
||||
function parseCategoryPath(categoryString) {
|
||||
return categoryString
|
||||
.split(CATEGORY_SEPARATOR)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function stringifyCategoryPath(path) {
|
||||
return path.join(CATEGORY_SEPARATOR);
|
||||
}
|
||||
|
||||
function getPostsByCategory(posts, categoryPath) {
|
||||
const targetPathString = stringifyCategoryPath(categoryPath);
|
||||
console.log(`Target Path: "${targetPathString}"`);
|
||||
|
||||
return posts.filter((post) => {
|
||||
const categories = post.data.category;
|
||||
const categoryArray =
|
||||
typeof categories === "string" ? [categories] : categories;
|
||||
|
||||
return categoryArray.some((cat) => {
|
||||
const catPath = parseCategoryPath(cat);
|
||||
const catPathString = stringifyCategoryPath(catPath);
|
||||
console.log(
|
||||
` Checking Post: "${post.data.title}", Category: "${cat}", Parsed: "${catPathString}"`
|
||||
);
|
||||
|
||||
const match =
|
||||
catPathString === targetPathString ||
|
||||
catPathString.startsWith(targetPathString + CATEGORY_SEPARATOR);
|
||||
console.log(` Match: ${match}`);
|
||||
return match;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Mock Data
|
||||
const posts = [
|
||||
{
|
||||
data: {
|
||||
title: "Post 1",
|
||||
category: "Java > Spring",
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
title: "Post 2",
|
||||
category: ["Java > Spring"],
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
title: "Post 3",
|
||||
category: "Java > Spring > Boot",
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
title: "Post 4",
|
||||
category: "Java",
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
title: "Post 5",
|
||||
category: "Other > Spring",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const targetPath = ["Java", "Spring"];
|
||||
|
||||
console.log("Testing getPostsByCategory...");
|
||||
const result = getPostsByCategory(posts, targetPath);
|
||||
console.log(`Found ${result.length} posts.`);
|
||||
result.forEach((p) => console.log(` - ${p.data.title}`));
|
||||
Reference in New Issue
Block a user