读源码,造轮子,织雪纱奈的源码阅读之路
很好奇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 browserRendered 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}) (1+?) (?:#+ )?(?:n+|$)/.exec('# Marked in the browsernnRendered 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 browsernnRendered by marked.');
就这样被渲染了.
这个实例就分析完毕了,当然里面还有其他的细节,有兴趣的可以仔细看看,比如什么时候会带有id等等
我们总结一下marked.js是如何处理的
最重要的就是
1.先是正则,把符号匹配出来,#是heading,然后推入this.tokens数组里
每处理一行,推入一个(把scr通过正则处理成this.tokens))
2.对this.tokens的处理,通过正则匹配到就调用相应的render对象里的方法
3.render对象对输入的tokens进行处理返回out处理结果
- 1.每一个功能都分别定义一个函数 render渲染用, laxer词法解析用,实现定义好各种正则,很清晰明明了
- 2.静态方法的使用
总之看着很复杂,当你分析完一个或者写完一个明白一个后,其他的都是依此类推,就是力气活了
以上
读源码,造轮子,织雪纱奈的源码阅读之路
- n ↩
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。