这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战
引言
markdown 的语法相较其他来说,更简单、直接。
市面上有很多的 markdown 转 html 工具。但各家的 转换工具都只有固定的转换样式,而支持自定义样式的转换工具,如 markdown-it 虽然支持配置插件 来实现样式自定义,但门槛稍高,需要了解 其插件api。
所以希望实现一款支持以下功能的 markdown 转 html 工具,命名为 @md2html。
- 上手难度低
- 配置灵活
- 实时预览
原理
整体的执行过程,与我们常说的
javascript中的AST转换步骤 完全相同 , 只是具体应用到的语言从javascript换成了markdown。
- 首先将
markdown分解为语法树(用于描述markdown语法的json数据结构) - 其次遍历
语法树,在对应的语法节点执行转换的操作(转换后的html需要具有样式属性) - 然后拼装
转换后的html标签与自定义的CSS样式成完整的html文件并输出 - 最后构建Web markdown转换站点,监听
change事件,重复上述步骤以实现在线转换
技术栈
remark用于将markdown文本转换为语法树@md2html/traverse接收remark产出的语法树,并提供类似@babel/traverse的api 用于遍历所有节点@md2html/parse通过添加一系列的 markdown 节点遍历操作,来实现转换的核心步骤
代码分析
如何得到 markdown 的语法树 ?
import remark from 'remark'
const md = '# 标题'
const ast = remark().parse(md)
如何遍历 ast ?
由于没有找到类似 @babel/traverse 的遍历 ast 的库,所以考虑自己写一个 mardown-traverse。
-
方法信息
函数名:traverse入参:- @param1:
AST[ markdown的JSON结构语法树 ] - @param2:
visitors[ 遍历对象,key对应 语法树中的节点类型,value类型是函数,传入该节点信息 ]
-
AST 结构及 visitors 结构
// ast
{
"type": "root", // 节点类型(根节点)
"children": [ // 子节点
{
"type": "heading", // 节点类型(h1/h2/h3/h4/h5/h6)
"depth": 1, // 节点的相关信息
"children": []
}
]
}
// visitors
{
/* node: 该节点信息; utils: 遍历时 `mardown-traverse` 提供的工具方法 */
heading(node, utils) {
const { depth } = node
}
}
- 核心代码
function traverse(ast, visitors, parentNode = null) {
// 提供给visitors的工具方法
const util = {
parentNode: parentNode, // 记录当前节点的父亲
// ...
}
if (visitors[ast.type]) {
// 进入该节点
if (visitors[ast.type].enter) {
visitors[ast.type].enter(ast, util)
}
}
if (ast.children && ast.children.length) {
ast.children.map(child => traverse(child, visitors, ast))
}
if (visitors[ast.type]) {
// 离开该节点
if (visitors[ast.type].leave) {
visitors[ast.type].leave(ast, util)
}
}
}
如何替换节点
以 # 标题 为例,该markdown 对应的 节点是 heading , 只需要在上述传入的visitors中配置 heading 节点的替换逻辑即可。
const result = []
traverse(ast, {
heading: {
// 实际 util中封装了对不同标签的标签名转换及样式转换,此处仅为举例说明流程
enter(node, util) {
const tagName = `h${node.depth}`
const style = `class="md2html-${tagName}"`
result.push(`<${tagName} ${style}>`) // <h1>
},
leave(node, util) {
result.push(`</h${node.depth}>`) // </h1>
}
}
})
至此,基本完成了 heading 节点的转换逻辑。
如何实现样式灵活可配置
通过上一步可观察到 # 标题 的最终标签具有该属性: class="md2html-h1" , 因此只需要配置一段 CSS, 具有以下代码即可实现。
.md2html-h1{
/* 具体样式*/
}