Tolaria 下划线系统属性约定(ADR-0008):如何在 Markdown frontmatter 中隔离用户属性与系统内部属性
2026/9/13 23:46:13 网站建设 项目流程

Tolaria 下划线系统属性约定(ADR-0008):如何在 Markdown frontmatter 中隔离用户属性与系统内部属性

【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria

导读

本文围绕 Tolaria(基于 Tauri + React 的 Markdown 知识库管理桌面应用)的架构决策记录 ADR-0008《Underscore convention for system properties》展开,剖析其"下划线前缀 = 系统属性"约定的设计动机、完整规则与双端实现。阅读本文后,你将掌握:如何在 note/type 文档的 frontmatter 中书写_前缀系统字段、哪些键被规范化(canonical)写入、如何兼容旧键读取、以及 Rust 与 TypeScript 两套解析器如何协同保证系统属性不会泄漏到属性面板、搜索与过滤中。

背景:属性面板为何会"越用越乱"

Tolaria 采用"文件系统即真相源(filesystem source of truth,见 ADR-0002)"的架构,notes、types、projects 等实体的全部配置都存放在 Markdown 文件的 YAML frontmatter 中。随着功能扩张,越来越多的配置型字段被塞进 frontmatter:置顶属性(pinned properties)、类型图标(type icons)、颜色(colors)、侧边栏标签(sidebar labels)、排序字段(sort order)等。

这些字段本质上属于系统内部元数据——它们驱动 UI 渲染与行为,普通用户在日常记笔记时通常不应直接编辑。如果它们与用户自定义属性混在同一个 Properties 面板中,面板就会变得杂乱,甚至可能被用户误改导致界面异常。

ADR-0008(见 docs/adr/0008-underscore-system-properties.md)要解决的核心问题正是:如何用一条简单、可读、跨端统一的规则,把"用户可见属性"与"系统内部属性"在 frontmatter 层面明确区分开。

决策:以_前缀声明系统属性

ADR 的最终决策可以概括为一条铁律:

任何名称以_开头的 frontmatter 字段都是系统属性。它从 Properties 面板中隐藏、不暴露给搜索与过滤,但在 raw editor(原始编辑模式)中仍然可编辑。frontmatter 解析器在把属性传给 UI 之前会过滤掉所有_*字段。

这条规则带来三个直接后果:

  1. 隐藏而非删除:系统属性只是从面向普通用户的界面(属性面板、搜索、过滤器)中隐去,文件内容本身不受影响,字段依然保留在 frontmatter 中。
  2. 保留可访问性:高级用户仍可通过 raw editor 直接读写这些字段,规避了"系统属性一旦隐藏就无法恢复"的问题。
  3. 一条通用规则:不需要维护"哪些键是系统键"的硬编码清单来判断显隐,前缀本身就足以表达身份。

为什么选择下划线前缀(备选方案对比)

ADR 文档记录了两个被否决的替代方案,理解它们有助于把握这条约定的边界:

方案思路优点缺点结论
Option A(采用)下划线前缀约定:_icon_color_order_pinned_properties规则简单、在原始文件中清晰可读、是普适的通用约定用户必须先知道这条约定,才能访问系统字段✅ 选定
Option B独立的 YAML 块或嵌套的_system:分离更彻底解析更复杂,破坏了扁平的 key-value frontmatter 模型❌ 否决
Option C独立的 sidecar 文件(如.meta.yml完全分离文件数量翻倍,同步难度加大❌ 否决

Option B 与 Option C 的共同问题是打破了 Tolaria 基于"单一 Markdown 文件 + 扁平 frontmatter"的数据模型(参见 ADR-0006 扁平 vault 结构 与 ADR-0025 type 字段规范化)。相比之下,下划线前缀方案零解析负担、零额外文件,代价仅仅是"用户需要知道约定"——而这一点由 UI 隐藏机制兜底,普通用户根本无需关心。

规范化系统属性清单与读写规则

ADR-0008 明确定义了首批规范化的系统属性键(canonical keys),并规定了与旧键的兼容策略:

Canonical key旧键(读取时兼容回退)写入方
_archivedArchivedarchivedArchive 操作
_trashedTrashedtrashedTrash 操作
_trashed_atTrashed attrashed_atTrash 操作
_favoriteFavorite 切换
_favorite_indexFavorite 排序

两条核心规则:

  • 写规则(Write rule):写入时永远使用带_前缀的 canonical 键
  • 读规则(Read rule):读取时同时接受 canonical 键与 legacy 键(不区分大小写),但不在读取时重写文件——历史文件的迁移是另一件独立的事(separate concern)。

这五对键是 ADR 记录的最低限,实际仓库中系统元数据键族远比此更大。从当前源码的 src-tauri/src/frontmatter/keys.rs 可以看到完整的已知键表,除上述五个外还包括:

  • _icon(别名icon)——类型/笔记图标
  • _order(别名order)——排序权重
  • _sidebar_label(别名sidebar_labelsidebar label)——侧边栏显示名
  • _pinned_properties——置顶属性列表
  • _sort(别名sort)——视图排序规则(如title:asc
  • _width(别名width)——编辑器/面板宽度模式
  • _display_list_properties_display——展示模式
  • _organized——组织状态标记

这些键的读取、写入、别名解析均集中在该表中维护,并有专门的测试保证_pinned_properties_list_properties_display等字段必须停留在 canonical 键表中(见 keys.rs 测试)。

真实示例:demo vault 中的类型文档

仓库自带的演示 vault demo-vault-v2/type/project.md 使用了 legacy 风格书写:

--- type: Type icon: rocket color: blue sidebar label: Projects ---

注意:这里iconcolorsidebar label都对应带_前缀的 canonical 系统键。根据 ADR 的"读规则",Rust 解析器会把这些 legacy 键规范化识别为_icon_color_sidebar_label,并阻止它们泄漏进用户属性集合。这也印证了 ADR 关于"读取兼容旧键、写入才规范化"的设计——现有 demo vault 无需批量改写即可正常运行。

双端实现:Rust 与 TypeScript 的协作过滤

ADR 明确要求"前端与后端解析器都过滤_*字段"。当前仓库中这一承诺由两套独立实现共同完成。

TypeScript 端:systemMetadata.ts 与 frontmatter.ts

前端核心在 src/utils/systemMetadata.ts:

  • SYSTEM_METADATA_ALIAS_GROUPS(第 1-14 行)定义了系统元数据的 alias 组,与 Rust 端的键表一一对应;
  • normalizePropertyKey(第 48-50 行)把键名 trim、转小写、空白替换为下划线,实现大小写不敏感;
  • isSystemMetadataKey(第 70-73 行)是判定入口:canonical.startsWith('_') || CANONICAL_BY_ALIAS.has(normalizePropertyKey(key))——既识别_前缀,也识别 legacy alias;
  • canonicalFrontmatterWriteKey(第 61-64 行)保证写入时总是落到 canonical 键。

frontmatter 解析器 src/utils/frontmatter.ts 在解析阶段就通过canonicalFrontmatterKey(第 2 行导入、第 117-119 行、第 132-143 行使用)做键的规范化与冲突归并,确保 UI 拿到的属性集合中不会混入系统字段的重复变体。

属性面板如何隐藏系统字段

在 UI 状态层,src/hooks/usePropertyPanelState.ts 的isHiddenPropertyKey决定了哪些键不进面板:

function isHiddenPropertyKey(key: string): boolean { const canonicalKey = canonicalSystemMetadataKey(key) if (canonicalKey === '_icon') return false return SKIP_KEYS.has(key.toLowerCase()) || isSystemMetadataKey(key) }

其中isSystemMetadataKey(key)兜底隐藏所有系统元数据键。值得注意的是_icon的特例放行——从代码结构看,_icon虽然以_开头,但在属性面板中仍作为可编辑图标字段向用户呈现(其余系统键一律隐藏)。这与 ADR 的"隐藏但不删除、高级用户可经 raw editor 访问"精神互补:对高频且安全的_icon提供友好入口,对_order_pinned_properties等内部键则完全交给 raw editor。

Rust 端:FrontmatterKeyRule 与 is_reserved

后端在 src-tauri/src/frontmatter/keys.rs 中用FrontmatterKeyRule结构统一描述每个已知键的read_keywrite_keyaliases与是否canonicalize_on_write。关键判定是is_reserved(第 144-146 行):

pub(crate) fn is_reserved(self) -> bool { self.normalized().starts_with('_') || is_known_frontmatter_key(self) }

任何以_开头的键(即使不在已知键表中)都被视为保留键;normalized()(第 140-142 行)做 trim、转小写、空白转下划线的规范化,与 TypeScript 端normalizePropertyKey行为一致。这个保留位被 src-tauri/src/vault/frontmatter.rs 等处的属性过滤逻辑引用:当键被判定为 reserved 时,它不会进入propertiesrelationships集合,自然也就不会暴露给搜索、过滤与 UI。

测试验证:系统元数据不泄漏

仓库用一组专门测试锁死了这条约定,见 src-tauri/src/vault/system_metadata_tests.rs:

  • parses_canonical_system_metadata_keys_icon: rocket_color: blue_order: 4_sidebar_label: Projects_sort: title:asc能被正确解析为类型元数据字段;
  • parses_legacy_system_metadata_keys_without_property_leaksiconcolorordersidebar labelsort这些 legacy 键被识别为系统字段,且不会泄漏进 properties/relationships
  • parses_known_aliases_from_central_key_rules_without_leaks:混合大小写与混合风格(Is A_colorsidebar_label)的键全部归一化,且都不进入用户属性集合;
  • ignores_unknown_underscore_keys_in_properties_and_relationships:未登记过的_internal: secret_hidden_link: "[[secret]]"同样被过滤——未知的_键也不会泄漏,而普通用户字段Owner正常保留;
  • ignores_invalid_note_width_modes_width: expanded(非法值)被静默忽略,说明系统键不仅管显隐,还会做取值合法性校验。

这套测试与 ADR 的"不重写旧键、只过滤不迁移"策略互为印证:无论是 canonical 还是 legacy 写法,最终到达 UI 的properties都只包含用户可编辑的属性。

实践指南:如何正确使用系统属性约定

结合 ADR 与源码,给出可落地的操作建议:

  1. 新增系统级字段必须用_前缀。ADR 的 Consequences 明确要求:未来所有系统级 frontmatter 字段都必须遵守_field_name命名约定,并在 keys.rs 与 systemMetadata.ts 的 alias 表中登记别名与写规则。
  2. 写入走 canonical,读取兼容 legacy。业务代码写 frontmatter 时调用canonicalFrontmatterWriteKey(TS 端)或依赖 Rust 端canonicalize_on_write规则,统一写_前缀键;读取时isSystemMetadataKey/is_reserved负责识别历史写法。不要在一次读写中顺手重写用户文件的旧键。
  3. 普通用户通过属性面板,高级用户通过 raw editor。面板已由isHiddenPropertyKey兜底隐藏系统键;需要微调_order_pinned_properties等时,切到 raw editor 直接编辑即可,保存后解析器会重新规范化。
  4. 关注再评估触发条件。ADR 给出的 re-evaluation trigger 是:当系统属性数量增长到足以支撑结构化子对象(sub-object)时,应重新评估是否仍用扁平_前缀约定,届时可回头参考 Option B 的嵌套方案。

总结

ADR-0008 用一条"_前缀 = 系统属性"的约定,在不引入额外文件、不破坏扁平 frontmatter 模型的前提下,干净地解决了"配置型字段污染用户属性面板"的问题。其价值在于约定与实现的双重一致性:Rust 的is_reserved与 TypeScript 的isSystemMetadataKey共享同一套语义(前缀 + alias 表),属性面板、搜索、过滤全部受益于解析阶段的统一过滤,而 raw editor 始终保有对系统字段的完整访问能力。对于任何希望在 Markdown 文件里混存"用户数据"与"应用配置"的知识库类应用,这套约定都提供了低成本、可迁移的参考范式。

进一步阅读:ADR 目录总览、frontmatter 字段参考、类型系统文档、属性相关概念。

【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria

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

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

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

立即咨询