Plate 富文本编辑器日期契约锁定与收口:从 Date Contract Expansion Lane 关闭审计看窄契约治理
【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate
本文以 plate 仓库中 2026-04-10-date-contract-expansion-lane-closeout-plan.md 为骨架,完整还原一次"功能泳道(Lane)关闭审计"的工程实践:当代码、测试与文档早已围绕一个窄日期契约收敛时,如何在路线图中正式宣告该泳道关闭,而不是继续制造虚假的队列压力。读完本文,你将掌握 plate 日期元素的完整数据契约(规范值、回退值、Markdown 往返形状)、关闭一条功能泳道的审计方法与决策模板,以及仓库中与之配套的源码、测试与验证命令。
一、背景:为什么会有 "Date Contract Expansion" 这条泳道
plate 的编辑器行为路线图 master-roadmap.md 中曾长期维护一条名为Lane 3: Date Contract Expansion的活跃待办泳道,其工作假设是:日期节点(date node)的数据契约还需要继续"扩宽"——例如引入更丰富的序列化负载、locale / timezone 语义、展示值与存储值分离等能力。
然而在撰写本计划之前,仓库中的代码与多数文档已经给出了相反的信号:
- 规范值
YYYY-MM-DD已经锁定为唯一 shipped 的日期节点值; - Markdown 写出形状固定为
<date value="YYYY-MM-DD" />; - 遗留的 child-text 日期仅以"读取兼容"的方式保留;
- 更丰富的序列化日期语义被明确推迟(deferred)。
这正是一种典型的"代码已关闭、路线图未关闭"状态。于是本计划的核心命题随之产生:
Lane 3 究竟还是一条约待实现的功能泳道,还是仓库其实已经交付了窄日期契约、剩下的只是路线图与文档层面的"真相收口"(truth cleanup)?
当前假设很明确:Lane 3 的大部分工作实际上已经完成,剩余工作更可能是真相清理,而不是又一次日期 schema 扩展批次。
二、窄日期契约:plate 日期元素的数据定律
在展开审计之前,先完整理解这条"窄契约"到底窄在哪里。它由四个相互锁定的结论构成,这也是整篇文章的事实基石。
2.1 规范值(canonical value)锁定为 YYYY-MM-DD
日期节点的存储值只承认一种规范形态:YYYY-MM-DD字符串,存放在节点的date字段上。这一约定同时体现在:
- 插入变换 insertDate.ts:插入时通过
normalizeDateValue(date ?? new Date())将输入规范化为date字段,随后以editor.tf.insertNodes写入一个type: editor.getType(KEYS.date)的 inline void 节点,并在其后追加一个{ text: ' ' }文本节点以避免丢失编辑器焦点; - 值工具 dateValue.ts:
formatDateValue负责把Date对象格式化为YYYY-MM-DD(使用pad补齐两位月日),parseCanonicalDateValue只接受匹配^\d{4}-\d{2}-\d{2}$且真实存在(年/月/日逐项回验)的日历日期; - 公开文档 date.mdx/(elements)/date.mdx) 的 Value Shape 一节直接给出字段表:
date为规范YYYY-MM-DD值,rawDate为不可规范化输入的回退值。
2.2 回退值(rawDate):不可规范化的文本不丢失
并非所有输入都能被安全地转成日历日期。normalizeDateValue的决策表(详见 dateValue.ts 与 date.mdx/(elements)/date.mdx) 的 Date Normalization 一节)如下:
| 输入 | 存储结果 |
|---|---|
有效的Date对象 | date: formatDateValue(value) |
有效的YYYY-MM-DD字符串 | date(日历日期校验通过时) |
| 无效的规范字符串 | rawDate |
Mon Mar 23 2026这类可被 JavaScript 安全解析的旧格式 | date: '2026-03-23' |
| 空字符串 | 不写任何日期字段 |
| 其他任意文本 | rawDate |
其中LEGACY_DATE_REGEX = /^[A-Z][a-z]{2} [A-Z][a-z]{2} \d{2} \d{4}$/用于识别旧版Date.prototype.toDateString()输出形态(如Mon Mar 23 2026),这类遗留输入会被升级为规范值;而sometime next week这类无法解析的松散文本则原样保留在rawDate中。
2.3 Markdown 写出形状锁定为属性形式
日期元素在 Markdown / MDX 中的写出形状是唯一且确定的:
Date: <date value="2026-03-23" />对应的底层规则位于 defaultRules.ts 的date规则中:
- deserialize:同时读取属性形式(
props.value)与遗留 child-text 形式(取首个文本子节点),统一交给normalizeDateValue决定写入date还是rawDate; - serialize:当
date存在且无rawDate时,写出propsToAttributes({ value: date })的自闭合属性标签;否则退回 child-text 形式写出rawDate ?? date ?? ''。
也就是说:规范值永远以属性形式写出,不可规范化的遗留文本永远以 child-text 形式写出,两条路径互不混淆。
2.4 遗留 child-text 仅保留读取兼容
<date>2024-01-01</date>、<date>Mon Mar 23 2026</date>这类旧式 child-text 写法只作为"读取兼容"路径存在,不再是写出形状。这一点由 dateElement.spec.ts 的四个用例完整固化:
round-trips inline date elements:<date>2024-01-01</date>读入后得到date: '2024-01-01',再序列化写出<date value="2024-01-01" />,且二次读回与首次读回结构一致(往返稳定);reads attribute-bearing date elements:<date value="2024-01-01" />直接读为规范值并原样写出;keeps non-normalizable legacy child text on the raw fallback path:<date>sometime next week</date>落到rawDate,写出时保持 child-text 形态;upgrades safe legacy child-text dates:<date>Mon Mar 23 2026</date>被升级为date: '2026-03-23',写出时变成属性形式。
这四个用例正是"窄契约"在测试层的法律文本。
2.5 更丰富的序列化语义保持显式推迟
以下三类能力不属于当前契约,且被明确标记为 deferred,而不是含糊的"以后再说":
- 展示值 vs 存储值的分离(display-vs-value split);
- locale / timezone 相关的负载语义;
- 超越当前规范属性形式的更丰富序列化日期负载。
在 editor-protocol-matrix.md 的协议矩阵中,日期一行明确写着:display-vs-value, locale/timezone, or richer date payload均属 deferred,且"heavier serialized date semantics beyond the current canonical attribute writer remain deferred until a separate date-expansion lane chooses them explicitly"——即只有出现新的产品证据并显式开新泳道,才会重新评估。
三、源码级证据:窄契约已经 shipped
计划中的 "Current Repo Read" 部分给出了四条代码证据链,我在仓库中逐一核实,全部成立:
| 契约环节 | 落地文件 | 核实结论 |
|---|---|---|
| 插入写入规范日期 | insertDate.ts | 插入默认值经normalizeDateValue归一化,不再产生toDateString() |
| 规范解析/格式化/回退标签 | dateValue.ts | 三种输入分支(Date 对象 / 规范串 / 遗留串)与rawDate兜底齐全 |
| 插件本体 | BaseDatePlugin.ts | key: KEYS.date,isElement / isInline / isVoid三标记,并通过extendEditorTransforms绑定editor.tf.insert.date |
| Markdown 往返测试 | dateElement.spec.ts | 四条用例锁定双形态读写与规范化升级 |
| 交互渲染器 | date-node.tsx | element.date || element.rawDate决定显示;日历选中时editor.tf.setNodes({ date: formatDateValue(date), rawDate: undefined }),只写规范值 |
| 静态渲染器 | date-node-static.tsx | 只读场景同样走getDateDisplayLabel(element),不发明额外序列化语义 |
两个渲染器都基于getDateDisplayLabel生成可见标签:Today/Yesterday/Tomorrow/ 本地化长日期 / 原始回退字符串,其中相对日期(今天/昨天/明天)基于日历日比较(isSameCalendarDay),杜绝了new Date(node.date)直接解析带来的时区漂移问题——这正是 2026-04-09-date-media-expansion-consensus-plan.md 中"canonical payload beats render convenience"与"parse canonicalYYYY-MM-DDas a calendar day without timezone drift"原则的最终落地形态。
此外,isPointNextToNode(isPointNextToNode.ts)提供了"光标是否紧邻某类型节点"的边界查询,用于 inline void 节点在行首/行尾/独占点的键盘访问与退格处理,配套测试见 isPointNextToNode.spec.tsx。
四、决策时刻:Option A 关闭 vs Option B 保持开启
计划的核心是一道二选一决策题,这也是任何"泳道收口"审计的标准决策模板。
Option A:关闭 Lane 3
适用条件:审计确认当前窄日期契约就是预期的正式交付答案。关闭意味着:
- 在路线图中正式关闭 Lane 3;
- 将日期负载开放问题标记为已解答;
- 将更丰富的日期负载、locale/timezone 语义、展示值与存储值分离排除在当前契约之外;
- 未来只有在出现新产品证据时,才以全新泳道的形式重启更丰富的日期工作。
Option B:保持 Lane 3 开启
仅当审计发现真实存在且尚未解决的扩宽决策时才使用,典型触发条件包括:
- 仍有一个被当前文档低估的、受支持的更丰富序列化负载;
- 存在缺失的旧内容兼容路径;
- 产品行为中已经隐含了必须落地的展示值/存储值分离。
计划同时给出了明确判断:当前仓库依据并不指向 Option B——代码、测试与公开文档已经围绕同一个窄日期契约对齐,没有发现真实的扩宽增量。
推荐结论
推荐选择Option A。理由直白:在没有真实扩宽增量(widening delta)的情况下保持泳道开启,只会制造"虚假的队列压力"(fake queue pressure)。这是一条非常值得借鉴的工程原则:功能是否完成,应以代码与测试为准,而不是以路线图的陈旧条目为准。
五、实施单元:三个收口动作
若审计确认无代码缺口,则整个收口只涉及三个单元:
Unit 1:日期契约审计
目标:确认是否真的缺少扩宽工作。审计清单(与仓库现状一一对应):
- insertDate.ts
- dateValue.ts
- dateElement.spec.ts
- date-node.tsx
- date-node-static.tsx
- date.mdx/(elements)/date.mdx)
- markdown.mdx/(serializing)/markdown.mdx)
审计需要回答四个问题:
- 规范
YYYY-MM-DD是否是唯一 shipped 的节点值? - 属性形式的 Markdown 写出是否已经是规范输出?
- 遗留 child-text 日期是否只作为兼容输入处理?
- 渲染器是否发明了法律(law/spec)中不存在的额外序列化语义?
预期结果:无需新增代码,或仅在发现一处矛盾时做一个极小的跟进修复。
Unit 2:路线图与研究文档收口
目标:让书面真相与已交付的日期契约一致。涉及文件:
- master-roadmap.md
- date-mdx-payload-contract.md
- 2026-04-09-date-media-expansion-consensus-plan.md
三项工作:
- 若 Unit 1 确认无真实扩宽缺口,则在路线图中关闭 Lane 3;
- 将日期负载开放问题标记为已解答,并写明当前产品答案;
- 将旧共识文档重构为历史背景,或添加状态块说明"日期半边已关闭,媒体半边仍然独立"。
仓库的最终状态完全符合该单元的预期:master-roadmap.md中 Lane 3 已标记为[x] Date Contract Expansion并注明"This lane is now closed for the current date contract";date-mdx-payload-contract.md 的 frontmatter 已改为status: answered,并在 Current answer 中完整列出 locked / deferred 清单;2026-04-09-date-media-expansion-consensus-plan.md 开头即为"Historical for the date half",明确日期半边已由 shipped 窄契约关闭、媒体/嵌入半边仍作为活跃泳道的背景上下文。
Unit 3:公开文档一致性
目标:确保用户可见文档与代码、路线图口径完全一致。涉及四个产物:
- date.mdx/(elements)/date.mdx)
- markdown.mdx/(serializing)/markdown.mdx)
- editor-protocol-matrix.md
- markdown-parity-matrix.md
三项工作:
- 确认四个产物使用完全相同的窄契约措辞;
- 删除任何暗示活跃泳道中仍有待定更丰富负载的过时表述;
- 将更丰富的日期负载保持为"显式推迟",而不是模糊的"以后再说"。
仓库现状同样吻合:markdown-parity-matrix.md将Date行标记为locked(仅针对窄契约),并列出EDIT-DATE-*系列测试覆盖;editor-protocol-matrix.md以deferred明确标注三类推迟语义。
六、验证命令:审计后的回归清单
计划给出的验证策略是分级的——如果 Unit 1 未发现代码差异,验证只涉及文档;只有在发现运行时/文档矛盾并需要改动代码时,才执行完整验证链:
pnpm install # 日期 Markdown 往返专项测试 bun test packages/markdown/src/lib/dateElement.spec.ts # 日期包全部测试 bun test packages/date/src/lib/**/*.spec.ts # 应用层渲染器测试(交互 + 静态) bun test apps/www/src/registry/ui/date-node.spec.tsx apps/www/src/registry/ui/date-node-static.spec.tsx # 构建与类型检查(仅过滤受影响包) pnpm turbo build --filter=./packages/date --filter=./packages/markdown --filter=./apps/www pnpm turbo typecheck --filter=./packages/date --filter=./packages/markdown --filter=./apps/www # 统一 lint pnpm lint:fix其中date-node.spec.tsx与date-node-static.spec.tsx正是 2026-04-09-date-media-expansion-consensus-plan.md 中"renderers 必须有规范日期显示行为的显式覆盖,而不仅是包级 transform 测试"要求的产物——它把"规范日期渲染无时区漂移、日历编辑写规范值、不可规范化遗留文本走回退路径"固化成了可回归的测试资产。
七、退出标准与下一泳道
Lane 3 达到可关闭状态需同时满足三个条件:
- 审计确认窄日期契约已经是 shipped 的产品答案;
- 路线图、parity、protocol、research 与公开文档就该答案达成一致;
- 任何更丰富的序列化日期语义都被清晰标记为 deferred,而不是含糊暗示。
下一泳道:Lane 4 Media / Embed Expansion
如果 Lane 3 按本计划关闭,下一个真实队列项将是Lane 4:媒体 / 嵌入扩展。它正是 2026-04-09-date-media-expansion-consensus-plan.md 中与日期"同批提出、却未被关闭"的另一半:媒体/嵌入的规范化目前仍分散在 parseIframeUrl.ts、parseVideoUrl.ts、parseTwitterUrl.ts等解析器与 submitFloatingMedia.ts 的提交时变换中,属于"有行为但缺显式契约"的状态,与日期形成鲜明对照——这也反向印证了:日期之所以能干净收口,正是因为它的契约先于 UI 被显式锁定,而媒体尚未走到这一步。
八、方法论总结
把这份关闭计划提炼成可复用的工程方法,一共五步:
- 先看代码,再信路线图:功能是否完成,以
insertDate.ts、dateValue.ts、defaultRules.ts、dateElement.spec.ts这些代码与测试为准; - 用测试固化契约:窄契约的每一条边界(属性写、child-text 读、遗留升级、回退兜底)都必须有对应的 spec 用例;
- 显式推迟而非模糊承诺:
deferred要写清"推迟了什么、为什么推迟、什么条件下才重启",而不是一句"以后再说"; - 文档同步收口:路线图、研究开放问题、协议矩阵、parity 矩阵、公开文档五类产物必须同口径;
- 无增量即关闭:没有真实的扩宽 delta,就不要让陈旧泳道继续占据队列制造虚假压力。
plate 的日期元素由此成为一个教科书级的"窄契约治理"样本:规范值、回退值、序列化形状、兼容路径四件事边界清晰,代码、测试、文档三层证据相互印证,最终以一个干净的 Lane 关闭审计画上句号。
【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考