TinaCMS MDX 块组件多富文本字段解析:parseMDX / serializeMDX 的往返一致性机制解析
【免费下载链接】tinacmsTinaCMS is the leading open-source headless CMS that supports Markdown and Visual Editing. Your content is stored in your own GitHub repo 🦙 ❤️项目地址: https://gitcode.com/GitHub_Trending/ti/tinacms
TinaCMS 的 MDX 处理器(位于packages/@tinacms/mdx)负责将基于 Markdown 的内容与 Tina 内部 AST(富文本编辑器可识别的节点树)互相转换。本文以仓库中的测试夹具mdx-blocks-multiple-rich-text-fields为核心,深入剖析「一个块组件同时携带多个富文本字段(属性字段 + children 字段)」时,TinaCMS 如何完成解析(MDX → 节点树)、序列化(节点树 → MDX)以及往返一致性(round-trip)验证,帮助你理解在 Tina 的 rich-text 字段中定义带富文本属性的模板组件时的底层行为。
场景总览:一个块组件包含多个富文本字段
在 TinaCMS 中,rich-text类型的字段支持通过templates定义可嵌入的块组件(如 Call-to-action 按钮、卡片、横幅等)。这些组件既可以拥有普通的标量属性(字符串、布尔值等),也可以拥有本身就是富文本内容的属性,甚至可以拥有作为子内容(children)的富文本块。
本测试场景的目标文件是 out.md,其内容只有短短几行,却浓缩了这一完整能力:
<Cta description={<> Read our privacy policy [here](http://example.com) </>} > Click **here**! </Cta>观察这份 MDX 源码可以提炼出三个关键点:
- 块组件
<Cta>使用了 JSX 属性语法:description={<>...</>}通过 Fragment(<>)语法将一段 Markdown 内容作为属性值传入; - 属性内的 Markdown 被完整保留:
[here](http://example.com)是一个标准的 Markdown 链接,说明属性富文本支持内联 Markdown 语法; - 组件 children 也是富文本:
Click **here**!中的**here**是粗体标记,说明块内容同样按富文本解析。
这份文件是测试的「期望输出」快照(snapshot),它与输入文件 in.md 内容完全一致——这正是「序列化结果与原始输入一致」这一往返一致性断言的核心证据。
字段配置:如何定义一个含多富文本字段的模板
驱动这一场景的是 field.ts,它定义了测试用的RichTextField:
import { RichTextField } from '@tinacms/schema-tools'; export const field: RichTextField = { name: 'body', type: 'rich-text', parser: { type: 'mdx' }, templates: [ { name: 'Cta', label: 'Call-to-action', fields: [ { type: 'rich-text', name: 'description' }, { type: 'rich-text', name: 'children' }, ], }, ], };逐项拆解这份配置:
| 配置项 | 取值 | 作用 |
|---|---|---|
name | 'body' | 字段名,代表整篇内容体;` |
type | 'rich-text' | 声明这是富文本字段,内容由编辑器与 MDX 处理器共同管理 |
parser.type | 'mdx' | 声明使用 MDX 语法解析器而非普通 Markdown(普通 Markdown 无法表达 JSX 组件与属性) |
templates[].name | 'Cta' | 块组件名,与 MDX 中的<Cta>标签一一对应 |
templates[].fields[0] | { type: 'rich-text', name: 'description' } | 定义属性型富文本字段 |
templates[].fields[1] | { type: 'rich-text', name: 'children' } | 定义内容型富文本字段 |
这里有一个容易被忽视但非常关键的细节:children也以普通字段的形式出现在fields列表中。当 MDX 处理器遇到<Cta>...</Cta>时,会依据fields中的children字段声明,将标签内的内容解析为一个富文本子树(root 节点),而不是简单当作普通字符串。
从源码结构看,field.ts中的parser: { type: 'mdx' }是切换到 MDX 解析路径的开关。在 parse/index.ts 中,公开入口parseMDX(value, field, imageCallback)会先调用fromMarkdown(value, field)完成语法树构建,再经postProcessor(compact(tree), field, imageCallback)做后处理;而stringifyMDX(stringify/index.ts)则先做preProcess与normalizeMarkWhitespace,最终由toTinaMarkdown输出。整个链路都依赖field中的templates与fields元数据来识别组件与富文本字段的边界。
解析结果:node.json 中的 AST 结构
当parseMDX处理上面的输入后,会得到一份与 node.json 完全一致的节点树。这份 JSON 是整个测试场景的「中间态」,它揭示了内部 AST 的精确结构:
{ "type": "root", "children": [ { "type": "mdxJsxFlowElement", "name": "Cta", "children": [ { "type": "text", "text": "" } ], "props": { "description": { "type": "root", "children": [ { "type": "p", "children": [ { "type": "text", "text": "Read our privacy policy " }, { "type": "a", "url": "http://example.com", "title": null, "children": [ { "type": "text", "text": "here" } ] } ] } ] }, "children": { "type": "root", "children": [ { "type": "p", "children": [ { "type": "text", "text": "Click " }, { "type": "text", "text": "here", "bold": true }, { "type": "text", "text": "!" } ] } ] } } } ] }这份 AST 值得逐层拆解:
- 顶层
root:整篇文档的根节点,与 Tina 富文本编辑器中的文档根对应; mdxJsxFlowElement:块级 JSX 元素的内部表示。name: "Cta"与字段模板中的模板名对应;其children里有一个空的text节点,用于占位;props.description:属性型富文本字段的解析结果。它本身是一个完整的root子树,内部是p(段落)节点,段落里依次是普通文本节点"Read our privacy policy "与链接节点a(url为http://example.com,title为null,子节点为文本"here")。这说明属性富文本与正文富文本使用完全相同的节点体系;props.children:内容型富文本字段的解析结果,同样是一个root子树,段落p内包含三个文本节点,其中"here"被标记为"bold": true,对应输入中的**here**。
由此可以确认:在 TinaCMS 的 MDX 内部模型中,「多个富文本字段」并非扁平化存储,而是每个字段都拥有独立完整的 root 子树,彼此互不干扰。这种设计保证了在可视化编辑器中,用户可以分别编辑 CTA 的描述文字(属性区)与按钮/正文内容(子内容区),两边的 Markdown 语法(链接、粗体等)都会被独立且正确地解析和渲染。
往返一致性:parseMDX 与 serializeMDX 的闭环验证
测试的核心逻辑位于 index.test.ts:
import { expect, it } from 'vitest'; import { parseMDX } from '../../../parse'; import { serializeMDX } from '../../../stringify'; import * as util from '../util'; import { field } from './field'; import input from './in.md?raw'; it('matches input', () => { const tree = parseMDX(input, field, (v) => v); expect(util.print(tree)).toMatchFile(util.nodePath(__dirname)); const string = serializeMDX(tree, field, (v) => v); expect(string).toMatchFile(util.mdPath(__dirname)); });这个用例由两个断言构成一条完整的闭环:
- 正向:parseMDX 与 node.json 快照比对。
parseMDX(input, field, (v) => v)将 in.md 解析为节点树,util.print(tree)序列化为 JSON 字符串后与 node.json 做快照比对,验证「MDX → AST」的正确性; - 反向:serializeMDX 与 out.md 快照比对。再将树传给
serializeMDX还原为 MDX 文本,与 out.md 比对,验证「AST → MDX」的正确性。
由于 in.md 与 out.md 逐字相同,这一测试实际验证了「多富文本字段块组件在解析再序列化后,输出与输入完全一致」的往返一致性(round-trip)保证。这是 TinaCMS 确保「内容在编辑器中修改后写回 Markdown 文件不丢格式」的基石——任何一端丢失属性富文本、错误折叠 children、或破坏 Fragment 语法的改动,都会立刻被这个快照测试捕获。
值得注意(v) => v这个恒等函数:它是imageCallback参数(图片路径回调),测试中直接原样返回,避免图片路径处理干扰对多富文本字段行为的观察。
测试基础设施:快照机制如何工作
index.test.ts依赖的util(tests/util.ts)提供了统一的快照工具:
print(tree):先通过removePosition递归删除 AST 中的position信息(源码位置元数据,会随输入换行格式变化,不适合做快照比对),再以 2 空格缩进输出 JSON;nodePath(dir)/mdPath(dir):分别定位当前测试目录下的node.json与out.md;expect.extend({ toMatchFile }):基于jest-file-snapshot提供文件级快照匹配能力。
「删除position再做比对」这一点值得展开:mdast解析出的节点普遍带有position(起始/结束行列号),它们属于解析过程的产品而非内容本身。测试统一剥离它们,既保证了快照在不同编辑环境下的稳定性,也让node.json只聚焦于内容语义——这正是上面 AST 结构能被直观阅读的原因。
同类场景:该测试在测试矩阵中的位置
在 tests 目录 中,围绕「块组件 + 富文本」存在一整组互补的测试场景,每个场景一个目录,统一使用 in.md / out.md / node.json / field.ts / index.test.ts 的结构:
mdx-blocks-rich-text-children:仅含 children 富文本的块组件基础场景;mdx-blocks-rich-text-children-on-one-line:children 富文本全部写在同一行的边界情况;mdx-blocks-rich-text-children-with-an-empty-object-value:children 值为空对象时的容错行为;mdx-blocks-autoformat-nested-mdx/mdx-blocks-autoformat-nested-mdx-null-children:嵌套 MDX 的自动格式化及 children 为 null 的边界;mdx-block-scalar-fields:块组件携带标量(非富文本)属性的对照场景;mdx-block-object-list-field/mdx-block-object-list-template:对象列表字段与对象列表模板场景。
本场景mdx-blocks-multiple-rich-text-fields在这些测试中的独特地位在于:它是唯一同时覆盖「属性富文本字段」与「children 富文本字段」的场景,直接检验多富文本字段共存时 parse/serialize 两个方向的行为,与mdx-block-scalar-fields(标量属性)形成鲜明对照:标量属性在 AST 中以普通值存储,而富文本属性在 AST 中以root子树递归存储。
总结与实践要点
围绕 out.md 这一核心夹具,可以得到以下可直接应用于实践的认识:
- 模板字段声明决定解析行为:在 Tina 的 rich-text 模板中,凡是声明为
type: 'rich-text'的字段(包括children),其内容在 MDX 中都会按独立富文本子树解析;声明为其他类型的字段则按普通属性处理; - 属性富文本使用 Fragment 语法:在 MDX 中给组件传富文本属性必须写作
prop={<>\n Markdown 内容\n</>},这是 Tina 处理器约定的可识别格式,与测试输入中的写法保持一致; - 往返一致性是格式保真的前提:Tina 内容以 Markdown 文件存于仓库,编辑过程即「parse → 编辑 AST → serialize」的闭环。此类快照测试保证多富文本字段在这种闭环下不丢格式、不改变顺序、不破坏链接与粗体等标记;
- 回归测试结构可复用:若要为自定义模板组件新增行为验证,可仿照本测试在
tests/下新建目录,提供field.ts、in.md与期望的node.json/out.md,即可获得 parse 与 serialize 双向快照保护。
理解这一机制后,你便能解释为什么 TinaCMS 能在保留仓库内 Markdown 可读性的同时,支持在可视化编辑器中编辑块组件内的多个富文本区域——这正是本测试场景所锚定的核心能力。
【免费下载链接】tinacmsTinaCMS is the leading open-source headless CMS that supports Markdown and Visual Editing. Your content is stored in your own GitHub repo 🦙 ❤️项目地址: https://gitcode.com/GitHub_Trending/ti/tinacms
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考