☰
Dendron 块锚点(Block Anchor)与块引用机制深度解析:从 scratch 笔记到源码实现
2026/9/29 7:09:13 网站建设 项目流程
  • 知识管理
  • 知识库

【免费下载链接】dendron

The personal knowledge management (PKM) tool that grows as you do!

项目地址:https://gitcode.com/gh_mirrors/de/dendron
点击查看免费下载

本篇技术指南以 Dendron 工作区中一份典型的 scratch 草稿笔记(scratch.2021.08.31.004036.md)为切入点,完整讲解 Dendron 的块锚点(Block Anchor)语法、同文件块引用(Block Reference)用法,以及它们背后的解析与渲染原理。读完本文,你将掌握如何在自己的笔记中定义^anchor-id、通过![[#^anchor-id]]引用任意块级内容,并理解引用区间、列表裁剪、错误处理等底层机制。


一、示例文档:一份 scratch 笔记里藏着什么

先看原始文档的完整内容(test-workspace/vault/scratch.2021.08.31.004036.md):

--- id: cu0BgWKqOikg2khGwrD1q title: '004036' desc: '' updated: 1630385002240 created: 1630384838453 --- ## temporibus Ut temporibus quidem quis corrupti nihil corporis - Ad libero molestias voluptas quo cupiditate ut quisquam - Id quibusdam debitis facilis illum et ratione minima. ^facilis - In quibusdam quia enim explicabo est quibusdam molestiae. ^DHWVfFIaPjYv ![[#^facilis]] ![[#^DHWVfFIaPjYv]]

这份笔记虽然短小,却完整演示了 Dendron 块级引用体系中的三个关键要素:

  1. Frontmatter 元数据:id(笔记唯一 ID)、title、desc、updated、created(毫秒时间戳)。这是 Dendron 所有笔记的标准头部,由引擎在创建笔记时自动生成并维护。
  2. 块锚点的定义:在列表项末尾使用^facilis、^DHWVfFIaPjYv这样的标记,为特定块(block)打上可寻址的"锚点"。
  3. 同文件块引用:通过![[#^facilis]]、![[#^DHWVfFIaPjYv]]把锚点对应的内容"嵌入"到文档中。

![[...]]是 Dendron 的 note reference(笔记引用)语法,其中#^anchor-id部分表示"本文件中名为^anchor-id的块锚点"。由于引用省略了文件名前缀,它隐式指向当前笔记自身——这一点可以在源码中得到印证:在 packages/unified/src/remark/noteRefsV2.ts 中,解析器发现link.from?.fname === ""时,会把它替换为当前处理中的笔记名。

二、块锚点语法:命名规则与放置位置

2.1 语法规则

块锚点的语法是在目标块末尾追加^+ 锚点 ID,例如:

这是一段普通文本。 ^my-paragraph

定义锚点的正则表达式定义在 packages/unified/src/remark/blockAnchors.ts:

export const BLOCK_LINK_REGEX = /^\^([\w-]+)\w*(\n|$)/; export const BLOCK_LINK_REGEX_LOOSE = /\^([\w-]+)/;

据此可以得出命名约束:

  • 锚点 ID 由字母、数字、下划线(_)和短横线(-)组成,即[\w-]+;
  • 锚点标记^id之后必须紧接换行或行尾(\n|$);
  • 宽松匹配模式下(matchLoose: true,默认开启),锚点可以出现在字符串任意位置,只要符合^([\w-]+)即可。

源码注释明确指出:允许下划线是相对 Obsidian 的一个扩展("The underscores are an extension over Obsidian"),同时还允许锚点后存在空白。

2.2 锚点可以挂在哪些块上

从工作区的链接测试文档 dendron.ref.links.block-anchors.md 可以看到,块锚点几乎可以放在所有块级元素上:

  • 段落末尾:Suscipit optio debitis et aut ratione totam et asperiores. ^first-paragraph
  • 列表项末尾(含嵌套子项):* Omnis totam rerum provident enim omnis in earum. ^first-item,以及缩进的子项^fourth-item
  • 独立成行,引用前一个块:表格后面单独一行^table,代码块后面单独一行^code

关于最后一种"独立成行"的语义,源码中有明确说明。在 packages/unified/src/remark/noteRefsV2.ts 的findBlockAnchor中:

if ( foundAncestors[0].ancestor.children.length === 1 && foundAncestors[0].ancestor.children[0].type === DendronASTTypes.BLOCK_ANCHOR ) { // If located by itself after a block, then the block anchor refers to the previous block return { type: "block", index: foundIndex - 1 }; }

即:如果锚点单独占一行,它指向前一个块。所以^table这样的写法实际引用的是它上方的表格,^code引用的是上方的代码块。

2.3 锚点 ID 的生成

在示例文档中,^facilis是人为命名的锚点。但 Dendron 也支持自动生成 ID 的锚点(如^DHWVfFIaPjYv、^nPm286FpKzGj这类随机短字符串),便于在输入引用时通过自动补全快速定位。在引用目标处输入![[#^时,编辑器会基于工作区索引出的所有锚点进行提示。

三、块引用语法:三种形式与同文件引用

块引用本质上是 note reference 的一种,统一使用![[...]]包裹,并按锚点类型分为:

引用形式示例含义
同文件块引用![[#^facilis]]引用当前文件中名为facilis的块
跨文件块引用![[dendron.ref.links.target#^123]]引用其他笔记中的块
区间块引用![[dendron.welcome#^start:#^end]]引用从起点锚到终点锚之间的内容

其中同文件引用最常用,正是示例文档所演示的形式。其内部处理逻辑在 packages/unified/src/remark/noteRefsV2.ts:解析出链接后,若发现fname为空,就把当前笔记的文件名补上,随后走与普通跨文件引用完全一致的渲染管线。

除了块锚点,note reference 还支持标题锚点(header anchor,如![[dendron.welcome#header1]])和正文开始/结束锚点(^begin/^end)。判断锚点类型的函数位于 packages/common-all/src/utils/index.ts:

export function isBlockAnchor(anchor?: string): boolean { // not undefined, not an empty string, and the first character is ^ return !!anchor && anchor[0] === "^"; }

也就是说,以^开头即视为块锚点;不带^的锚点按标题锚点处理。

四、源码级原理:引用如何被解析与切片

4.1 解析层:从^id到 AST 节点

blockAnchors是 unified 的 remark 插件(packages/unified/src/remark/blockAnchors.ts)。它在解析器注册了一个名为blockAnchor的内联 tokenizer,通过locator在文本中定位^字符,命中BLOCK_LINK_REGEX后产出类型为blockAnchor的 AST 节点,并记录锚点 ID。

4.2 切片层:findAnchor 与 prepareNoteRefIndices

当 note reference 带有#^id锚点时,渲染器会在目标笔记的 AST 上执行findAnchor(packages/unified/src/remark/noteRefsV2.ts),返回以下五种定位结果之一:

  • block:普通块(锚点在块内部或紧邻其后的独立行);
  • list:锚点位于列表项内,需要做特殊裁剪;
  • header:标题锚点(MdastUtils.findHeader负责查找);
  • block-begin/block-end:^begin/^end特殊锚点,分别指向第一个标题之前与文档末尾。

定位后再由prepareNoteRefIndices(packages/unified/src/remark/noteRefsV2.ts)计算起止区间:

  • 未指定结束锚点时,块的结束位置就是该块自身的结束(end = { type: "block", index: start.index });
  • 若起点是标题,则智能延伸到下一个同级或更高级标题之前;
  • #^begin不能作为结束锚点、#^end不能作为起始锚点,否则渲染错误;
  • 支持,offset语法做行内偏移:![[dendron.welcome#^anchor,2]]表示从锚点所在块之后第 2 个元素开始;
  • 嵌套引用深度上限为 3(MAX_REF_LVL = 3),超出时报too many nested note refs。

4.3 列表项的特殊裁剪逻辑

示例文档的锚点都位于嵌套列表中,这也是块引用最易出错的场景。noteRefsV2.ts为此实现了三组专门的裁剪函数:

  • removeListItems:按起止位置把列表兄弟项裁剪掉(先裁尾部再裁头部,避免索引偏移);
  • removeExceptSingleItem:当anchorStart === anchorEnd(如![[#^item:#^item]])时,只保留单个列表项本身,去掉其全部子项。这正是 dendron.ref.links.md 中![[#^0NFOQ4Hi4frn:#^0NFOQ4Hi4frn]]对应的测试意图——"Targeting a single list item without its children";
  • removeSingleItemNestedLists:若裁剪后外层列表只剩一个单项,则用内层多子项列表替换,避免产生无意义的单层包裹。

需要特别说明:在示例文档中,![[#^facilis]]引用的锚点位于嵌套子项上,因此渲染结果会包含该子项及其所属的父列表结构(引用的是"包含该锚点的最顶层祖先列表项"),而非仅一行文本——这是列表类块引用与普通段落块引用在行为上的关键差异。

五、渲染层:不同输出目标下的差异

块锚点在最终输出时会根据渲染目标(DendronASTDest)有不同的表现,逻辑见 packages/unified/src/remark/blockAnchors.ts:

目标行为
MD_DENDRON(Dendron 内部 Markdown)原样输出^id,保留锚点标记
MD_REGULAR(普通 Markdown)直接剥离锚点(普通 Markdown 无此概念)
MD_ENHANCED_PREVIEW(增强预览)输出带id的可点击锚点链接<a class="block-anchor anchor-heading">
HTML(发布)输出blockAnchor2htmlRaw生成的锚点元素

引用渲染端则由convertNoteRefToHAST统一处理(packages/unified/src/remark/noteRefsV2.ts),它会按目标笔记的 AST 切片出区间、补齐脚注定义,再交给后续的 prettify / iframe(config.dev.enableExperimentalIFrameNoteRef)等流程。发布场景下还会应用发布规则(SiteUtils.canPublish),未发布的笔记会被渲染为空段落。

六、错误处理与边界情况

仓库中的测试文档(dendron.ref.links.md)专门辟有 "note reference error messages" 一节,对应源码中可见的错误分支:

  • 起点锚点不存在:Start anchor xxx not found;
  • 终点锚点不存在:End anchor xxx not found;
  • ^end用作起始锚点:报错 "the '^end' anchor cannot be used as the starting anchor";
  • 目标笔记不存在 / 通配符无匹配:分别报 "No note with name ... found in cache during parsing" 与 "There are no matches for ...";
  • 同名笔记歧义:发布模式下若存在多个同名笔记且未指定 vault 前缀,渲染会报错并提示 "Please specify the vault prefix"(duplicateNoteBehavior配置可控制该行为);
  • 引用不存在的内容:例如![[void]]、![[dendron://vault/void]]、![[void.*]]等,均在测试文档中覆盖。

七、如何在自己的工作区复现与验证

要亲手验证本文的全部机制,可以按以下步骤操作:

  1. 创建 scratch 笔记:在任一 vault(如test-workspace/vault)中新建scratch.demo.md,写入示例文档中的内容(## temporibus标题、嵌套列表、两个^id锚点、两条![[#^id]]引用)。
  2. 查看预览:在 VSCode 中打开 Dendron 插件的 Markdown 预览面板,观察![[#^facilis]]处是否嵌入了对应列表项内容,并可点击块锚点跳转。
  3. 切换区间语法:将引用改为![[#^facilis:#^DHWVfFIaPjYv]],观察起止两个列表项之间的范围被整体引用。
  4. 查看发布输出:使用dendron publish系列命令(或直接复用仓库中的 test-workspace/dendron.yml 发布配置),对比 HTML 中锚点元素的id与class="block-anchor"。
  5. 对照测试快照:仓库的渲染测试快照 blockAnchors.spec.ts.snap 覆盖了"段落末尾、表格后、代码块后"等场景的 HTML 输出,noteRefv2.spec.ts.snap 则覆盖了块引用的区间切片结果,可作为行为判定的权威参考。

八、小结

从一份不足二十行的 scratch 草稿,可以完整观察 Dendron 块级引用体系的落地点:^id定义锚点、![[#^id]]同文件引用、区间与偏移控制、列表裁剪、多目标渲染与错误处理。这一机制的工程骨架集中在两个文件——解析与渲染插件 packages/unified/src/remark/blockAnchors.ts 与引用处理器 packages/unified/src/remark/noteRefsV2.ts,配合 packages/unified/src/remark/utils.ts 中的LinkUtils.parseNoteRef完成从字符串到结构化链接的转换。理解这条链路后,无论是撰写复杂嵌套笔记、构建可复用内容块,还是排查引用渲染异常,都能有的放矢。

  • 知识管理
  • 知识库

【免费下载链接】dendron

The personal knowledge management (PKM) tool that grows as you do!

项目地址:https://gitcode.com/gh_mirrors/de/dendron
点击查看免费下载

相关推荐

上一篇:Flame Widgets 实战指南:在 Flutter Widget 树中集成游戏级 UI 组件
下一篇:LMCache 多进程模式部署指南:Docker 与 Kubernetes 实战、Isolated IPC 与生产调优

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

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

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

立即咨询