13 · 无头 API 与 CLI
src/api.ts展示了如何把浏览器产品中的核心价值提炼为环境无关函数:调用方提供 HTML、URL、模板和 DOM parser,核心返回完整笔记,不碰 UI 与存储。
一、API 边界
clip({
html,
url,
template,
documentParser,
propertyTypes?,
parsedDocument?
}) => Promise<ClipResult>调用方负责:
- 获取 HTML;
- 提供 DOM parser;
- 选择/加载 template;
- 写文件或发送到目标系统。
API 负责:
- Defuddle 抽取;
- HTML → Markdown;
- variables;
- selector resolver;
- template compile;
- property format/frontmatter;
- note name sanitize;
- 返回结果。
这是干净的 dependency inversion:DOM 是能力接口,不是固定实现。
二、DocumentParser
interface DocumentParser {
parseFromString(html: string, mimeType: string): any
}接口故意很小。浏览器可以传 DOMParser,Node CLI 传 linkedom。API 只要求结果具备 documentElement/querySelectorAll 等结构能力。
parsedDocument 允许调用方复用已为 schema trigger 解析的 document,避免重复 parse。
三、Selector 的环境适配
API 创建两种 adapter:
createAsyncResolver(doc)
供 AST renderer 直接求 selector: identifier,解析 selector、可选 attribute、HTML/text 模式。
createSelectorProcessor(doc)
供 compiler deferred legacy path,使用正则拆完整占位符并应用 filters。
两者都落到共享 extractContentBySelector(),只是入口语义不同。
四、clip() 逐步执行
parse/reuse document
→ Defuddle.parse()
→ createMarkdownContent()
→ buildVariables()
→ create resolvers
→ compile noteName
→ compile properties in Promise.all
→ merge property type map
→ generateFrontmatter
→ compile note body
→ assemble fullContentProperties 用 Promise.all,因为每个值可能含 selector async resolver;它们相互独立,可以并行。
五、Trigger matching
matchTemplate(templates, url, schema?) 分两轮:
- URL prefix 与
/regex/; - schema trigger。
URL 轮不需要解析 document,成本低。只有 URL 全部未命中、且存在 schema trigger 时,CLI 才运行 Defuddle 取 schema。
schema trigger 支持:
schema:@Movie
schema:@Movie.genre
schema:@Movie.genre=Drama返回“第一个匹配模板”,所以模板目录顺序/加载顺序会影响同优先级冲突。
六、CLI 参数
obsidian-clipper <url> -t <template-or-dir>
-o, --output <path> 写文件
--html <path|-> 从文件/stdin 读 HTML
--vault <name> 覆盖 vault
--open 发送 Obsidian
--uri 强制 URI 而非 Obsidian CLI
--silent 不抢焦点
--property-types <path> property type JSONURL 始终必填,即使 --html 从本地读,因为相对链接、domain、template trigger 与变量仍需要语义 URL。
七、模板文件与目录
单文件
直接 JSON.parse,作为唯一 Template。
目录
读取所有 .json,先尝试 URL trigger;若无匹配且至少有 schema trigger,再 parse HTML + Defuddle。匹配成功将模板路径写 stderr,正文仍可纯净输出到 stdout。
这是良好的 CLI 约定:诊断走 stderr,数据走 stdout。
八、HTML 输入策略
优先级:
--html - stdin
--html file fs.readFileSync
无 --html fetch(url)支持 stdin 使 CLI 可组合:
curl -L https://example.com/article \
| obsidian-clipper https://example.com/article \
--html - --template ./article.json \
> note.md九、输出策略
--open openInObsidian(fullContent, ...)
--output fs.writeFileSync
default process.stdout.write--open 时 vault 优先取 CLI 参数,其次 template.vault,最后空字符串。path/behavior 取 template。
十、API 使用示例
import { clip } from 'obsidian-clipper/api'
const result = await clip({
html,
url: 'https://example.com/post',
template,
documentParser: new DOMParser(),
})
await saveSomewhere(result.noteName, result.fullContent)实际 parser 需符合 parseFromString 方法;Node 可用 linkedom/jsdom 适配。
十一、浏览器版与 API 的语义差异
| 能力 | 浏览器 | API/CLI |
|---|---|---|
| async Defuddle | parseAsync + timeout fallback | 当前核心 parse() |
| selection | 可读真实用户选区 | 无,除非调用方注入变量 |
| live DOM | selector 通过 content message | selector 查询 parsed doc |
| highlights | 从 storage/tab 获取 | 默认无 |
| prompt Interpreter | UI/provider settings | API core 不执行 |
| 保存 | URI/download/clipboard | 调用方/CLI |
| settings | extension storage | 显式 options |
环境无关不等于功能完全等价。API 提炼的是“HTML + Template → Note”的确定性核心。
十二、CLI 的失败语义
参数缺失、未知 option、fetch 非 2xx、模板目录为空/无匹配、JSON 解析和 clip 异常都会:
- 打印可读信息到 stderr;
process.exit(1);- 不输出半份 Markdown 到 stdout(模板 renderer 的局部错误策略除外)。
自动化调用方应检查 exit code,不只检查文件是否存在。
十三、为何 API 仍依赖 Defuddle
抽取器是内容语义核心,若 API 只导出 template compiler,调用方会各自实现 DOM 清洗,浏览器与 CLI 输出迅速漂移。把 Defuddle 包进 clip() 保证同一网页/模板在不同环境尽量一致。
十四、可扩展方向
- 为 clip options 增加预提取 content adapter;
- 把 async Defuddle 能力带到 Node;
- 注入 prompt resolver 而不把 provider SDK 写进 core;
- 暴露 validation-only API;
- 返回 diagnostics 而不只 console error;
- writer adapters(Git、HTTP、database)。
扩展时仍应保持 core 不读取 fs、storage 或 browser globals。
十五、本章检查点
- 能说明 API/调用方的责任边界;
- 能解释为什么 DocumentParser 是依赖注入;
- 能追踪目录模板的两阶段匹配优化;
- 能区分 stdout/stderr 与三个输出出口;
- 能说明浏览器版与 API 的功能差异;
- 能判断新增能力是否会污染 environment-agnostic core。