Plate 富文本编辑器日期契约锁定与收口:从 Date Contract Expansion Lane 关闭审计看窄契约治理
2026/9/16 14:29:10 网站建设 项目流程

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 的四个用例完整固化:

  1. round-trips inline date elements<date>2024-01-01</date>读入后得到date: '2024-01-01',再序列化写出<date value="2024-01-01" />,且二次读回与首次读回结构一致(往返稳定);
  2. reads attribute-bearing date elements<date value="2024-01-01" />直接读为规范值并原样写出;
  3. keeps non-normalizable legacy child text on the raw fallback path<date>sometime next week</date>落到rawDate,写出时保持 child-text 形态;
  4. 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.tskey: KEYS.dateisElement / isInline / isVoid三标记,并通过extendEditorTransforms绑定editor.tf.insert.date
Markdown 往返测试dateElement.spec.ts四条用例锁定双形态读写与规范化升级
交互渲染器date-node.tsxelement.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)

审计需要回答四个问题:

  1. 规范YYYY-MM-DD是否是唯一 shipped 的节点值?
  2. 属性形式的 Markdown 写出是否已经是规范输出?
  3. 遗留 child-text 日期是否只作为兼容输入处理?
  4. 渲染器是否发明了法律(law/spec)中不存在的额外序列化语义?

预期结果:无需新增代码,或仅在发现一处矛盾时做一个极小的跟进修复。

Unit 2:路线图与研究文档收口

目标:让书面真相与已交付的日期契约一致。涉及文件:

  • master-roadmap.md
  • date-mdx-payload-contract.md
  • 2026-04-09-date-media-expansion-consensus-plan.md

三项工作:

  1. 若 Unit 1 确认无真实扩宽缺口,则在路线图中关闭 Lane 3;
  2. 将日期负载开放问题标记为已解答,并写明当前产品答案;
  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

三项工作:

  1. 确认四个产物使用完全相同的窄契约措辞;
  2. 删除任何暗示活跃泳道中仍有待定更丰富负载的过时表述;
  3. 将更丰富的日期负载保持为"显式推迟",而不是模糊的"以后再说"。

仓库现状同样吻合:markdown-parity-matrix.mdDate行标记为locked(仅针对窄契约),并列出EDIT-DATE-*系列测试覆盖;editor-protocol-matrix.mddeferred明确标注三类推迟语义。

六、验证命令:审计后的回归清单

计划给出的验证策略是分级的——如果 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.tsxdate-node-static.spec.tsx正是 2026-04-09-date-media-expansion-consensus-plan.md 中"renderers 必须有规范日期显示行为的显式覆盖,而不仅是包级 transform 测试"要求的产物——它把"规范日期渲染无时区漂移、日历编辑写规范值、不可规范化遗留文本走回退路径"固化成了可回归的测试资产。

七、退出标准与下一泳道

Lane 3 达到可关闭状态需同时满足三个条件:

  1. 审计确认窄日期契约已经是 shipped 的产品答案;
  2. 路线图、parity、protocol、research 与公开文档就该答案达成一致;
  3. 任何更丰富的序列化日期语义都被清晰标记为 deferred,而不是含糊暗示。

下一泳道:Lane 4 Media / Embed Expansion

如果 Lane 3 按本计划关闭,下一个真实队列项将是Lane 4:媒体 / 嵌入扩展。它正是 2026-04-09-date-media-expansion-consensus-plan.md 中与日期"同批提出、却未被关闭"的另一半:媒体/嵌入的规范化目前仍分散在 parseIframeUrl.ts、parseVideoUrl.tsparseTwitterUrl.ts等解析器与 submitFloatingMedia.ts 的提交时变换中,属于"有行为但缺显式契约"的状态,与日期形成鲜明对照——这也反向印证了:日期之所以能干净收口,正是因为它的契约先于 UI 被显式锁定,而媒体尚未走到这一步。

八、方法论总结

把这份关闭计划提炼成可复用的工程方法,一共五步:

  1. 先看代码,再信路线图:功能是否完成,以insertDate.tsdateValue.tsdefaultRules.tsdateElement.spec.ts这些代码与测试为准;
  2. 用测试固化契约:窄契约的每一条边界(属性写、child-text 读、遗留升级、回退兜底)都必须有对应的 spec 用例;
  3. 显式推迟而非模糊承诺deferred要写清"推迟了什么、为什么推迟、什么条件下才重启",而不是一句"以后再说";
  4. 文档同步收口:路线图、研究开放问题、协议矩阵、parity 矩阵、公开文档五类产物必须同口径;
  5. 无增量即关闭:没有真实的扩宽 delta,就不要让陈旧泳道继续占据队列制造虚假压力。

plate 的日期元素由此成为一个教科书级的"窄契约治理"样本:规范值、回退值、序列化形状、兼容路径四件事边界清晰,代码、测试、文档三层证据相互印证,最终以一个干净的 Lane 关闭审计画上句号。

【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate

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

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

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

立即咨询