ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TinaCMS 富文本 Shortcode 嵌套与 Rich-Text Children:从 match 模板定义到 Markdown 无损往返

TinaCMS 富文本 Shortcode 嵌套与 Rich-Text Children:从 match 模板定义到 Markdown 无损往返 TinaCMS 富文本 Shortcode 嵌套与 Rich-Text Children从 match 模板定义到 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/tinacmsTinaCMS 的tinacms/mdx包在 Markdown 解析层引入了短代码shortcode支持让内容作者可以用{{% feature-panel Test %}}这类 Hugo 风格语法在富文本正文中嵌入结构化的可编辑块。本文以仓库中packages/tinacms/mdx/src/next/tests/markdown-shortcodes-rich-text-children-3测试目录为解剖样本完整拆解 shortcode 模板的字段契约、带children的富文本嵌套解析、序列化还原链路以及快照测试如何保证解析 → 序列化的无损往返——读完你既能照抄配置写出可运行的模板也能理解底层实现原理。一、本文主角一个测试夹具 out.md关联文档 out.md 全文只有 7 行但它不是一篇说明文档而是一份快照测试的期望输出Testing again {{% feature-panel Test %}} {{% pull-quote fooTesting %}} Things {{% /pull-quote %}}它属于tinacms/mdx包中src/next新一代 Markdown 解析器注释表明该实现引入自 commit 651b6b53b Add next module for mdx behavior的测试体系。同一目录下还有四个配套文件共同组成一个完整的往返测试用例文件作用in.md测试输入与 out.md 内容逐字节一致out.md快照期望输出node.json解析后的结构化节点树快照field.ts定义该富文本字段与 shortcode 模板的 schemaindex.test.tsvitest 测试入口in.md与out.md完全一致本身就是一个重要结论这段包含嵌套 shortcode 的 Markdown经过解析再序列化后可以原样还原。而 out.md 中隐藏的技术要点有三层{{% feature-panel Test %}}是一个块级flowshortcode带一个无键值参数Test{{% pull-quote fooTesting %}} ... {{% /pull-quote %}}是带富文本子节点rich-text children的嵌套 shortcode子内容Things会被解析成一个独立的嵌套富文本树两个相邻的块级 shortcode 之间保留空行说明块级元素序列化时使用了容器语义containerFlow。二、模板即语法契约field.ts 中的 match 配置shortcode 并不是解析器硬编码识别的而是由富文本字段上的templates通过match属性声明的。看 field.tsimport { RichTextField } from tinacms/schema-tools; export const field: RichTextField { name: body, type: rich-text, parser: { type: markdown }, templates: [ { name: featurePanel, label: Feature Panel, match: { start: {{%, end: %}}, name: feature-panel, }, fields: [ { name: _value, required: true, isTitle: true, label: Value, type: string, }, ], }, { name: pullQuote, label: Pull Quote, match: { start: {{%, name: pull-quote, end: %}}, }, fields: [ { name: foo, label: foo label, type: string }, { name: children, label: Children, type: rich-text, }, ], }, ], };逐个拆解这里的契约match.start/match.end定义 shortcode 的左右定界符这里使用 Hugo 风格的{{%与%}}。tinacms/mdx的测试套件还覆盖了{{/}}见 markdown-shortcodes-rich-text-children/in.md、WordPress 风格、Markdoc 风格等其他定界符组合见下文第七节说明定界符是完全可配置的。match.nameshortcode 在 Markdown 文本中的语法名。注意它在序列化时使用{{% feature-panel %}}而模板的namefeaturePanel是内部节点名。_value字段这是 TinaCMS 的一个约定——当 shortcode 携带无键值参数如Test时解析器会自动把它映射到名为_value的字段上。在 mdast 处理实现中exitMdxJsxTagAttributeValueLiteral明确做了if (attribute.name ) { attribute.name _value; }的归一化处理。children字段rich-text 类型模板字段列表中一旦出现名为children的富文本字段该 shortcode 就不是叶子节点其内部内容会被解析为嵌套的富文本子树。这正是 util.ts 中计算leaf标志的依据leaf: !template.fields.some((f) f.name children)——没有 children 字段的模板是叶子序列化时采用自闭合写法有 children 的模板则需要成对的开闭标签。三、解析链路从 Markdown 文本到结构化节点树先看 index.test.ts 如何驱动这条链路import { parseMDX } from ../../parse; import { stringifyMDX } 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); const string stringifyMDX(tree, field, (v) v); expect(util.print(tree)).toMatchFile(util.nodePath(__dirname)); expect(string).toMatchFile(util.mdPath(__dirname)); });解析入口是 parse/index.ts 中的parseMDX(value, field, imageCallback)内部流程为fromMarkdown(value, field) // 基于 micromark / mdast 的解析 → compact(tree) // 压缩相邻同类节点 → postProcessor(tree, field, imageCallback) → remarkToSlate(...) // 转换为 Tina 内部富文本表示3.1 模式如何变成解析规则fromMarkdown在 markdown.ts 中把mdxJsx扩展接入 micromark。扩展构造逻辑在 shortcodes/lib/syntax.ts将每个模板的match归一化为Pattern结构{ start, end, name, templateName, type: inline | flow, leaf }按pattern.start的首字符建立索引flowRules[firstCharacter]与textRules[firstCharacter]分别挂载jsxFlow/jsxText构造器因此多个 shortcode 共用同一前缀时会被追加到同一规则数组若开启skipHTML会禁用htmlFlow/htmlTexttoken避免原生 HTML 解析与 shortcode 语法冲突。3.2 标签解析与 _value 映射真正的标签级解析在 shortcodes/mdast/index.ts 的mdxJsxFromMarkdown中完成。与本文案例直接相关的关键点开标签进入enterMdxJsxTag用栈结构跟踪标签读到属性时按mdxJsxAttribute压入attributes数组无键值属性归一化exitMdxJsxTagAttributeValueLiteral中无名属性被改写为_value见上文第二节的源码引用并把字面量经parseEntities解析为字符串闭标签校验exitMdxJsxTag中若tag.close tail.name ! tag.name会抛出end-tag-mismatch的VFileMessage即标签名不匹配是硬错误节点命名找到匹配的 pattern 后节点名取pattern.templateName || tag.namemdast/index.ts所以 node.json 中显示的是featurePanel/pullQuote这样的模板名容错降级shouldFallback机制会把无法配对的开闭标记还原为普通文本节点而不是直接报错中断这是markdown-shortcodes-invalid-*系列用例的行为基础。3.3 嵌套富文本子节点如何收敛为树解析出mdxJsxFlowElement后真正的富文本子节点魔法发生在 parse/post-processing.tsif (node.children.length) { let tree; if (node.type mdxJsxTextElement) { tree postProcessor( { type: root, children: [{ type: paragraph, children: node.children }] }, field, imageCallback ); } else { tree postProcessor( { type: root, children: node.children }, field, imageCallback ); } props.children tree; } node.props props; delete node.attributes; node.children [{ type: text, text: }];要点是对 shortcode 内部的子节点以root为根递归调用postProcessor再做一遍完整后处理得到的整棵子树被放进props.children即children字段的值而节点自身的children被重置为一个空文本节点。这样就形成了shortcode 属性里套一棵富文本文档树的嵌套结构——正是 node.json 中所呈现的样子{ type: root, children: [ { type: p, children: [{ type: text, text: Testing again }] }, { type: mdxJsxFlowElement, name: featurePanel, children: [{ type: text, text: }], props: { _value: Test } }, { type: mdxJsxFlowElement, name: pullQuote, children: [{ type: text, text: }], props: { foo: Testing, children: { type: root, children: [ { type: p, children: [{ type: text, text: Things }] } ] } } } ] }可以看到Test进入props._valuefooTesting进入props.foo而Things则完整地变成了props.children下的一棵root → p → text子树。此外util.ts 的hoistAllTemplates会把字段树中所有嵌套 rich-text 字段上的模板递归打平统一参与顶层解析——也就是说即使children子树里又声明了自己的模板也能被同一套模式表识别。四、序列化链路从节点树还原 shortcode 语法序列化入口是 stringify/index.ts 的stringifyMDXpreProcess(value, field, imageCallback) → normalizeMarkWhitespace(...) → toTinaMarkdown(mdTree, field)核心的 Markdown 输出逻辑在 stringify/to-markdown.ts通过getFieldPatterns(field)重新收集模式表传给mdxJsxToMarkdown({ patterns })扩展转义策略与match的关系源码注释明确说明一旦模板声明了match就假定用户需要默认转义保证{{不会被转义成{{\这类形式而parser.skipEscaping提供了all完全不转义与html放行两个可选档位供那些由其他工具负责解析 Markdown 的场景使用。shortcode 的具体还原逻辑同样在 shortcodes/mdast/index.ts 的mdxElement处理器中开标签输出为pattern.start patternName即{{% feature-panel_value字段还原为裸值序列化属性时if (left _value) { result right; }因此_value: Test输出为Test而不是_valueTestmdast/index.ts普通键值属性输出为keyvalue默认双引号支持quoteSmart智能切换引号属性较多或超出行宽时支持按行缩进换行attributesOnTheirOwnLine子节点输出对mdxJsxFlowElement先输出开标签与pattern.end再以containerFlow输出子内容Things前后各补一个换行对行内元素则用containerPhrasing闭标签输出pattern.start / patternName pattern.end即{{% /pull-quote %}}mdast/index.ts叶子模板无 children在自闭合后不再输出闭标签。正是这些规则保证了 out.md 中{{% feature-panel Test %}}与{{% pull-quote fooTesting %}} Things {{% /pull-quote %}}的精确还原。五、无损往返如何被快照测试锁定往返测试的根基是 tests/util.ts 提供的快照机制print(tree)先把树中的position字段递归剔除再输出格式化 JSON从而让快照与源码位置信息解耦nodePath/mdPath分别指向用例目录下的node.json与out.md通过expect.extend({ toMatchFile })基于jest-file-snapshot将实际结果与快照文件逐字节比对。这意味着out.md不仅是文档更是一份可回归验证的契约只要未来修改了解析器或序列化器导致{{% ... %}}的任一输出细节定界符、属性引号、空行、闭标签格式发生漂移index.test.ts就会立刻失败。这解释了为什么 out.md 的每一行、每一个空行都值得认真对待——它们都是经过测试背书的行为规范。六、边界与变体同族测试用例的横向印证src/next/tests目录下围绕 shortcode 能力形成了一个完整的用例族可与本文案例互相印证markdown-shortcodes-rich-text-children / -2使用{{/}}定界符 children 富文本。对比两者 in.md 与 markdown-shortcodes-rich-text-children-2/in.md 可见即使输入写成紧凑的{{some-feature}}两者最终的 out.md 都会统一还原为带空格的规范写法{{ some-feature }}说明输出格式是规范化的而不是对输入的机械复刻markdown-shortcodes-inline其 field.ts 中模板声明了inline: true对应行内 shortcode 模式text 规则markdown-shortcodes-invalid / -invalid-2 / -3 / -4 / -unclosed从用例内容看这些用例覆盖声明允许 children 却未提供标签未闭合等异常输入验证了shouldFallback降级为普通文本、或直接丢弃无效 shortcode 的行为例如 markdown-shortcodes-invalid-4/out.md 中 shortcode 本身未出现在输出中unrecognized-shortcodes未注册的{{ some-other shortcode }}会被原样保留为普通文本不会静默丢弃见 unrecognized-shortcodes/out.mdmarkdown-shortcodes-markdoc / wordpress-style / wordpress-style-2从测试目录命名可以推断这些用例分别验证 Markdoc 风格{% %}与 WordPress 风格短代码的兼容解析佐证定界符完全由match驱动。七、实战落地要点要在自己的 TinaCMS 项目中使用这套能力配置要点归纳如下字段声明富文本字段必须声明parser: { type: markdown }并在templates中注册 shortcode 模板每个模板给出match.start/match.end/match.name无键值参数需要支持{{% name value %}}这种写法时务必在模板fields中定义名为_value的字段可按需设置required、isTitle、label嵌套富文本需要包裹正文内容如本文的 pull-quote 引用块时在模板中定义{ name: children, type: rich-text }字段解析器会自动完成嵌套树的收敛与还原命名约定match.name决定 Markdown 文本中的语法名feature-panel模板name是内部节点名featurePanel两者可以不同但建议保持语义一致转义档位若你的内容还要经过其他 Markdown 工具链可按需使用parser.skipEscaping: all | html控制输出转义回归保障任何对 shortcode 解析/序列化行为的改动都应参照markdown-shortcodes-rich-text-children-3/index.test.ts的模式补充输入 → 树快照 → 输出快照三件套用快照测试锁住往返一致性。结语一份只有 7 行的 out.md背后是一条完整的模板契约 → micromark 解析 → 嵌套树收敛 → 规范化序列化 → 快照回归链路。理解它你就同时掌握了 TinaCMS 富文本 shortcode 的配置语法与其底层实现原理_value的无键值映射、children的递归子树、leaf的自闭合判定以及{{% /pull-quote %}}这类闭标签的精确还原规则。后续在项目里新增或排查 shortcode 问题时可顺着 parse/index.ts、post-processing.ts、mdast/index.ts 与 to-markdown.ts 这几条主线逐层定位。【免费下载链接】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),仅供参考
返回列表