☰
obsidian-weread-plugin 模板编辑器窗口:三栏布局、Nunjucks 实时预览与主题系统集成
2026/10/13 11:06:16 网站建设 项目流程
  • 知识管理
  • 知识库

【免费下载链接】obsidian-weread-plugin

Obsidian Weread Plugin is a plugin to sync Weread(微信读书) hightlights and annotations into your Obsidian Vault.

项目地址:https://gitcode.com/gh_mirrors/ob/obsidian-weread-plugin
点击查看免费下载

本文基于仓库设计文档 template-editor-window.md 展开,详解 obsidian-weread-plugin(微信读书划线同步到 Obsidian 的插件)中模板编辑器窗口的设计与实现:三栏布局(模板说明 / 编辑器 / 实时预览)、300ms 防抖渲染管线、Nunjucks 模板变量与过滤器体系,以及它与主题管理系统(Theme / activeThemeId)的集成方式。读完你可以理解该编辑器从输入到预览到保存的完整调用链,并掌握TemplateEditorWindow的编程式调用与 Nunjucks 模板语法。

一、设计目标与功能概览

obsidian-weread-plugin 将微信读书的划线、想法(笔记)和热门划线渲染成 Markdown 笔记写入 Vault,渲染逻辑由 Nunjucks 模板驱动。模板编辑器窗口就是这个模板的可视化编辑/调试工具,核心目标是:让普通用户在不动手改 JSON 配置的情况下,边写模板、边看渲染效果、边对照语法说明,从而快速定制自己的读书笔记格式。

设计文档中定义的功能特性包括:

三栏布局

  1. 左侧:模板说明文档(设计规格:宽 300px,可在 250-500px 间拖动调整)
    • 显示完整的 Nunjucks 模板语法说明
    • 包含可用变量、过滤器和示例代码
    • 支持拖动调整宽度
  2. 中间:模板编辑器(自适应宽度)
    • 语法高亮的代码编辑器
    • 支持 Nunjucks 模板语法
    • 实时语法验证
  3. 右侧:实时预览(自适应宽度)
    • 300ms 防抖的实时渲染
    • 使用示例数据预览效果
    • 错误提示显示

窗口功能

  • 原生窗口体验:设计文档提出使用 ElectronBrowserWindow提供完整的原生窗口功能
  • 可拖动标题栏:通过标题栏拖动整个窗口
  • 最小尺寸: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,核心方法结构如下:

方法 / 成员行号职责
constructorL21-L37接收app、initialTemplate、onSave回调,可选initialTrimBlocks、readOnly、themeName;内部创建Renderer实例
shouldCloseOnEscL40-L42返回false,禁用 ESC 关闭,防止误触丢失编辑内容
onOpenL44-L170构建标题栏、三栏容器、两个功能开关、监听编辑器输入事件
updatePreviewL172-L218用示例数据渲染当前模板并写入预览区,处理空结果与异常
handleCancelL220-L229有未保存修改时弹出确认对话框
handleSaveL231-L241先做语法校验,通过后才调用onSave(template, trimBlocks)
onCloseL243-L249清空 DOM、清理防抖定时器(对应文档 "内存管理:窗口关闭时清理监听器" 的注意点)
buildSampleNotebookL251-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 渲染:开启后预览经 ObsidianMarkdownRenderer.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 ModalElectron 原生窗口
布局两栏(编辑器+预览)三栏(说明+编辑器+预览)
大小调整8方向拖拽调整原生窗口缩放
窗口拖动自定义拖拽实现原生标题栏拖拽
最大化自定义按钮原生最大化按钮
最小尺寸600×400px1200×600px
初始尺寸95vw×90vh1600×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: true
  • settings.activeThemeId记录当前激活主题,getActiveTheme()用于同步流程取模板;legacy 主题(source === 'legacy')始终回落使用顶层settings.template
  • 编辑入口由isReadOnly控制:内置/社区主题只能 "预览 + 复制并自定义",自定义主题可以进入TemplateEditorWindow编辑并saveTheme保存

十、注意事项与未来改进

设计文档列出的注意事项,与源码印证如下:

  1. 仅支持桌面端:BrowserWindow仅在 Electron 环境中可用(扫码登录等桌面功能同样依赖此判断,如 settingTab.ts 中Platform.isDesktopApp的分支)
  2. 内存管理:确保窗口关闭时清理 IPC 监听器——当前实现在onClose()中contentEl.empty()并clearTimeout防抖定时器(L243-L249)
  3. 调试:可以使用 Electron DevTools 调试窗口内容
  4. 性能:使用 300ms 防抖优化实时预览性能(L162-L169)

文档规划的未来改进方向:

  • 添加快捷键支持(Cmd+S 保存等)
  • 支持多标签页编辑多个模板
  • 添加模板历史记录
  • 支持模板导入导出
  • 添加语法高亮和自动补全
  • 支持实时预览滚动同步

其中 "支持模板导入导出" 在 themeManagerModal.ts 已部分落地(主题级导出,含manifest元数据)。

十一、相关文件索引

路径说明
docs/template-editor-window.md本文依据的设计文档(最后更新 2025-11-22)
src/components/templateEditorWindow.ts编辑器窗口实现(三栏布局、防抖预览、保存校验)
src/renderer.tsNunjucks 渲染器、过滤器注册、validate/renderWithTemplate
src/assets/templateInstructions.html左栏模板语法说明文档
src/themes/notebookTemplate.njk 等三个内置主题模板范例
src/components/themeManagerModal.ts主题管理器(编辑器的当前主要调用方)
src/settingTab.ts设置页 "模板设置" 段落
src/settings.tsBUILT_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.

项目地址:https://gitcode.com/gh_mirrors/ob/obsidian-weread-plugin
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询