markdown-it是怎么将markdown转为html的

免责声明:下面的内容由ai阅读源码生成,有错误欢迎指出

现在很多chatbox组件都会涉及到将markdown渲染为html,我们有很多可选择的库:

这里我主要讲解一下markdown-it,markdown-it是怎么将markdown转为html的。

下面是一个看不懂的流程图:

graph TD A[Markdown 输入] --> B[MarkdownIt 实例] B --> C[parse 方法] C --> D[Core Parser 处理] D --> E[Token 流生成] E --> F[render 方法] F --> G[Renderer 处理] G --> H[HTML 输出] subgraph "Core Parser 阶段" D --> D1[normalize 标准化输入] D1 --> D2[block 块级解析] D2 --> D3[inline 行内解析] D3 --> D4[linkify 链接识别] D4 --> D5[replacements 文本替换] D5 --> D6[smartquotes 智能引号] D6 --> D7[text_join 文本合并] end subgraph "Block Parser" D2 --> B1[heading 标题] D2 --> B2[paragraph 段落] D2 --> B3[blockquote 引用] D2 --> B4[list 列表] D2 --> B5[code 代码块] D2 --> B6[fence 围栏代码] D2 --> B7[table 表格] D2 --> B8[hr 分割线] end subgraph "Inline Parser" D3 --> I1[text 纯文本] D3 --> I2[emphasis 强调] D3 --> I3[link 链接] D3 --> I4[image 图片] D3 --> I5[backticks 行内代码] D3 --> I6[escape 转义] D3 --> I7[entity HTML实体] end subgraph "Renderer 阶段" G --> R1[遍历 Token 流] R1 --> R2[匹配渲染规则] R2 --> R3[生成 HTML 片段] R3 --> R4[组合最终 HTML] end

1.Markdown-it是怎么将Markdown转化为html的:

整体流程简单概述:

graph LR A[Markdown 文本] --> B[解析阶段] B --> C[Token 流] C --> D[渲染阶段] D --> E[HTML 字符串] subgraph "这段代码的位置" C --> F[md.renderer.render] F --> E end

核心代码demo:

js 复制代码
// ===== 创建核心状态对象 =====
// StateCore 是 markdown-it 的核心状态类,用于管理整个解析过程
const StateCore = md.core.State;

// 创建一个新的状态实例
// 参数说明:
// - testMarkdown: 要解析的 Markdown 源文本
// - md: MarkdownIt 实例,包含所有解析器和配置
// - {}: 环境对象,用于存储解析过程中的额外数据(如引用链接等)
const state = new StateCore(testMarkdown, md, {});

console.log('\n初始状态:');
// 显示源文本的长度
console.log('- src 长度:', state.src.length);
// 显示初始 token 数量(应该为 0,因为还没开始解析)
console.log('- tokens 数量:', state.tokens.length);

// ===== 手动执行核心规则链 =====
// 获取所有核心规则的函数数组
// 这些规则按顺序执行:normalize -> block -> inline -> linkify -> replacements -> smartquotes -> text_join
const coreRules = md.core.ruler.getRules('');

console.log('\n执行 Core 规则链:');

// 逐个执行每个核心规则,并监控 token 数量的变化
coreRules.forEach((rule, index) => {
  // 记录执行规则前的 token 数量
  const beforeTokens = state.tokens.length;
  
  // 执行当前规则,规则会修改 state 对象
  // 每个规则都会对 state.tokens 数组进行操作
  rule(state);
  
  // 记录执行规则后的 token 数量
  const afterTokens = state.tokens.length;
  
  // 输出规则执行的效果:规则名称和 token 数量变化
  // rule.name 可能为 undefined,所以使用 'anonymous' 作为后备
  console.log(`${index + 1}. ${rule.name || 'anonymous'}: ${beforeTokens} -> ${afterTokens} tokens`);
});

// ===== 分析最终生成的 Token 流 =====
console.log('\n最终 Token 流:');

// 遍历所有生成的 token,显示其详细信息
state.tokens.forEach((token, index) => {
  // 显示 token 的基本信息:
  // - type: token 类型(如 heading_open, paragraph_open, inline 等)
  // - tag: 对应的 HTML 标签(如 h1, p 等)
  // - nesting: 嵌套级别(1=开启标签, 0=自闭合, -1=关闭标签)
  console.log(`${index}: ${token.type} (${token.tag}) nesting:${token.nesting}`);
  
  // 如果 token 有内容,显示内容
  // 通常 inline 类型的 token 会有 content
  if (token.content) {
    console.log(`    content: "${token.content}"`);
  }
  
  // 如果 token 有子 token(通常是 inline 类型的 token)
  // 子 token 包含了行内元素的详细解析结果
  if (token.children && token.children.length > 0) {
    console.log(`    children: ${token.children.length} tokens`);
    
    // 遍历并显示所有子 token
    token.children.forEach((child, childIndex) => {
      // 显示子 token 的类型和内容
      // 子 token 可能是:text, strong_open, strong_close, em_open, em_close 等
      console.log(`      ${childIndex}: ${child.type} "${child.content}"`);
    });
  }
});

console.log('\n=== 渲染阶段详细步骤 ===');

// 演示渲染过程
const html = md.renderer.render(state.tokens, md.options, {});
console.log('\n最终 HTML 输出:');
console.log(html);

renderer函数(md.renderer.render)的内容大致为:

js 复制代码
// 默认的 token 渲染逻辑
Renderer.prototype.renderToken = function (tokens, idx, options) {
  const token = tokens[idx]
  let result = ''
  
  // 根据 nesting 值决定是开启标签、关闭标签还是自闭合标签
  if (token.nesting === 1) {
    // 开启标签: <h1>, <p>, <strong> 等
    result = '<' + token.tag
  } else if (token.nesting === -1) {
    // 关闭标签: </h1>, </p>, </strong> 等
    result = '</' + token.tag + '>'
  } else {
    // 自闭合标签: <br />, <img /> 等
    result = '<' + token.tag
  }
  
  return result
}

接下来结合流程图再过一遍整体流程:

阶段1:解析流程图

graph TD A[Markdown 输入] --> B[创建 StateCore] B --> C[normalize 规则] C --> D[标准化换行符和BOM] D --> E[block 规则] E --> F[按行扫描文本] F --> G{匹配块级规则?} G -->|标题| H[生成 heading tokens] G -->|段落| I[生成 paragraph tokens] G -->|列表| J[生成 list tokens] G -->|代码块| K[生成 code tokens] H --> L[inline 规则] I --> L J --> L K --> L L --> M[处理 inline tokens 的内容] M --> N[生成最终 Token 流]

阶段2:Token 流结构

以下面的markdown为例:

js 复制代码
[
  {
    type: 'heading_open',
    tag: 'h1',
    nesting: 1,
    markup: '#'
  },
  {
    type: 'inline',
    content: '标题',
    children: [
      { type: 'text', content: '标题' }
    ]
  },
  {
    type: 'heading_close',
    tag: 'h1',
    nesting: -1,
    markup: '#'
  },
  {
    type: 'paragraph_open',
    tag: 'p',
    nesting: 1
  },
  {
    type: 'inline',
    content: '这是 **粗体** 文本。',
    children: [
      { type: 'text', content: '这是 ' },
      { type: 'strong_open', tag: 'strong', nesting: 1 },
      { type: 'text', content: '粗体' },
      { type: 'strong_close', tag: 'strong', nesting: -1 },
      { type: 'text', content: ' 文本。' }
    ]
  },
  {
    type: 'paragraph_close',
    tag: 'p',
    nesting: -1
  }
]

阶段3:渲染流程图

源码demo终端输出:

shell 复制代码
=== Markdown-it 源码深度分析 ===

1. MarkdownIt 实例结构分析:
- core: object - 核心解析器
- block: object - 块级解析器
- inline: object - 行内解析器
- renderer: object - 渲染器
- linkify: object - 链接识别器
- options: object - 配置选项

2. 解析器规则链分析:
Core 规则: [
  'normalize',
  'block',
  'inline',
  'linkify$1',
  'replace',
  'smartquotes',
  'text_join'
]
Block 规则: [
  'table',     'code',
  'fence',     'blockquote',
  'hr',        'list',
  'reference', 'html_block',
  'heading',   'lheading',
  'paragraph'
]
Inline 规则: [
  'text',
  'linkify',
  'newline',
  'escape',
  'backtick',
  'strikethrough_tokenize',
  'emphasis_tokenize',
  'link',
  'image',
  'autolink',
  'html_inline',
  'entity'
]
Inline2 规则: [
  'link_pairs',
  'strikethrough_postProcess',
  'emphasis_post_process',
  'fragments_join'
]

3. 渲染器规则分析:
默认渲染规则: [
  'code_inline',
  'code_block',
  'fence',
  'image',
  'hardbreak',
  'softbreak',
  'text',
  'html_block',
  'html_inline'
]

4. 详细解析过程演示:

输入 Markdown:
# 标题

这是 **粗体** 文本。

=== 解析阶段详细步骤 ===

初始状态:
- src 长度: 19
- tokens 数量: 0

执行 Core 规则链:
1. normalize: 0 -> 0 tokens
2. block: 0 -> 6 tokens
3. inline: 6 -> 6 tokens
4. linkify$1: 6 -> 6 tokens
5. replace: 6 -> 6 tokens
6. smartquotes: 6 -> 6 tokens
7. text_join: 6 -> 6 tokens

最终 Token 流:
0: heading_open (h1) nesting:1
1: inline () nesting:0
    content: "标题"
    children: 1 tokens
      0: text "标题"
2: heading_close (h1) nesting:-1
3: paragraph_open (p) nesting:1
4: inline () nesting:0
    content: "这是 **粗体** 文本。"
    children: 5 tokens
      0: text "这是 "
      1: strong_open ""
      2: text "粗体"
      3: strong_close ""
      4: text " 文本。"
5: paragraph_close (p) nesting:-1

=== 渲染阶段详细步骤 ===

最终 HTML 输出:
<h1>标题</h1>
<p>这是 <strong>粗体</strong> 文本。</p>


5. 自定义规则演示:

测试自定义规则:
输入: ::: 这是自定义块
输出: <div class="custom-block">这是自定义块</div>


6. 性能分析:
解析时间: 2.824ms
渲染时间: 0.492ms
Token 数量: 258
HTML 长度: 1831

=== 分析完成 ===

2.如果markdown格式不完整,流式输出,怎么处理:

  1. markdown-it 本身不支持真正的流式解析
  2. 强调规则需要完整的分隔符对才能工作
  3. 实际应用中需要实现缓冲和延迟渲染策略
  4. 可以通过自定义解析器实现更好的流式体验

模拟AI流式输出终端输出:

shell 复制代码
=== Markdown-it 流式输出深度分析 ===

📈 流式输入各阶段分析:

🔍 详细分析: "***天气"
==================================================
📝 初始状态:
   文本: "***天气"
   长度: 5

🔄 Tokenize 阶段:
   位置 0: 匹配规则 emphasis_tokenize
   位置 3: 匹配规则 text

📊 生成的 tokens (3 个):
   0: text "*"
   1: text "*"
   2: text "*"

🎯 分隔符栈 (3 个):
   0: "*" 长度:3 开启:true 关闭:false 结束:-1
   1: "*" 长度:3 开启:true 关闭:false 结束:-1
   2: "*" 长度:3 开启:true 关闭:false 结束:-1

⚡ Post-process 阶段:
   分隔符匹配结果:

📋 最终 tokens (3 个):
   0: text "*" 
   1: text "*" 
   2: text "*" 

🎨 HTML 输出: <p>***天气</p>

🔍 详细分析: "***天气*"
==================================================
📝 初始状态:
   文本: "***天气*"
   长度: 6

🔄 Tokenize 阶段:
   位置 0: 匹配规则 emphasis_tokenize
   位置 3: 匹配规则 text
   位置 5: 匹配规则 emphasis_tokenize

📊 生成的 tokens (5 个):
   0: text "*"
   1: text "*"
   2: text "*"
   3: text "天气"
   4: text "*"

🎯 分隔符栈 (4 个):
   0: "*" 长度:3 开启:true 关闭:false 结束:-1
   1: "*" 长度:3 开启:true 关闭:false 结束:-1
   2: "*" 长度:3 开启:true 关闭:false 结束:-1
   3: "*" 长度:1 开启:false 关闭:true 结束:-1

⚡ Post-process 阶段:
   分隔符匹配结果:

📋 最终 tokens (5 个):
   0: text "*" 
   1: text "*" 
   2: text "*" 
   3: text "天气" 
   4: text "*" 

🎨 HTML 输出: <p>**<em>天气</em></p>

🔍 详细分析: "***天气**"
==================================================
📝 初始状态:
   文本: "***天气**"
   长度: 7

🔄 Tokenize 阶段:
   位置 0: 匹配规则 emphasis_tokenize
   位置 3: 匹配规则 text
   位置 5: 匹配规则 emphasis_tokenize

📊 生成的 tokens (6 个):
   0: text "*"
   1: text "*"
   2: text "*"
   3: text "天气"
   4: text "*"
   5: text "*"

🎯 分隔符栈 (5 个):
   0: "*" 长度:3 开启:true 关闭:false 结束:-1
   1: "*" 长度:3 开启:true 关闭:false 结束:-1
   2: "*" 长度:3 开启:true 关闭:false 结束:-1
   3: "*" 长度:2 开启:false 关闭:true 结束:-1
   4: "*" 长度:2 开启:false 关闭:true 结束:-1

⚡ Post-process 阶段:
   分隔符匹配结果:

📋 最终 tokens (6 个):
   0: text "*" 
   1: text "*" 
   2: text "*" 
   3: text "天气" 
   4: text "*" 
   5: text "*" 

🎨 HTML 输出: <p>*<strong>天气</strong></p>

🔍 详细分析: "***天气***"
==================================================
📝 初始状态:
   文本: "***天气***"
   长度: 8

🔄 Tokenize 阶段:
   位置 0: 匹配规则 emphasis_tokenize
   位置 3: 匹配规则 text
   位置 5: 匹配规则 emphasis_tokenize

📊 生成的 tokens (7 个):
   0: text "*"
   1: text "*"
   2: text "*"
   3: text "天气"
   4: text "*"
   5: text "*"
   6: text "*"

🎯 分隔符栈 (6 个):
   0: "*" 长度:3 开启:true 关闭:false 结束:-1
   1: "*" 长度:3 开启:true 关闭:false 结束:-1
   2: "*" 长度:3 开启:true 关闭:false 结束:-1
   3: "*" 长度:3 开启:false 关闭:true 结束:-1
   4: "*" 长度:3 开启:false 关闭:true 结束:-1
   5: "*" 长度:3 开启:false 关闭:true 结束:-1

⚡ Post-process 阶段:
   分隔符匹配结果:

📋 最终 tokens (7 个):
   0: text "*" 
   1: text "*" 
   2: text "*" 
   3: text "天气" 
   4: text "*" 
   5: text "*" 
   6: text "*" 

🎨 HTML 输出: <p><em><strong>天气</strong></em></p>

================================================================================
🎯 核心问题分析
================================================================================

❌ 问题根源:
1. markdown-it 的强调规则使用"分隔符栈"算法
2. 该算法需要在 post-process 阶段匹配开始和结束分隔符
3. 只有找到匹配的分隔符对,才会生成强调 token
4. 在流式输出中,结束分隔符可能还未到达

🔍 具体流程:
1. Tokenize 阶段:将所有 '*' 标记添加到分隔符栈
2. Post-process 阶段:从后往前遍历分隔符栈,寻找匹配对
3. 只有找到匹配对的分隔符才会被转换为 em/strong token
4. 未匹配的分隔符保持为普通文本

💡 为什么 "***天气" 不会被加粗:
- 分隔符栈中只有开始的 "***",没有结束的 "***"
- Post-process 阶段找不到匹配的结束分隔符
- 所有 "*" 保持为普通文本 token


================================================================================
🛠️ 流式输出解决方案
================================================================================

方案1: 延迟渲染 (最常用)
- 维护一个缓冲区,等待更多内容
- 只渲染"安全"的部分(确定不会改变的内容)
- 对于未完成的标记,延迟到有足够上下文时再处理

方案2: 增量解析
- 实现自定义的流式解析器
- 维护解析状态,支持增量更新
- 只在确定匹配时才输出格式化内容

方案3: 预测性解析
- 基于上下文和模式预测可能的结束标记
- 提供"临时"渲染,后续可能需要回滚
- 适用于交互式编辑器

方案4: 混合模式
- 对于简单格式(如单个*)立即处理
- 对于复杂格式(如***)使用延迟策略
- 平衡实时性和准确性


📝 实用流式解析策略演示:

🧪 测试实用流式解析器:
步骤 1: 添加 "今天" -> 输出 "今天" (累计: "今天")
步骤 2: 添加 "***" -> 输出 "" (累计: "今天")
步骤 3: 添加 "天" -> 输出 "" (累计: "今天")
步骤 4: 添加 "气" -> 输出 "" (累计: "今天")
步骤 5: 添加 "***" -> 输出 "<strong><em>天气</em></strong>" (累计: "今天<strong><em>天气</em></strong>")
步骤 6: 添加 "很好" -> 输出 "" (累计: "今天<strong><em>天气</em></strong>")
最终: 剩余 "**很好" (完整结果: "今天<strong><em>天气</em></strong>**很好")

✅ 分析完成!

🎯 关键结论:
1. markdown-it 本身不支持真正的流式解析
2. 强调规则需要完整的分隔符对才能工作
3. 实际应用中需要实现缓冲和延迟渲染策略
4. 可以通过自定义解析器实现更好的流式体验

想要实现demo的可以后台私信我发送喵

相关推荐
国家不保护废物2 分钟前
10万条数据插入页面:从性能优化到虚拟列表的终极方案
前端·面试·性能优化
文心快码BaiduComate17 分钟前
七夕,画个动态星空送给Ta
前端·后端·程序员
web前端12321 分钟前
# 多行文本溢出实现方法
前端·javascript
文心快码BaiduComate21 分钟前
早期人类奴役AI实录:用Comate Zulu 10min做一款Chrome插件
前端·后端·程序员
人间观察员23 分钟前
如何在 Vue 项目的 template 中使用 JSX
前端·javascript·vue.js
布列瑟农的星空25 分钟前
大话设计模式——多应用实例下的IOC隔离
前端·后端·架构
EndingCoder30 分钟前
安装与环境搭建:准备你的 Electron 开发环境
前端·javascript·electron·前端框架
蓝银草同学1 小时前
前端离线应用基石:深入浅出 IndexedDB 完整指南
前端·indexeddb
龙在天1 小时前
什么是SourceMap?有什么作用?
前端
雪中何以赠君别1 小时前
Vue 2 与 Vue 3 双向绑定 (v-model) 区别详解
前端·javascript·vue.js