
TinaCMS MDX 块模板多富文本字段解析从 in.md 输入到 AST 结构的完整剖析【免费下载链接】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/tinacmsTinaCMS 的 MDX 引擎允许在块Block模板中同时定义多个富文本字段其中children字段承载块标签内的正文内容其余富文本字段则以 JSX 属性表达式的形式传入。本文以仓库中的mdx-blocks-multiple-rich-text-fields测试用例为骨架结合解析器与序列化器源码完整还原多个富文本字段块从 MDX 输入到 AST、再回到 MDX 的往返链路并给出可直接复用的配置写法与原理说明。一、案例全景一个包含两个富文本字段的 Cta 块在 TinaCMS 中富文本字段type: rich-text可以通过templates声明一组可在编辑器内嵌入的块。当某个块模板需要同时承载一段富文本属性和一段块内正文时就构成了多富文本字段场景。本案例的输入文件 in.md 只有 7 行却完整覆盖了这一场景Cta description{ Read our privacy policy [here](http://example.com) /} Click **here**! /Cta可以看到这里存在两个独立携带富文本内容的通道description通过JSX 属性表达式传入一段富文本内含一个指向http://example.com的链接here标签体childrenCta.../Cta之间的内容是一段包含粗体**here**的富文本。这种写法在真实站点中非常常见——例如一个行动号召组件description用于渲染说明文案children用于渲染按钮或链接文案。二、模板配置如何声明多富文本字段的块这两个富文本字段必须在 Schema 配置中显式声明才能被编辑器与 MDX 解析器识别。测试用例的字段定义位于 field.tsimport { 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 }, ], }, ], };几个关键点外层字段body是文章正文的富文本字段parser: { type: mdx }声明使用 MDX 语法解析而非纯 Markdown模板字段templates数组中的Cta模板声明了两个字段——description与children。其中children是约定俗成的特殊字段名它表示该块标签内的正文内容其余富文本字段如description则被解析为块的属性rich-text 字段递归嵌套模板字段本身也可以是rich-text类型TinaCMS 的解析器会递归收集所有层级的模板这意味着富文本块可以继续嵌套富文本块构成任意深度的组合结构。三、解析器视角in.md 被转换成什么 AST理解多富文本字段的关键在于观察解析后的抽象语法树AST。测试用例的期望快照 node.json 展示了parseMDX的输出结构{ 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 透露了核心设计块节点形态Cta被解析为mdxJsxFlowElement节点name为Cta富文本属性字段description被解析为props下的一个嵌套的完整root树。[here](http://example.com)成为a节点含url与title: null文本Read our privacy policy 被拆分为独立的text节点并包裹在p段落节点中。也就是说属性富文本拥有与正文同等的、完整的 Markdown/MDX 表达力children 富文本字段标签体内的内容同样被解析为props.children下的独立root树其中粗体**here**表示为带bold: true标记的text节点空文本占位块节点的children数组内存在一个空字符串text节点这是解析器为 JSX 元素保留的容器占位。四、底层原理模板提升与字段模式匹配为什么解析器能准确识别description属性是一段富文本、并为其单独建树答案在 util.ts 的getFieldPatterns与hoistAllTemplates中。export const getFieldPatterns (field: RichTextField) { const patterns: Pattern[] []; const templates: RichTextTemplate[] []; hoistAllTemplates(field, templates); templates?.forEach((template) { // ... if (template.match) { patterns.push({ start: template.match.start, end: template.match.end, name: template.match.name || template.name, templateName: template.name, type: template.inline ? inline : flow, leaf: !template.fields.some((f) f.name children), }); } }); return patterns; };模板提升hoistinghoistAllTemplates会遍历field.templates把每个模板加入列表若模板字段中还有rich-text字段则递归提升其内部的模板。注释明确解释了原因Markdown 解析器不关心字符串中的位置难以在一次遍历中判断节点是否嵌套在父字段内部因此干脆把所有模板都按顶层处理leaf 判定leaf: !template.fields.some((f) f.name children)—— 一个块是否叶子块取决于它是否包含名为children的字段。包含children的模板如本例的Cta是有正文容器的非叶子块匹配模式通过template.match.start / end / name生成解析器可用的模式Pattern用于在解析阶段识别自定义组件。随后parse/markdown.ts 将这些模式注入micromark/mdast-util-from-markdown扩展链const tree mdastFromMarkdown(value, { extensions: [ gfm(), mdxJsx({ acorn, patterns, addResult: true, skipHTML }), ], mdastExtensions: [gfmFromMarkdown(), mdxJsxFromMarkdown({ patterns })], });而公开入口 parse/index.ts 的parseMDX(value, field, imageCallback)在fromMarkdown之后还会执行compact(tree)与postProcessor合并相邻同类型节点并完成图片回调等后处理最终产出上文展示的规整 AST。五、往返验证parseMDX 与 serializeMDX 的闭环测试多富文本字段块不只是能解析还必须在编辑器保存时无损序列化回 MDX。测试 index.test.ts 用两个断言锁定了这条闭环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快照逐字一致通过 tests/util.ts 的print去除position定位信息后做 JSON 快照对比第二段断言serializeMDX的输出与out.md快照一致保证 AST 能还原为等价的 MDX 源码。序列化端入口位于 stringify/index.ts其流程为normalizeMarkWhitespace(preProcess(value, field, imageCallback))后交给toTinaMarkdown。序列化器必须对props下的富文本root树与children富文本树分别处理前者输出为属性{\n ...\n/}的 JSX 表达式块后者输出为标签体内容——这正是本例中description与块体两种富文本能共存于同一块的根本保证。六、延伸与边界多富文本块在 TinaCMS 测试矩阵中的位置在 tests 目录 中mdx-blocks-multiple-rich-text-fields并非孤例它与一组兄弟用例共同构成了块/短代码富文本的回归矩阵mdx-blocks-rich-text-children系列验证仅含children富文本的块以及单行、空对象值等边界如mdx-blocks-rich-text-children-on-one-line、mdx-blocks-rich-text-children-with-an-empty-object-valuemarkdown-shortcodes-rich-text-children系列验证通过match正则定义的短代码风格的富文本块mdx-block-scalar-fields、mdx-block-object-list-field验证块携带标量字段、对象列表字段等非富文本属性时与富文本共存的解析行为mdx-jsx-*系列验证条件表达式、模板字符串、一元表达式等 JSX 属性语法在富文本属性中的表现。这些用例共同说明当你在 TinaCMS 的 Schema 中为一个块模板声明多个rich-text字段时解析器会为每个字段维护独立的富文本子树而children是唯一被约定为标签体正文的字段名——掌握这一约定就能在设计复杂内容模型时准确预判 AST 结构与序列化结果。七、实战小结配置层面在rich-text字段的templates中声明块模板把需要属性化的富文本命名为普通字段名如description把标签体正文命名为children写作层面属性富文本以attr{\n ... \n/}的 JSX 表达式书写正文富文本直接写在标签体内两者都支持链接、粗体等完整 Markdown 语法原理层面hoistAllTemplates递归收集所有嵌套模板leaf依据是否含children判定解析后每个富文本通道都是一棵独立的root子树序列化时再按属性/正文两条路径还原验证层面借助parseMDXserializeMDX的双向快照测试可确保新增或调整块结构时解析与序列化行为不回归。以 in.md、field.ts 与 node.json 三件套为参照你可以在自己的 TinaCMS 内容模型中放心使用一个块、多个富文本字段的组合能力。【免费下载链接】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),仅供参考