实现一个简单的 markdown 转译器
实现一个 markdown 转译器,就是一行一行的解析规则,就像人眼睛看的那样来操作,一行一行的看,然后匹配规则,如果是无序列表这种多行的规则,就判断如果是无序列表,就继续往下看一行,直到规则匹配完,记录匹配的位置索引,继续向下看一行,最后将解析到所有的 html 规则放到数组里面,返回即可。这里首先是根据换行将每一行的记录到数组里面,根据索引,一行行向下看,循环里面的规则,直到索引等于数组的长度就退出循环。
主函数
本地写一个 markdown 文件,然后读取文件的内容,传给转译器进行转译;
const main = () => {
const fs = require('fs');
const path = require('path')
const file = path.join(__dirname, './test.md');
const source = fs.readFileSync(file, 'utf-8');
console.log(MdtoHtml(source).join('\n'));
}
main();
MdtoHtml
就是实现的转译函数,这个test.md 的内容如下:
## 这是一个标题
### 无序列表
+ React
+ Vue
+ webpack
+ Antd
这一行文字里面有**加粗**
+ vue
`代码片段`
现在就先转译这几个规则吧,MdtoHtml
函数只要是包装一下,为了能看到转译的效果,直接console.log
const MdtoHtml = (source) => {
// 判断参数类型
if(typeof source === 'undefined' || source === null){
throw new Error('input parameter is udefined or null')
}
if(typeof source !== 'string'){
throw new Error(`input parameter type is ${Object.prototype.toString.call(source)}, not expected string`)
}
// 处理所有的 \n, \t, \r 为 \n
// markdown 转译器
const htmls = transCompiler(source.replace(/\r\n | \r/, '\n'));
console.log(htmls);
}
接下来transCompiler
函数才是最主要的,处理拿到的每一行的规则数组。
const transCompiler = (source) => {
const lines = source.split('\n');
// 返回转译过的 html 标签
return transToDocument(lines);
}
const transToDocument = (lines) => {
// 记录转译的当前行索引
let cur = 0;
// 存储转译每一行的 html 标签
let htmls = [];
// 循环转译每一行的 md 规则,如果 cur === length,跳出循环,如果有匹配的规则就进行下一轮的循环;
// 转译每一条 markdown 规则
while(true){
if(cur === lines.length){
break;
}
// 当前行
const line = lines[cur];
// 处理标题
if(block.titleReg.test(line)){
const { index, html } = handleTitle(lines, cur);
htmls.push(html);
cur = index;
continue;
}
// 转译无序列表
if(block.ulReg.test(line)){
const { index, html } = handleUlList(lines, cur);
htmls.push(html);
cur = index;
continue;
}
// 转译单行
const html = singleLine(line);
htmls.push(html);
cur++;
}
return htmls.join('\n');
}
每一个规则都独立写到一个函数中,最后返回处理的html
字符串和索引即可,处理的规则根据区块元素和区段元素划分;
// 区块元素匹配的正则
const block = {
// 标题
titleReg: /^(\#{1,6}) (.+)/,
codeReg: /.*\`(.+)\`.*/,
boldReg: /.*\*\*(.+)\*\*.*/,
ulReg: /^\+ (.+)/,
}
// 区段元素
const section = {
}
处理标题的函数 handleTitle
,拿到当前行,解析,索引往下加一行,表示这行处理完,继续看下一行的规则。
const handleTitle = (lines, cur) => {
const [ , hash, content ] = block.titleReg.exec(lines[cur]);
const tag = `h${hash.length}`;
const html = `<${tag}>${content}</${tag}>`;
return {
index: cur + 1,
html
}
}
处理无序列表handleUlList
,看当前行是否满足无序的正则,如果满足就继续向下看,索引加1,不满足就退出循环,然后处理匹配到的行,拼接成ul
,返回新的索引,和 html 字符串。
const handleUlList = (lines, cur) => {
let next = cur;
while(true){
next++;
if(!block.ulReg.test(lines[next])){
break;
}
}
//转译拿到的 N 行
let htmls = [];
for(let i = cur; i < next; i++){
const li = lines[i];
const result = block.ulReg.exec(li);
const html = `<li>${result[1]}</li>`
htmls.push(html);
}
// 首尾加闭合标签
htmls.unshift('<ul>');
htmls.push('</ul>');
return {
index: next,
html: htmls.join('\n')
}
}
再就是处理单行的函数,如果单行里面有code 或加粗,直接替换:
const singleLine = (line) => {
return line.replace(block.codeReg, (match, code) => {
// 单行代码
return `<code>${code}</code>`
}).replace(block.boldReg, (match, bold) => {
// 单行加粗
return `<b>${bold}</b>`
});
}
最后这个简单的 markdown 转译器就写完了,处理的规则很少,只领会精神,全部处理的话,非常庞大,以后慢慢加吧。运行转译器,转译之后的内容如下:
<h3>无序列表</h3>
<ul>
<li>React</li>
<li>Vue</li>
<li>webpack</li>
<li>Antd</li>
</ul>
<b>加粗</b>
<ul>
<li>vue</li>
</ul>
<code>代码片段</code>
github 地址:https://github.com/mxcz213/koa-test
分支切到 md_to_html
,完整代码在 md_to_html/static/js/mdtohtml.js
里面。