如何将Token列表转换为React元素?附Markdown渲染场景示例
基于Token列表生成React元素的简易Markdown渲染实现
需求背景
需要实现一个仅支持行内格式的简易Markdown渲染器(无块级元素、代码块),将解析后的Token列表转换为React.createElement调用的嵌套元素结构,而非HTML字符串。
示例场景
复杂嵌套示例
Markdown原文:
Foo *italic **bold-italic** italic* **bold** Blah
解析出的Token序列:
{ type: text, content: "Foo " } { type: em_open } { type: text, content: "italic " } { type: strong_open} { type: text, content: "bold-italic" } { type: strong_close} { type: text, content: " italic" } { type: em_close } { type: text, content: " " } { type: strong_open} { type: text, content: "bold" } { type: strong_close} { type: text, content: " Blah" }
需生成嵌套的React元素,对应多层createElement调用。
基础加粗示例
Markdown原文:
**foo**
Token序列:
{ type: strong_open } { type: text, content: "foo" } { type: strong_close }
期望生成:
createElement(Text, { fontWeight: "bold" }, [ "foo" ]);
斜体+加粗嵌套示例
Markdown原文:
***foo***
Token序列:
{ type: em_open } { type: strong_open } { type: text, content: "foo" } { type: strong_close } { type: em_close }
期望生成:
createElement(Text, { fontStyle: "italic" }, [ createElement(Text, { fontWeight: "bold" }, [ "foo" ]) ]);
核心实现方案:栈(Stack)模式
处理嵌套格式最经典的方式就是用栈结构,因为Token的开/关顺序天然符合栈的“后进先出”特性。具体步骤如下:
1. 定义Token与React元素的映射
先把em_open、strong_open这类开标记,对应到React组件和样式props:
const openTokenMap = { em_open: { component: 'Text', props: { fontStyle: 'italic' } }, strong_open: { component: 'Text', props: { fontWeight: 'bold' } } };
2. 初始化栈结构
栈中每个元素保存当前层级的组件信息、props和子元素数组,初始时放入一个根容器(比如React Fragment):
const stack = [{ component: 'Fragment', props: {}, children: [] }];
3. 遍历Token处理逻辑
逐个遍历Token,根据类型执行不同操作:
- 开标记(*_open):根据映射创建新的上下文对象,压入栈顶,作为当前活跃的元素容器。
- 关标记(*_close):弹出栈顶的上下文,将其转换为
createElement调用后,加入到新栈顶元素的子数组中。 - 文本Token:若内容非空,直接将文本内容加入当前栈顶元素的子数组;空文本直接跳过。
完整代码实现
import { createElement, Fragment } from 'react'; const renderMarkdownTokens = (tokens) => { const openTokenMap = { em_open: { component: 'Text', props: { fontStyle: 'italic' } }, strong_open: { component: 'Text', props: { fontWeight: 'bold' } } }; const stack = [{ component: Fragment, props: {}, children: [] }]; for (const token of tokens) { const currentCtx = stack[stack.length - 1]; if (openTokenMap[token.type]) { // 处理开标记,压入新上下文 const { component, props } = openTokenMap[token.type]; stack.push({ component, props, children: [] }); } else if (token.type.endsWith('_close')) { // 处理关标记,弹出并添加到父级子元素 if (stack.length > 1) { const closedCtx = stack.pop(); const parentCtx = stack[stack.length - 1]; parentCtx.children.push( createElement(closedCtx.component, closedCtx.props, closedCtx.children) ); } } else if (token.type === 'text') { // 处理文本,跳过空内容 if (token.content !== '') { currentCtx.children.push(token.content); } } } // 根元素处理:如果只有一个子元素直接返回,否则用Fragment包裹 const rootChildren = stack[0].children; return rootChildren.length === 1 ? rootChildren[0] : createElement(Fragment, {}, rootChildren); };
空文本Token处理
在遍历文本Token时,直接判断token.content是否为空,为空则不加入子元素数组,避免生成无效的空文本节点。
方案优势
- 天然适配嵌套格式的Token顺序,逻辑直观易懂;
- 扩展性强:新增格式(如下划线、删除线)只需在
openTokenMap中添加对应映射即可; - 自动处理多层嵌套,无需手动维护层级关系。
内容的提问来源于stack exchange,提问作者Archimedes Trajano
相关产品推荐
相关产品推荐

