一直以来想以一种优雅的方式引用文献,最后自己实现了这样一个插件~这篇文章简单介绍一下插件并提下开发过程中遇到的一些事情~


hexo-reference-plus

插件已经发到npm~可以直接使用yarn或npm安装~具体见github~

插件的效果可以见本文~本插件具有以下特点:别名规则-自动编号,支持tooltip弹出提示,简单的语法与使用~

插件开发心历

引用是指在说话或写作中引用现成的话,如诗句、格言、成语等,以表达自己思想感情的修辞方法。引用可分为明引暗引两种。明引指直接引用原文,并加上引号,或者是只引用原文大意,不加引号,但是都注明原文的出处。暗引指不说明引文出处,而将其编织在自己的话语中,或是引用原句,或是只引大意。运用引用辞格,既可使文章言简意赅,有助于说理抒情;又可增加文采,增强表现力。[1]

上大学以来越来越接触到很多学术论文,再加上很多内容难免会涉及到对前人的参考,所以引用或者脚注对很多人而言都是不可缺少的一种内容形式。

markdown作为一种轻量的语法形式,其基础语法中并没有包含引用的标准。 但是客观需求是存在的,因此出现了许多扩充语法的标准,例如 MultiMarkdown-5 [2]。这一标准在hexo的默认渲染器中并不支持,于是也出现了像hexo-render-markedhexo-reference这样的插件。 他们的语法大概是这样的:

1
2
3
这是一句话[^1]是这样的,没错。
各种各种话~
[^1]: 然后你会得到这个。

然后你就会得到这个:

这是一句话1是这样的,没错。
各种各种话~

1. 然后你会得到这个。

我使用的主题 maupassant-hexo[3] 内置了hexo-render-marked这一渲染器,因此在我的博客里也得以使用这种语法(一开始我并没有发现)。

正如括号所言,我并没有发现这个事实,于是我安装了hexo-reference。他很好用没有太大的问题。但是想到之前写一些不算论文的论文时总会遇到关于参考文献的编号问题,这时候一般是推荐使用一些文献管理软件。但是到了博客这种场景下,似乎又有些没有必要。当然你也可以使用 $\LaTeX$[4],但是……你懂。

没有更好的方法了么?没得话就自己造个吧,于是最后就开发了这个插件,不能算是优雅,但是姑且是可用的。下面介绍下这个插件的基本原理~

开发原理

两个标签的基本实现

使用hexo的tag api来设计了两个标签,ref与references,前者用于实现脚标的渲染,后者则把规定格式排序输出html。

他们的代码如下:

1
2
3
4
5
6
7
8
9
hexo.extend.tag.register('references', function (args, content) {
let reference = referParser(content);
return `<ul id='refplus'>${reference.join('')}</ul>`;
}, { ends: true });

hexo.extend.tag.register('ref', function (args) {
args = args.map(arg => `<sup class='refplus-num'><a href="#ref-${arg}">[${arg}]</a></sup>`)
return args.join('');
});

首次处理中,references中的函数将他们进行编号,ref则输出一个值为[别名]的sup标签,里面包裹一个link进行锚点定位。

referParser是一个简单的函数封装,他首先去除空行后分割,然后枚举每一条文献读取他们的别名放入id中并进行编号,对应前面的锚点定位。

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = (content) => {
let reference = content.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm,"").split('\n');
reference = reference.map((ref, index) => {
let alias_start = ref.indexOf("[");
let alias_end = ref.indexOf("]", 1);
if (alias_start == -1 && alias_end == -1) console.error("[hexo-reference-plus]Wrong grammar! Please use [your-alias] as the start!");
let alias = ref.slice(alias_start + 1, alias_end);
let content = ref.slice(alias_end + 1);
return `<li id='ref-${alias}' data-num='${index + 1}'>[${index + 1}] ${content}</li>`
})
return reference;
}

这里变好放入了dataset:data-num中便于后续处理。references标签处理完毕了,那么ref中如何将别名转化为编号呢。

原理其实很简单,根据ref中的别名找到reference中的编号即可,这部分我觉得可以放到hexo中直接生成,而不是使用前端输出。

linkedom

1
2
3
4
5
hexo.extend.filter.register('after_post_render', (data) => {
if (!(data.refplus)) return data;
data.content = htmlReplacer(data.content);
return data
});

这里使用filter将渲染后的内容拦截出来,取出content并使用htmlReplacer进行处理。这里就是将ref标签中别名转换为序号的方法,

我们知道,node.js属于非浏览器环境,这意味着我们无法直接使用 winndow 对象及document之类的东西。那么如何在 node.js 中进行dom操作呢?其实一般是两种方法:

  1. 使用chromedriver或者其他无头浏览器。
  2. 使用模拟dom环境的相关库。

为了性能和优雅,我们肯定优先选择后者,因此如果你简单的搜索,很容易找到 jsdom 这个东西。当然也有类似的cheerio(cheerio是类似于jQuery的api,而jsdom则是类似于原生的,我们这里选择jsdom)。

使用jsdom非常简单,如果感兴趣网上也有很多资料,你可以去搜索一下。但是jsdom都有一个问题:慢。 当然慢也不是很慢,我测试了一段不是很长的文本,jsdom输出大概是30ms,其实也不是很慢,但是我好奇google了一下jsdom performance,就在一篇文章中[5]发现了jsdom的一些缺点,并发现了一个类似的库:linkedom。他和jsdom的使用基本是相同的,并且同样的文本,linkedom完成我的所需操作只需要3ms。

下面介绍下我们的 htmlReplacer 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
const {parseHTML} = require('linkedom');

module.exports = (data) => {
const { document } = parseHTML(data);
document.querySelectorAll(".refplus-num").forEach((ref) => {
let refid = ref.firstChild.href;
let refnum = document.querySelector(refid)?.dataset?.num;
if(!(refnum)) console.error(`[hexo-reference-plus] Alias error, unknown alias: ${refid}!`)
ref.firstChild.innerHTML = `[${refnum}]`;
});
let outerHtml = document.toString();;
return outerHtml
}

我们引入linkedom中的parseHTML,他将返回一系列对象,我们拿出来我们想要的document
1
const { document }  = parseHTML(data);

接下来就是简单dom操作,我们先读取每个编码位置对应的别名,再找到对应文献区域的编号,并将它们替换过去。

Tooltip:文本提示のtippy.js

hexo-references是我们的老前辈和灵感来源。他使用hint.css做了文本提示便于浏览页面的时候更好的查看内容,但是问题也随之而来,既然叫hint.css那么很明显他是个pure css的东西,这就意味着对于不同位置的判断他无法做到很好的适配。

再则,由于我博客主题的原因,试过的几个tooltip的库都不能很好的展示效果。于是最后找到了tippy.js,这是一个基于Popper,一个弹窗位置控制库的库。(汉语欧化biss)。

他的引入与使用非常简单,唯一的缺点就是引入文献的体积膨胀到了50kb左右。

缺点:审视与展望

以上几部分大致介绍了这个插件的基本原理与功能,这里做些缺点的审视与展望。

缺点1: 使用标签语言降低了markdown的可读性。

确实是这样的,尽管标签已经尽量精简化,但是在文本中插入一坨这东西实属有种怪怪的感觉。 也许后期可以再次引入hexo-reference的方案使用正则替换。

缺点2:想要参考文献与注释混排的场景无法兼容

上文中你也看到了,并且我们也知道参考文献和注释绝对不是一回事儿。这点我略有想法,可以设计作用域之类的东西,或者只是单纯的分成两种语法。

缺点3:tippy.js体积过于膨胀。

希望能找到替代吧。

参考文献 & 注释

  • [1] 刘蔼萍主编. 现代汉语[M]. 重庆:重庆大学出版社, 2016.10.
  • [4] 一种基于TEX的排版系统,由美国计算机科学家莱斯利·兰伯特在20世纪80年代初期开发。Wikipedia。(把注释和参考文献混在一起并不对,这里是一个错误示范)
  • [5] LinkeDOM: A JSDOM Alternative. https://webreflection.medium.com/linkedom-a-jsdom-alternative-53dd8f699311