Files
note2any/src/core/gallery-processor.ts
2025-10-16 16:10:58 +08:00

150 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 文件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');
}));
}
}