- 知识管理
- 知识库
【免费下载链接】obsidian-weread-plugin
Obsidian Weread Plugin is a plugin to sync Weread(微信读书) hightlights and annotations into your Obsidian Vault.
本文基于仓库设计文档 template-editor-window.md 展开,详解 obsidian-weread-plugin(微信读书划线同步到 Obsidian 的插件)中模板编辑器窗口的设计与实现:三栏布局(模板说明 / 编辑器 / 实时预览)、300ms 防抖渲染管线、Nunjucks 模板变量与过滤器体系,以及它与主题管理系统(Theme / activeThemeId)的集成方式。读完你可以理解该编辑器从输入到预览到保存的完整调用链,并掌握TemplateEditorWindow的编程式调用与 Nunjucks 模板语法。
一、设计目标与功能概览
obsidian-weread-plugin 将微信读书的划线、想法(笔记)和热门划线渲染成 Markdown 笔记写入 Vault,渲染逻辑由 Nunjucks 模板驱动。模板编辑器窗口就是这个模板的可视化编辑/调试工具,核心目标是:让普通用户在不动手改 JSON 配置的情况下,边写模板、边看渲染效果、边对照语法说明,从而快速定制自己的读书笔记格式。
设计文档中定义的功能特性包括:
三栏布局
- 左侧:模板说明文档(设计规格:宽 300px,可在 250-500px 间拖动调整)
- 显示完整的 Nunjucks 模板语法说明
- 包含可用变量、过滤器和示例代码
- 支持拖动调整宽度
- 中间:模板编辑器(自适应宽度)
- 语法高亮的代码编辑器
- 支持 Nunjucks 模板语法
- 实时语法验证
- 右侧:实时预览(自适应宽度)
- 300ms 防抖的实时渲染
- 使用示例数据预览效果
- 错误提示显示
窗口功能
- 原生窗口体验:设计文档提出使用 Electron
BrowserWindow提供完整的原生窗口功能 - 可拖动标题栏:通过标题栏拖动整个窗口
- 最小尺寸:1200×600px(初始尺寸:1600×900px)
- 响应式布局:自动适应窗口大小
- 深色主题:专为深色模式优化的 UI
从源码结构看,当前仓库快照中的 TemplateEditorWindow 继承自 Obsidian 的Modal(对应设计文档对比表中的 "Modal" 形态,初始尺寸 95vw×90vh),但已完整实现了上述三栏布局与全部编辑/预览交互;而 ElectronBrowserWindow原生窗口模式在本项目其他模块中确有实际使用,例如 WereadBrowserWindow 通过remote.BrowserWindow创建带独立 session partition 的微信读书浏览窗口,wereadLoginModel.ts 和 wereadLogoutModel.ts 也采用同一模式。也就是说,文档描述的 "BrowserWindow 方案" 与当前 Modal 实现共享同一套三栏编辑器结构,区别主要在宿主窗口的承载方式。
二、组件结构:设计视图与实际实现
设计文档给出的组件结构树如下(这是设计视角的调用结构,其中 Constructor/open/IPC 部分对应原生窗口方案):
templateEditorWindow.ts ├── Constructor │ ├── 创建 BrowserWindow │ ├── 设置窗口参数 │ └── 初始化 Renderer ├── loadContent() │ └── 加载 HTML 内容 ├── generateHTML() │ ├── 生成完整的 HTML/CSS/JS │ ├── 嵌入模板说明文档 │ └── 设置事件监听器 ├── open() │ ├── 设置 IPC 通信 │ ├── update-preview: 更新预览 │ ├── save-template: 保存模板 │ └── close-window: 关闭窗口 └── buildSampleNotebook() └── 生成示例数据对照当前仓库的实际源码 src/components/templateEditorWindow.ts,核心方法结构如下:
| 方法 / 成员 | 行号 | 职责 |
|---|---|---|
constructor | L21-L37 | 接收app、initialTemplate、onSave回调,可选initialTrimBlocks、readOnly、themeName;内部创建Renderer实例 |
shouldCloseOnEsc | L40-L42 | 返回false,禁用 ESC 关闭,防止误触丢失编辑内容 |
onOpen | L44-L170 | 构建标题栏、三栏容器、两个功能开关、监听编辑器输入事件 |
updatePreview | L172-L218 | 用示例数据渲染当前模板并写入预览区,处理空结果与异常 |
handleCancel | L220-L229 | 有未保存修改时弹出确认对话框 |
handleSave | L231-L241 | 先做语法校验,通过后才调用onSave(template, trimBlocks) |
onClose | L243-L249 | 清空 DOM、清理防抖定时器(对应文档 "内存管理:窗口关闭时清理监听器" 的注意点) |
buildSampleNotebook | L251-L603 | 生成一份结构完整的示例Notebook数据,供预览使用 |
构造函数签名体现了编辑器的两种工作模式:
constructor( app: App, initialTemplate: string, // 当前模板字符串 onSave: (template: string, trimBlocks: boolean) => void, // 保存回调 initialTrimBlocks?: boolean, // 可选:自动去空白初始值 readOnly = false, // 只读(预览主题)模式 themeName?: string )只读模式下编辑器被禁用(editorEl.disabled = true),标题显示为预览: ${themeName},且隐藏保存按钮——这正是主题管理器 "预览" 功能复用的路径。
三、三栏布局的实现细节
左侧:模板说明文档
说明文档是一个独立的 HTML 资产 templateInstructions.html,在编辑模式下注入左栏:
// 仅编辑模式显示 if (!this.readOnly) { const instructionsPanel = container.createDiv('weread-editor-instructions'); instructionsPanel.innerHTML = templateInstructions; }Webpack 配置中.html被声明为asset/source(见 webpack.config.js),因此import templateInstructions from '../assets/templateInstructions.html'得到的是字符串,可直接填充到 DOM。文档中的可用变量、过滤器清单见下文"模板语法与可用变量"一节。
中间:模板编辑器
中间栏使用textarea承载模板源码,面板标题区分编辑/预览模式:模板编辑 (Nunjucks)或模板内容。样式定义在 style.css(.weread-editor-panel、.weread-editor-textarea)中,整体布局容器为.weread-editor-container(L49),采用 Flexbox 组织三栏,标题栏样式在 L28。
右侧:实时预览与防抖
预览区的核心交互是300ms 防抖(与设计文档一致):
// src/components/templateEditorWindow.ts L162-L169 this.editorEl.addEventListener('input', () => { if (this.debounceTimer) { activeWindow.clearTimeout(this.debounceTimer as unknown as number); } this.debounceTimer = activeWindow.setTimeout(() => { this.updatePreview(); }, 300); });预览面板头部还承载了三个功能开关:
- ✂️ 自动去空白(trimBlocks):切换后立即重渲染;只读模式下展示但禁用
- 🔥 热门划线:仅只读模式展示且禁用(热门划线展示由插件全局设置控制,见
syncPopularHighlightsToggle) - 📝 Markdown 渲染:开启后预览经 Obsidian
MarkdownRenderer.renderMarkdown渲染为格式化视图;关闭则用<pre>显示原始文本(L199-L212)
updatePreview()的渲染管线为:
const templateStr = this.editorEl.value; const sampleNotebook = this.buildSampleNotebook(); const preview = this.renderer.renderWithTemplate(templateStr, sampleNotebook, this.trimBlocks);错误处理分两级:渲染结果为空时提示⚠️ 渲染结果为空,请检查模板语法;Nunjucks 抛异常时捕获并显示❌ + error.message(L213-L217)。这对应文档中 "错误提示显示" 的特性。
示例数据:buildSampleNotebook()
预览不依赖真实的微信读书登录态,而是使用内嵌的示例书籍《中国哲学简史》(bookId651358)构造的Notebook对象,覆盖了三类渲染入口:
metaData:书名、作者、封面 URL、ISBN、分类、出版社、简介、readInfo(阅读时长、总阅读天数、完成时间戳等)chapterHighlights:4 个章节、8 条划线,覆盖普通划线、用户划线 + 热门(isPopular+isUserHighlight)、纯热门划线(带popularCount共读人数)、带批注(reviewContent)四种形态popularHighlights:按章节聚合的热门划线(带totalCount)bookReview:chapterReviews(章节笔记)与bookReviews(整书笔记,含mdContent)
这份数据保证用户在编辑器里能一次性看到模板所有分支(if/for/过滤器)的真实渲染效果。
四、渲染引擎:Renderer 与 Nunjucks 过滤器
预览与保存共用的渲染器是 src/renderer.ts,它封装了 Nunjucks 环境:
export class Renderer { constructor() { const env = nunjucks.configure({ autoescape: false }); addDateFilters(env); }关键点:autoescape: false—— 因为模板输出的是 Markdown 笔记,转义会破坏{{ }}、<、>等 Markdown 语法;Nunjucks 标签本身不是 HTML,无需自动转义。
内置过滤器
addDateFilters(L6-L50)注册了以下过滤器,与说明文档 HTML 中的清单一一对应:
| 过滤器 | 作用 | 默认格式 |
|---|---|---|
formatDate | 秒级时间戳 → 日期 | YYYY-MM-DD |
formatDateTime | 秒级时间戳 → 日期时间 | YYYY-MM-DD HH:mm:ss |
formatTime | 秒级时间戳 → 时间 | HH:mm:ss |
split | 按分隔符拆分为数组 | — |
replace | 字符串或正则替换(支持/pattern/flags写法) | — |
trim | 去除首尾空白 | — |
时间过滤器基于 Obsidian 内置的window.moment实现,因此模板中可以直接写{{ finishedDate | formatDate("YYYY年MM月DD日") }}这样的自定义格式。
渲染入口与校验
render(entry)(L68-L106):同步流程正式渲染笔记时调用,从settingsStore取当前激活主题的模板与trimBlocks,兼容 legacy 主题(source === 'legacy'时回落到顶层settings.template)renderWithTemplate(templateStr, entry, trimBlocks)(L115-L142):编辑器预览专用,为临时模板字符串创建独立的 Nunjucks 环境,trimBlocks与lstripBlocks同步开启validate(template)(L58-L66):用空上下文试渲染,捕获异常返回布尔值——这就是保存前 "实时语法验证" 的落地方式
trimBlocks的语义:开启后自动移除模板标签({% %})前后的换行和空格,让用户可以用缩进写出更易读的模板代码而不产生多余空行。
五、模板语法与可用变量
左侧说明文档 templateInstructions.html 是编辑器内置的 "随附参考手册",其内容如下(此处完整继承,并补充实现侧佐证):
功能开关
- ✂️ 自动去空白:开启后自动移除模板标签(
{% %})前后的换行和空格,让你可以用缩进写出更易读的模板代码 - 📝 Markdown 渲染:开启后右侧预览会渲染为格式化的 Markdown,关闭则显示原始文本
Filter 过滤器
时间戳字段(readInfo中的readingBookDate、finishedDate等)为秒级时间戳,可用以下过滤器转换为可读格式:
timestamp | formatDate→2020-01-29,格式 YYYY-MM-DD,可传入自定义格式timestamp | formatDateTime→2020-01-29 08:00:00,格式 YYYY-MM-DD HH:mm:ss,可传入自定义格式timestamp | formatTime→08:00:00,格式 HH:mm:ss,可传入自定义格式
示例:
{{ finishedDate | formatDate }} {{ finishedDate | formatDateTime }} {{ finishedDate | formatDate("YYYY年MM月DD日") }} {{ readingBookDate | formatDateTime("YYYY/MM/DD HH:mm") }}可用变量
元数据变量(metaData)
| 变量 | 含义 |
|---|---|
{{ title }} | 书名 |
{{ author }} | 作者 |
{{ cover }} | 封面 |
{{ intro }} | 书籍简介 |
{{ bookId }} | 微信图书 ID |
{{ publishTime }} | 出版时间 |
{{ noteCount }} | 划线数量 |
{{ reviewCount }} | 笔记数量 |
{{ isbn }} | ISBN |
{{ category }} | 分类 |
{{ publisher }} | 出版社 |
{{ readInfo }} | 阅读信息,点击查看详情 |
readInfo 阅读信息
| 变量 | 含义 |
|---|---|
{{ readingTime }} | 阅读时长(秒) |
{{ totalReadDay }} | 总阅读天数 |
{{ continueReadDays }} | 连续阅读天数 |
{{ readingBookCount }} | 阅读书籍数 |
{{ readingBookDate }} | 开始阅读时间(时间戳) |
{{ finishedDate }} | 完成阅读时间(时间戳) |
{{ readingProgress }} | 阅读进度 |
划线变量(chapterHighlights)
| 变量 | 含义 |
|---|---|
{{ chapterUid }} | 章节 ID |
{{ chapterTitle }} | 章节标题 |
{{ createTime }} | 创建时间 |
{{ range }} | 划线范围 |
{{ markText }} | 划线文本 |
笔记(bookReview)
| 变量 | 含义 |
|---|---|
{{ reviewId }} | reviewId |
{{ chapterUid }} | 章节 ID |
{{ chapterTitle }} | 章节标题 |
{{ createTime }} | 创建时间 |
{{ abstract }} | 摘录内容 |
{{ content }} | 笔记内容 |
{{ mdContent }} | Markdown 笔记格式 |
{{ type }} | 类型 |
{{ range }} | 范围 |
模板上下文与内置主题示例
渲染时注入的上下文对象为{ metaData, chapterHighlights, bookReview, popularHighlights, syncPopularHighlightsToggle }(见 renderer.ts L119-L125),其中syncPopularHighlightsToggle是控制是否展示热门划线的条件变量。仓库内置三个主题模板可作为学习范本:notebookTemplate.njk(合并式,划线与想法 inline 展示)、separatedTemplate.njk(分离式,笔记统一在底部)、wereadOfficialTemplate.njk(微信官方笔记风格)。以合并式模板为例,展示章节标题按level映射到 Markdown 标题、划线生成微信读书 deeplink 的典型写法:
{% for chapter in chapterHighlights %} {% if chapter.level == 1 %} ## {{ chapter.chapterTitle }} {% for highlight in chapter.highlights %} {% set rangeParts = highlight.range | split("-") %} {% set deeplink = "weread://bestbookmark?bookId=" + metaData.bookId + "&chapterUid=" + highlight.chapterUid + "&rangeStart=" + rangeParts[0] + "&rangeEnd=" + rangeParts[1] %} {% if highlight.isPopular and highlight.isUserHighlight %} > 📌🔥 {{ highlight.markText | trim }} > 🔥 {{ highlight.popularCount }} 人共读 > ⏱ {{ highlight.createTime }} ^{{ highlight.bookmarkId }} {% endif %} {% endfor %} {% endfor %}(片段取自 notebookTemplate.njk L19-L43,展示了split、trim过滤器与字符串拼接的用法。)
六、IPC 通信设计(原生窗口方案)
设计文档为 BrowserWindow 方案规划的进程间通信协议如下:
渲染进程 → 主进程
update-preview:发送模板字符串以更新预览save-template:保存模板close-window:关闭窗口
主进程 → 渲染进程
preview-updated:返回渲染结果或错误信息
从源码结构看,当前仓库快照中不存在ipcMain/ipcRenderer的对应实现——因为当前TemplateEditorWindow运行在 Obsidian 主窗口页面内,等价能力由进程内回调完成:预览直接同步调用renderer.renderWithTemplate,保存通过构造函数注入的onSave回调写回设置。文档中的 IPC 通道属于原生窗口形态的设计规划;若未来TemplateEditorWindow迁移到独立BrowserWindow(如登录窗口 wereadBrowserWindow.ts 的模式),这套通道命名即为落地蓝图。
七、使用方式与调用链
1. 在设置页面中调用(设计文档示例)
设计文档给出了设置页集成编辑器的调用范式:
// settingTab.ts private template(): void { const descFragment = document.createRange() .createContextualFragment(templateInstructions); new Setting(this.containerEl) .setName('笔记模板') .setDesc(descFragment) .addButton((button) => { return button .setButtonText('编辑模板') .setCta() .onClick(() => { const editorWindow = new TemplateEditorWindow( get(settingsStore).template, (newTemplate: string) => { settingsStore.actions.setTemplate(newTemplate); } ); editorWindow.open(); }); }); }保存动作setTemplate在 settings.ts L414-L419 中将新模板写入 settings store 并持久化。
当前仓库的实际演进:从源码结构看,settingTab.ts 的template()段落现在不再直接创建编辑器,而是提供 "主题管理" 入口,打开ThemeManagerModal(管理内置/自定义/社区主题);TemplateEditorWindow的调用点转移到了 themeManagerModal.ts:
// 预览(只读) const editorWindow = new TemplateEditorWindow( this.app, templateForPreview, () => {}, theme.trimBlocks, true, // readOnly theme.name ); editorWindow.open(); // 编辑(保存回主题) const editorWindow = new TemplateEditorWindow( this.app, theme.template, (newTemplate: string, newTrimBlocks: boolean) => { settingsStore.actions.saveTheme({ ...latestTheme, template: newTemplate, trimBlocks: newTrimBlocks }); new Notice('主题已保存'); }, theme.trimBlocks, false, theme.name );这体现了保存回调签名为(template, trimBlocks)的原因:一个主题同时携带模板文本与去空白开关两个属性。
2. 编程调用
import { TemplateEditorWindow } from './components/templateEditorWindow'; // 创建编辑器窗口 const editor = new TemplateEditorWindow( currentTemplate, // 当前模板字符串 (newTemplate: string) => { // 保存回调 // 处理保存的模板 console.log('新模板:', newTemplate); } ); // 打开窗口 editor.open();实际调用时按当前签名需额外传入app作为第一参数(new TemplateEditorWindow(app, template, onSave, ...)),并以editorWindow.open()打开——与 themeManagerModal.ts 中的用法一致。
3. 保存流程的校验闸门
保存并非直接落盘,handleSave() 先用renderer.validate(template)做一次试渲染:
const isValid = this.renderer.validate(template); if (!isValid) { new Notice('模板语法错误,请检查后再保存!'); return; } this.onSave(template, this.trimBlocks); new Notice('模板已保存!');配合取消时的未保存变更确认(模板已修改但未保存,确定要关闭吗?)与shouldCloseOnEsc禁用 ESC,构成完整的防误操作链路。
八、与之前实现的对比
设计文档给出的 Modal 与 BrowserWindow 两代实现对比:
| 特性 | 之前 (Modal) | 现在 (BrowserWindow) |
|---|---|---|
| 窗口类型 | Obsidian Modal | Electron 原生窗口 |
| 布局 | 两栏(编辑器+预览) | 三栏(说明+编辑器+预览) |
| 大小调整 | 8方向拖拽调整 | 原生窗口缩放 |
| 窗口拖动 | 自定义拖拽实现 | 原生标题栏拖拽 |
| 最大化 | 自定义按钮 | 原生最大化按钮 |
| 最小尺寸 | 600×400px | 1200×600px |
| 初始尺寸 | 95vw×90vh | 1600×900px |
| 说明文档 | 在设置页面 | 集成在窗口左侧 |
| 主题适配 | CSS 变量 | 独立样式 |
结合当前源码可以确认:现在的 Modal 实现已经吸收了 "现在" 一栏的大部分特性——三栏布局、95vw×90vh 初始尺寸(L56-L59)、说明文档集成在窗口左侧、样式完全使用独立的.weread-editor-*类族(style.css L14 起)。两代实现共享的是编辑器核心(三栏 + 防抖预览 + 语法校验),差异集中在窗口宿主:Modal 受限于 Obsidian 主窗口内部,而 BrowserWindow 方案可获得系统级的最小化/最大化/独立缩放能力。
九、主题系统集成
模板编辑器不是孤立的,它嵌入了 settings.ts 定义的主题体系。Theme接口(models.ts L606-L617)为:
export interface Theme { id: string; name: string; description?: string; template: string; trimBlocks: boolean; isBuiltIn: boolean; isReadOnly: boolean; source: 'builtin' | 'custom' | 'legacy'; author?: string; version?: string; }BUILT_IN_THEMES提供三个内置只读主题:builtin_merged(合并式)、builtin_separated(分离式)、builtin_official(微信官方笔记主题),全部trimBlocks: truesettings.activeThemeId记录当前激活主题,getActiveTheme()用于同步流程取模板;legacy 主题(source === 'legacy')始终回落使用顶层settings.template- 编辑入口由
isReadOnly控制:内置/社区主题只能 "预览 + 复制并自定义",自定义主题可以进入TemplateEditorWindow编辑并saveTheme保存
十、注意事项与未来改进
设计文档列出的注意事项,与源码印证如下:
- 仅支持桌面端:
BrowserWindow仅在 Electron 环境中可用(扫码登录等桌面功能同样依赖此判断,如 settingTab.ts 中Platform.isDesktopApp的分支) - 内存管理:确保窗口关闭时清理 IPC 监听器——当前实现在
onClose()中contentEl.empty()并clearTimeout防抖定时器(L243-L249) - 调试:可以使用 Electron DevTools 调试窗口内容
- 性能:使用 300ms 防抖优化实时预览性能(L162-L169)
文档规划的未来改进方向:
- 添加快捷键支持(Cmd+S 保存等)
- 支持多标签页编辑多个模板
- 添加模板历史记录
- 支持模板导入导出
- 添加语法高亮和自动补全
- 支持实时预览滚动同步
其中 "支持模板导入导出" 在 themeManagerModal.ts 已部分落地(主题级导出,含manifest元数据)。
十一、相关文件索引
| 路径 | 说明 |
|---|---|
| docs/template-editor-window.md | 本文依据的设计文档(最后更新 2025-11-22) |
| src/components/templateEditorWindow.ts | 编辑器窗口实现(三栏布局、防抖预览、保存校验) |
| src/renderer.ts | Nunjucks 渲染器、过滤器注册、validate/renderWithTemplate |
| src/assets/templateInstructions.html | 左栏模板语法说明文档 |
| src/themes/notebookTemplate.njk 等 | 三个内置主题模板范例 |
| src/components/themeManagerModal.ts | 主题管理器(编辑器的当前主要调用方) |
| src/settingTab.ts | 设置页 "模板设置" 段落 |
| src/settings.ts | BUILT_IN_THEMES、setTemplate等设置动作 |
| style.css | 编辑器窗口样式(.weread-editor-*) |
注:本文以当前仓库快照源码为准。设计文档描述的部分 BrowserWindow/IPC 细节属于原生窗口方案的设计规划,当前TemplateEditorWindow以 Obsidian Modal 形式承载,核心编辑/预览能力与文档描述一致。
- 知识管理
- 知识库
【免费下载链接】obsidian-weread-plugin
Obsidian Weread Plugin is a plugin to sync Weread(微信读书) hightlights and annotations into your Obsidian Vault.
相关推荐
如何快速集成media-kit到Flutter项目?3分钟入门教程
如何快速集成media kit到Flutter项目?3分钟入门教程 media kit是一个功能强大的跨平台音视频播放器,专为Flutter和Dart开发,支持
音视频跨平台3个步骤快速掌握ComfyUI UltimateSDUpscale图像放大技术
3个步骤快速掌握ComfyUI UltimateSDUpscale图像放大技术 ComfyUI UltimateSDUpscale图像放大 扩展是一个强大的AI
人工智能计算机视觉图像处理媒体生成Bowhead技术指标全解析:18种交易信号算法实战应用
Bowhead技术指标全解析:18种交易信号算法实战应用 Bowhead作为一款强大的PHP交易机器人框架,集成了18种主流技术指标算法,帮助交易者构建自动化交
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考