Tolaria 标题与文件名同步机制全解析:从 title=slug 到 H1 优先的架构演进
【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria
导读
本文以 ADR-0007 Title equals filename (slug sync) 为核心,完整还原 Tolaria(一个基于纯 Markdown 文件的知识库桌面应用)如何建立"标题即文件名"的确定性映射,并追踪其被 ADR-0044、ADR-0055、ADR-0068 相继取代的演进脉络。读完你将掌握:slug 化的精确规则、打开时同步与原子重命名的底层实现、wikilink 联动更新的机制,以及现行"H1 优先 + 可配置自动重命名"体系的完整工作流。
背景:扁平化 vault 之后,文件名成为笔记身份标识
在 ADR-0006 Flat vault structure 落地后,所有用户笔记都以扁平.md文件存放在 vault 根目录,类型只由type:frontmatter 字段决定,绝不从文件夹位置推断。这一决策直接带来一个连锁问题:当文件夹不再承载语义时,文件名成为笔记在文件系统与 wikilink 解析中唯一的稳定身份标识。
而在 ADR-0007 之前,标题是从正文第一个 H1 提取的。这种方案非常脆弱——用户可能无意中删除或改写 H1,却不自知这会影响笔记的"身份",导致标题与文件名长期脱节。因此需要一个清晰、确定性的 title↔filename 映射规则。
决策:title = slugify(filename),双向同步
ADR-0007 的核心决策可以概括为一条规则:
每篇笔记的文件名都是
slugify(title).md;title:frontmatter 字段是人工可读标题的唯一事实来源(source of truth)。打开笔记时,若两者发生分歧,系统将 title 字段同步为与文件名一致(文件名胜出);重命名时,标题与文件名被原子地同时更新。
具体到行为层,该 ADR 列出了四条关键落地约束(均能在当前源码中找到对应实现):
extract_title只读 frontmatter 的title:字段,绝不读 H1;当 title 缺失时回退到slug_to_title()(连字符转空格、单词首字母大写)。sync_title_on_open在打开笔记时自动纠正脱钩的 frontmatter。rename_note原子地同时更新title:frontmatter 与文件名,并联动更新整个 vault 内的 wikilink 引用。- slug 冲突检测防止产生重复文件名;编辑器内的 H1 块通过 CSS 隐藏,由编辑器上方的独立
TitleField组件作为主要标题编辑入口。
备选方案对比
| 方案 | 思路 | 优点 | 缺点 |
|---|---|---|---|
| A(采纳) | title = slugify(filename)双向同步 | 确定性、可预测,wikilink 按标题/文件名主干解析 | 含特殊字符的标题在文件名中被简化 |
| B | UUID 文件名 + 标题仅存 frontmatter | 文件名永不变化 | vault 在 Finder/终端中不可读,违背"纯 Markdown 文件"原则(参见 ADR-0002) |
| C | 基于 H1 提取标题,无显式 title 字段 | 无额外字段 | 脆弱,H1 易被误删误改,且与文件名完全脱钩 |
方案 A 之所以胜出,是因为它在"人类可读的文件系统"与"程序可预测的身份解析"之间取得了平衡:文件名既是磁盘上的真实文件,又是 wikilink 的解析键。
源码级实现剖析
slug 化规则:title_to_slug
标题到文件名的转换实现在 src-tauri/src/vault/rename.rs 的title_to_slug:
- 全部转小写;
- 保留字母与数字(含 Unicode 字母),其余字符一律映射为
-; - 按
-切分后过滤空段再合并,避免连续的连字符; - 若结果为空白(如纯符号标题),回退为
untitled。
对应的单元测试test_title_to_slug与test_title_to_slug_preserves_unicode_letters给出了精确样例:
"Weekly Review" → "weekly-review" "My Note! " → "my-note" "你好" → "你好" "项目-2025 ✦ Q1" → "项目-2025-q1" "!?" / "---" → "untitled" // 纯符号标题回退注意 Unicode 支持是刻意的设计:rename.rs的测试同时断言了中文文件名(你好.md)在文件系统、frontmatter、git rename 检测三条链路上的完整性(test_rename_note_with_cjk_title_writes_unicode_filename、test_detect_renames_preserves_chinese_markdown_paths)。
反向推导:slug_to_title
从文件名主干反推人类可读标题的实现在 src-tauri/src/vault/parsing.rs:按-切分单词,每个单词首字母转大写,再用空格连接。例如career-tracks→Career Tracks。这构成了"无 title 字段时"的回退标题来源。
打开时同步:sync_title_on_open
src-tauri/src/vault/title_sync.rs 完整实现了"文件名胜出"的三条规则:
- title 缺失→ 从文件名主干推导标题并写入 frontmatter;
- title 存在但其 slug 与文件名主干不一致→ 用文件名推导值覆盖 title;
- 两者一致→ 空操作(
SyncAction::InSync)。
解析器只认title:与"title":两种前缀(TITLE_PREFIXES),并去除首尾空白与引号。值得注意的细节:该函数会保留其他所有 frontmatter 字段,测试test_sync_preserves_other_frontmatter验证了type、status等字段在同步过程中不被破坏;即使文件完全没有 frontmatter,也会自动补上---\ntitle: ...\n---(test_sync_adds_frontmatter_when_none_exists)。
原子重命名:rename_note
rename_note(src-tauri/src/vault/rename.rs)是 ADR-0007"重命名原子性"承诺的实现:
- 校验新标题非空;
- 计算期望文件名
title_to_slug(new_title) + ".md"; - 若标题与文件名均未变化则空操作;
- 标题有变则先通过
update_frontmatter_content更新title:字段(绝不修改正文 H1——H1 是正文内容); - 文件名有变则通过
RenameWorkspace事务化重命名(涉及崩溃安全的事务恢复机制recover_pending_rename_transactions); - 最后
finalize_rename用build_wikilink_pattern构造针对旧标题、旧路径主干的正则,遍历 vault 内所有.md文件,把[[旧引用]]重写为[[新路径]]。
测试test_rename_note_updates_wikilinks验证了联动效果:重命名weekly-review.md为sprint-retrospective.md后,[[Weekly Review]]被重写为[[note/sprint-retrospective]],并精确统计出updated_files: 2。RenameResult同时返回failed_updates计数,供 UI 提示需要人工处理的引用。
架构演进:从 title 优先到 H1 优先
ADR-0007 模型有一个隐含假设:用户会先输入标题再写正文。实际使用中这造成了摩擦——新建笔记被迫先起名、TitleField常驻编辑器顶部造成视觉干扰、外部改名后 "title = filename slug" 契约又显得脆弱。于是系统在一个月内经历了三次 ADR 迭代:
ADR-0044:H1 成为主要标题来源(2026-04-07)
ADR-0044 翻转了事实来源:正文第一个# H1是规范显示标题,title:frontmatter 降级为向后兼容字段。新笔记以untitled-{type}-{timestamp}.md创建且不带title:;保存时若笔记已有 H1,则自动重命名为其 slug(冲突时追加-2、-3后缀)。VaultEntry新增has_h1: bool字段,前端据此隐藏TitleField与图标选择器;面包屑栏改显示文件名主干,让用户始终知道真实文件标识。
ADR-0055:编辑器正文是唯一标题表面(2026-04-11)
ADR-0055 清除了残留的双标题面问题:无 H1 笔记的旧回退标题行被彻底移除,删除 H1 不会再"复活"旧 UI。标题显示优先级保持 H1 → frontmattertitle:→ 文件名推导,但界面不再为后两种提供独立编辑控件。
ADR-0068(现行):自动重命名可配置化(2026-04-17)
ADR-0068 指出 ADR-0044/0055 的"保存即自动重命名"过于刚性——部分用户希望 H1 立即驱动显示标题,却想让合成文件名untitled-*保持稳定直到显式重命名。最终决策:编辑器正文仍是唯一标题表面,但 untitled 笔记的 H1 自动重命名变为安装本地设置initial_h1_auto_rename_enabled,默认开启。对应字段在 src-tauri/src/settings.rs 以Option<bool>持久化,保存管线在调度 untitled 重命名前会查询该设置。
现行体系的工作流与实现
标题解析三级优先级
extract_title(src-tauri/src/vault/parsing.rs)现在的解析顺序:
- 正文(剥离 frontmatter 后)第一个非空行的
# H1(extract_h1_title,要求 H1 位于首个非空行); - 遗留 frontmatter
title:字段(向后兼容); - 文件名主干经
slug_to_title推导。
untitled 自动重命名:一次性的 H1 快照
auto_rename_untitled(src-tauri/src/vault/rename.rs)是典型的**一次性(one-shot)**触发:仅当文件名匹配untitled-{something}-{digits}.md模式(is_untitled_filename)且正文存在 H1 时才执行,随后复用rename_note完成 frontmatter、文件名、wikilink 的全链路更新。
外部重命名检测
vault 外(如 Finder 或终端)的改名由detect_renames兜底:它调用git diff HEAD --name-status --diff-filter=R -M对比工作区与 HEAD(src-tauri/src/vault/rename.rs),识别重命名对后经update_wikilinks_for_renames批量修复引用——这弥补了"外部改名导致 title/filename 契约失效"的历史痛点。
实践要点与注意事项
- 文件名是身份,标题是展示:wikilink 按文件名主干/路径解析(见 ADR-0010),因此面包屑始终显示文件名主干而非显示标题,避免"看着标题却找不到文件"的认知偏差。
- 自动重命名是保存副作用:保存 untitled 笔记触发重命名会引入文件移动,指向旧文件名的 wikilink 在重命名传播前可能短暂失效(ADR-0044 明确记录了这一风险)。若你的工作流依赖大量外部引用,可在设置中关闭
initial_h1_auto_rename_enabled,改为从面包屑显式重命名。 - untitled 文件会累积:关闭自动重命名或用户放弃未写 H1 的草稿时,vault 会堆积
untitled-*文件,需要定期清理(系统不提供自动清除)。 - H1 只在首非空行生效:
extract_h1_title只认可正文第一个非空行上的 H1;把标题写在中间或使用# 标题之外的形式(如#标题)都不会被识别为标题来源。 - 特殊字符会被 slug 简化:含
!、*、?等符号的标题在文件名中会被折叠为连字符,纯符号标题甚至回退为untitled——这是 ADR-0007 就明示的取舍。
总结
从 ADR-0007 的title = slugify(filename),到 ADR-0068 的 "H1 唯一标题表面 + 可配置自动重命名",Tolaria 用四次 ADR 完成了标题体系的收敛:文件名承担稳定的身份标识,H1 承担自然的书写体验,frontmattertitle:保留向后兼容,所有重命名操作经事务化管道联动修复 wikilink。理解这条演进链,不仅有助于把握 Tolaria 当前的行为,也能为任何"文件系统即数据源"的 Markdown 应用提供标题设计参考。
【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考