- 知识管理
- 知识库
【免费下载链接】dendron
The personal knowledge management (PKM) tool that grows as you do!
本篇技术指南以 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 块级引用体系中的三个关键要素:
- Frontmatter 元数据:
id(笔记唯一 ID)、title、desc、updated、created(毫秒时间戳)。这是 Dendron 所有笔记的标准头部,由引擎在创建笔记时自动生成并维护。 - 块锚点的定义:在列表项末尾使用
^facilis、^DHWVfFIaPjYv这样的标记,为特定块(block)打上可寻址的"锚点"。 - 同文件块引用:通过
![[#^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.*]]等,均在测试文档中覆盖。
七、如何在自己的工作区复现与验证
要亲手验证本文的全部机制,可以按以下步骤操作:
- 创建 scratch 笔记:在任一 vault(如
test-workspace/vault)中新建scratch.demo.md,写入示例文档中的内容(## temporibus标题、嵌套列表、两个^id锚点、两条![[#^id]]引用)。 - 查看预览:在 VSCode 中打开 Dendron 插件的 Markdown 预览面板,观察
![[#^facilis]]处是否嵌入了对应列表项内容,并可点击块锚点跳转。 - 切换区间语法:将引用改为
![[#^facilis:#^DHWVfFIaPjYv]],观察起止两个列表项之间的范围被整体引用。 - 查看发布输出:使用
dendron publish系列命令(或直接复用仓库中的 test-workspace/dendron.yml 发布配置),对比 HTML 中锚点元素的id与class="block-anchor"。 - 对照测试快照:仓库的渲染测试快照 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!
相关推荐
wp-calypso 中页面锚点平滑滚动机制详解:scroll-to-anchor 模块实现剖析
wp calypso 中页面锚点平滑滚动机制详解:scroll to anchor 模块实现剖析 wp calypso(WordPress.com 的 Java
前端CMSAnt Design Anchor 自定义锚点高亮:深入解析 getCurrentAnchor 的用法与源码实现
Ant Design Anchor 自定义锚点高亮:深入解析 getCurrentAnchor 的用法与源码实现 锚点(Anchor)是页面内导航的核心组件,而
前端UI组件设计系统local-deep-research 库文档块级引用修复深度解析:` Sources` 逐块锚点渲染的实现与原理
local deep research 库文档块级引用修复深度解析: Sources 逐块锚点渲染的实现与原理 在 local deep research 中,
AI应用人工智能大模型RAGAI Agent深度研究本地部署后端前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考