Overleaf で卒論なり修論なりを書いてもらおうと思って、フォーマット(jsarticle.cls)を色々いじってきた。
その一環として、参考文献の書き方のフォーマットを当方の研究室でこれまで指定してきたフォーマットに合わせるために、bibtexのスタイルファイルjunsrt.bstをいじることにした。
junsrt.bstの取得
Ovealeafに組み込まれているjunsrt.bstそのものは当然ながら直接いじれないので、以下のリンクから手元にダウンロード。
https://ftp.yz.yamagata-u.ac.jp/pub/CTAN/biblio/pbibtex/base/junsrt.bst
bstファイルの勉強
clsと同じように再定義を適当にしとけばよいかと思ってたが、bstはまったく違うものだった。
以下のリンク先でしばしお勉強。
https://qiita.com/HexagramNM/items/7c59f307e55010caf693
http://mirrors.ibiblio.org/CTAN/biblio/pbibtex/base/jbtxhak.pdf
とりあえず、以下が理解したところ。
- どうもやってることは、thebibliography環境に流し込むためのbibitem要素を作ってるだけ。その作り方(フォーマット合わせ)を細かく独自関数を定義しながらやってるっぽい。
- スタック(プッシュ・ポップ)型の文法構造だということ
- 条件分岐文は基本的に4行1ブロックで構成されていて、最初の行に条件が書かれ、Trueなら2行目に記載の処理、Falseなら3行目に記載の処理、4行目にブロックを閉じるための
if$が置かれている。各処理は{}でさらに細かく書いていくことが可能。条件文の入れ子も可能。 - 繰り返し文は基本的に3行1ブロック。最初の行に繰り返しの条件が書かれ、2行目に条件がTrueの時に行う処理を書く。3行目に
while$を書く。(ifもwhileもスタック構造であることを考えるとわかりやすい。要するに、スタック構造の場合、Last-in-First-outなので、演算子や制御子は最後にくる。つまり、最初はとにかくなんかデータがどんどん積みあがっていってて、最後に制御子が来ることで、その前のデータが何のためのものかが分かるという文法。なので、最後にif なりwhileなりが来てる) - いわゆる数であることを示すためには
#を数字の前につける。例えば#1は要するに数として1という意味。例えばnameptr #1 +はnameptrに保存されている数に1を足した数字をポップする。
junsrt.bstはインデントがグタグタだったので、この理解を確認しがてら、インデントを整えてみた。
junsrt.bstの仕組み
どうやらスタイルを整える時の仕組みとして、まずは.bibファイル中の各エントリーの@タグの文献の種類をチェックし、その種類とマッチする関数をjunsrt.bst(以下.bst)から探し出して実行する、という流れになっている。
例えば@articleであれば、articleという関数が.bstの中で定義されているので、それを実行している。
同じように@bookならbookという関数が実行される。
で、各関数の中では、著者やタイトル、ジャーナル名やページ番号、発行所などの情報が順次、成形がされている。
成形作業そのものは、別の関数を定義していて、例えば著者名であれば、format.authorという関数が使われる。
以下、著者名成形を詳しく見ながら、.bstの中での処理の構造を整理していく。
articleの中の著者名の成形
ちょっと著者名の部分を詳しく見てみる。
まず、@articleの中では最初、以下のように記述されている。
FUNCTION {article}
{ output.bibitem
format.authors "author" output.check
new.block
...
最初のoutput.bibitemは.bst内で定義されている関数で、thebibliography環境に流し込むための\bibitem{...}を作ってる。
次いで、format.authors "author" output.checkの行についてみてみる。
format.authorsの中身
最初のformat.authorsは.bst内で定義された関数で、以下のような定義になってる。
FUNCTION {format.authors}
{ author empty$
{ "" }
{ author format.names }
if$
}
最初の1行目の1つ目にあるauthorには、.bibファイルの各エントリで定義されているauthor={...}の中身がリスト化されて入ってる。これをスタックに入れることを意図して、ここにauthorが書かれてる。
で、empty$は、スタックからデータを1つポップさせて、ポップさせたデータが空かどうかをチェックする組み込み関数で二つの引数{}が付いていて、True、つまり空なら1つ目の引数に指定された処理が実行される。一方、False、つまり空でないなら2つ目の引数に指定された処理が実行される。
ということで、ここでは、authorが空か(定義されてない場合も含め)をチェックして、空なら""を返す。
もし空でないなら、author format.namesが実行される。
author format.namesについて、まず最初のauthor はさっきと同じでスタックに著者リストを一まとめのデータとしてプッシュする。
ついでformat.namesは.bstで定義されている関数で、名前のとおりスタックに入ってる変数に入ってる著者リストデータから、1つ1つ著者名を取り出して、成形して、\bibitemに書き込む著者名文字列をスタックに出力する、という処理をしている。
ちなみに、改めてauthorをスタックに入れてるのは、emptyを処理するときにauthorをスタックからポップさせてるから。
ってことで、著者名の表記を修正したい場合(例えば、英文著者名の1st nameとfamily nameの書き方を変えたい場合)には、format.namesの中身を弄ればよい。
'format.authors "author" output.check'
ってことで、format.authorsによって、もし著者名が定義されてないか空なのであれば、スタックに""がプッシュされる。著者名が定義されているなら、論文に表記させたい形に成形された著者名の文字列がプッシュされる。
ついで"author"という文字列がスタックにプッシュされる。これでスタックの中身は以下の通りとなってる。
3つ目にあるoutput.checkは.bstで定義された関数で、以下のように定義されている。
FUNCTION {output.check}
{ 't :=
duplicate$ empty$
{ pop$ "empty " t * " in " * cite$ * warning$ }
'output.nonnull
if$
}
1行目で't:=とあるが、これでスタックの1つ目に入ってる"author"をポップさせてtっていう変数に代入してる。BIBTEXの関数は関数といってもスクリプト言語に似てるようだ。要するにそれぞれの処理を単にサブルーチンとして纏めてるだけで、呼び出されるたびその中身が展開されてる。つまり、この関数では1行目に't:=としか書かれておらず代入すべき値がないのだが、これはもともと "author" output.checkから来てるので、詰まるところ"author" 't:=という処理をしてる。
で、2行目ではduplicate$ empty$とある。duplicate$は組み込み関数で、スタックに入ってるデータを1つポップさせ、それをコピーして元のデータと共にスタックにプッシュする。要するに、スタックの1つ目のデータを複製してるということ。なのでスタックには同じデータが2つ並んでる形になっている。
最初スタックにあった“author”はtに代入させるときにポップさせてるので、今スタックにあるのは著者名文字列のみ。なのでこの著者名文字列が複製される。
続いて、empty$はさっきと同じなので、ここではスタックから1つポップさせて、それが空なら1つ目の処理(エラー処理)を、空でないなら、output.nonnullっていう.bstで定義されてる関数を実行する。
(ちなみに一つ目の処理を細かく見ると、pop$はスタックからデータをポップさせるだけで出力はしない、という組み込み関数。ここではスタックが空になる。その後"empty " t * では"empty "って言う文字列とtの中身(すなわち“author”)を結合させてスタックに戻してる。で、"in " *で、ふたたびスタックに入ってるものに"in "っていう文字列を、同様にcite$ *でkey を結合させ後、それを組み込み関数のwarning$に流し込んでる。)
duplicate$でデータを複製してるのは、スタックからデータをポップさせたら、そのデータはスタックから消えてしまうから。もしduplicateをしてなかったら、empt$が著者名文字列をスタックからポップさせてしまうので、そのあとoutput.nonnullを実行しても空文字が出力されるだけになってしまう。
output.nonnull
こいつはこいつで結構厄介な処理してる。
中身はこんな感じ。
FUNCTION {output.nonnull}
{ 's :=
output.state mid.sentence =
{ ", " * write$ }
{ output.state after.block =
{ add.period$
write$
newline$
"\newblock " write$
}
{ output.state before.all =
'write$
{ add.period$ " " * write$ }
if$
}
if$
mid.sentence 'output.state :=
}
if$
s
}
まず1行目では、スタックの1つ目、すなわち著者名文字列をポップさせて、sっていう変数に保存してる。
次いでで、output.stateっていう変数とmid.sentenceっていう変数を比較して、=なら1つ目の処理、≠なら二つ目の処理をさせる。
この2つの変数は何ぞや?って感じだが、mid.sentenceっていうのは、junsrt.bstの上の方で定義された定数。
一方、output.stateはザクっと言えば、現在書き込もうとしている要素が、1つのセンテンスの途中のものなのか、そうでないのかを識別してる。
例えば、ジャーナル名や巻号番号、ページ、発行年といった各要素は基本的に1つのセンテンスとして記述されるので、その場合には各要素の境目は, となる。一方、センテンスが終わる時にはピリオドが打たれてる。さらに、ブロックが終了するときにはピリオドが打たれたあとnewline$で改行がされ、さらに\newblock と書かれて新しいブロックがスタートする。
ってことで、要素を順に記述していくときに、各要素が全体の中でどういう位置づけの要素なのかを示すフラグがoutput.stateであり、このoutput.nonnullって関数はoutput.stateの状態に応じて、前の要素との間の境目の記述を変えて、それと合わせながら、スタックの内容を出力している。
著者名文字列の場合で考えると、この時点のoutput.stateは、article関数が最初に処理する、output.bibtem関数で設定されていて、before.allと同じ値になっている。
ってことで、'writte$が発火。ただこの時点ではスタックに何もないので実質的に何も書き込みはされてない。もし各文献の著者名文字列の前に何か共通のタグをつけたいなら、write$の前に何かを書けばよい。ちなみにシングルクォーテーション'は、条件分岐の際に1つの関数だけの処理をさせたい場合に、組み込み関数の前につけるもの。別に{write$}としてても構わない(はず。説明を読む限りは)
そのあとにmid.sensence ’output.state := ってことで、output.stateにmid.sentenceを代入して終了。output.stateの前にシングルクォーテーションがついてのは、おそらくグローバル変数であることを示すためだと思う。参照するだけならクオーテーションはいらないけど、代入させるならいるみたい。
最後にsってことで著者名文字列をスタックに書き出して終了。
new.block
ようやく最初のarticleに戻って、new.blockが呼び出される。
これは、次のようにjunsrt.bstで定義されている。
FUNCTION {new.block}
{ output.state before.all =
'skip$
{ after.block 'output.state := }
if$
}
ってことで、いまはoutput.stateがmid.sentenceになってるから、これがafter.blockに書き換えられる。
なので、この後article関数の中では、タイトルが著者名の処理と同様の処理で出力されるが、output.nonnull関数の中でタイトルを出力する前に以下のコードが発火し、著者名文字列の最後にピリオドが打たれる。
add.period$
write$
newline$
"\newblock " write$
articleの著者名文字列の末尾をピリオドから別のものにする
ここまでの解釈から、著者名文字列のピリオドを、例えばコロン:であったりコンマにしたい場合には、まずは、output.nonnullの中のoutput.state after.block =のすぐ下のadd.period$を消しせばよい。
ただ注意しないといけないのは、ここでadd.priod$を消すと、単に著者名文字列を出力させるときのピリオドが消えるというだけでなく、それ以外でブロックが切り替わるときのすべてのピリオドが消える。
さらに、ここに": "とかを書き加えると、同じようにその他のすべてのブロックの切り替わりのところで": "が付いてしまう。
なので、add.priod$自体は消すしかないが、著者名の終わりだけ": "にしたければ、結局のところ、article関数の直下にある、fomat.authorsの出力(つまり著者名文字列)そのものに、直接": "を付け足す。
ってことで、
format.authors "author" output.check
になってる箇所をこうしてやればよい。
format.authors ": " * "author" output.check
これで、format.authorsの出力に": "が付いたものがスタックの1つ目に入り、2つ目に"author"が入り、それらがoutput.checkに送られて出力される。
(output.nonnullの中のadd.period$を消したので、著者名だけでなく、すべてのブロック切り替わりのピリオドが消えるので、それらを手動で入れていく必要がある。もちろん、ここの処理で著者名用フラグを別につくって、今見てるのが著者名の場合にだけadd.priodしないという条件分岐を入れてもよいが、今回は面倒くさくなったのでやめた。)
articleの論文タイトルをダブルクォーテーションで括る
基本的に著者名と同じような処理をしてるので、article関数の中のformat.titleとある行を弄ればよい。
ってことで
format.title "title" output.check
を
"``" format.title * "'', " * "title" output.check
としてやればよい。
日本語論文と英語論文でダブルクォーテーションの付け方を変える。
日本語論文の場合、クォーテーションを閉じてからコンマをつけることが多い一方、英語論文の場合、コンマを打ってからダブルクォーテーションを置くのが一般的。要するに、
日本語の場合:著者名: “タイトル”, ジャーナル名...
英語論文の場合:authors: "title," jounal...
ってことで、日本語と英語で使い分けるには、is.kanji.str$という関数を使う。これは、関数名の通りだが、日本語文字列かどうかを判定してくれて、Trueなら1つ目の処理、Falseなら二つ目の処理をする。
これを使って、以下のようにすればよい。
title is.kanji.str$
{"``" format.title * "'', " * "title" output.check}
{"``" format.title * ",'' " * "title" output.check}
if$
新しいフィールドを作る
bibtexでは通常、authorやtitle, journalなど、予め各エントリに含められるべきフィールドが定められているが、それらは.bstファイルの最初の方で以下のような形で定義されている。
ENTRY
{ address
author
booktitle
chapter
edition
editor
howpublished
institution
journal
key
month
note
number
organization
pages
publisher
school
series
title
type
volume
year
yomi
}
bibtexが実行されると、bibファイルの各エントリのフィールドの中身が順次、これらの変数に格納されていく。いうまでもなく、フィールド名と同じ名前になっている変数に、そのフィールド値が格納されていく。
で、これを眺めてピンときたのが、このフィールド値取得の処理は、単純に各エントリのフィールド名に一致する変数にフィールド値を順に入れていっているだけなのではないか、ということ。
また、どこかの資料で、「bibファイルの各エントリの中に、未定義のフィールドがあってもそれは無視されるから、自分なりのメモをmemo={},といった形で残してても構わない」といった情報が書かれてたが、それはつまり、対応する変数が定義されていないので、そのフィールドは無視されている、ということなのではないか。
ということは、この変数定義の中に新しいフィールド名を記載しておけば、その名前のフィールドを.bibファイルのエントリの中で新しく使うことができるのではないか?
ということで実際に試してみたら、その通りにできた。
例えば、通常、dayというフィールドは存在しないが、以下のように、ENTRY関数の中で定義しておけば、.bibファイルのエントリーで使うことができ、そのフィールド値は.bst内でdayという変数で受け取ることができる。
ENTRY
{
...
day
}
@misc{藤野2021,
author = {藤野秀則},
...
year = {2021},
month = {1},
day = {27}
}
新しい資料種別とそれに合わせた成形のための関数をつくる
何となく.bstファイルを眺めて、article とか book とか inproceedings とか関数名がエントリー名になっていることからピンときたのだが、これらの関数名は、.bibファイルに記載する@以下の資料の種別識別子と同じになっている。
ってことで、恐らくは、これらの関数は、.bibを読み込んだ際に、各エントリを順次読み込むなかで@以下の識別子に従って、.bstから呼び出されているんだろう。
となれば、関数名さえ一致していれば新し資料識別子を作ることだってできるはず。
そこで以下のようにwebpageって名前の新しい関数を作り、.bibファイルにも@webpageっていう新しいタイプの資料をエントリさせてみた。
なお、併せて、ENTRYにurlとdayというフィールドを追記しておいた。
ENTRY
{...
略
...
url
day
}
・・・
(略)
・・・
%%新たに定義したwebpageという資料種別に対応した関数
%%出力として、URLとURLの確認日を書き込む。
FUNCTION {webpage}
{ output.bibitem
author empty$
{""}
{format.authors ": " * output}
if$
title url new.block.checkb
title empty$
{""}
{ format.title ". " * output }
if$
url new.block.checka
"\url{" url * "} " * output
"(" year * ". " * month * ". " * day * ". 確認)" * "access data" output.check
new.block
fin.entry
empty.misc.check
}
...
@webpage{Qiita,
author = {藤野秀則},
title = {Overleafを使った日本語論文の作成},
url = {https://qiita.com/fujino-fpu/items/d92d185da730e25743cb}
year = {2021},
month ={1},
day ={27}
}
