update at 2025-09-22 18:54:59

This commit is contained in:
douboer
2025-09-22 18:54:59 +08:00
parent 81c6f52b69
commit 3d2171e837
9 changed files with 493 additions and 72 deletions

View File

@@ -34,10 +34,78 @@ import { CardDataManager } from './markdown/code';
import { debounce } from './utils';
import { PrepareImageLib, IsImageLibReady, WebpToJPG } from './imagelib';
import { toPng } from 'html-to-image';
import * as path from 'path';
import { stat, readdir } from 'fs/promises';
const FRONT_MATTER_REGEX = /^(---)$.+?^(---)$.+?/ims;
// gallery 配置迁移到 NMPSettingsgalleryPrePath, galleryNumPic
// 匹配示例:{{<gallery dir="/img/guanzhan/1" figcaption="毕业展"/>}}{{<load-photoswipe>}}
// figcaption 可选
const GALLERY_SHORTCODE_REGEX = /{{<gallery\s+dir="([^"]+)"(?:\s+figcaption="([^"]*)")?\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;
// 简单:前 n可扩展为随机
return all.slice(0, limit);
}
async function transformGalleryShortcodes(md: string, prePath: string, numPic: number): Promise<string> {
// 逐个替换(异步)—— 使用 replace + 手动遍历实现
const matches: { full: string; dir: string; caption?: string }[] = [];
md.replace(GALLERY_SHORTCODE_REGEX, (full, dir, caption) => {
matches.push({ full, dir, caption });
return full;
});
let result = md;
for (const m of matches) {
const absDir = path.join(prePath, m.dir.replace(/^\//, '')); // 拼接绝对路径
const imgs = await listLocalImages(absDir);
const picked = pickImages(imgs, numPic);
if (picked.length === 0) {
// 无图则清空短代码(或保留原样,这里按需求替换为空)
result = result.replace(m.full, '');
continue;
}
const repl = picked.map(f => `![[${f}]]`).join('\n');
result = result.replace(m.full, repl);
}
// 处理无 dir 的块级 gallery只做本地名提取不访问文件系统
result = result.replace(GALLERY_BLOCK_REGEX, (full: string, inner: string) => {
const names: string[] = [];
inner.replace(FIGURE_IN_GALLERY_REGEX, (_f: string, src: string) => {
if (!src) return _f;
const clean = src.split('#')[0].split('?')[0];
const base = clean.split('/').pop();
if (base && /(png|jpe?g|gif|bmp|webp|svg)$/i.test(base)) {
names.push(base);
}
return _f;
});
if (names.length === 0) return '';
return names.map(n => `![[${n}]]`).join('\n');
});
return result;
}
export class ArticleRender implements MDRendererCallback {
app: App;
itemView: ItemView;
@@ -168,6 +236,12 @@ export class ArticleRender implements MDRendererCallback {
md = md.replace(FRONT_MATTER_REGEX, '');
}
// 处理 gallery 短代码 -> wikilink 图片列表
md = await transformGalleryShortcodes(md, this.settings.galleryPrePath, this.settings.galleryNumPic);
// 自定义行级语法转换: [fig .../] 以及 || 前缀块
md = this.applyCustomInlineBlocks(md);
// 将标准 markdown 图片语法转为 wikilink 语法,便于现有 LocalImageManager 识别
if (this.settings.enableMarkdownImageToWikilink) {
// 匹配 ![alt](path/to/name.ext);不跨行;忽略包含空格的 URL 末尾注释
@@ -195,6 +269,35 @@ export class ArticleRender implements MDRendererCallback {
this.setArticle(this.errorContent(e));
}
}
// 自定义 fig 与 || 语法转换
private applyCustomInlineBlocks(md: string): string {
const figPattern = /\[fig([^\n\]]*?)\/\]/g; // [fig text/]
md = md.replace(figPattern, (_m, inner) => {
const content = inner.trim();
return `<span style="font-style: italic; font-size: 14px; background-color: #f5f5f5; padding: 2px;">${content}</span>`;
});
// 颜色映射:默认|| 与 ||g, ||r, ||b, ||y
const blockStyles: Record<string, string> = {
'': "background-color:#E5E4E2;",
'r': "color:white;background-color:#6F4E37;",
'g': "background-color:#BCE954;",
'b': "background-color:#B6B6B4;",
'y': "background-color:#FFFFC2;",
};
const baseStyle = "font-family:'Microsoft YaHei',sans-serif;font-size:14px; padding:10px;border-radius:20px;line-height:30px;";
// 仅匹配行首 ||[flag]? 空格 之后的单行内容,避免吞并后续多行
md = md.split(/\n/).map(line => {
// 例如 || 内容, ||r 内容
const m = line.match(/^\|\|(r|g|b|y)?\s+(.*)$/);
if (!m) return line;
const flag = m[1] || '';
const text = m[2];
const style = baseStyle + (blockStyles[flag] || blockStyles['']);
return `<p style="${style}">${text}</p>`;
}).join('\n');
return md;
}
getCSS() {
try {
const theme = this.assetsManager.getTheme(this.currentTheme);
@@ -248,6 +351,17 @@ export class ArticleRender implements MDRendererCallback {
}
const file = this.app.workspace.getActiveFile();
if (!file) return res;
// 避免频繁刷屏:仅当路径变化或超过 3s 再输出一次
try {
const globalAny = window as any;
const now = Date.now();
if (!globalAny.__note2mp_lastPathLog ||
globalAny.__note2mp_lastPathLog.path !== file.path ||
now - globalAny.__note2mp_lastPathLog.time > 3000) {
console.log('[note2mp] active file path:', file.path);
globalAny.__note2mp_lastPathLog = { path: file.path, time: now };
}
} catch {}
const metadata = this.app.metadataCache.getFileCache(file);
if (metadata?.frontmatter) {
const keys = this.assetsManager.expertSettings.frontmatter;
@@ -260,6 +374,10 @@ export class ArticleRender implements MDRendererCallback {
res.digest = this.getFrontmatterValue(frontmatter, keys.digest);
res.content_source_url = this.getFrontmatterValue(frontmatter, keys.content_source_url);
res.cover = this.getFrontmatterValue(frontmatter, keys.cover) || frontmatter['cover'] || frontmatter['image'];
// frontmatter 给出的 cover/image 为空字符串时视为未设置
if (typeof res.cover === 'string' && res.cover.trim() === '') {
res.cover = undefined;
}
res.thumb_media_id = this.getFrontmatterValue(frontmatter, keys.thumb_media_id);
res.need_open_comment = frontmatter[keys.need_open_comment] ? 1 : undefined;
res.only_fans_can_comment = frontmatter[keys.only_fans_can_comment] ? 1 : undefined;
@@ -274,6 +392,33 @@ export class ArticleRender implements MDRendererCallback {
res.pic_crop_1_1 = '0_0.525_0.404_1';
}
}
else if (this.originalMarkdown?.startsWith('---')) {
// 元数据缓存未命中时的手动轻量解析(不引入 yaml 依赖,逐行扫描直到第二个 ---
try {
const lines = this.originalMarkdown.split(/\r?\n/);
if (lines[0].trim() === '---') {
let i = 1;
for (; i < lines.length; i++) {
const line = lines[i];
if (line.trim() === '---') break;
const m = line.match(/^(\w[\w-]*):\s*(.*)$/); // key: value 形式
if (!m) continue;
const k = m[1].toLowerCase();
const v = m[2].trim();
if (k === 'title' && !res.title) res.title = v;
if (k === 'author' && !res.author) res.author = v;
if ((k === 'image' || k === 'cover') && !res.cover && v) {
// image 可能是路径,取 basename
const clean = v.replace(/^"|"$/g, '').split('#')[0].split('?')[0];
const base = clean.split('/').pop();
if (base) res.cover = `![[${base}]]`;
}
}
}
} catch (err) {
console.warn('fallback frontmatter parse failed', err);
}
}
// 如果未显式指定封面,尝试从正文首图( markdown 或 wikilink ) 提取,按出现顺序优先
if (!res.cover && this.originalMarkdown) {
@@ -307,6 +452,20 @@ export class ArticleRender implements MDRendererCallback {
if (candidates.length > 0) {
candidates.sort((a,b)=> a.idx - b.idx);
res.cover = `![[${candidates[0].basename}]]`;
} else if (!res.cover) {
// 没有找到任何图片候选,应用默认封面(如果配置了)
const def = this.settings.defaultCoverPic?.trim();
if (def) {
if (/^!\[\[.*\]\]$/.test(def) || /^https?:\/\//i.test(def)) {
res.cover = def;
} else {
const base = def.split('/').pop();
if (base) res.cover = `![[${base}]]`;
}
if (res.cover) {
try { console.log('[note2mp] use default cover:', def, '->', res.cover); } catch {}
}
}
}
}
return res;