update at 2025-10-16 16:10:58

This commit is contained in:
douboer
2025-10-16 16:10:58 +08:00
parent 9f3a4e8812
commit 28942bea17
17 changed files with 1843 additions and 23 deletions

View File

@@ -0,0 +1,150 @@
/**
* 文件gallery-processor.ts
* 作用:处理文章中的图库短代码
*
* 职责:
* 1. 解析和转换 gallery 短代码
* 2. 处理本地图片目录扫描
* 3. 生成 wikilink 格式的图片引用
*/
import { stat, readdir } from 'fs/promises';
import * as path from 'path';
import { NMPSettings } from '../settings';
// gallery 配置迁移到 NMPSettingsgalleryPrePath, galleryNumPic
// 匹配示例:{{<gallery dir="/img/guanzhan/1" figcaption="毕业展" mppickall=1/>}}{{<load-photoswipe>}}
// 支持可选 figcaption 以及 mppickall=1/0无引号数字或布尔若 mppickall=1 则选取目录内全部图片
const GALLERY_SHORTCODE_REGEX = /{{<gallery\s+dir="([^"]+)"(?:\s+figcaption="([^"]*)")?(?:\s+mppickall=(?:"(1|0)"|'(1|0)'|(1|0)))?\s*\/?>}}\s*{{<load-photoswipe>}}/g;
// 块级 gallery
// {{<gallery>}}\n{{<figure src="/img/a.png" caption=".." >}}\n...\n{{</gallery>}}
// 需要提取所有 figure 的 src basename 生成多行 wikilink
const GALLERY_BLOCK_REGEX = /{{<gallery>}}([\s\S]*?){{<\/gallery>}}/g;
// figure 支持 src 或 link 属性,两者取其一
const FIGURE_IN_GALLERY_REGEX = /{{<figure\s+(?:src|link)="([^"]+)"[^>]*>}}/g;
/**
* 列出本地图片目录中的图片文件
*/
async function listLocalImages(dirAbs: string): Promise<string[]> {
try {
const stats = await stat(dirAbs);
if (!stats.isDirectory()) return [];
} catch {
return [];
}
try {
const files = await readdir(dirAbs);
return files.filter(f => /(png|jpe?g|gif|bmp|webp|svg)$/i.test(f)).sort();
} catch {
return [];
}
}
/**
* 从图片列表中选择指定数量的图片
*/
function pickImages(all: string[], limit: number): string[] {
if (all.length <= limit) return all;
// 均匀采样
const step = all.length / limit;
const picked: string[] = [];
for (let i = 0; i < limit; i++) {
const index = Math.floor(i * step);
picked.push(all[index]);
}
return picked;
}
/**
* 图库处理器类
*/
export class GalleryProcessor {
private settings: NMPSettings;
constructor(settings: NMPSettings) {
this.settings = settings;
}
/**
* 处理文章中的图库短代码
*/
async processGalleryShortcodes(content: string): Promise<string> {
// 处理目录式 gallery
content = await this.processDirectoryGalleries(content);
// 处理块级 gallery
content = await this.processBlockGalleries(content);
return content;
}
/**
* 处理目录式图库短代码
*/
private async processDirectoryGalleries(content: string): Promise<string> {
const matches = Array.from(content.matchAll(GALLERY_SHORTCODE_REGEX));
for (const match of matches) {
const [fullMatch, dir, figcaption = '', pickall1, pickall2, pickall3] = match;
const pickall = pickall1 || pickall2 || pickall3;
const shouldPickAll = pickall === '1';
try {
const galleryPrePath = this.settings.galleryPrePath;
const dirAbs = path.join(galleryPrePath, dir);
const allImages = await listLocalImages(dirAbs);
if (allImages.length === 0) {
console.warn(`[GalleryProcessor] 目录 ${dirAbs} 中没有找到图片`);
continue;
}
const selectedImages = shouldPickAll
? allImages
: pickImages(allImages, this.settings.galleryNumPic);
// 生成 wikilink 格式的图片引用
const wikilinks = selectedImages.map(img => {
const imgPath = path.join(dir, img).replace(/\\/g, '/');
return `![[${imgPath}]]`;
});
let replacement = wikilinks.join('\n');
if (figcaption) {
replacement = `> ${figcaption}\n\n${replacement}`;
}
content = content.replace(fullMatch, replacement);
} catch (error) {
console.error(`[GalleryProcessor] 处理图库失败: ${dir}`, error);
}
}
return content;
}
/**
* 处理块级图库短代码
*/
private processBlockGalleries(content: string): Promise<string> {
return Promise.resolve(content.replace(GALLERY_BLOCK_REGEX, (match, blockContent) => {
const figureMatches = Array.from(blockContent.matchAll(FIGURE_IN_GALLERY_REGEX));
if (figureMatches.length === 0) {
return match; // 保持原样
}
const wikilinks = figureMatches.map(([, src]) => {
const basename = path.basename(src);
return `![[${basename}]]`;
});
return wikilinks.join('\n');
}));
}
}