最近因为约稿要投一个很偏的期刊,参考文献格式奇葩,现成的\(\LaTeX\)参考文献格式不能满足要求,需要自己手动配置。一些博客中已经介绍了使用makebst
工具交互式生成相应bst文件的做法。好处是相对学习成本比较小,坏处就是被人牵着走还不一定能完成需求。
本文介绍的,是掌握BibTeX语法后,直接修改bst文件来设置\(\LaTeX\)参考文献格式的方法。
BibTeX语言一窥
学习BibTeX语法后从头手搓一个bst当然是一种方案,不过稍微取巧一些的是从一些现成的bst出发进行修改,最后满足需求即可。从宏观来看,bst文件只是某种格式或者配置文件,语法应该是比较直截了当的。然而即使是\(\LaTeX\)内置的简单格式如unsrt,其bst文件unsrt.bst
也有18 kB,约一千行代码。所有内置参考文献的bst文件都可以从CTAN下载。
unsrt.bst
文件中的代码没有太多注释,粗看起来并不好懂。比如以下代码,大体上应该可以判断出来这段代码定义了渲染引用article时参考文献格式的函数,但具体运行逻辑不明朗。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19FUNCTION {article}
{ output.bibitem
format.authors "author" output.check
new.block
format.title "title" output.check
new.block
crossref missing$
{ journal emphasize "journal" output.check
format.vol.num.pages output
format.date "year" output.check
}
{ format.article.crossref output.nonnull
format.pages output
}
if$
new.block
note output
fin.entry
}
一个有一定编程基础的人,看完这段代码自然会有如下几个问题:
- 函数(
FUNCTION
)的形参和实参是什么?返回值如何定义? - 频繁出现在两个单词之间的
.
代表什么? - 出现在单词之后的
$
有什么含义? {}
括起来的是代码块吗?何时需要何时不需要?"
引号括起来的部分,是否表示字符串?如果是,为什么最终渲染出来的引文中并没有"author"
、"title"
等字符串?
只要将这些问题一一解决,BibTeX的语法也就基本清楚了,这时修改bst文件手动设置\(\LaTeX\)参考文献格式也就有如探囊取物。
BibTeX语言基本模型:逆波兰表示法
逆波兰表示法在计算机科学界可说是鼎鼎大名了,其优点是很容易解析(parse)成堆栈操作并实现为计算机代码。例如表达式5 1 3 + 2 \ *
:
- 首先5,1,3依次入栈
- 对栈顶的1和3做加法操作,结果4入栈
- 加号后的2入栈
- 对栈顶的4和2做除法操作,结果2入栈
- 再对栈顶的5和2做乘法操作,结果10入栈
- 运算结束
BibTeX是一种脚本语言,基本模型就是通过逆波兰表示法维护一个堆栈,不同的指令对应于不同的堆栈操作,其目标是将参考文献的\(\LaTeX\)代码打印出来。函数与指令没有本质区别。在定义函数时,不需要指定形参和返回值,只需要在函数中对堆栈进行操作就行了。换句话说,假如函数有参数,在调用函数前需要手动将参数压栈。这就回答了上面提到的函数如何定义的问题。从这种函数定义方式上讲,可以说BibTeX是一种比较落后的语言。
BibTeX语言的基本数据类型和内置函数
BibTeX语言中,基本数据类型只有两类:整数和字符串,对应的变量使用前需要在bst文件头部INTEGER
和STRING
部分声明。BibTeX语言还有一类数据类型是bib文件中的条目(可以理解为某种对象),需要在头部ENTRY
部分声明。定义整数时,在数字前加井号,如#1
。定义字符串时,用双括号括起来,如"hello"
。上文示例代码中的"author"
等字符串最后并没出现在参考文献的渲染结果的原因是此时"author"
等字符串只是入栈,而并没有加入到输出中。实际上,后续的函数output.check
就会将其出栈。之所以这么做的原因是如果出错output.check
在打印错误原因时需要"author"
这一字符串(类似于“Error when processing author”)。换句话说"author"
只是output.check
的一个参数。
BibTeX的内置函数包括+
、<
等常见操作。另外=
代表比较而:=
代表赋值。当包含字母时,内置函数由$
结尾,如pop$
。上文示例BibTeX代码中的missing$
就是这样一种内置函数,其作用是判断当前栈顶的“条目”是否为空,返回整数作为布尔值。if$
这样的条件分支语句也被当做是函数,它会出栈当前栈顶的三个元素。第一个(三个元素中靠近栈底的)如果严格大于0即布尔真,那么就会执行第二个元素中的代码块,否则会执行第三个元素中的代码块。
至此,我们可以理解上文实例代码中的控制流了:1
2
3
4
5
6
7
8
9crossref missing$
{ journal emphasize "journal" output.check
format.vol.num.pages output
format.date "year" output.check
}
{ format.article.crossref output.nonnull
format.pages output
}
if$
如果当前处理的参考文献没有crossref
条目,那么在渲染该参考文献时就采用第一个{}
中括起来的代码,反之则用第二个。可见{}
的作用是暂缓解释器对括起来的代码的执行,而将其作为一个整体入栈,主要目的是和if$
配合。当代码块较简单如只有一个单词时,可以在该单词前加上'
达到相同的目的,效果与用{}
括起来相同。
其它的内置BibTeX函数还有很多,可参考官方文档或半官方文档。这些函数中有一个非常特别的format.name$
函数,顾名思义专门对人名进行格式化,其特别之处在于针对不同的人名格式又定义了一个自己的DSL(禁止套娃。这个函数名中的.
并没有特殊含义,在BibTeX中.
可以像正常字符一样出现在函数名当中。
BibTeX简单示例
至此,本文第一节提出的若干个问题已经全部得到了解决(见加粗部分),BibTeX的语法也逐渐清晰了起来。下面介绍一个实例:如果针对unsrt格式,想设置发表年份斜体,应该怎么做?从上面的示例代码不难发现目标即是format.date
函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16FUNCTION {format.date}
{ year empty$
{ month empty$
{ "" }
{ "there's a month but no year in " cite$ * warning$
month
}
if$
}
{ month empty$
'year
{ month " " * year * }
if$
}
if$
}
可以看出该函数的控制流分为四种情况,首先year empty$
是否定义了年份,其次month empty$
是否定义了月份。那由于我们的目的是改年份格式,没有定义年份的就不用管了,只看第二个代码块即第10到14行。当月份为空时,执行第一个代码块'year
也就是简简单单把年份入栈(注意输出是后面函数的工作),其中'
的作用前面已经提到,就是将简单的语句year
变成代码块,与{year}
相同。当月份不为空时,代码month " " * year *
中出现了运算符*
。这个运算符的作用不是乘法而是字符串相连(实际上BibTeX内生不支持乘法),所以这行代码的意思实际上是把三个字符串相连,换成Python的话说就是1
f"{month} {year}"
如此分析后,怎样设置年份斜体就呼之欲出了,以没有定义月份时为例,只需将'year
替换为1
{"\textit{" year * "}" *}
这行代码初看有点难懂,因为它是为了生成代码的代码。外侧的一对{}
作用是让BibTeX解释器把这一段当成代码块不要立刻执行。内侧的一对{}
是处于引号中的,目的是让渲染出的结果中有\textit{}
。而*
的作用是连接栈顶的两个字符串。
按照这种方式编码bst文件,任意的参考文献格式都可以轻松定义。