从Markdown出击 - 文档数据转换解决方案

@onvno 2017-09-07 02:06:11发表于 iuap-design/blog

文档数据转换

项目中文档页面做了数据分离,需要将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();