init
Some checks failed
Clean ESA Versions on Main / clean-esa-versions (push) Has been cancelled

This commit is contained in:
2026-01-02 00:03:49 +08:00
commit 7b7e32ddd4
348 changed files with 148701 additions and 0 deletions

View 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 图片语法: ![alt](path)
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 };

View 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()

View 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
View 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`)

View 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}`));