Skip to content

07 · Frontmatter 与保存协议

抽取和模板编译只产生字符串;真正的“耐久保存”还要处理属性类型、YAML、文件名、目标 vault、追加语义与浏览器限制。

一、保存前的四份编译产物

对一个 Template,popup/API 会分别编译:

text
noteNameFormat    → noteName
path              → target path(浏览器路径中按流程处理)
properties[*]     → typed property values
noteContentFormat → Markdown body

不能只编译 body,因为标题、路径和每个 property 都可以引用同一套变量、selector 与 filters。

二、Property 的最小模型

ts
interface Property {
  id?: string
  name: string
  value: string
  type?: string
}

类型可以来自 property 自身,也可由 settings 的 propertyTypes 映射补充。formatPropertyValue(value, type, templateValue) 在生成 YAML 前做领域格式化。

常见类型考虑:

  • text:普通字符串;
  • number:避免作为带引号文本;
  • checkbox/boolean:规范 true/false;
  • date/datetime:保持 Obsidian 可识别格式;
  • list/multitext:拆成 YAML sequence;
  • tags:清理与列表语义;
  • link:保留 wiki/URL 需要的字符串形式。

三、Frontmatter 生成

浏览器主流程与环境无关 API 都有 frontmatter helper,核心目标一致:

yaml
---
title: "Example"
authors:
  - Alice
  - Bob
published: 2026-08-01
draft: false
---

生成器必须处理:

  • key 中的特殊字符;
  • string 是否需要 quote;
  • 多行文本;
  • 数组与空数组;
  • 数字/布尔值不被错误字符串化;
  • 空 property 的保留策略;
  • 与模板原始值/类型的联合判断。

项目选择集中生成,而不是让模板作者手写 ---,因为集中生成更容易保证 YAML 正确。

四、noteName 的双层清理

API 先编译 noteNameFormat,再调用 sanitizeFileName(),空结果回退 Untitled。下载动作还会替换 /\?%*:|"<> 等字符。

这里区分两个目标:

  • Obsidian note name:受 vault 路径与平台规则影响;
  • 浏览器下载文件名:必须满足本地文件系统和 Downloads API。

模板里的 safe_name 让用户显式控制,最终保存层仍保底清理,形成防御纵深。

五、三种主保存行为

SaveBehavior

text
addToObsidian
saveFile
copyToClipboard

popup.ts:1278determineMainAction() 根据 settings 决定主按钮;其他动作可放进 secondary menu。

1. Add to Obsidian

obsidian-note-creator.ts 负责构建 Obsidian URL 并打开。

2. Save file

通过浏览器下载能力创建 .md,适合未安装 Obsidian或希望手动归档的用户。

3. Copy to clipboard

优先现代 clipboard,受限环境可通过 content script textarea + execCommand('copy') 降级。

六、Obsidian URI 不是单一 create

Template.behavior 支持六种语义:

text
create
append-specific
append-daily
prepend-specific
prepend-daily
overwrite

保存层需要把它们映射到 Obsidian 能理解的 URI/CLI 参数,而不是在浏览器中直接写 vault 文件。

数据通常包括:

text
vault
path / note name
content
behavior
silent/focus preference

URI 方案的优势是浏览器扩展不需要文件系统权限;限制是 URL 长度、编码、焦点切换与本地 Obsidian 版本能力。

七、Clipboard 作为可靠桥

tryClipboardWrite(fileContent, obsidianUrl) 暗示长内容或浏览器限制下的两步策略:

  1. 把完整内容写入剪贴板;
  2. 打开携带控制参数的 Obsidian URL;
  3. Obsidian 侧再读取/落盘。

剪贴板不是最终存储,而是浏览器 sandbox 与桌面应用之间的传输通道。

八、silentOpen 与用户焦点

剪藏工具频繁把用户从浏览器拉到 Obsidian 会打断工作。silentOpen 表达的是交互策略:尽可能完成保存但不抢焦点。

这类设置看似 UI 偏好,实际要一直传播到 URI/CLI 的最末层,因此被纳入 Settings 与 save function 参数。

九、Popup 的保存时序

handleClipObsidian() 大致执行:

text
读取当前 DOM fields
  → 获取/确认 template
  → compile note name, properties, content
  → generate frontmatter
  → assemble fileContent
  → saveToObsidian(...)
  → incrementStat(addToObsidian)
  → addHistoryEntry(url/title/vault/path)
  → update UI success state / close if configured

编译发生在点击时,而不只发生在 popup 初始化时。原因是用户可能编辑 note name、properties、prompt response 或正文预览。

十、历史与统计为什么分开存

  • statsstorage.sync:只有小型计数,适合跨设备同步;
  • historystorage.local:最多 1000 条,含 URL/title/path,体量大且隐私敏感,不适合同步配额。

incrementStat() 先加载 settings、增加计数、保存;若有 URL,再写 history。Reader mode 也使用同一统计管线。

十一、CLI 保存出口

CLI 调用 clip() 后有三种出口:

text
stdout                  默认,适合 shell pipe
--output <file>         Node fs 写入
--open                  Obsidian CLI 或 URI

--uri 强制 URI,--silent 控制 focus,--vault 覆盖 template vault。优先级是显式 CLI 参数高于模板默认值。

十二、API 的 ClipResult

环境无关 API 不替调用方保存,只返回完整中间结果:

ts
interface ClipResult {
  noteName: string
  frontmatter: string
  content: string
  fullContent: string
  properties: Property[]
  variables: Record<string, string>
}

为什么同时返回拆分和合并结果?

  • fullContent 可直接写文件;
  • frontmatter/content 便于调用方二次组合;
  • properties/variables 便于调试、预览或审计模板。

十三、失败语义

场景处理
模板局部错误记录并尽量给部分输出
noteName 为空Untitled
clipboard API 失败content script/legacy copy 降级
URI 打开失败抛出/展示保存错误,不应假报成功
history 写失败不应反向破坏已完成的笔记保存
下载文件名非法最终 sanitize
CLI fetch 非 2xxstderr + exit 1
模板目录无匹配列出扫描数量 + exit 1

保存动作应尽量是“主事务优先、遥测次要”。

十四、隐私边界

保存链路可能处理完整页面 HTML、API key、vault 名、历史 URL。源码体现的边界:

  • 笔记正文默认本地处理;
  • 只有 Interpreter 开启且模板含 prompt 时才把上下文发给选定模型;
  • history 留在 local;
  • provider key 存于扩展 sync settings,需要用户理解同步风险;
  • debug logs 在 production 通过 global define 被裁剪。

模板设计者尤其要注意 fullHtml 作为 prompt context 可能包含页面上不希望发送的内容。

十五、可改进点与取舍

从架构角度,保存层未来可进一步抽象成 adapter:

text
CompiledNote
  → ObsidianUriWriter
  → BrowserDownloadWriter
  → ClipboardWriter
  → NodeFileWriter

当前实现已在函数层分离,但浏览器 UI 仍知道较多动作细节。是否继续抽象取决于是否增加新目标(REST API、Git、移动 share sheet 等);没有新目标时,过早接口化也会增加理解成本。

十六、本章检查点

  • 能说出模板的四份独立编译产物;
  • 能解释 typed property 为什么必须先于 YAML;
  • 能区分 create/append/prepend/daily 等行为;
  • 能说明 sync stats 与 local history 的划分;
  • 能解释 API 为什么只返回 ClipResult 而不保存;
  • 能识别 Interpreter 是保存链路中唯一可能向外发送正文的功能。

基于 Obsidian Web Clipper 1.7.1 源码快照的独立学习笔记