MD转换 - 从海底出击

文档数据转换

项目中文档页面做了数据分离,需要将summary.md转为json,页面在渲染后加载,虽说增加了加载时间,却也省去了发布的繁琐。针对需要的转换整理了一些资料:

相关文档

初期思路

最初文档只需要支持两级,直接上手使用了正则匹配,实现两级目录。

实践中有思路如下:

  • 文件读取单行数据:此处使用了hack方法。

    // 单行数据
    fs.readFileSync(item).toString().split('\n').forEach(function(line){
    处理单行数据
    })
  • 针对单行数据进行正则匹配:返回 subject字符 - 文章标题 、 对象包含level级别及tit,link

    function matchLine(str){
    let tit,link,level;
    if(str.indexOf('#')== -1 && str.trim()){
      const result = /^\*/.test(str);
      level = result ? 1 : 2;
      if(level==1){
        tit = str.match(/\[([\S\s]*)\]/) ? str.match(/\[([\S\s]*)\]/)[1] : str.substring(1).trim();
        link = '';
        if(str.match(/\((\S*)\)/)){
          link = str.match(/\((\S*)\)/)[1].replace(/^\//,'');
        }
      } else if(level==2){
        tit = str.match(/\[([\S\s]*)\]/)[1];
        link = str.match(/\((\S*)\)/)[1].replace(/^\//,'');
      }
      return {
        "level": level,
        "tit": tit,
        "link": link
      }
    } else if(str.indexOf('#')!= -1 && str.trim()) {
      let subject = str.trim().replace(/^#+\s+(.*)/,'$1');
      return subject;
    } else {
      return {}
    }
    }
  • 返回数据写入对象:

    let obj = {};
    obj.ary = [];
    obj.title = '';
    if(typeof back == 'string') {
        obj.title = back;
      } else if(back.level && back.level == 1){
        obj.ary.push({
          tit: back.tit,
          link: back.link
        })
      } else if(back.level && back.level == 2) {
        if(!obj.ary[lastIndex].sub){
          obj.ary[lastIndex].sub=[];
        }
        obj.ary[lastIndex].sub.push({
          "tit": back.tit,
          "link": back.link
        })
      }
  • 对象转字符,写入文件:为返回数据格式化结果,增加了参数配置

    const objStr = JSON.stringify(obj,null, 4);
    const outPath = item.replace(SET.ENTRY,SET.OUT).replace('.md','.json');
    fse.ensureFileSync(outPath);
    fse.writeFileSync(outPath,objStr,'utf-8');

后期思路

后期业务需要增加多级目录,正则匹配不是个明智的选择,单行数据和写入对象这两部分做了调整,思路如下:

  • md先转换为html:使用第三方marked库
  • node端html的DOM树转为json对象:使用cheerio

因此部分有合适的第三方包做铺路,代码思路比较清晰,就补贴出具体代码了,需要注意一点是:使用marked转换时,markdown内容存在空行情况下,输出的dom结构会不同,此处单独做了兼容处理。

存在空行的summary会将一级目录编译为:

// 无连接
<li>
<p>一级目录</p>
...
<li>

// 有链接
<li>
<p><a href="path/to/get">一级目录</a>
...
</p>

默认会将一级目录转换为:

// 无连接
<li>
一级目录
...
</li>

// 有链接
<li>
<a href="path/to/get">一级目录</a>
...
</li>

因需要读取不同标签及纯文本内容,做以下处理:

var local = menu.eq(i);
var local_head = has(local,'p') ? local.children('p') : local;

if(local_head.children('a').length) {
    lister.link = local_head.children('a').attr('href');
    lister.tit = local_head.children('a').text();
} else {
    lister.link = '';
    lister.tit = local_head.contents().filter(function(){
    return this.nodeType == 3
}).text().replace('\n','');
}

通过此思路,实现summary无限层级转换为json.

通用方案

在转换中,没有找到比较好的方案,于是顺带将代码做了一个打包,输出一个一键命令行转换工具:

# install
$ npm install s2json -g

# enter project
$ cd your-project

# build: 源目录若在doc,输出在dist,则执行
$ s2json --entry doc --out dist

具体使用说明见:github

思考

markdown <> json <>html, 三者的转换有哪些方案, 整理如下:

markdown -> html

markdown -> json

  • s2json:王婆卖瓜自卖自夸

json -> html

html -> json

其他

  • pandoc:万能格式转换,包含epub等转换,但实际使用转json效果不佳

整理说明

  • 项目初期考虑的越周全,后期更新扩展会更容易,此次方案上前后反差较大。

  • 站在markedcheerio巨人的脚上实现了多级数据。

  • cheeriojsdom两个库虽然都可以选择dom,但从使用来讲,站在jquery肩膀上的cheerio简直完美。

附带cheerio一点使用中的小坑。


cheerio基本demo

var cheerio = require('cheerio'),
var html = `
<h2 class="title">Hello world</h2>
<ul id="fruits">
  <li class="apple">Apple</li>
  <li class="orange">Orange</li>
  <li class="pear">Pear</li>
</ul>
`
$ = cheerio.load(html, {
  decodeEntities: false
});

// 以下按照jQuery方法即可提取元素
$('h2.title').text()

cheerio常见FAQ

  • children - 查找当前层级

  • find - 查找所有层级

  • cheerio html 方法中文字体被转换

    cheerio本身默认是转实体的
    cheerio.load(html,{decodeEntities: false}); 加个参数
  • jquery cheerio 获取元素文本内容,不包括后代

    $('.element').contents().filter(function () {
      return this.nodeType == 3;
    }).text();

@2017-09-06 13:39