背景

最近在移植WordPress的Sakura主题到Halo上(实际上只是参照样式重写了)。评论这里需要使用marked,Halo官方提供的表情不太适合我,且我早就想扩展一个带表情的marked了。因此正好借着这个机会,扩展一个带表情的marked。

后续也想扩展一下文章页面的marked,只是目前还没有插件。因此暂时先只改评论的即可,到时候通用一套语法即可。

【注:当前文章大部分内容为分析,篇幅较长,如果不想看过程,直接跳转至扩展即可】

本篇文章基于如下内容

marked.js:https://github.com/markedjs/marked
说明文档:https://marked.js.org/#/README.md#README.md

计划

根据我的计划,目前需要实现增加如下的功能

  • 扩展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) {// escapeif (token = this.tokenizer.escape(src)) {src = src.substring(token.raw.length);tokens.push(token);continue;}......}return tokens;

src即为当前需要解析的字符串,然后当前方法循环解析字符串,并将字符串转换成token,之后保存在tokens中。
很容易可以看出,当前方法使用了一个tokenizer对象将字符串解析成Token对象。该对象存在于Tokenizer.js中

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])};}}
......

在这个方法中,很明显使用了 exec() 方法,该方法会检索字符串中的正则表达式的匹配,返回一个数组。如果未能找到,则返回null。
所以此方法调用了rules对象,将字符串进行解析,然后返回一个数组。那么该对象内存的则应该是正则表达式。该对象存在于rules.js中,我们接着看rules.js。

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;}

parseInline即为行内的解析器。它按顺序循环Tokens,取出Token中保存的数据,而后调用renderer对象将token渲染成字符串并拼接起来。
renderer对象在Renderer.js中。

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 IDsreturn '<h' + level + '>' + text + '</h' + level + '>\n';}
......

很容易可以看出来,Renderer就是渲染字符串的地方。使用Parse中解析的值然后渲染成字符串并返回。
分析到这一步,整个思路就非常清楚了。

分析总结

经过上面的分析,可以得出marked的渲染步骤,共有如下几个步骤

  1. 编写正则表达式,用于将字符串解析成数组
// rules.js
escape: /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,
  1. 使用正则表达式解析目标字符串,并转化成token
// Tokenizer.jsescape(src) {const cap = this.rules.inline.escape.exec(src);if (cap) {return {type: 'escape',raw: cap[0],text: escape(cap[1])};}}
  1. 循环解析字符串,将其转换成tokens
// Lexer.js
token = this.tokenizer.escape(src)
// 添加至tokens中
tokens.push(token);
  1. 将tokens按照特定的格式,使用渲染器进行渲染
// Parser.js
out += renderer.text(token.text);
  1. 编写某个格式的HTML渲染
// Renderer.jsbr() {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.jsinlineTokens(src, tokens = [], inLink = false, inRawBlock = false) {let token;while (src) {// bilibili表情 f(x)=∫(xxx)sec²xdxif (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;}// 帖吧表情/BBcodeEmojiif (token = this.tokenizer.codeEmoji(src)) {src = src.substring(token.raw.length);if (token.type) {tokens.push(token);}continue;}......}}

使用tokens进行解析

在Parser.js中,循环tokens,并对每个token按照类型进行解析
之后将获取到的html片段拼接

// 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: '****'};
}

至此,解析已经完成。然后就可以对源码进行打包了。

打包

先修改原有的package.json 中的配置为自己的,主要是name,desc,author,version
也可以不修改,看个人。然后执行如下命令

// 安装依赖
npm install
// 执行代码规范性检查(可选)
npm run test:lint
// 打包
npm run build

执行之后,生成的marked.min.js就可以直接使用了。
当然了,我这里需要再发布到npm上,那就需要再执行下面的语句,将代码发布

npm publish

至此,扩展就已经全部完成!

marked扩展语法(增加自定义表情)相关推荐

  1. PHP7 windows增加自定义扩展和编译PHP源代码

    PHP7 windows增加自定义扩展和编译PHP源代码 需要用到的材料 ①确定需要编译的版本,查看PHPINFO,确定PHP版本,VC版本和PHP位数.根据PHP VC版本下载对应的Visual S ...

  2. AM8 自定义表情包的实现方法

    AM8 自定义表情包的实现方法 效果描述 AM8 安装后,在\Activesoft\AMm8\emotions 目录内存储的是默认的表情符号.但有的时候我们需要增加一些新的表情符号,AM8 系统支持自 ...

  3. dw重新定义html标记,扩展DW:自定义第三方标签解析

    扩展 Dreamweaver:自定义第三方标签的解析 因为最近一直在做 Dreamweaver 插件的开发,中文的资料非常少,自己英文又差,查看英文资料的时候不由头昏脑胀.迫不得已把其中一些重要的内容 ...

  4. 了解常用 Markdown 扩展语法

    虽然 Markdown 扩展语法不在 CommonMark Spec 标准中,但许多 Markdown 编辑器也都尽量支持,因此学习一些常用的 Markdown 扩展语法也是有必要的.本文介绍的主要是 ...

  5. Python正则表达式之扩展语法(5)

    非捕获组和命名组 精心设计的正则表达式可能会划分很多组,这些组不仅可以匹配相关的子串,还能够对正则表达式本身进行分组和结构化.在复杂的正则表达式中,由于有太多的组,因此通过组的序号来跟踪和使用会变得困 ...

  6. 在项目中增加自定义icon图标

    以MUI框架为例,内容来自于MUI官网. mui如何增加自定义icon图标 mui框架遵循极简原则,在icon图标集上也是如此,mui仅集成了原生系统中最常用的图标:其次,mui中的图标并不是图片,而 ...

  7. Python正则表达式子模式扩展语法与应用

    正则表达式语法实际上是独立于任何语言的,在大多数编程语言都可以使用相同的语法.常见正则表达式语法请参考Python使用正则表达式处理字符串 正则表达式使用圆括号"()"表示一个子模 ...

  8. Cesium 实战 - AGI_articulations 扩展:模型自定义关节动作

    Cesium 实战 - AGI_articulations 扩展:模型自定义关节动作 简要概述 两种方式实现模型组件动作 模型添加关节(articulations) 1.导入模型(J15.glb) 2 ...

  9. Android融云自定义表情

    融云官方文档链接:https://www.rongcloud.cn/docs/android.html#ui_customize_extension 一开始实现功能的时候被官方知识库问答各种贴链接搞懵 ...

  10. Qt 实现聊天软件中自定义表情包(随笔记录)

    简述: QT实现自定义表情包,通过对(能够设置表情的行列数 , 表情的大小,表情的个数.最大行数等) 效果: 代码如下: EmoticonsWidget主要实现表情包窗口. EmoticonsWidg ...

最新文章

  1. Java黑皮书课后题第6章:**6.28(梅森素数)如果一个素数可以写成2^p-1的形式,其中p是某个正整数,那么这个素数就称作梅森素数。编写程序,找出p≤31的所有梅森素数,然后显示如下结果
  2. python slice类型_复合类型Slice python中的list
  3. ionic4 hammerjs手势事件左滑右滑
  4. mysql hang分析_mysql hang
  5. C++数据结构与算法 动态规划
  6. Matlab计算矩阵和函数梯度
  7. 多线程实时数据采集MFC VISUAL C++ /C++
  8. 二级C语言试题结构,2008年4月计算机等级考试二级C语言试题结构分析
  9. delphi android 打印机,delphi中如何检测打印机状态?(在线等) ( 积分: 100 )
  10. js让html转excel时间格式,js读取excel中日期格式转换问题
  11. 如何用计算机接收光纤网络电视,家里只有一根网络电缆. 电脑和电视如何共享互联网?如何在机顶盒和路由器之间建立连接?...
  12. PicGo配置阿里云OSS
  13. airpods版本号_怎么看airpods版本号 苹果airpods查看固件版本教程详解
  14. java不使用科学计数法_java不用科学计数法
  15. dwg格式转换成jpg图片
  16. 微信jsapi支付流程
  17. jar a java exception has occured_Java Virtual Machine报错:A Java Exception has occured
  18. 立创EDA——PCB的布局(四)
  19. python对excel分列转多行
  20. Mac虚拟机VMware Fusion如何强制关机虚拟系统

热门文章

  1. Mac怎么切换主显示器 Mac设置主显示器
  2. 富滇银行 Windows7 无法使用U盾【证书信息读取失败,请选择正确的富滇银行网银证书!】 解决办法
  3. Python数据处理二
  4. 再不跳槽,应届毕业生拿的都比我多了!
  5. VUE项目中打印/转换图片打印
  6. 公众号淘客怎么运营推广,找到适合自己的的推广方法才有效
  7. 利用arcmap提取河流中心线
  8. 劳动与社会保障法-作业
  9. 我为什么放弃网易博客
  10. WordPress主题justnews仿某码屋资源下载站源码-整站打包