背景
最近在移植WordPress的Sakura主题到Halo上(实际上只是参照样式重写了)。评论这里需要使用marked,Halo官方提供的表情不太适合我,且我早就想扩展一个带表情的marked了。因此正好借着这个机会,扩展一个带表情的marked。
后续也想扩展一下文章页面的marked,只是目前还没有插件。因此暂时先只改评论的即可,到时候通用一套语法即可。
【注:当前文章大部分内容为分析,篇幅较长,如果不想看过程,直接跳转至扩展即可】
本篇文章基于如下内容
计划
根据我的计划,目前需要实现增加如下的功能
- 扩展bilibili表情
- 扩展文字表情
- 扩展通用的Emoji表情
-
增加通用的动态表情暂不考虑 - 将自定义的marked打包至npm
分析
工欲善其事必先利其器。 扩展marked之前一定要了解清楚,如何扩展?是否拥有不修改源码的扩展方法?如果有如何扩展?如果没有,如何修改源码?抱着这个想法,我阅读了 marked.js 的官方文档以及源码。
分析文档
在marked.js的官方文档中,我找到了 Extending Marked 这章内容。
刚开始,我认为也许通过官方的扩展Marked文档即可实现,但仔细阅读之后,发现并没有那么简单。
使用官方的扩展语法,只能扩展已有的渲染器方法。因为他们需要一个方法,例如 _heading(string src) ,进而通过该方法的返回值,来修改渲染样式。_简而言之,官方的方法只能针对于已有的语法,然后修改其渲染方式,如渲染标题时,我们可以不使用默认的H1、H2,而改用div自定义渲染等等。即官方已经替你解析完毕,你要做的只是按照自己想要的方式去渲染即可
而我们想实现的功能,是新添加一个解析,使用自己的解析语法,因此官方的这种方法不符合我们的要求。
继续翻看文章,在最后发现了如下内容

讯享网
这里可以看到大致流程,marked 使用 lexer 解析 markdown 文档成一个 tokens,然后使用 tokens 转换成 html。这是最重要的两步,那么问题来了,自定义的markdown语法如何解析成tokens? 又如何从 tokens 渲染成 html? 这里并没有提及,那么剩下的就只能从源码去看了。
分析源码
Lexer.js
根据文档,marked使用Lexer将markdown转换成tokens,则直接查看 Lexer.js,下图是Lexer的方法

其中获取Tokens分为了blockTokens/inlineTokens。根据方法名就能联想到分别是块级和行内的区别。由于我们想要新添加的表情属于行内,因此只需在inlineTokens中添加即可。
继续分析Lexer.js,inlineTokens方法的代码如下所示
inlineTokens(src, tokens = [], inLink = false, inRawBlock = false) {
let token; while (src) {
// escape if (token = this.tokenizer.escape(src)) {
src = src.substring(token.raw.length); tokens.push(token); continue; } ...... } return tokens;
讯享网
Tokenizer.js
直接查看Tokenizer.js,找到如下代码
讯享网...... escape(src) {
const cap = this.rules.inline.escape.exec(src); if (cap) {
return {
type: 'escape', raw: cap[0], text: escape(cap[1]) }; } } ......
rules.js

很明显,这里即为保存各种正则表达式的地方。
到这一步位置,将字符串转换为Tokens应该就很明确了。
那么,接下来的重点是,如果将Tokens渲染成html。前面也都没有看到有如何渲染的方法,那么,现在就需要我们找到调用Tokens的地方,根据代码,得到在marked.js中调用了Lexer.lex() 方法来获取Tokens。

marked.js

根据源码可以知道,在Parser.parse()中调用了tokens。
Parser.js
parseInline(tokens, renderer) {
renderer = renderer || this.renderer; let out = '', i, token; const l = tokens.length; for (i = 0; i < l; i++) {
token = tokens[i]; switch (token.type) {
...... case 'escape': {
out += renderer.text(token.text); break; } } } return out; }
Renderer.js
讯享网...... heading(text, level, raw, slugger) {
if (this.options.headerIds) {
return '<h' + level + ' id="' + this.options.headerPrefix + slugger.slug(raw) + '">' + text + '</h' + level + '>\n'; } // ignore IDs return '<h' + level + '>' + text + '</h' + level + '>\n'; } ......
分析总结
经过上面的分析,可以得出marked的渲染步骤,共有如下几个步骤
- 编写正则表达式,用于将字符串解析成数组
// rules.js escape: /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,
- 使用正则表达式解析目标字符串,并转化成token
讯享网// Tokenizer.js escape(src) {
const cap = this.rules.inline.escape.exec(src); if (cap) {
return {
type: 'escape', raw: cap[0], text: escape(cap[1]) }; } }
- 循环解析字符串,将其转换成tokens
// Lexer.js token = this.tokenizer.escape(src) // 添加至tokens中 tokens.push(token);
- 将tokens按照特定的格式,使用渲染器进行渲染
讯享网// Parser.js out += renderer.text(token.text);
- 编写某个格式的HTML渲染
// Renderer.js br() {
return this.options.xhtml ? '<br/>' : '<br>'; }
根据以上思路,就可以立马开工添加表情了。甚至以后如果有其他东西也很方便进行扩展。
扩展
由于我们添加的表情属于行内元素,因此均只考虑行内代码。bilibiliEmoji对应的markdown语法为: f(x)=∫(xxx)sec²xdx
textEmoji对应的markdown语法为:(⌒▽⌒)(``被吃掉了>_<)
codeEmoji对应的markdown语法为: :xxx:
其中xxx为表情的名字
编写正则
找到rules.js文件,在行内元素的对象中添加三条解析语句
讯享网// rules.js // 正则表达式 const inline = {
bilibiliEmoji: /^f\(x\)=∫\(([^A-Z]\w+?)\)sec²xdx/, textEmoji: /^`([^a-zA-Z]+?)`/, codeEmoji: /^:([^A-Z]\w+?):/ ...... }
另外,还要确保不能将:,`以及f开头的字符串识别为文本,因此还需要改动一下text的正则表达式
const inline = {
// 增加了!\[`:f*] text: /^(`+|[^`])(?:[\s\S]*?(?:(?=[\\<!\[`:f*]|\b_|$)|[^ ](?= {2,}\n))|(?= {2,}\n))/ } // 如果开启了gfm,则还需要改动这个!\[`:f*] inline.gfm = merge({
}, inline.normal, {
text: /^(`+|[^`])(?:[\s\S]*?(?:(?=[\\<!\[`:f*~]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@))|(?= {2,}\n|[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@))/ });
转换字符串为Token
找到Tokenizer.js,在构造函数下新增三条转换语句
讯享网// Tokenizer.js bilibiliEmoji(src) {
const cap = this.rules.inline.bilibiliEmoji.exec(src); if (cap) {
if (cap[0].length > 1) {
return {
type: 'bilibiliEmoji', raw: cap[0], text: cap[1] }; } } } textEmoji(src) {
const cap = this.rules.inline.textEmoji.exec(src); if (cap) {
if (cap[0].length > 1) {
return {
type: 'textEmoji', raw: cap[0], text: cap[1] }; } } } codeEmoji(src) {
const cap = this.rules.inline.codeEmoji.exec(src); if (cap) {
if (cap[0].length > 1) {
return {
type: 'codeEmoji', raw: cap[0], text: cap[1] }; } } }
将表情token纳入tokens中
在Lexer.js的inlineTokens中,判断字符串的类别并将token添加到tokens中去
// Lexer.js inlineTokens(src, tokens = [], inLink = false, inRawBlock = false) {
let token; while (src) {
// bilibili表情 f(x)=∫(xxx)sec²xdx if (token = this.tokenizer.bilibiliEmoji(src)) {
src = src.substring(token.raw.length); if (token.type) {
tokens.push(token); } continue; } // 文字表情 if (token = this.tokenizer.textEmoji(src)) {
src = src.substring(token.raw.length); if (token.type) {
tokens.push(token); } continue; } // 帖吧表情/BBcodeEmoji if (token = this.tokenizer.codeEmoji(src)) {
src = src.substring(token.raw.length); if (token.type) {
tokens.push(token); } continue; } ...... } }
使用tokens进行解析
讯享网// Parser.js parseInline(tokens, renderer) {
renderer = renderer || this.renderer; let out = '', i, token; const l = tokens.length; for (i = 0; i < l; i++) {
token = tokens[i]; switch (token.type) {
case 'bilibiliEmoji': {
out += renderer.bilibiliEmoji(token.text); break; } case 'textEmoji': {
out += renderer.textEmoji(token.text); break; } case 'codeEmoji': {
out += renderer.codeEmoji(token.text); break; } ...... } } }
生成HTML片段
最后根据Parser.js中的解析,调用Renderer.js中renderer对象的方法渲染html片段
// Renderer.js ...... bilibiliEmoji(text) {
let href = text + '.png'; href = cleanUrl(this.options.sanitize, this.options.bilibiliEmojiUrl, href); return '<span class="emotion-inline emotion-item">' + '<img src="' + href + '" class="img"></span>'; } textEmoji(text) {
return text; } codeEmoji(text) {
let href = 'icon_' + text + '.gif'; href = cleanUrl(this.options.sanitize, this.options.codeEmojiEmojiUrl, href); return '<img src="' + href + '" alt=":' + text + ':" class="smilies">'; } .....
增加默认配置
在默认的配置文件(defaults.js)中,新增表情的地址,这样可以保证之后可以随意切换表情资源所在的地址
讯享网// defaults.js function getDefaults() {
return {
baseUrl: null, breaks: false, gfm: true, headerIds: true, headerPrefix: '', highlight: null, langPrefix: 'language-', mangle: true, pedantic: false, renderer: null, sanitize: false, sanitizer: null, silent: false, smartLists: false, smartypants: false, tokenizer: null, walkTokens: null,
xhtml: false, // 新增的表情地址 bilibiliEmojiUrl: '', codeEmojiEmojiUrl: '' }; }
至此,解析已经完成。然后就可以对源码进行打包了。
打包
// 安装依赖 npm install // 执行代码规范性检查(可选) npm run test:lint // 打包 npm run build
讯享网npm publish
至此,扩展就已经全部完成!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/128739.html