Skip to content

markdown-it

markdown-it 是一个流行的 JavaScript Markdown 解析器,它将 Markdown 文本解析成 HTML 标记。下面是 markdown-it 的渲染流程:

  1. markdown-it 首先会将输入的 Markdown 文本转换成 Token 流,每个 Token 代表一个 Markdown 元素,如标题、段落、代码块等。
  2. 接着,markdown-it 会将 Token 流传递给插件系统,插件可以对 Token 进行修改、添加或删除。
  3. 经过插件系统处理后,markdown-it 会将 Token 流转换成 AST(抽象语法树),AST 是一个树形结构,每个节点代表一个 Markdown 元素,如标题、段落、代码块等。
  4. 最后,markdown-it 会将 AST 转换成 HTML 标记,这个过程被称为渲染。

总的来说,markdown-it 的渲染流程可以归纳为以下几个步骤:输入 Markdown 文本 -> Token 流 -> 插件处理 -> AST -> HTML 标记。

安装 markdown-it

sh
pnpm add @types/markdown-it -D
pnpm add markdown-it

将 markdown 内容渲染成 html

ts
import MarkdownIt from 'markdown-it'

const md = MarkdownIt({
  // 允许在文件中写 HTML 标签
  html: true,
  // 自动将 url 转换为链接
  linkify: true,
})

const html = md.render('# Markdown')

插件使用

ts
export function snippetPlugin(md: MarkdownIt, srcDir: string) {

}

markdown-it-container

https://github.com/markdown-it/markdown-it-container#readme

javascript
import MarkdownIt from 'markdown-it'
import MarkdownItContainer from 'markdown-it-container'

md.use(MarkdownItContainer, 'demo', {
  marker: ':',
  validate(params) {
    return params.trim().match(/^demo(.*)$/)
  },
  render(tokens, idx) {
    const m = tokens[idx].info.trim().match(/^demo(.*)$/)
    if (tokens[idx].nesting === 1) {
      const description = m && m.length > 1 ? m[1] : ''
      const content
        = tokens[idx + 1].type === 'fence' ? tokens[idx + 1].content : ''
      return `<demo-block>
      ${description ? `<div>${md.render(description)}</div>` : ''}
      <!--element-demo: ${content}:element-demo-->
      `
    }
    return '</demo-block>'
  },
})

markdown
<demo-block>
  <div>
    <p>Progress 组件设置<code>percentage</code>属性即可,表示进度条对应的百分比,<strong>必填</strong>,必须在 0-100。通过 <code>format</code> 属性来指定进度条文字内容。</p>
  </div>
  <!--element-demo:
    <el-progress :percentage="50">
      </el-progress>
      <el-progress :percentage="100" :format="format"></el-progress>
      <el-progress :percentage="100" status="success"></el-progress>
      <el-progress :percentage="100" status="warning"></el-progress>
      <el-progress :percentage="50" status="exception"></el-progress>

      <script>
        export default {
          methods: {
            format(percentage) {
              return percentage === 100 ? '满' : `${percentage}%`
            },
          },
        }
      </script>

:element-demo-->

        <pre><code class="language-html">&lt;el-progress :percentage=&quot;50&quot;&gt;&lt;/el-progress&gt;

&lt;el-progress :percentage=&quot;100&quot; :format=&quot;format&quot;&gt;&lt;/el-progress&gt;
&lt;el-progress :percentage=&quot;100&quot; status=&quot;success&quot;&gt;&lt;/el-progress&gt;
&lt;el-progress :percentage=&quot;100&quot; status=&quot;warning&quot;&gt;&lt;/el-progress&gt;
&lt;el-progress :percentage=&quot;50&quot; status=&quot;exception&quot;&gt;&lt;/el-progress&gt;

&lt;script&gt;
export default {
methods: {
format(percentage) {
return percentage === 100 ? '满' : `${percentage}%`
},
},
}
&lt;/script&gt;
</code></pre>
</demo-block>

markdown-it-chain

vue
<script lang="js">
import * as Vue from 'vue'
export default {
  name: 'ComponentDoc',
  components: {
    ElementDemo0: (function () {
      const { resolveComponent: _resolveComponent, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue

      function render(_ctx, _cache) {
        const _component_el_progress = _resolveComponent('el-progress')

        return (_openBlock(), _createBlock('div', null, [
          _createVNode(_component_el_progress, { percentage: 50 }),
          _createVNode(_component_el_progress, {
            percentage: 100,
            format: _ctx.format
          }, null, 8 /* PROPS */, ['format']),
          _createVNode(_component_el_progress, {
            percentage: 100,
            status: 'success'
          }),
          _createVNode(_component_el_progress, {
            percentage: 100,
            status: 'warning'
          }),
          _createVNode(_component_el_progress, {
            percentage: 50,
            status: 'exception'
          })
        ]))
      }

      const democomponentExport = {
        methods: {
          format(percentage) {
            return percentage === 100 ? '满' : `${percentage}%`
          },
        },
      }
      return {
        render,
        ...democomponentExport
      }
    })(),
  }
}
</script>

<template>
  <section class="content element-doc">
    <demo-block>
      <div>
        <p>
          Progress
          组件设置<code>percentage</code>属性即可,表示进度条对应的百分比,<strong>必填</strong>,必须在
          0-100。通过 <code>format</code> 属性来指定进度条文字内容。
        </p>
      </div>
      <template #source>
        <ElementDemo0 />
      </template>
      <pre><code class="language-html">&lt;el-progress :percentage=&quot;50&quot;&gt;&lt;/el-progress&gt;
&lt;el-progress :percentage=&quot;100&quot; :format=&quot;format&quot;&gt;&lt;/el-progress&gt;
&lt;el-progress :percentage=&quot;100&quot; status=&quot;success&quot;&gt;&lt;/el-progress&gt;
&lt;el-progress :percentage=&quot;100&quot; status=&quot;warning&quot;&gt;&lt;/el-progress&gt;
&lt;el-progress :percentage=&quot;50&quot; status=&quot;exception&quot;&gt;&lt;/el-progress&gt;

&lt;script&gt;
  export default {
    methods: {
      format(percentage) {
        return percentage === 100 ? '满' : `${percentage}%`
      },
    },
  }
&lt;/script&gt;
</code></pre>
    </demo-block>
  </section>
</template>

代码块渲染

ts
import { MarkdownRenderer } from 'vitepress'
import { FenceDemoTag } from './constants'
import { genDemoByCode } from './utils'

export function fencePlugin(md: MarkdownRenderer) {
  const defaultRender = md.renderer.rules.fence

  md.renderer.rules.fence = (tokens, idx, options, env, self) => {
    const token = tokens[idx]
    if (token.info.trim() !== FenceDemoTag)
      return defaultRender!(tokens, idx, options, env, self)

    const content = token.content
    const path = env.path

    // 自定义渲染
    const demoScripts = genDemoByCode(md, env, path, content)
    return demoScripts
  }
}

代码高亮

安装 prismjs

sh
pnpm add prismjs
pnpm add -D @types/prismjs

将代码片段转换成 HTML

ts
import prism from 'prismjs'

// 要高亮的代码片段字符串
const code = `var data = 1;`

// Returns a highlighted HTML string
const html = prism.highlight(code, Prism.languages.javascript, 'javascript')

Prism 会自动加载默认的 markup, css, clikejavascript. 可以使用 loadLanguages() 加载自己所需要的高亮语言

ts
import prism from 'prismjs'
import loadLanguages from 'prismjs/components/index'

// required to make embedded highlighting work...
loadLanguages(['markup', 'css', 'javascript'])

// The code snippet you want to highlight, as a string
const code = `var data = 1;`

// Returns a highlighted HTML string
const html = prism.highlight(code, Prism.languages.javascript, 'javascript')
ts
import prism from 'prismjs'
import loadLanguages from 'prismjs/components/index'

// required to make embedded highlighting work...
loadLanguages(['markup', 'css', 'javascript'])

function getLangCodeFromExtension(extension: string) {
  const extensionMap: Record<string, string> = {
    vue: 'markup',
    html: 'markup',
    md: 'markdown',
    rb: 'ruby',
    ts: 'typescript',
    py: 'python',
    sh: 'bash',
    yml: 'yaml',
    styl: 'stylus',
    kt: 'kotlin',
    rs: 'rust',
  }

  return extensionMap[extension] || extension
}

function wrap(str: string, lang: string) {
  return `<pre class="language-${lang}"><code>${str}</code></pre>`
}

export default (str: string, lang: string) => {
  lang = getLangCodeFromExtension(lang)

  if (!prism.languages[lang]) {
    try {
      loadLanguages([lang])
    }
    catch (error) {
      throw new Error(
        `Syntax highlight for language "${lang}" is not supported.`,
      )
    }
  }

  if (prism.languages[lang]) {
    const code = prism.highlight(str, prism.languages[lang], lang)
    return wrap(code, lang)
  }

  return wrap(str, 'text')
}

添加语法高亮

https://github.com/vuejs/vitepress/issues/1533

ts
import { BUNDLED_LANGUAGES } from 'shiki'

// Include `cs` as alias for csharp
BUNDLED_LANGUAGES.find(lang => lang.id === 'csharp').aliases.push('cs')

// Include `fs` as alias for fsharp
BUNDLED_LANGUAGES.find(lang => lang.id === 'fsharp').aliases.push('fs')

自定义渲染

包含 token 的渲染规则。可以进行更新和扩展。

ts
export function mdDemoPlugin(md: MarkdownRenderer) {
  const addRenderRule = (type: string) => {
    const defaultRender = md.renderer.rules[type]

    md.renderer.rules[type] = (tokens, idx, options, env, self) => {
      const token = tokens[idx]
      console.log('token: ', token)
      const { content } = token

      if (!content.startsWith(`<demo `))
        return defaultRender!(tokens, idx, options, env, self)

      return ''
    }
  }

  addRenderRule('html_block')
  addRenderRule('html_inline')
}

Reference