当前位置: 首页 > news >正文

Markdown渲染引擎——js技能提升

3. Markdown渲染引擎

功能概述

实现功能完整的Markdown渲染引擎,支持数学公式、流程图、图表、代码高亮等高级功能。

技术难点

  • 自定义Markdown解析器
  • 数学公式LaTeX渲染
  • 流程图和图表集成
  • 代码语法高亮
  • 插件系统设计

实现思路

3.1 Markdown解析器核心
// src/markdown/index.ts
import MarkdownIt from 'markdown-it'
import { full as emoji } from 'markdown-it-emoji'
import markdownItAbbr from 'markdown-it-abbr'
import markdownItFootnote from 'markdown-it-footnote'
import markdownItDeflist from 'markdown-it-deflist'
import markdownItIns from 'markdown-it-ins'
import markdownItMark from 'markdown-it-mark'
import markdownItTaskLists from 'markdown-it-task-lists'
import markdownItToc from 'markdown-it-toc-done-right'
import mila from 'markdown-it-link-attributes'
import markdownItAttrs from 'markdown-it-attrs'
import markdownItKatex from 'markdown-it-katex'import { renderer } from './tokenRenderer'
import type { ParsedToken, MarkdownPlugin } from './types'export type { MarkdownPlugin } from './types'class MarkdownParser {private static instance: MarkdownParser | null = nullprivate md: MarkdownItprivate tokenRenderer: ReturnType<typeof renderer>private initialized: boolean = falseprivate plugins: MarkdownPlugin[] = []private constructor() {this.md = new MarkdownIt({html: true,linkify: true,typographer: true,})this.tokenRenderer = renderer()}public static getInstance(): MarkdownParser {if (!MarkdownParser.instance) {MarkdownParser.instance = new MarkdownParser()}return MarkdownParser.instance}private initialize() {if (this.initialized) return this// 注册所有插件this.md.use(emoji)this.md.use(markdownItAttrs)this.md.use(markdownItAbbr)this.md.use(markdownItFootnote)this.md.use(markdownItDeflist)this.md.use(markdownItIns)this.md.use(markdownItMark)this.md.use(markdownItToc)this.md.use(markdownItTaskLists, { enable: true })this.md.use(mila, {matcher(href: string) {return href.startsWith('https:') || href.startsWith('http:')},attrs: {target: '_blank',rel: 'noopener noreferrer',},})this.md.use(markdownItKatex)this.initialized = truereturn this}// 注册自定义插件registerPluginRenderer(plugin: MarkdownPlugin) {this.plugins.push(plugin)this.tokenRenderer.registerPlugin(plugin)}parse(md: string): any[] {if (!this.initialized) {this.initialize()}const tokens = this.md.parse(md, {})return this.tokenRenderer.renderTokens(tokens)}
}export function createMarkdownParser(): MarkdownParser {return MarkdownParser.getInstance()
}
3.2 Token渲染器
// src/markdown/tokenRenderer.ts
import { h } from 'vue'
import type { ParsedToken, TokenAttributes, MarkdownPlugin } from './types'
import { TAG_MAP } from './constants'
import { getStyles } from './theme'
import { NImage } from 'naive-ui'
import {CodeHighlight,MathBlock,EchartsBlock,MermaidBlock,
} from './components'class TokenRenderer {private static instance: TokenRenderer | null = nullprivate plugins: MarkdownPlugin[] = []private constructor() {}static getInstance(): TokenRenderer {if (!TokenRenderer.instance) {TokenRenderer.instance = new TokenRenderer()}return TokenRenderer.instance}// 注册插件registerPlugin(plugin: MarkdownPlugin) {this.plugins.push(plugin)}// 获取插件组件private getPluginComponent(fenceType: string) {const plugin = this.plugins.find(p => p.type === fenceType)return plugin?.component}// 渲染叶子节点private renderLeaf(token: ParsedToken,renderTokens: (tokens: ParsedToken[]) => any[]) {switch (token.type) {case 'text':return token.content || ''case 'emoji':return token.contentcase 'softbreak':return ' 'case 'hardbreak':return h('br')case 'html_inline':return h('span', { innerHTML: token.content })case 'html_block':return h('div', { innerHTML: token.content })case 'math_inline':return h(MathBlock, { formula: token.content, displayMode: false })case 'math_block':return h(MathBlock, { formula: token.content, displayMode: true })case 'code_inline':return h('code', { class: getStyles(token) }, token.content)case 'fence': {const fenceType = (token.info?.trim() || '').toLowerCase()const pluginComponent = this.getPluginComponent(fenceType)if (pluginComponent) {return h(pluginComponent, { content: token.content })}if (fenceType === 'echarts') {return h(EchartsBlock, { option: token.content })}if (fenceType === 'mermaid') {return h(MermaidBlock, { code: token.content })}return h(CodeHighlight, {code: token.content || '',type: fenceType || 'text',})}case 'hr':return h('hr', { class: getStyles(token) })case 'image':return h(NImage, this.getAttributes(token))default:return this.renderUnknownToken(token, renderTokens)}}// 渲染Token数组renderTokens(tokens: ParsedToken[]): any[] {const nodes: any[] = []let i = 0while (i < tokens.length) {const token = tokens[i]const tokenType = token.typeif (tokenType.endsWith('_open')) {let depth = 1let j = i + 1while (j < tokens.length) {if (tokens[j].type.endsWith('_open')) depth++if (tokens[j].type.endsWith('_close')) depth--if (depth === 0) breakj++}const tagName = this.getTagName(token)const attrs = this.getAttributes(token)const childrenTokens = tokens.slice(i + 1, j)const childrenNodes = this.renderTokens(childrenTokens)const vnode = h(tagName, attrs, childrenNodes)nodes.push(vnode)i = j + 1} else if (tokenType === 'inline') {const childrenNodes = this.renderTokens(token.children || [])nodes.push(...childrenNodes)i++} else {const leafNode = this.renderLeaf(token, tokens =>this.renderTokens(tokens))if (leafNode !== null) {nodes.push(leafNode)}i++}}return nodes}
}export function renderer(): TokenRenderer {return TokenRenderer.getInstance()
}
3.3 数学公式渲染组件
// src/markdown/components/MathBlock/index.tsx
import { defineComponent, computed } from 'vue'
import katex from 'katex'
import 'katex/dist/katex.min.css'export default defineComponent({name: 'MathBlock',props: {formula: { type: String, required: true },displayMode: { type: Boolean, default: false },},setup(props) {const rendered = computed(() => {try {return katex.renderToString(props.formula, {throwOnError: false,displayMode: props.displayMode,})} catch (e) {console.error('公式渲染错误:', e)return '<span style="color:red">公式错误</span>'}})return () => (<spanclass={props.displayMode ? 'katex katex-block' : 'katex'}innerHTML={rendered.value}/>)},
})
3.4 流程图渲染组件
// src/markdown/components/MermaidBlock/index.tsx
import { defineComponent, ref, watch, onBeforeUnmount, onMounted } from 'vue'
import mermaid from 'mermaid'
import { NImage } from 'naive-ui'function getUniqueId() {return 'mermaid-svg-' + Math.random().toString(36).substr(2, 9)
}// 将SVG转换为data URL
function svgToDataUrl(svgString: string): string {const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' })return URL.createObjectURL(svgBlob)
}export default defineComponent({name: 'MermaidBlock',props: {code: { type: String, required: true },},setup(props) {const imageUrl = ref('')const uniqueId = getUniqueId()const containerRef = ref<HTMLDivElement | null>(null)const renderMermaid = () => {if (!containerRef.value) returnmermaid.initialize({startOnLoad: true,theme: 'neutral',flowchart: { useMaxWidth: false },})mermaid.render(uniqueId, props.code, containerRef.value).then(r => {imageUrl.value = svgToDataUrl(r.svg)}).catch(err => {console.error('Mermaid渲染错误:', err)})}onMounted(renderMermaid)watch(() => props.code, renderMermaid)onBeforeUnmount(() => {if (!imageUrl.value) returnURL.revokeObjectURL(imageUrl.value)})return () => (<div class="bg-gray-800 p-4 rounded-md mb-4 flex items-center justify-center"><divref={containerRef}class="absolute -left-[9999px] -top-[9999px] w-[800px] h-[400px]"/><NImagesrc={imageUrl.value}alt="Mermaid Diagram"previewed-img-props={{ class: 'bg-gray-800' }}/></div>)},
})
3.5 图表渲染组件
// src/markdown/components/EchartsBlock/index.tsx
import { defineComponent, onMounted, ref, watch } from 'vue'
import * as echarts from 'echarts'export default defineComponent({name: 'EchartsBlock',props: {option: { type: String, required: true },},setup(props) {const chartRef = ref<HTMLDivElement | null>(null)let chartInstance: echarts.ECharts | null = nullconst renderChart = () => {if (chartRef.value) {if (!chartInstance) {chartInstance = echarts.init(chartRef.value)}try {const optionObj = JSON.parse(props.option)chartInstance.setOption(optionObj)} catch (e) {console.error('图表渲染错误:', e)chartInstance.clear()chartInstance.setOption({title: {text: '图表解析中',left: 'center',top: 'center',},})}}}onMounted(renderChart)watch(() => props.option, renderChart)return () => <div class="w-full mb-4 min-h-[300px]" ref={chartRef}></div>},
})

关键技术点

  1. 插件系统: 可扩展的插件架构设计
  2. Token解析: 自定义Token渲染逻辑
  3. 数学公式: KaTeX集成和错误处理
  4. 流程图: Mermaid渲染和SVG处理
  5. 图表集成: ECharts动态渲染

http://www.dtcms.com/a/352524.html

相关文章:

  • MyBatis-Flex是如何避免不同数据库语法差异的?
  • 【electron】一、安装,打包配置
  • 全面赋能政务领域——移动云以云化升级推动政务办公效能跃迁
  • 【硬件-笔试面试题-61】硬件/电子工程师,笔试面试题(知识点:RC电路中的充电时间常数)
  • vue3 + jsx 中使用native ui 组件插槽
  • babel使用及其底层原理介绍
  • Java 集合笔记
  • 第二章 进程与线程
  • 简明 | Yolo-v3结构理解摘要
  • Python-机器学习概述
  • ruoyi-vue(十二)——定时任务,缓存监控,服务监控以及系统接口
  • Python 轻量级的 ORM(对象关系映射)框架 - Peewee 入门教程
  • CentOS 7 升级 OpenSSH 10.0p2 完整教程(含 Telnet 备份)
  • 性能瓶颈定位更快更准:ARMS 持续剖析能力升级解析
  • 告别繁琐运维,拥抱自动化:EKS Auto Mode 实战指南
  • C代码学习笔记(二)
  • RK3506 开发板:嵌入式技术赋能多行业转型升级
  • 大数据时代UI前端的智能化升级路径:基于用户行为数据的预测性分析
  • PMP项目管理知识点-⑨项⽬资源管理
  • 大模型应用编排工具Dify之插件探索
  • 【LeetCode - 每日1题】求对角线最长矩形的面积
  • Claude 的优势深度解析:大模型竞争格局中的隐藏护城河
  • NX773HSA19美光固态闪存D8BJND8BJQ
  • inline内联函数
  • TensorFlow 深度学习:使用 feature_column 训练心脏病分类模型
  • 【软考论文】论可观测性架构技术的应用
  • 【资源】Github资源整理
  • C6.3:发射结交流电阻
  • Vue3 + Element Plus实现表格多行文本截断与智能Tooltip提示
  • 【黑客技术零基础入门】2025最新黑客工具软件大全,零基础入门到精通,收藏这篇就够了!