この記事は,TeX & LaTeX Advent Calendar 2019の18日目の記事になります。17日目は t_kemmochi さんによるLaTeXの数式を映えさせるでした。
19日目は hid_alma1026 さんのご担当です。
はじめに
今年が初めての投稿になります。東大TeX愛好会の広報担当のXs_TeXと申します。どうぞよろしくお願いいたします。
さて,先日の愛好会のゼミで,目次のカスタマイズについて扱いました。(本来であれば索引のカスタマイズも扱いたかったと思ったのですが,時間が無くて断念しました)
さて,LaTeXには目次の自動出力機能が存在します。こちらをカスタマイズして,自分の好きなように目次を出力させる方法を考えてみることにしました。というわけで,まずはLaTeXのデフォルトでどのように目次が出力されるのかを解析し,その上で自分なりのカスタマイズ方法を考えてみることにします。
なお,私はLaTeXの初心者であり至らないところ,勘違いしているところなどあるかと存じます。その際はご指摘くださると幸いです。
LaTeXの標準での目次出力機構
例えば簡単のため,以下の文章を考えます。
\documentclass[b5j,10pt,autodetect-engine,dvipdfmx]{jsarticle}
\begin{document}
\tableofcontents
\section{セクション}
\subsection{サブセクション}
\end{document}
これをコンパイルすると,無事に目次が出力されます。では,この機構について考えてみます。順に実行していきます。(全部を理解するのは難しそうでしたので,重要そうな所だけ見ています。また,jsarticleで定義されているものの中には,もともとlatex.ltxで定義されているものもありました。)
\documentclass…
略
\begin{document}
\begin
はlatex.ltxで以下のように定義されています。
\DeclareRobustCommand\begin[1]{%
\@ifundefined{#1}%
{\def\reserved@a{\@latex@error{Environment #1 undefined}\@eha}}%
{\def\reserved@a{\def\@currenvir{#1}%
\edef\@currenvline{\on@line}%
\csname #1\endcsname}}%
\@ignorefalse
\begingroup\@endpefalse\reserved@a}
これの6行目から,\begin{document}
は\document
と展開されるようです。それでは,\document
の定義箇所を探してみます。定義が長そうでしたので,重要そうな所だけを抽出してみました。
\def\document{\endgroup
…(略)…
\begingroup\@floatplacement\@dblfloatplacement
\makeatletter\let\@writefile\@gobbletwo
\global \let \@multiplelabels \relax
\@input{\jobname.aux}%
\endgroup
\if@filesw
\immediate\openout\@mainaux\jobname.aux
\immediate\write\@mainaux{\relax}%
\fi
}
ここから,aux関連では,\jobname.auxが存在する場合は読み込み,その後auxをはじめから作り,はじめにauxに\relaxを書き込んでいることが分かります。
\tableofcontents
\tableofcontents
はjsarticle.clsにて以下のように定義されています。
\newcommand{\tableofcontents}{%
\settowidth\jsc@tocl@width{\headfont\presectionname\postsectionname}%
\settowidth\@tempdima{\headfont\appendixname}%
\ifdim\jsc@tocl@width<\@tempdima\relax\setlength\jsc@tocl@width{\@tempdima}\fi
\ifdim\jsc@tocl@width<2zw \divide\jsc@tocl@width by 2 \advance\jsc@tocl@width 1zw\fi
\section*{\contentsname}%
\@mkboth{\contentsname}{\contentsname}%
\@starttoc{toc}%
}
目次作成を実行するのに重要なのは8行目の\@starttoc{toc}
のようですので,\@starttoc
の定義を探します。\@starttoc
はlatex.ltxで以下のように定義されています。
\def\@starttoc#1{%
\begingroup
\makeatletter
\@input{\jobname.#1}%
\if@filesw
\expandafter\newwrite\csname tf@#1\endcsname
\immediate\openout \csname tf@#1\endcsname \jobname.#1\relax
\fi
\@nobreakfalse
\endgroup}
ここから,\@starttoc{toc}
を実行すると,まず\jobname.tocがあれば読み込み,その後\jobname.tocへの書き出しストリーム番号として\tf@toc 番を指定します。以降,\jobname.tocへの書き出しは\write\tf@toc{..}で出来るようになります。
\section{セクション}
こちらも順番に展開していきます。この命令はjsarticle.clsで定義されています。\if@twocolumnがtrueの場合とfalseの場合とありますので,今回はtrueの場合で言及したいと思います。
\newcommand{\section}{%
\@startsection{section}{1}{\z@}%
{0.6\Cvs}{0.4\Cvs}%
{\normalfont\large\headfont\raggedright}}
よって,\section{セクション}
は\@startsection{section}{1}{\z@}{0.6\Cvs}{0.4\Cvs}{\normalfont\large\headfont\raggedright}{セクション}
というふうに展開されます。
\@startsection
はjsarticle.clsで以下のように定義されています。
\def\@startsection#1#2#3#4#5#6{%
\if@noskipsec \leavevmode \fi
\par
\@tempskipa #4\relax
\if@english \@afterindentfalse \else \@afterindenttrue \fi
\ifdim \@tempskipa <\z@
\@tempskipa -\@tempskipa \@afterindentfalse
\fi
\if@nobreak
\everypar{}%
\else
\addpenalty\@secpenalty
\ifdim \@tempskipa >\z@
\if@slide\else
\null
\vspace*{-\baselineskip}%
\fi
\vskip\@tempskipa
\fi
\fi
\noindent
\@ifstar
{\@ssect{#3}{#4}{#5}{#6}}%
{\@dblarg{\@sect{#1}{#2}{#3}{#4}{#5}{#6}}}}
目次関連で主に重要なのは最後の行になります。\@startsection{section}{1}{\z@}{0.6\Cvs}{0.4\Cvs}{\normalfont\large\headfont\raggedright}{セクション}
は,\@dblarg{\@sect{section}{1}{\z@}{0.6\Cvs}{0.4\Cvs}{\normalfont\large\headfont\raggedright}}{セクション}
と展開されます。
\@dblarg
およびその周辺は,latex.ltxで以下のように定義されています。
\long\def\@dblarg#1{\kernel@ifnextchar[{#1}{\@xdblarg{#1}}}
\long\def\@xdblarg#1#2{#1[{#2}]{#2}}
これは,sectionにおけるオプション引数の処理を行っています。つまり,sectionは'\section{セクション}'とも'\section[目次用のタイトル]{セクション}'とも打つことが出来ます。後者の場合は目次に出力するタイトルと,本文中で出力するタイトルを分離することが出来ます。ここでの処理は,オプション引数を持っていない場合はオプション引数に第2引数を代入します。つまり,\@dblarg{\@sect{section}{1}{\z@}{0.6\Cvs}{0.4\Cvs}{\normalfont\large\headfont\raggedright}}{セクション}
は,\@sect{section}{1}{\z@}{0.6\Cvs}{0.4\Cvs}{\normalfont\large\headfont\raggedright}[{セクション}]{セクション}
と展開されます。
それでは\@sect
の定義を探します。こちらはjsarticle.clsで定義されています。
\def\@sect#1#2#3#4#5#6[#7]#8{%
\ifnum #2>\c@secnumdepth
\let\@svsec\@empty
\else
\refstepcounter{#1}%
\protected@edef\@svsec{\@seccntformat{#1}\relax}%
\fi
\@tempskipa #5\relax
\ifdim \@tempskipa<\z@
\def\@svsechd{%
#6{\hskip #3\relax
\@svsec #8}%
\csname #1mark\endcsname{#7}%
\addcontentsline{toc}{#1}{%
\ifnum #2>\c@secnumdepth \else
\protect\numberline{\csname the#1\endcsname}%
\fi
#7}}% 目次にフルネームを載せるなら #8
\else
\begingroup
\interlinepenalty \@M % 下から移動
#6{%
\@hangfrom{\hskip #3\relax\@svsec}%
#8\@@par}%
\endgroup
\csname #1mark\endcsname{#7}%
\addcontentsline{toc}{#1}{%
\ifnum #2>\c@secnumdepth \else
\protect\numberline{\csname the#1\endcsname}%
\fi
#7}% 目次にフルネームを載せるならここは #8
\fi
\@xsect{#5}}
ここで重要なのは最後の方の,
\addcontentsline{toc}{#1}{%
\ifnum #2>\c@secnumdepth \else
\protect\numberline{\csname the#1\endcsname}%
\fi
#7}
になります。#2>\c@secnumdepthは考えないことにするので,実質は以下のように実行されます。
\addcontentsline{toc}{#1}{\protect\numberline{\csname the#1\endcsname} #7}
第1引数はsection,第7引数はセクションですから,以下のように展開されます。
\addcontentsline{toc}{section}{\protect\numberline{\csname thesection\endcsname} セクション}
今度は\addcontentsline
の定義を探すことにします。これはlatex.ltxで以下のように定義されています。
\def\addcontentsline#1#2#3{%
\addtocontents{#1}{\protect\contentsline{#2}{#3}{\thepage}%
\protected@file@percent}}
よって,\addcontentsline{toc}{section}{\protect\numberline{\csname thesection\endcsname} セクション}
は,\addtocontents{toc}{\protect\contentsline{section}{\protect\numberline{\csname thesection\endcsname} セクション}{\thepage}\protected@file@percent}
と展開されることになります。
最後に,\addtocontents
の定義を探します。こちらもlatex.ltxで定義されています。
\long\def\addtocontents#1#2{%
\protected@write\@auxout
{\let\label\@gobble \let\index\@gobble \let\glossary\@gobble}%
{\string\@writefile{#1}{#2}}}
で,
\protected@write\@auxout
{\let\label\@gobble \let\index\@gobble \let\glossary\@gobble}%
\string\@writefile{toc}{\protect\contentsline{section}{\protect\numberline{\csname thesection\endcsname} セクション}{\thepage}\protected@file@percent}
が実行されます。つまり,auxファイルに対して,\@writefile{toc}{\contentsline{section}{\numberline{(セクション番号)} セクション}{(ページ番号)}\protected@file@percent}
を書き出します。
ちなみに \contentsline
とは何なのか
\contentsline
は,上の例より引数を3つとり,第1引数が深さ(section,subsectionなど),第2引数がタイトル,第3引数がページ数となっていることが分かります。latex.ltxで以下のように定義されています。
\def\contentsline#1{\csname l@#1\endcsname}
よって,tocファイルが読み込まれた際に,\contentsline{section}{\numberline{(セクション番号)} セクション}{(ページ番号)}
は,
\l@section{\numberline{(セクション番号)} セクション}{(ページ番号)}
と展開されます。\l@section
はjsarticle.clsにて以下のように定義されています。
\newcommand*{\l@section}[2]{%
\ifnum \c@tocdepth >\z@
\addpenalty{\@secpenalty}%
\addvspace{1.0em \@plus\jsc@mpt}%
\begingroup
\parindent\z@
\rightskip\@tocrmarg
\parfillskip-\rightskip
\leavevmode\headfont
%\setlength\@lnumwidth{4zw}% 元1.5em [2003-03-02]
\setlength\@lnumwidth{\jsc@tocl@width}\advance\@lnumwidth 2zw
\advance\leftskip\@lnumwidth \hskip-\leftskip
#1\nobreak\hfil\nobreak\hbox to\@pnumwidth{\hss#2}\par
\endgroup
\fi}
ここで実際に目次の出力が実行されています。ちなみに,jsarticle.clsには他にも\l@part
,\l@subsection
,\l@subsubsection
など色々定義されています。
\subsection{サブセクション}
今度は\subsection
の定義ですが,\section
と大差ありません。\subsection
は,jsarticle.clsで以下のように定義されています。こちらの場合も\if@twocolumnがtrueの場合について言及しています。
\newcommand{\subsection}{\@startsection{subsection}{2}{\z@}%
{\z@}{\if@slide .4\Cvs \else \z@ \fi}%
{\normalfont\normalsize\headfont}}
ここから分かるとおり,\subsection
も同様に\@startsection
に展開されます。その後は同じような処理が行われます。ただ,subsectionなので,目次に書き出されるときに最終的に実行されるのは\l@section
ではなく\l@subsection
になります。
\end{document}
まずは\end
の定義を探します。こちらもそれらしきものがlatex.ltxにありました。
\edef\end
{\unexpanded{%
\romannumeral
\ifx\protect\@typeset@protect
\expandafter %1
\expandafter %2
\expandafter %1
\expandafter %3 expands the \csname inside \end<space>
\expandafter %1
\expandafter %2 expands \end<space>
\expandafter %1 expands the \else
\z@
\else
\expandafter\z@\expandafter\protect
\fi
}%
\expandafter\noexpand\csname end \endcsname
}
\@namedef{end }#1{%
\csname end#1\endcsname\@checkend{#1}%
\expandafter\endgroup\if@endpe\@doendpe\fi
\if@ignore\@ignorefalse\ignorespaces\fi}
よって,\enddocument
の定義を探します。latex.ltxを参照しました。
\def\enddocument{%
\let\AtEndDocument\@firstofone
\@enddocumenthook
\@checkend{document}%
\clearpage
\begingroup
\if@filesw
\immediate\closeout\@mainaux
\let\@setckpt\@gobbletwo
\let\@newl@bel\@testdef
\@tempswafalse
\makeatletter \@@input\jobname.aux
\fi
\@dofilelist
\ifdim \font@submax >\fontsubfuzz\relax
\@font@warning{Size substitutions with differences\MessageBreak
up to \font@submax\space have occurred.\@gobbletwo}%
\fi
\@defaultsubs
\@refundefined
\if@filesw
\ifx \@multiplelabels \relax
\if@tempswa
\@latex@warning@no@line{Label(s) may have changed.
Rerun to get cross-references right}%
\fi
\else
\@multiplelabels
\fi
\fi
\endgroup
\deadcycles\z@\@@end}
ここで\jobname.auxを読み込んでいます。
実行
長々とお付き合いくださりありがとうございました。では,ここまでの議論を踏まえて,最初にお示ししたファイルを実行したときにどういう挙動になるかを順に追ってみます。(なお,TL2019のuplatex+dvipdfmxを使用しています。)
一度目のタイプセット
はじめに,\begin{document}
で\jobname.auxが存在しないですから,これを作り\relax
を書き込みます。その後,\tableofcontents
で\jobname.tocが存在しないですから,これを作ります。(目次部分は空のまま)次に,\section{セクション}
でauxファイルに対して,\@writefile{toc}{\contentsline {section}{\numberline {1} セクション}{1}\protected@file@percent }
と書き出します。次に,\subsection{サブセクション}
でauxファイルに対して,\@writefile{toc}{\contentsline {subsection}{\protect\numberline {1.1} サブセクション}{1}\protected@file@percent }
と書き出します。(カウンタ類は実際に実行した際の数値を入れています)
この時点で,auxファイルには以下のように書き出されているはずです。
\relax
\@writefile{toc}{\contentsline {section}{\numberline {1}セクション}{1}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {1.1}サブセクション}{1}\protected@file@percent }
最後に\end{document}
を実行します。こちらは\jobname.auxを読み込んでいます。つまり,上で示したコードが実行されます。なお,\@writefile
は以下のように定義されています。
\long\def\@writefile#1#2{%
\@ifundefined{tf@#1}\relax
{%
\add@percent@to@temptokena
\@empty#2\protected@file@percent
\add@percent@to@temptokena
\immediate\write\csname tf@#1\endcsname{\the\@temptokena}%
}%
}
ここで\jobname.toc
に以下のように書き込みが行われます。
\contentsline {section}{\numberline {1}セクション}{1}%
\contentsline {subsection}{\numberline {1.1}サブセクション}{1}%
二度目のタイプセット
2度目も同様に実行されます。ただし,すでにauxとtocファイルが存在しているので,そこは挙動が異なります。
はじめに,\begin{document}
で\jobname.auxが存在しますから,これを実行した後に空にして\relax
を書き込みます。その後,\tableofcontents
で\jobname.tocを読み込み,目次を組み立てます。以下は同様なので省略します。
カスタマイズ
ここまで,解析を行ってきましたが,カスタマイズをする上では以下のようにしたいと考えました。ちなみに,カスタマイズは大学のサークルの部誌を作るために行いました。
- sectionは目次に入れたくない
- 部誌は各自が記事を持ち寄る形式である。よって,記事ごとに子ファイルを作成し,これを読み込むが,このときにのみ目次に書き込まれるようにしたい
- 目次に記事の作成者などの情報も入れたいので,引数を増やしたい
この3点を目標としました。
まず,1点目が難しいハードルでした。例えば,先ほど述べたことから,記事を\addcontentsline{toc}{kiji}{ほげ}
などとしたうえで,\l@kiji
を定義すればその部分の出力は問題なく出来るでしょう。しかし,その場合は\section
を使用した際の目次への出力の抑制が出来ません。(使用しなければ良いと言われればそれはそうなのですが・・最終的には結局使わないようにしました。)
また,3点目の引数の不足も,現状の提供されている仕組みでは不可能だという結論に至りました。というわけで,以下のようなオリジナルの命令を定義しました。(部誌で使用したのとは少しコードを書き換えています)
\def\目次出力{%
\記事見出し*{}{}{}{}{}{目次}
\begin{multicols*}{2}
\tableofcontents
\end{multicols*}
}
\def\tableofcontents{{\@starttoc{toc}}}
ここはあまり変わりません。大きくいじったのは次の部分です。
\def\contentslineoriginal#1#2#3#4#5{
\ifthenelse{\equal{#1}{chapter}}{%
\noindent \begin{tikzpicture}
\path[use as bounding box] (0,0) rectangle (\linewidth,40pt);
\coordinate (O) at (22pt,19pt);
\begin{scope}[rotate=45]
\draw[draw=black!80] (O) circle [x radius=22pt, y radius=10pt];
\filldraw[draw=white,line width=0.3pt] ([xshift=22pt]O) circle [radius=1.6pt];
\filldraw[draw=white,line width=0.3pt] ([xshift=-22pt]O) circle [radius=1.6pt];
\end{scope}
\begin{scope}[rotate=-45]
\draw[draw=black!80] (O) circle [x radius=22pt, y radius=10pt];
\filldraw[draw=white,line width=0.3pt] ([xshift=22pt]O) circle [radius=1.6pt];
\filldraw[draw=white,line width=0.3pt] ([xshift=-22pt]O) circle [radius=1.6pt];
\end{scope}
\node at ([xshift=11pt,yshift=-11pt]O) {\small 投};
\node at (O) {%
\gtfamily #3};
\draw[line width=.5pt] (0,0) -- (\linewidth,0);
\node[anchor=south west] at ([xshift=45pt,yshift=2pt]0,0) {{\huge \gtfamily #2}};
\node[anchor=south east] at ([xshift=-3pt,yshift=2pt]\linewidth,0) {\Large #5};
\end{tikzpicture}\nopagebreak}{}%
\ifthenelse{\equal{#1}{article}}{%
\noindent \begin{tikzpicture}
\path[use as bounding box] (0,0) rectangle (\linewidth,30pt);
\node[anchor=south west] at ([xshift=15pt,yshift=12pt]0,0) {\large \gtfamily #2};
\node[anchor=south east] at ([xshift=-3pt,yshift=-2pt]\linewidth,0) {\small \gtfamily \makebox[5zw][l]{#3\ #4}\hspace{10pt}\makebox[3zw][r]{#5}};
\shade[left color=black,right color=white] (.25\linewidth,-0.25pt) rectangle (\linewidth,0.25pt) ;
\fill[fill=black,radius=1.4pt] (.25\linewidth,0) circle ;
\end{tikzpicture}}{}%
}
このように,contentslineを書き換え,引数が正しく(望まれた物が)与えられている場合のみ目次が出力される機構を考えました。
また,各記事を読み込むときの定義は以下のようにしました。
\def\@@記事見出し#1#2#3#4#5#6{\@記事見出し{#1}{#2}{#3}{#4}{#5}{#6}
\addcontentslineoriginal{toc}{article}{#6}{#2}{#3}
}
\def\addcontentslineoriginal#1#2#3#4#5{%\contentsline では引数が足りないので,拡張してみた。
\addtocontents{#1}{\protect\contentslineoriginal{#2}{#3}{#4}{#5}{\thepage}}
\protected@file@percent}
細かい引数の情報などは省略しますが,このようにして引数の数を拡張しました。ちなみに,\@記事見出し
は見出しを出力するために定義したコマンドになります。
最後に
実際にLaTeXで使われているコマンドを解析することでここまでカスタマイズできるのか,と思うとわくわくします。来年こそはもっと面白い記事を出したいです・・・最後までお付き合いくださりありがとうございました。