150 lines
5.0 KiB
TypeScript
150 lines
5.0 KiB
TypeScript
/**
|
||
* 文件: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)
|
||
// 匹配示例:{{<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');
|
||
}));
|
||
}
|
||
} |