读marked.js源码 -- markdown是怎么变成html的
很好奇markdown是怎么变成html的,偶然间看到 github上的这一篇星星✨很高的源码,就来研究研究 下载打开在lib文件夹下的marked.js大概1600行左右的样子,没有组件化,所有的js都是在一个页面,读着很是顺畅哈~(这里没有贬低多个文件的啊,不要生气,嘻嘻😄其实是有的,一个文件就是看着简单) 在html引入js
<script src="./marked.js"></script>
然后在js中写下
<script>
document.getElementById('content').innerHTML =
marked('# Marked in the browser\n\nRendered by **marked**.');
</script>
神奇的现象发生了页面变漂亮了??? 那么就随marked函数看一看是如何实现的吧 首先我们定位到1461行,看到了一个我们所熟悉亲切的marked函数,接受3个参数,src是markdown, opt是配置{}, callback是回调
// 1461
mark(src, opt, callback)
// 1544
Parser.parse(Lexer.lex(src, opt), opt);
// 返回了真正的带有标签的文本,所以接下来研究Lexer对象和Parser.parse
Lexer.lex(src, opt)
// 163
Lexer.lex = function(src, options) {
var lexer = new Lexer(options);
return lexer.lex(src);
};
Lexer.prototype.lex = function(src) {
src = src
.replace(/\r\n|\r/g, '\n')
.replace(/\t/g, ' ')
.replace(/\u00a0/g, ' ')
.replace(/\u2424/g, '\n');
// console.log('178#', src)
console.log('178#',this.token(src, true)[0],this.token(src, true)[1])
// {type: "heading", depth: 1, text: "Marked in the browser"}
// {type: "paragraph", text: "Rendered by **marked**."}
return this.token(src, true);
};
this.token(src, true);返回数组,数组长度为3,[0]是Marked in the browser, [1]是Rendered by marked.字段
我们看185行 Lexer.prototype.token cap = this.rules.heading.exec(src)成立
["# Marked in the browser↵↵", "#", "Marked in the browser", index: 0, input: "# Marked in the browser↵↵Rendered by marked.", groups: undefined]
if (cap = this.rules.heading.exec(src)) {
src = src.substring(cap[0].length);
this.tokens.push({
type: 'heading',
depth: cap[1].length,
text: cap[2]
});
continue;
}
追踪
this.rules.heading.exec(src)
Lexer.rules = block;
inline是一个对象,包含一整套匹配规则
// 14
var block = {
newline: /^\n+/,
code: /^( {4}[^\n]+\n*)+/,
fences: noop,
hr: /^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,
heading: /^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/,
nptable: noop,
blockquote: /^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,
list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,
html: '^ {0,3}(?:' // optional indentation
+ '<(script|pre|style)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)' // (1)
+ '|comment[^\\n]*(\\n+|$)' // (2)
+ '|<\\?[\\s\\S]*?\\?>\\n*' // (3)
+ '|<![A-Z][\\s\\S]*?>\\n*' // (4)
+ '|<!\\[CDATA\\[[\\s\\S]*?\\]\\]>\\n*' // (5)
+ '|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:\\n{2,}|$)' // (6)
+ '|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=\\h*\\n)[\\s\\S]*?(?:\\n{2,}|$)' // (7) open tag
+ '|</(?!script|pre|style)[a-z][\\w-]*\\s*>(?=\\h*\\n)[\\s\\S]*?(?:\\n{2,}|$)' // (7) closing tag
+ ')',
def: /^ {0,3}\[(label)\]: *\n? *<?([^\s>]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,
table: noop,
lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,
paragraph: /^([^\n]+(?:\n(?!hr|heading|lheading| {0,3}>|<\/?(?:tag)(?: +|\n|\/?>)|<(?:script|pre|style|!--))[^\n]+)*)/,
text: /^[^\n]+/
};
我们找到了 this.rules.heading是block里的heading,那么接下来就是exec exec就是正则!正则!正则!
/^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/.exec('# Marked in the browser\n\nRendered by **marked**.')
正则匹配结果是 数组 ["# Marked in the browser↵↵,"#","Marked in the browser"]
// 236
if (cap = this.rules.heading.exec(src)) {
// src是剩下的也就说Rendered by **marked**.,当src长度为0的时候,循环结束
src = src.substring(cap[0].length);
// 判断 是heading
// push type
// depth是正则匹配"#"的长度
// text 是匹配出来的的结果
this.tokens.push({
type: 'heading',
depth: cap[1].length,
text: cap[2]
});
continue;
}
接下来继续解析src,我们看到第491行这里被解析了,上面第正则都未匹配成功
if (top && (cap = this.rules.paragraph.exec(src))) {
src = src.substring(cap[0].length);
this.tokens.push({
type: 'paragraph',
text: cap[1].charAt(cap[1].length - 1) === '\n'
? cap[1].slice(0, -1)
: cap[1]
});
continue;
}
我们看一下paragraph和以及被处理的结果 paragraph在block声明的时候被定义了
{ paragraph: /^([^\n]+(?:\n(?!hr|heading|lheading| {0,3}>|<\/?(?:tag)(?: +|\n|\/?>)|<(?:script|pre|style|!--))[^\n]+)*)/}
cap[0]= "Rendered by **marked**."
cap[1]= "Rendered by **marked**."
此时src为空字符串,直接停止,跳出循环
此时this.tokens为
this.tokens = [
{type: "heading", depth: 1, text: "Marked in the browser"},
{type: "paragraph", text: "Rendered by **marked**."]
此时返回了最初的Parser.parse(Lexer.lex(src, opt), opt); 词法分析器 Lexer.lex(src, opt)调用的结果是this.tokens 接下来就让我们分析Parser.parse吧
// Static Parse Method 静态方法,为什么不直接new,还需要绕这么一个大圈子
Parser.parse = function(src, options) {
var parser = new Parser(options);
return parser.parse(src);
};
/**
* Parse Loop
*/
//1115
Parser.prototype.parse = function(src) {
//this.options是默认配置
this.inline = new InlineLexer(src.links, this.options);
// use an InlineLexer with a TextRenderer to extract pure text
this.inlineText = new InlineLexer(
src.links,
merge({}, this.options, {renderer: new TextRenderer()})
);
this.tokens = src.reverse();
var out = '';
while (this.next()) {
out += this.tok();
}
return out;
};
//1138
Parser.prototype.next = function() {
// tokens被反转然后pop即吧tokens的[0]赋值给this.token
return this.token = this.tokens.pop();
};
this.inline和 this.inlineText 很相似都是InlineLexer的实例化,区别在于this.inlineTextlink里有一写配置 // 1167
tok原型对象里是对this.token的处理
Parser.prototype.tok = function() {
switch (this.token.type) {
case 'space': {
return '';
}
case 'hr': {
return this.renderer.hr();
}
// 我们来到heading这里,接下来就是分析最重要的render对象了
case 'heading': {
return this.renderer.heading(
this.inline.output(this.token.text),
this.token.depth,
unescape(this.inlineText.output(this.token.text)));
}
case 'code': {
return this.renderer.code(this.token.text,
this.token.lang,
this.token.escaped);
}
case 'table': {
var header = '',
body = '',
i,
row,
cell,
j;
// header
cell = '';
for (i = 0; i < this.token.header.length; i++) {
cell += this.renderer.tablecell(
this.inline.output(this.token.header[i]),
{ header: true, align: this.token.align[i] }
);
}
header += this.renderer.tablerow(cell);
for (i = 0; i < this.token.cells.length; i++) {
row = this.token.cells[i];
cell = '';
for (j = 0; j < row.length; j++) {
cell += this.renderer.tablecell(
this.inline.output(row[j]),
{ header: false, align: this.token.align[j] }
);
}
body += this.renderer.tablerow(cell);
}
return this.renderer.table(header, body);
}
case 'blockquote_start': {
body = '';
while (this.next().type !== 'blockquote_end') {
body += this.tok();
}
return this.renderer.blockquote(body);
}
case 'list_start': {
body = '';
var ordered = this.token.ordered,
start = this.token.start;
while (this.next().type !== 'list_end') {
body += this.tok();
}
return this.renderer.list(body, ordered, start);
}
case 'list_item_start': {
body = '';
var loose = this.token.loose;
if (this.token.task) {
body += this.renderer.checkbox(this.token.checked);
}
while (this.next().type !== 'list_item_end') {
body += !loose && this.token.type === 'text'
? this.parseText()
: this.tok();
}
return this.renderer.listitem(body);
}
case 'html': {
// TODO parse inline content if parameter markdown=1
return this.renderer.html(this.token.text);
}
case 'paragraph': {
//this.inline.output把**marked**处理成<strong>marked</strong>
return this.renderer.paragraph(this.inline.output(this.token.text));
}
case 'text': {
return this.renderer.paragraph(this.parseText());
}
}
};
// this.inline.output把**marked**处理成<strong>marked</strong>
this.renderer.paragraph(this.inline.output(this.token.text));
/**
* Static Lexing/Compiling Method
*/
InlineLexer.output = function(src, links, options) {
var inline = new InlineLexer(links, options);
return inline.output(src);
};
output对象里匹配到strong,调用renderer.strong
// strong
if (cap = this.rules.strong.exec(src)) {
src = src.substring(cap[0].length);
out += this.renderer.strong(this.output(cap[4] || cap[3] || cap[2] || cap[1]));
continue;
}
// span level renderer
Renderer.prototype.strong = function(text) {
return '<strong>' + text + '</strong>';
};
// 1574
renderer: new Renderer(),
marked.Renderer = Renderer;
// 911
function Renderer(options) {
this.options = options || marked.defaults;
}
// 946
Renderer.prototype.heading = function(text, level, raw) {
// 如果配置中就id就返回带有id
if (this.options.headerIds) {
return '<h'
+ level
+ ' id="'
+ this.options.headerPrefix
+ raw.toLowerCase().replace(/[^\w]+/g, '-')
+ '">'
+ text
+ '</h'
+ level
+ '>\n';
}
// ignore IDs
没有id就直接返回标签加纹板
return '<h' + level + '>' + text + '</h' + level + '>\n';
};
// 同理
// paragraph对this.token说[2]做了处理
Renderer.prototype.paragraph = function(text) {
return '<p>' + text + '</p>\n';
};
heading接受三个参数text是文本内容,level是heading的级别 1就是H1,2就是H2,raw目前是和level是一样的 经过两轮处理 此时我们的out已经有了结果
<h1 id="marked-in-the-browser">Marked in the browser</h1>
<p>Rendered by <strong>marked</strong>.</p>
document.getElementById('content').innerHTML =marked('# Marked in the browser\n\nRendered by marked.'); 就这样被渲染了. 这个实例就分析完毕了,当然里面还有其他的细节,有兴趣的可以仔细看看,比如什么时候会带有id等等 我们总结一下marked.js是如何处理的 最重要的就是
- 先是正则,把符号匹配出来,#是heading,然后推入this.tokens数组里 每处理一行,推入一个(把scr通过正则处理成this.tokens))
- 对this.tokens的处理,通过正则匹配到就调用相应的render对象里的方法
- render对象对输入的tokens进行处理返回out处理结果
优点
- 每一个功能都分别定义一个函数 render渲染用, laxer词法解析用,实现定义好各种正则,很清晰明明了
- 静态方法的使用
总之看着很复杂,当你分析完一个或者写完一个明白一个后,其他的都是依此类推,就是力气活了
以上