基于 React + MarkdownIt 的 Markdown 渲染器实践:支持地图标签和长按复制
在 React 小程序中,Markdown 内容不能直接渲染成组件。本文分享一个 基于 React + MarkdownIt 的 Markdown 渲染器实践,支持自定义样式、地图标签、列表渲染,以及长按复制功能。
功能概览
实现的核心功能如下:
- 
Markdown 渲染
支持标题、段落、引用、列表、内联样式(加粗、斜体、删除线、标记、行内代码)、图片等。
 - 
自定义样式
通过
MarkdownStyleConfig定义段落、标题、列表、内联文本、图片和地图样式,实现灵活排版。 - 
地图标签解析
支持
<map latitude="xx" longitude="yy" name="位置" />标签。MarkdownIt 默认不识别
<map>,所以通过自定义插件生成 token,再在 AST 渲染阶段生成 Map 组件。 - 
长按复制
微信小程序中
<Text>支持userSelect长按复制,但内联<Text>嵌套 Image/Map 会导致样式错乱。解决方式:
- AST 构建阶段:内联和块级内容分开,Image/Map 不嵌套在 Text 内。
 - 渲染阶段 :尽量用 Text 包裹内联内容,在最外层加 
userSelect。 
 
MarkdownIt 和 Token
MarkdownIt 是高性能可扩展 Markdown 解析器,特点:
- 输出 token,方便二次处理
 - 支持插件扩展
 - 支持 HTML 标签
 
Token 类型
MarkdownIt 输出 token,主要分为:
- 块级元素(Block Tokens)
paragraph、heading、blockquote、bullet_list、ordered_list、list_item等 - 行内元素(Inline Tokens)
text、strong、em、del、mark、code_inline、link、image、softbreak、hardbreak
自定义插件生成的map_inline 
内联元素通过 isInline(type) 判断。
解析 <map> 标签
MarkdownIt 默认不识别 <map> 标签,我们通过插件生成 token:
            
            
              javascript
              
              
            
          
          function markdownItMapPlugin(md: MarkdownIt) {
  md.block.ruler.before("html_block", "map_block", (state, startLine, endLine, silent) => { ... });
  md.inline.ruler.before("html_inline", "map_inline", (state, silent) => { ... });
}
        插件会匹配 <map ...>,生成 map_block 或 map_inline token,并保留属性用于 AST 构建。
Token → AST 核心算法
将 MarkdownIt token 转成 AST 的核心目标:
- 保留块级元素嵌套关系
 - 支持内联多层嵌套(strong、em、del、mark、code_inline、link)
 - 分离块级节点和 Image/Map,保证长按复制不乱
 
内联 Token 转 AST
            
            
              arduino
              
              
            
          
          function inlineTokensToAST(tokens: any[]): ASTNode[] { ... }
        逻辑:
- 
遍历内联 token:
text→ text 节点softbreak/hardbreak→ break 节点- 内联样式 
_open/_close→ 递归解析子 token link_open→ 解析链接子 tokenimage→ image 节点map_inline→ map 节点
 - 
递归处理嵌套,支持任意深度的内联样式
 
包装连续内联节点
            
            
              matlab
              
              
            
          
          function wrapInlineNodes(nodes: ASTNode[]): ASTNode[] { ... }
        逻辑:
- 
初始化 buffer 存放连续内联节点
 - 
遇到块级节点 / Image / Map:
- flush buffer,把连续内联节点合并成一个 
inline节点 - 块级节点直接加入结果数组
 
 - flush buffer,把连续内联节点合并成一个 
 - 
避免内联节点与块级节点混合,Image/Map 不在 Text 内
 
✅ 这样做的好处:
- 渲染阶段可以安全地在最外层 
<Text>添加userSelect,支持长按复制 - 保持内联节点样式继承
 
Tokens → AST 核心流程
            
            
              ini
              
              
            
          
          export function mdToAST(markdown: string): ASTNode[] {
  const tokens = md.parse(markdown, {});
  const root: ASTNode = { id: genNodeId(), type: "root", children: [] };
  const stack: ASTNode[] = [root];
  for (const token of tokens) {
    const parent = stack[stack.length - 1];
    if (token.type.endsWith("_open")) {
      // 块级节点开始
      const node: ASTNode = {
        id: genNodeId(),
        type: token.type.replace(/_open$/, ""),
        tag: token.tag,
        attrs: attrsToMap(token.attrs),
        children: [],
      };
      parent.children!.push(node);
      stack.push(node);
    } else if (token.type.endsWith("_close")) {
      // 块级节点结束
      stack.pop();
    } else if (token.type === "inline") {
      // 内联节点递归解析
      parent.children!.push(...wrapInlineNodes(inlineTokensToAST(token.children || [])));
    } else if (token.type === "map_block" || token.type === "map_inline") {
      // Map 节点独立处理
      const attrs = parseAttrsFromToken(token);
      parent.children!.push({ id: genNodeId(), type: "map", attrs });
    } else if (token.type === "image") {
      parent.children!.push({ id: genNodeId(), type: "image", attrs: attrsToMap(token.attrs) });
    } else if (token.type === "text") {
      parent.children!.push({ id: genNodeId(), type: "text", text: token.content });
    } else if (token.type === "softbreak" || token.type === "hardbreak") {
      parent.children!.push({ id: genNodeId(), type: "break" });
    }
  }
  return root.children || [];
}
        核心设计思想
- stack 管理块级嵌套 :
_open入栈,_close出栈 - 内联节点包装 :连续内联节点合并成 
inline节点,阻止 Image/Map 嵌套在 Text 内 - Map/Image 独立节点:保证渲染和复制不会乱
 - 唯一 ID :每个 AST 节点生成唯一 
id,React 渲染时安全使用key 
AST 渲染
            
            
              javascript
              
              
            
          
          function renderNode(node: ASTNode, styleConfig: MarkdownStyleConfig) {
  switch (node.type) {
    case "paragraph":
    case "heading":
      return (
        <Text key={node.id} userSelect style={styleConfig[node.type]}>
          {node.children?.map(c => renderInlineNode(c, styleConfig))}
        </Text>
      );
    case "bullet_list":
      return <View key={node.id}>{node.children?.map((c, i) => renderListItem(c, styleConfig, i + 1, "bullet"))}</View>;
    case "ordered_list":
      return <View key={node.id}>{node.children?.map((c, i) => renderListItem(c, styleConfig, i + 1, "ordered"))}</View>;
    case "image":
      return <Image key={node.id} src={node.attrs?.src} style={styleConfig.image} />;
    case "map":
      return <MapNode node={node} styleConfig={styleConfig} />;
    default:
      return null;
  }
}
        MapNode 核心实现
            
            
              ini
              
              
            
          
          const MapNode = ({ node, styleConfig }: { node: ASTNode; styleConfig: MarkdownStyleConfig }) => {
  const latitude = parseFloat(node.attrs?.latitude);
  const longitude = parseFloat(node.attrs?.longitude);
  const markers = [{ id: 0, latitude, longitude, width: 20, height: 26 }];
  const handleTap = () => {
    wx.openLocation({ latitude, longitude, name: node.attrs?.name, scale: 18 });
  };
  return (
    <View key={node.id} style={styleConfig.mapContainer}>
      <Text style={styleConfig.mapTitle}>{node.attrs?.name}</Text>
      <Map
        latitude={latitude}
        longitude={longitude}
        markers={markers}
        enableScroll={false}
        enableZoom={false}
        style={{ width: "100%", height: "100%" }}
        onTap={handleTap}
      />
    </View>
  );
};
        总结
这套渲染器特点:
- Markdown 全量渲染:段落、标题、列表、内联样式、图片、地图
 - 长按复制优化 :AST 构建阶段分离内联与块级元素,渲染阶段最外层 Text 加 
userSelect - 地图标签支持 :自定义 MarkdownIt 插件解析 
<map>,生成小程序 Map 组件 - 自定义样式 :通过 
MarkdownStyleConfig灵活控制渲染效果 
逻辑清晰、可扩展性强,适合小程序富文本展示和交互场景。