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;
|
||||
|
||||
@@ -270,6 +270,48 @@ export class NoteToMpSettingTab extends PluginSettingTab {
|
||||
});
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Gallery 根路径')
|
||||
.setDesc('用于 {{<gallery dir="..."/>}} 短代码解析;需指向本地图片根目录')
|
||||
.addText(text => {
|
||||
text.setPlaceholder('例如 /Users/xxx/site/static 或 相对路径')
|
||||
.setValue(this.settings.galleryPrePath || '')
|
||||
.onChange(async (value) => {
|
||||
this.settings.galleryPrePath = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.setAttr('style', 'width: 360px;');
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Gallery 选取图片数')
|
||||
.setDesc('每个 gallery 短代码最多替换为前 N 张图片')
|
||||
.addText(text => {
|
||||
text.setPlaceholder('数字 >=1')
|
||||
.setValue(String(this.settings.galleryNumPic || 2))
|
||||
.onChange(async (value) => {
|
||||
const n = parseInt(value, 10);
|
||||
if (Number.isFinite(n) && n >= 1) {
|
||||
this.settings.galleryNumPic = n;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
});
|
||||
text.inputEl.setAttr('style', 'width: 120px;');
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('默认封面图片')
|
||||
.setDesc('当文章无任何图片/短代码时使用;可填 wikilink 文件名或 http(s) URL')
|
||||
.addText(text => {
|
||||
text.setPlaceholder('例如 cover.png 或 https://...')
|
||||
.setValue(this.settings.defaultCoverPic || '')
|
||||
.onChange(async (value) => {
|
||||
this.settings.defaultCoverPic = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.setAttr('style', 'width: 360px;');
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('启用空行渲染')
|
||||
.addToggle(toggle => {
|
||||
|
||||
@@ -44,6 +44,11 @@ export class NMPSettings {
|
||||
isLoaded: boolean = false;
|
||||
enableEmptyLine: boolean = false;
|
||||
enableMarkdownImageToWikilink: boolean = true; // 自动将  转为 ![[file.ext]]
|
||||
// gallery 相关配置:根目录前缀 & 选取图片数量
|
||||
galleryPrePath: string;
|
||||
galleryNumPic: number;
|
||||
// 无图片时的默认封面(wikilink 或 URL 均可)
|
||||
defaultCoverPic: string;
|
||||
|
||||
private static instance: NMPSettings;
|
||||
|
||||
@@ -73,7 +78,12 @@ export class NMPSettings {
|
||||
this.excalidrawToPNG = false;
|
||||
this.expertSettingsNote = '';
|
||||
this.enableEmptyLine = false;
|
||||
this.enableMarkdownImageToWikilink = true;
|
||||
this.enableMarkdownImageToWikilink = true;
|
||||
// 默认值:用户原先硬编码路径 & 前 2 张
|
||||
this.galleryPrePath = '/Users/gavin/myweb/static';
|
||||
this.galleryNumPic = 2;
|
||||
// 默认封面:使用当前笔记同目录下的 cover.png (若存在会被后续流程正常解析;不存在则无效但可被用户覆盖)
|
||||
this.defaultCoverPic = 'cover.png';
|
||||
}
|
||||
|
||||
resetStyelAndHighlight() {
|
||||
@@ -104,6 +114,9 @@ export class NMPSettings {
|
||||
expertSettingsNote,
|
||||
ignoreEmptyLine,
|
||||
enableMarkdownImageToWikilink,
|
||||
galleryPrePath,
|
||||
galleryNumPic,
|
||||
defaultCoverPic,
|
||||
} = data;
|
||||
|
||||
const settings = NMPSettings.getInstance();
|
||||
@@ -161,6 +174,15 @@ export class NMPSettings {
|
||||
if (enableMarkdownImageToWikilink !== undefined) {
|
||||
settings.enableMarkdownImageToWikilink = enableMarkdownImageToWikilink;
|
||||
}
|
||||
if (galleryPrePath) {
|
||||
settings.galleryPrePath = galleryPrePath;
|
||||
}
|
||||
if (galleryNumPic !== undefined && Number.isFinite(galleryNumPic)) {
|
||||
settings.galleryNumPic = Math.max(1, parseInt(galleryNumPic));
|
||||
}
|
||||
if (defaultCoverPic !== undefined) {
|
||||
settings.defaultCoverPic = String(defaultCoverPic).trim();
|
||||
}
|
||||
settings.getExpiredDate();
|
||||
settings.isLoaded = true;
|
||||
}
|
||||
@@ -186,6 +208,9 @@ export class NMPSettings {
|
||||
'expertSettingsNote': settings.expertSettingsNote,
|
||||
'ignoreEmptyLine': settings.enableEmptyLine,
|
||||
'enableMarkdownImageToWikilink': settings.enableMarkdownImageToWikilink,
|
||||
'galleryPrePath': settings.galleryPrePath,
|
||||
'galleryNumPic': settings.galleryNumPic,
|
||||
'defaultCoverPic': settings.defaultCoverPic,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user