update at 2025-09-22 18:54:59
This commit is contained in:
@@ -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 配置迁移到 NMPSettings(galleryPrePath, 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) {
|
||||
// 匹配 ;不跨行;忽略包含空格的 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;
|
||||
|
||||
Reference in New Issue
Block a user