Skip to content

13 · 无头 API 与 CLI

src/api.ts 展示了如何把浏览器产品中的核心价值提炼为环境无关函数:调用方提供 HTML、URL、模板和 DOM parser,核心返回完整笔记,不碰 UI 与存储。

一、API 边界

ts
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

ts
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() 逐步执行

text
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 fullContent

Properties 用 Promise.all,因为每个值可能含 selector async resolver;它们相互独立,可以并行。

五、Trigger matching

matchTemplate(templates, url, schema?) 分两轮:

  1. URL prefix 与 /regex/
  2. schema trigger。

URL 轮不需要解析 document,成本低。只有 URL 全部未命中、且存在 schema trigger 时,CLI 才运行 Defuddle 取 schema。

schema trigger 支持:

text
schema:@Movie
schema:@Movie.genre
schema:@Movie.genre=Drama

返回“第一个匹配模板”,所以模板目录顺序/加载顺序会影响同优先级冲突。

六、CLI 参数

text
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 JSON

URL 始终必填,即使 --html 从本地读,因为相对链接、domain、template trigger 与变量仍需要语义 URL。

七、模板文件与目录

单文件

直接 JSON.parse,作为唯一 Template。

目录

读取所有 .json,先尝试 URL trigger;若无匹配且至少有 schema trigger,再 parse HTML + Defuddle。匹配成功将模板路径写 stderr,正文仍可纯净输出到 stdout。

这是良好的 CLI 约定:诊断走 stderr,数据走 stdout。

八、HTML 输入策略

优先级:

text
--html -       stdin
--html file    fs.readFileSync
无 --html      fetch(url)

支持 stdin 使 CLI 可组合:

bash
curl -L https://example.com/article \
  | obsidian-clipper https://example.com/article \
      --html - --template ./article.json \
  > note.md

九、输出策略

text
--open         openInObsidian(fullContent, ...)
--output       fs.writeFileSync
default        process.stdout.write

--open 时 vault 优先取 CLI 参数,其次 template.vault,最后空字符串。path/behavior 取 template。

十、API 使用示例

ts
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 DefuddleparseAsync + timeout fallback当前核心 parse()
selection可读真实用户选区无,除非调用方注入变量
live DOMselector 通过 content messageselector 查询 parsed doc
highlights从 storage/tab 获取默认无
prompt InterpreterUI/provider settingsAPI core 不执行
保存URI/download/clipboard调用方/CLI
settingsextension 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。

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