前言
使用nodejs模块marked解析Markdown文章
优点:
- 前后端,不同OS 都可使用
- 开源,易扩展
- 速度比较快 (通过RegEx匹配解析)
应用举例:
- 作为markdown编辑器
- 网上各个markdown编辑器的规则、功能、风格、依赖平台等都有不统一,导出不理想,自定制有局限,无法做扩展
- 自由加入各个开发项目中
- static site generator
- online editor
- ...
- 作为markdown编辑器
1. 搭建测试环境
> mkdir node-marked
> cd node-marked
> npm init
> npm install marked --save-dev
> npm install highlight.js --save-dev
2. 使用 Marked Cmd
> npm install marked -g
> marked -i articles/01.md
> marked -i articles/01.md -t
> marked -i articles/01.md -o pages/01.html
参数说明
- -o, –output: 指定输出文件,默认为当前控制台
- -i, –input: 指定输入文件或最后一个参数,默认为当前控制台输入
- -t, –tokens: 输出token流代替HTML
- –pedantic: 只解析符合markdown.pl定义的,不修正markdown的错误
- –gfm: 启动Github样式的Markdown,参考 github-flavored-markdown
- –breaks: 支持Github换行符,必须打开gfm选项
- –tables: 支持Github表格,必须打开gfm选项
- –sanitize: 原始输出,忽略HTML标签
- –smart-lists: 优化列表输出
- –lang-prefix [prefix]: 设置前置样式
- –no-etc: 选择的反正标识
- –silent: 不输出错误信息
- -h, –help: 帮助信息
3. 使用 Marked API
API 具体可参考 GitHub Marked
marked(markdownString [,options] [,callback])
3.1 Minimal usage
var marked=require("marked");
console.log(marked("I am using marked api!"));
> node 01
<p>I am using marked api!</p>
3.2 Setting options example
var marked=require("marked");
var highlight=require("highlight.js");
marked.setOptions({
gfm: true,
tables: true,
breaks: true,
pedantic: false,
sanitize: false,
smartLists: true,
smartypants: false,
codePrefix:"hljs",
tableClass:"table",
highlight:function (code,lang) {
return highlight.highlightAuto(code,[lang]).value;
}
});
console.log(marked(markdownString));
> node 01
<h3 id="i-am-using-marked-api-">I am using marked api!</h3>
<pre><code class="lang-js"> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Hello world!'</span>);
</code></pre>
3.3 Browser
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Marked in the browser</title>
<script src="lib/marked.js"></script>
</head>
<body>
<div id="content"></div>
<script type="text/javascript">
document.getElementById('content').innerHTML = marked('# Marked in browser\n\nRendered by **marked**.');
</script>
</body>
</html>
4. Marked 解析说明
function marked(src,opt,callback){
...
opt = merge({}, marked.defaults, opt);
...
tokens = Lexer.lex(src, opt);
...
out=Parser.parse(tokens,opt);
...
}
4.1 解析过程
- Lexer: Block Level Render => tokens
- Parser: Parse tokens => html each token => Render && InlineLexer(Inline Level Render ) => html
4.2 核心类介绍
1. Lexer ( Block Level Render )
var block={...} //定义块级解析规则(正则表达式)
//newline,code,fences,hr,heading,nptable,lheading,blockquote,list,html,def,table,paragraph,text
Lexer.rules=block;
Lexer.lex = function(src, options) {
var lexer = new Lexer(options);
return lexer.lex(src);
};
Lexer.prototype.lex //调用 Lexer.prototype.token
Lexer.prototype.token //根据rules解析返回tokens数组
2. Parser ( Parse Tokens to HTML )
Parser.parse = function(tokens, options, renderer) {
var parser = new Parser(options, renderer);
return parser.parse(tokens);
};
Parser.prototype.parse //调用 Parser.prototype.tok 逐个解析token
Parser.prototype.tok //根据token.type调用Render的相应方法进行渲染
ps: 有些token在调用Render渲染前会预先调用InlineLex进行预处理
3. Render ( Render Each Token to HTML )
Renderer.prototype.code
Renderer.prototype.blockquote
Renderer.prototype.html
Renderer.prototype.heading
Renderer.prototype.hr
Renderer.prototype.list
Renderer.prototype.listitem
Renderer.prototype.paragraph
Renderer.prototype.table
Renderer.prototype.tablerow
Renderer.prototype.tablecell
Renderer.prototype.strong
Renderer.prototype.em
Renderer.prototype.codespan
Renderer.prototype.br
Renderer.prototype.del
Renderer.prototype.link
Renderer.prototype.image
Renderer.prototype.text
4. InlineLexer ( Inline Level Renderer )
var inline={...} //定义行级解析规则(正则表达式)
//escape,autolink,url,tag,link,reflink,nolink,strong,em,code,br,del,text
InlineLexer.rules = inline;
InlineLexer.output = function(src, links, options) {
var inline = new InlineLexer(links, options);
return inline.output(src);
};
InlineLexer.prototype.output //根据rules解析,返回字符串
ps: 有些匹配项会调用Render的相应方法进行渲染
5. Marked 扩展
在了解了marked的解析过程后, 对于一些无法通过marked本身提供的配置选项完成的特殊功能, 可以自己在上面拓展一些功能(这也是会选择marked的原因之一)
这里列举两个比较常用的拓展:
- 增加
TOC(HeaderAnchor)
功能;- 增加解析
meta
功能;
5.1 拓展:增加 TOC 功能 ( HeaderAnchor )
[TOC]
1. Lexer
var block = {
...
toc: /^\[TOC\]\n/,
};
Lexer.prototype.token = function(src, top, bq) {
...
var tocIndex=-1;
while(src){
...
if(cap=this.rules.toc.exec(src)){
//console.log("token toc");
src = src.substring(cap[0].length);
this.tokens.push({type:'toc'});
tocIndex=this.tokens.length-1;
continue;
}
...
}
if(tocIndex>=0){
var toc=[];
for(var i=0;i<this.tokens.length;i++){
if(this.tokens[i].type==='heading')
toc.push(this.tokens[i]);
}
this.tokens[tocIndex]={type:"toc",datas:toc};
}
return this.tokens;
};
2. Parser
Parser.prototype.tok = function() {
switch (this.token.type){
case 'toc':{
//console.log("parse toc");
var datas=this.token.datas;
for(var i=0;i<datas.length;i++){
datas[i].raw=datas[i].text;
datas[i].text=this.inline.output(datas[i].text);
}
return this.renderer.toc(datas);
}
...
}
};
3. Render
Renderer.prototype.toc = function(datas) {
//console.log("render toc");
var tocStr='',indentStr='',item=undefined;
for(var i=0;i<datas.length;i++){
item=datas[i];
indentStr='';
for(var j=0;j<item.depth-1;j++)
indentStr+=' ';
var id=this.options.headerPrefix+item.raw.toLowerCase().replace(/[^\w]+/g, '-');
tocStr+="<li>"+indentStr+"<a href='#"+id+"'>"+item.text+"</a></li>";
}
return this.list(tocStr,false);
};
4. 使用示例
var marked=require("./marked-extend");
var fs=require("fs");
fs.readFile("articles/01.md",'utf8',function(err,data){
var result=marked(data);
fs.writeFile("pages/01-1.html",result);
});
5.2 拓展:增加解析 MetaHeader
---
title: 用Middleman搭建静态博客
tags: web, static
description: Build Static Blog by Middleman
---
1. Lexer
var block = {
...
meta:/^-{3,}\s*?\n([\s\S]*?)\n-{3,}/,
metaItem:/(\S+)\s*?:\s*([\s\S]*?)(?=$|\n)/g,
};
Lexer.prototype.token = function(src, top, bq) {
...
var tocIndex=-1;
while(src){
...
if(cap=this.rules.meta.exec(src)){
src=src.substring(cap[0].length);
var meta={};
while((item=this.rules.metaItem.exec(cap[1]))!=null){
//console.log(item[1]+":"+item[2]);
meta[item[1]]=item[2];
}
this.tokens.push({type:"meta",datas:meta});
continue;
}
...
}
return this.tokens;
};
Lexer.prototype.getToken=function(tokens,type){
var token=undefined;
for(var i=0;i<tokens.length;i++){
if(tokens[i].type===type){
token=tokens[i];
break;
}
}
return token;
};
// Static Method
Lexer.getToken=Lexer.prototype.getToken;
2. Parser
Parser.prototype.tok = function() {
switch (this.token.type){
case 'meta':{
return "";
}
...
}
};
3. 使用示例
var marked=require("./marked-extend");
var fs=require("fs");
fs.readFile("articles/01.md",'utf8',function(err,data){
var Lexer=marked.Lexer;
var lexer=new Lexer();
var tokens=lexer.lex(data);
var tocToken=lexer.getToken(tokens,"toc");
console.log("toc:");
console.log(tocToken);
var metaToken=lexer.getToken(tokens,"meta");
console.log("meta:");
console.log(metaToken);
var result=marked.parser(tokens);
fs.writeFile("pages/01-2.html",result);
});
6. HTML 结果页面扩展
6.1 使用 Highlight.js 进行语法高亮
1.Highlight介绍 ( 参考 LiveDemo | Usage | Doc&API )
使用在线Highligh ( 可在线定制,参考 Getting highlight.js,CDN highlight.js )
<link rel="stylesheet" type="text/css" href="http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.9.1/styles/default.min.css" />
<script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.9.1/highlight.min.js"/>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.9.1/languages/markdown.min.js"></script>
<script type="text/javascript">
hljs.initHighlightingOnLoad();
</script>
使用本地构建的Highligh
> bower install highlight --save-dev
> cd bower_components/highlight
//构建Highligh
> npm install
> node tools/build.js -t all // build all [ node cdn browser]
(或者只构建browser,使用 node tools/build.js -t browser)
PS: 若执行报类似如下错误,请升级node,或下载highlight#8 ( bower install highlight#8 )
build = require(`./${commander.target}`);
^
SyntaxError: Unexpected token ILLEGAL
at Module._compile (module.js:439:25)
at Object.Module._extensions..js (module.js:474:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:312:12)
at Function.Module.runMain (module.js:497:10)
at startup (node.js:119:16)
at node.js:902:3
使用构建的browser highlight
<link rel="stylesheet" type="text/css" href="../bower_components/highlight/build/browser/demo/styles/default.css" />
<script type="text/javascript" src="../bower_components/highlight/build/browser/highlight.pack.js"></script>
<script type="text/javascript">
hljs.initHighlightingOnLoad();
</script>
PS:
- 构建出的node highlight 和通过 npm install highlight.js 获取的highlight是一致的
- 要在前端网页中使用node highlight,还需通过browserify插件转换 (具体使用可参考gulp篇)
2. 在Marked中使用highlight.js
前端只加载highlight css文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Marked Highlight</title>
<link rel="stylesheet" type="text/css" href="../bower_components/highlight/build/browser/demo/styles/tomorrow-night-bright.css" />
</head>
<body>
{content}
</body>
</html>
后端调用highlight解析markdown的code内容
var marked=require("./marked-extend");
var fs=require("fs");
var highlight=require("highlight.js");
marked.setOptions({
langPrefix:"hljs ",
highlight:function (code,lang) {
return highlight.highlightAuto(code,[lang]).value;
}
});
fs.readFile("articles/test.md",'utf8',function(err,data){
var result=marked(data);
var tpl=fs.readFileSync("template/highlight-03.html",'utf-8');
fs.writeFile("pages/highlight-03.html",tpl.replace("{content}",result));
});
3.调用区别
- 前端调用
hljs.initHighlightingOnLoad();
识别解析<pre><code>...</code></pre>
块- 若无language设置,则自动识别添加一种languange
<pre><code class="hljs html">...</code></pre>
- 若有languange设置
- 是已注册的languange,则使用相应的languange js进行解析
<pre><code class="hljs javascript">...</code></pre>
- 是未注册的languange,则不解析
<pre><code class="nohighlight">...</code></pre>
- 是已注册的languange,则使用相应的languange js进行解析
- 若无language设置,则自动识别添加一种languange
- 后端marked解析code
- 若无languange设置,则不会在
<code>
标签上增加class - 若有languange设置,则会在
<code>
标签上增加class(langPrefix+languange)
- 若无languange设置,则不会在
Renderer.prototype.code = function(code, lang, escaped) {
if (this.options.highlight) {
var out = this.options.highlight(code, lang);
if (out != null && out !== code) {
escaped = true;
code = out;
}
}
if (!lang) {
return '<pre><code>'+ (escaped ? code : escape(code, true)) + '\n</code></pre>';
}
return '<pre><code class="'
+ this.options.langPrefix
+ escape(lang, true)
+ '">'
+ (escaped ? code : escape(code, true))
+ '\n</code></pre>\n';
};
PS: 可以修改前后端以统一两种调用highlight的效果
6.2 增加 LaTex 公式支持
使用 MathJax (参考 LaTex,MathJax Doc)
//从CDN加载MathJax
<script type="text/javascript"
src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
</script>
//或加载通过bower install MathJax --save-dev 获取到本地的MathJax
<script type="text/javascript" src="../bower_components/MathJax/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
//可添加配置,eg:
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
...
});
</script>
7. Issues
7.1 Marked TOC heading id 乱码
marked中解析出的
<hx>
标签id生成规则为:this.options.headerPrefix+ raw.toLowerCase().replace(/[^\w]+/g, '-')
( 将文本中空格替换为-
)若是中文或其他特殊字符会导致难以Anchor
自定义h
标签的id生成规则:
1.Lexer
Lexer.prototype.token = function(src, top, bq) {
...
while(src){
...
}
if(tocIndex>=0 || this.options.reHeader){
var toc=[];
for(var i=0;i<this.tokens.length;i++){
if(this.tokens[i].type==='heading'){
toc.push(this.tokens[i]);
this.tokens[i].index=toc.length; // add heading index
}
}
this.tokens[tocIndex]={type:"toc",datas:toc};
}
return this.tokens;
};
2.Parser
Parser.prototype.tok = function() {
switch (this.token.type) {
...
case 'heading': {
return this.renderer.heading(
this.inline.output(this.token.text),
this.token.depth,
this.token.text,this.token.index); // add heading index
}
...
}
}
3.Render
Renderer.prototype.heading = function(text, level, raw, index) {
return '<h'+ level
+ ' id="'
+ this.options.headerPrefix
//+ raw.toLowerCase().replace(/[^\w]+/g, '-')
+ (index?index:raw.toLowerCase().replace(/[^\w]+/g, '-'))
+ '">'
+ text+ '</h'+ level+ '>\n';
};
Renderer.prototype.toc = function(datas) {
...
//var id=this.options.headerPrefix+item.raw.toLowerCase().replace(/[^\w]+/g, '-');
var id=this.options.headerPrefix+(item.index?item.index:item.raw.toLowerCase().replace(/[^\w]+/g, '-'));
...
};
7.2 Highlight & Markdown Conflict with MathJax
1.约定markdown中的LaxTex公式
含有LaxTex公式的包含在<pre></pre>
标签中
2. 添加MathJax配置
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
tex2jax: {
//skipTags remove 'pre' entry
skipTags: ['script', 'noscript', 'style', 'textarea','code']
}
});
</script>
8. 最终版
- 项目源码 github
9. 各种Markdown编辑器
- Windos版,推荐 Haroopad
- OS X 版,推荐 Haroopad,MacDown,TextNut
- Online版,推荐 简书,马克飞象
- 如果不嫌麻烦,也可以使用sublime+plugin自定制
10. 参考
- markdown
- marked
- markdown editor
- highlight
- mathJax