/** * 文件:gallery-processor.ts * 作用:处理文章中的图库短代码 * * 职责: * 1. 解析和转换 gallery 短代码 * 2. 处理本地图片目录扫描 * 3. 生成 wikilink 格式的图片引用 */ import { stat, readdir } from 'fs/promises'; import * as path from 'path'; import { NMPSettings } from '../settings'; // gallery 配置迁移到 NMPSettings(galleryPrePath, galleryNumPic) // 匹配示例:{{}}{{}} // 支持可选 figcaption 以及 mppickall=1/0(无引号数字或布尔),若 mppickall=1 则选取目录内全部图片 const GALLERY_SHORTCODE_REGEX = /{{}}\s*{{}}/g; // 块级 gallery: // {{}}\n{{
}}\n...\n{{}} // 需要提取所有 figure 的 src basename 生成多行 wikilink const GALLERY_BLOCK_REGEX = /{{}}([\s\S]*?){{<\/gallery>}}/g; // figure 支持 src 或 link 属性,两者取其一 const FIGURE_IN_GALLERY_REGEX = /{{]*>}}/g; /** * 列出本地图片目录中的图片文件 */ async function listLocalImages(dirAbs: string): Promise { 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 { // 处理目录式 gallery content = await this.processDirectoryGalleries(content); // 处理块级 gallery content = await this.processBlockGalleries(content); return content; } /** * 处理目录式图库短代码 */ private async processDirectoryGalleries(content: string): Promise { 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 { 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'); })); } }