markdown-it 中的code_block解析算法和设计(源码阅读)

136 阅读3分钟

最近研究 Markdown 解析的技巧,如果解析 Markdown 文档,识别文本内容类型是一个关键步骤。markdown-it是一款热门的、开源的、高效库的且易于可扩展的解析器开源库。今天深入该库部分源码,看看该库使用哪些技巧对Markdown中的code_block(代码块)语法解析。

一、Markdown 代码块语法

markdown 标准语法中,想要创建代码块,可以将每一行缩进至少四个空格或者制表符,如

    <html>
      <head>
      </head>
    </html>

渲染效果如下

<html>
  <head>
  </head>
</html>

要创建不用缩进的代码块,可以使用围栏式代码块,就是使用三个反引号```

二、 功能解析

markdown-it仓库中对代码块解析源码如链接: https://github.com/markdown-it/markdown-it/blob/master/lib/rules_block/code.mjs

// Code block (4 spaces padded)

export default function code (state, startLine, endLine/*, silent */) {
  if (state.sCount[startLine] - state.blkIndent < 4) { return false }

  let nextLine = startLine + 1
  let last = nextLine

  while (nextLine < endLine) {
    if (state.isEmpty(nextLine)) {
      nextLine++
      continue
    }

    if (state.sCount[nextLine] - state.blkIndent >= 4) {
      nextLine++
      last = nextLine
      continue
    }
    break
  }

  state.line = last

  const token   = state.push('code_block', 'code', 0)
  token.content = state.getLines(startLine, last, 4 + state.blkIndent, false) + '\n'
  token.map     = [startLine, state.line]

  return true
}
  1. 该方法的第一条语句是识别是否满足markdown代码块的条件

    // markdown 代码行要求每行前面至少四个空格
    if (state.sCount[startLine] - state.blkIndent < 4) { return false }
    

    其中,state.sCount[startLine]state.blkIndent 分别代码当前行的空格数和块级缩进基准值。只有当实际缩进大于等于四个空格时候,才视为代码块的起始。如果不是代码块,则返回false,留给下一个规则解析,否则解析完成返回true,意味着该规则函数成功处理特定的 markdonw 结构。

  2. 确定代码块的范围

    下面这个循环是检查后续行,并确定代码块的开始、结束行数

    while (nextLine < endLine) {
      if (state.isEmpty(nextLine)) {
        nextLine++
        continue
      }
    
      if (state.sCount[nextLine] - state.blkIndent >= 4) {
        nextLine++
        last = nextLine
        continue
      }
      break
     }
    
    • 空行处理:在这个循环中,函数逐行检查,如果是空行就跳转到下一行,
    • 缩进检查:如果行的缩进大于四个空格,则视为代码块的一部分,更新 last 变量,记录代码结束的位置
    • 结束循环:如果既不是空行且行缩进小于四空格,则认为代码块结束,退出循环
  3. 生成代码Token

    确定了代码块的访问,函数即可以创建新的 token,复制 content,并将记录解析的行位置

    const token = state.push('code_block', 'code', 0)
    token.content = state.getLines(startLine, last, 4 + state.blkIndent, false) + '\n'
    token.map = [startLine, state.line]
    

    其中state.getLines方法提取从起始行位置,到结束行位置的内容,并移除四个空格。

三、总结

  1. 逐行扫描:通过逐行检查输入文本,确保对代码精准识别,其中起始、结束条件判断灵活,不管是规则修改(比如改成五个空格),还是数据源修改(比如改成文件流)都易于扩展。这种思想在许多领域都有应用,比如JPEG 编解码库
  2. Token的生成:在识别出代码后,立即生成响应的 token,这种设计使得解析与渲染的过程解耦。

四、参考

Markdown 教程

markdown-it 中文文档

markdown-it-docs