LoginSignup
5
5

More than 3 years have passed since last update.

LaTeXの`\section`マクロをテンプレートメソッドパターンで作り直す

Last updated at Posted at 2020-07-07

概要

いわゆるGoFのデザインパターン本に出てくる「テンプレートメソッドパターン」を使って、LaTeXの \section マクロを作り直してみます。

ポイント:

  • \section を、より小さいマクロの組み合わせとして定義する。
  • カスタマイズするとき、必要なマクロだけを上書きするだけで済む。
  • TeX言語では継承は使えないので、かわりにマクロの上書きを利用する。

オブジェクト指向言語じゃないのに「テンプレートメソッドパターン」とはおかしなことかもしれません。
しかし実装方法が、継承によるメソッドオーバーライドなのか、それともマクロ定義の上書きなのかという違いだけであり、考え方はテンプレートメソッドパターンそのものです。

(注:あるマクロを小さいマクロに分解するだけでは「テンプレートメソッドパターン」とは言いません。挙動を上書きできるように分解してこその「テンプレートメソッドパターン」です。)

なおTeXおよびLaTeXでは「マクロ」とは言わず、「コントロール・シーケンス」または「制御綴」と呼ぶのが正しいそうです。
しかしこの記事では、一般的な呼び名として普及している「マクロ」という用語を使います。

\section を定義する

\section をステップ・バイ・ステップで定義してみます。

Step 1. とても原始的な\Sectionマクロ

まずは、次のようなとても原始的な \Section マクロを考えます。
\section ではなく \Section にします。)

%% セクションタイトル用のマクロ
\newcommand\Section[1]{%      % #1: タイトル
  \vspace{2\Cvs}%             % タイトル上部に2行分のマージン(余白)
  \refstepcounter{section}%   % セクション用のカウンタを増やす
  {%
    \setlength{\parindent}{0pt}% インデント幅をゼロに
    \headfont\Large%          % フォントを大きめのゴシック体に変更
    \thesection%              % セクション番号を表示
    \hspace{1zw}%             % 全角1文字分の空白
    #1%                       % 引数(タイトル)を表示
    \par\nobreak%             % 改行、かつ直後での改ページを防ぐ
  }%
  \vspace{1\Cvs}%             % タイトル下部に1行分のマージン(余白)
}

使い方は \section と同じです(ただし*などを除く)。

\Section{セクションタイトルのサンプル}

Step 2. 上部と下部のマージンをマクロ化

次に、タイトルの上部と下部にマージン(余白)を入れている部分を、独立したマクロにします。

%% セクションタイトル用のマクロ
\newcommand\Section[1]{%      % #1: タイトル
  \Section@topmargin%         % !!!!!
  \refstepcounter{section}%
  {%
    \setlength{\parindent}{0pt}
    \headfont\Large%
    \thesection%
    \hspace{1zw}%
    #1%
    \par\nobreak%
  }%
  \Section@bottommargin%      % !!!!!
}

%% セクションタイトル上部のマージン(余白)
\newcommand{\Section@topmargin}{\vspace{2\Cvs}}    % 2行分

%% セクションタイトル下部のマージン(余白)
\newcommand{\Section@bottommargin}{\vspace{1\Cvs}} % 1行分

これで、マージンだけをカスタマイズできるようになりました。
たとえば次のようにマクロを上書きすれば、セクションごとに改ページできます。

%% マージンを入れるかわりに、改ページする
\renewcommand{\Section@topmargin}{%
  \ifnum\c@section=0%   % 最初のセクションなら
    \vspace{2\Cvs}%     % マージン(余白)を入れる
  \else%                % そうでないなら
    \clearpage%         % 改ページする
  \fi%
}

従来の \section 定義では、これをするだけでも \section の定義をコピーして上書きしなければいけませんでした。
\Section マクロを分解しておけば、全体を上書きすることなく(つまり他の部分に影響を与えることなく)、必要な箇所だけを変更できます。

Step 3. セクションタイトル部をマクロ化

続いて、セクションの番号とタイトルを表示する部分をマクロ化します。
ついでにフォント指定もマクロ化します。

%% セクションタイトル用のマクロ
\newcommand\Section[1]{%      % #1: タイトル
  \Section@topmargin%
  \refstepcounter{section}%
  {%
    \setlength{\parindent}{0pt}
    \Section@font%                    % !!!!!
    \Section@title{\thesection}{#1}%  % !!!!!
    \par\nobreak%
  }%
  \Section@bottommargin%
}

%% セクションタイトル上部のマージン(余白)
\newcommand{\Section@topmargin}{\vspace{2\Cvs}}

%% セクションタイトル下部のマージン(余白)
\newcommand{\Section@bottommargin}{\vspace{1\Cvs}}

%% セクションタイトルのフォント
\newcommand{\Section@font}{%
  \headfont\Large%            % フォントを大きめのゴシック体に変更
}

%% セクションタイトル
\newcommand{\Section@title}[2]{%  #1: 番号, #2: タイトル
  #1%                         % セクション番号を表示
  \hspace{1zw}%               % 全角1文字分の空白
  #2%                         % タイトルを表示
}

ここで、\Section@title の引数に(タイトルだけでなく)セクション番号も渡されていることに注意してください。
このおかげで、「セクション番号は \thesection で参照する」というLaTeX知識を持っていなくても \Section@title を上書きできます。

Step 4. タイトルデザインをカスタマイズ

ここまでできたら、タイトルデザインをカスタマイズしてみましょう。

%% 設定ファイルの値によってセクションデザインを変更
\def\@tempa{grayback}
\ifx\config@section@decoration\@tempa
  \renewcommand{\Section@title}[2]{%  #1: 番号, #2: タイトル
    \textcolor{lightgray}{%   % タイトルの背景を薄いグレーに
      \rlap{\rule[-0.6zh]{\textwidth}{2zh}}% 本文幅
    }%
    \textcolor{black}{%       % 章番号の背景色を黒に
      \rlap{\rule[-0.6zh]{3.5zw}{2zh}}% 全角3.5文字分の幅
    }%
    \makebox[3.5zw][c]{\textcolor{white}{#1}}% 白抜き文字で番号を表示
    \hspace{0.9zw}%           % 空白
    #2%                       % タイトルを表示
  }
\fi

こんなデザインになります。

 sectiondesign.png

このようなデザインの変更をするのに、必要な箇所のマクロだけを上書きすればいいのが分かります。

繰り返しますが、従来の \section の定義ではデザインを変更したければ \section 定義をコピペして変更、上下のマージン(余白)を変更するだけだとしても \section 定義をコピペして変更する必要がありました。

これに対し、テンプレートメソッドパターンを使えば \Section マクロをより小さいマクロの集まりとして定義できます。
そのおかげで、必要な箇所のマクロだけを上書きすればいいので、\Section 全体を上書きする必要はありません。

説明しなかったこと

今回のサンプルコードでは、次のようなことは説明を省略しました。

  • \Section* のように * をつけたときは番号をつけない
  • 番号がつく場合とつかない場合でデザインやマージン(余白)を切り替える
  • \addcontentsline{toc}{見出し種類}{タイトル} を使って目次を出力
  • \hyperref の機能を使ってセクションへのアンカーを設定
  • \@hangfrom{章番号\hspace{1zw}}{タイトル} を使って、長いタイトルが2行になるときの対策
  • 各種ペナルティの設定(←今回は実装しなかった)

実際のスタイルファイル(後述するsty/starter-heading.sty)ではこれらを考慮したので、ずっと複雑であり、マクロもより細かく分かれています。
しかし「テンプレートメソッドパターン」という考え方は変わりません。

実際のコード

ここで説明した方法は、「Re:VIEW Starter」というドキュメント生成ツールの中で使っています。

興味ある人はここにアクセスして、プロジェクトを作成し、zipファイルを解凍してください。
その中の sty/starter-heading.sty に実装コードが書かれてあります。

従来の \section の問題点

従来の \section には、カスタマイズしにくいこと以外にも問題があります。

問題点:「番号はつけないけど目次には出す」ができない

従来の \section では、\section* のように * をつけると番号をつけなくなります。
それだけでなく、目次にも出さなくなります。

つまり、* が2つの機能を持っています。

  • 番号をつけない
  • 目次に出さない

しかし、番号はつけないけど目次には出したいことは珍しくありません。
たとえば「まえがき」や「あとがき」では、チャプターやセクションには番号をつけません。
そこで \section* を使うのですが、そうすると目次にも出なくなります。

(LaTeXでは \frontmatter\mainmatter\backmatter を使うと、\chapter だけは「まえがき」や「あとがき」のチャプターに番号がつきません。しかし \section\subsection ではそのような手当はされていません。)

これは \section の仕様が間違っています。
本当なら「番号をつける・つけない」と「目次に出す・出さない」を個別に指定できるべきでした。

解決策:第1引数が空文字列なら目次に出さない

ところで \section には、タイトルとは別に「目次用のタイトル」を指定できます。

\section[目次用タイトル]{セクションタイトル}

通常は目次用タイトルを指定しないので、そのときは セクションタイトル がそのまま目次用タイトルとして使われます。

この引数を利用すれば、目次用タイトルに空文字列を指定したら目次に出さないという仕様にできます。
使い方は次のようになります。

\Section{AAA}         % 目次に「AAA」が出る
\Section[BBB]{AAA}    % 目次に「BBB」が出る
\Section[]{AAA}       % 目次に出ない!!!!!!!!!!

実装はこんな感じになるでしょう。

%% `\relax` は、他の言語でいうところのNULLみたいなもの。
%% ここでは第1引数 `#1` のデフォルト値を `\relax` (NULL) に指定している。
\newcommand{\Section}[2][\relax]{%
  \def\@tempa{#1}%
  \def\@tempb{\relax}%
  \ifx\@tempa\@tempb%      % 第1引数が指定されてなければ
    #2を目次として使う
  \else\ifx\@tempa\empty%  % 第1引数が空文字列なら
    目次に出さない
  \else%                   % 第1引数が空文字列でなければ
    #1を目次として使う
  \fi\fi%
  ...
}

要は、こういうことです。

  • 第1引数 #1 が未指定なら、第2引数 #2 を目次に出す。
  • 第1引数 #1 が空文字列なら、目次に出さない。
  • 第1引数 #1 がそれ以外なら、第1引数を目次に出す。

これは * の指定とは独立して行えます。
そのため、「番号をつける・つけない」と「目次に出す・出さない」を別個に指定できます。
前述の sty/starter-heading.sty ではこの方法を採用しています。

問題点:一部を変更したいだけなのに全体のコピーが必要

\section\subsection は実際にどのような定義になっているでしょうか。
クラスファイル jsbook.cls から該当箇所を引用します。

  \newcommand{\section}{%
    \if@slide\clearpage\fi
    \@startsection{section}{1}{\z@}%
    {\Cvs \@plus.5\Cdp \@minus.2\Cdp}% 前アキ
    {.5\Cvs \@plus.3\Cdp}% 後アキ
    {\normalfont\Large\headfont\raggedright}}

  \newcommand{\subsection}{\@startsection{subsection}{2}{\z@}%
    {\Cvs \@plus.5\Cdp \@minus.2\Cdp}% 前アキ
    {.5\Cvs \@plus.3\Cdp}% 後アキ
    {\normalfont\large\headfont}}

  \newcommand{\subsubsection}{\@startsection{subsubsection}{3}{\z@}%
    {\Cvs \@plus.5\Cdp \@minus.2\Cdp}%
    {\if@slide .5\Cvs \@plus.3\Cdp \else \z@ \fi}%
    {\normalfont\normalsize\headfont}}

  \newcommand{\paragraph}{\@startsection{paragraph}{4}{\z@}%
    {0.5\Cvs \@plus.5\Cdp \@minus.2\Cdp}%
    {\if@slide .5\Cvs \@plus.3\Cdp \else -1zw\fi}% 改行せず 1zw のアキ
    {\normalfont\normalsize\headfont\jsParagraphMark}}

  \newcommand{\subparagraph}{\@startsection{subparagraph}{5}{\z@}%
    {\z@}{\if@slide .5\Cvs \@plus.3\Cdp \else -1zw\fi}%
    {\normalfont\normalsize\headfont}}

こうして見ると、\section から \subparagraph までが \@startsection という1つのマクロに共通化されていることが分かります(これは book.cls でも同様です)。
おかげでそれぞれの定義はわずか数行で済んでいます。
おそらくですが、数行しかないんだから \section を変えたいときはまるごとコピーして変更すればいいという設計思想なのでしょう。

しかしこれはプログラミング的には悪手です。
たとえばマージンの高さを変更したい場合であっても、全体をコピーして上書きする必要があるので、もしオリジナルのほうがフォントを変更した場合、その変更はコピーには反映されません。
オリジナルの定義が一切変わらないという保証がない限り、このようは方法は推奨されません。
(今のところ、book.clsjsbook.cls では変更がされてないから問題になってないのでしょう。)

問題点:個別にカスタマイズしたいのにコードを共通化している

先ほど説明したように、クラスファイル book.clsjsbook.cls では見出し定義の \section から \subparagraph までを1つのマクロ(\@startsection)で共通化しています。
そのせいで、\@startsection を変更するとそれが(\chapter を除く)すべての見出しレベルに影響します。

すべての見出しレベルを同じようなデザインにしたいならこの方法でもいいのですが、実際には「\section\subdsection\subsubsection はデザインを大きく変えたい」という要望が多いです。
その場合、1つのマクロで共通化していることはマイナス要素でしかありません。

個別に変更やカスタマイズが必要なものを下手に共通化すると、変更やカスタマイズはとてもしにくくなります。
\@startsection はまさにその例です。

(なお \@seccntformat というマクロをうま〜く再定義すると、番号のデザインは変更できます。うま〜く定義すればね。)

問題点:性質が大きく異る \section\paragraph を同じマクロで定義している

\@startsection は、\chapter を除いた次の見出し定義で使われています。

  • \section
  • \subsection
  • \subsubsection
  • \paragraph
  • \subparagraph

しかし上3つはいいとして、下2つ(\paragraph\subparagraph)は見出しとしての性質が大きく違います。
この2つには番号はつかないし、目次にも出しません。
見出しの一種とはいえ、\paragraph\subparagraph\section などと同じように定義する必要はありません。

また \@startsection は次のような奇妙な実装になっています。

  • 第4引数がマイナスかどうかでインデントの挙動を変える。
  • 第5引数がマイナスかどうかでタイトル後の改行する・しないを変える。

なぜこんなことになっているのか、マクロ定義を読んだときは意味が分からなかったのですが、どうやら \@startsection\paragraph\subparagraph をサポートするためにこんな奇妙な仕様になっているようです(あくまで推測です)。

どう考えても、\@startsection マクロが \paragraph の定義までサポートするのはやめて、\paragraph\subparagraph は個別に定義したほうがシンプルになったことでしょう。
性質の異なるものを共通化したせいで、共通化部分が複雑になった失敗例です。
(どちらかというと、\chapter\@startsection で定義できるようにすべきでした。)

前述の sty/starter-heading.sty では、\paragraph\subparagraph\section とは関係なく定義しているので、定義がとても簡潔になりました。

問題点:ページ先頭でもセクションタイトルの上部にマージンが入る

jsbook.cls を使っていると、たとえページ先頭であってもセクションタイトルの上にマージン(余白)が入ります(book.cls なら入りません)。

jsbook.cls の中身を見ると、次のような箇所があります。

1134:      \addpenalty\@secpenalty
1135:      \ifdim \@tempskipa >\z@
1136:        \if@slide\else
1137:          \null
1138:          \vspace*{-\baselineskip}%
1139:        \fi
1140:        \vskip\@tempskipa
1141:      \fi

1140行目では \vskip を使っているので、一見するとページ先頭ならマージンは入らないように見えます。
しかし1137行目と1138行目のせいで、ページ先頭のはずが余計な文字が入るせいでページ先頭とみなされなくなってしまうのです。

jsbook.cls がなぜこんな余計な仕様になっているかは分かりませんでした。
また1136行目に \if@slide があるので、\@slidetrue を使えば回避できますが、\if@slide が使われているのはここだけではないので、他の箇所に影響が出ます。)

この問題を回避するために、たとえば \section 定義を次のようにしてみましょう。

  \renewcommand{\section}{%
    \@startsection{section}{1}{\z@}%
    {0pt}%                     % 上マージンをなくす
    {.5\Cvs \@plus.3\Cdp}%     % 下マージンは残す
    {%
      \vskip \Cvs \@plus.5\Cdp \@minus.2\Cdp\relax%  上マージンを入れる
      \normalfont\Large\headfont\raggedright%
    }%
  }

一見するとこれでうまくいきそうですが、実は hyperref パッケージを読み込むと、やはりページ先頭であっても上マージンが入ります(hyperref を使わなければこんな余計なマージンは入りません)。

これは hyperref\refspepcouter を上書きしているせいです。
\refspepcouter はチャプターやセクションの番号を増やすマクロですが、hyperref はこれを上書きして、番号を増やすだけでなくハイパーリンク用のアンカーもつけてしまいます。
このアンカーはもちろん目に見えませんが、LaTeXではテキスト要素とみなされてしまうので、ページ先頭であってもページ先頭だとはみなされなくなり、\vspace によるマージンが入ってしまいます。

前述の sty/starter-heading.sty ではこの問題に対処するために、上書きされた \refstepcounter を使わず、上書きされていない \refstepcounter を使いました。
またハイパーリンク用のアンカーは hyperref パッケージのマクロを使って明示的につけました。
これでようやく、ページ先頭では余計なマージンが入らないようになりました。
(もっと簡単な方法としては、\refstepcounter を呼び出す前に上マージンを入れる方法もあります。今回はマクロ定義の都合でできませんでした。)

問題点:いろんなパッケージが \section の実装に依存している

先ほど説明したように、hyperref パッケージはハイパーリンクを実現するために、\section の内部で使われている \refstepcounter マクロを上書きします。
つまり hyperref パッケージは \section の実装に強く依存しており、密結合になっています。

そのせいで、独自に \section マクロを定義すると hyperref パッケージの目次機能は機能しません(当然です)。
そのため、独自に \section マクロを定義するときは自力で hyperref の機能を呼び出して同じような機能を実現する必要があります。

これはうまい解決策はなさそうです。
ただし \section マクロが部分的にカスタマイズしやすくなっていれば、密結合の強度を弱めることはできたはずです。

まとめ

LaTeXの \section マクロを、テンプレートメソッドパターンを使って実現する方法を紹介しました。
この方法を使うと、必要な箇所のマクロだけを上書きすればいいので、\section のカスタマイズ性が格段に向上します。

また \section の仕様そのものにも問題があることを説明しました。
特に「番号をつける・つけない」と「目次に出す・出さない」を個別に指定できないのは、仕様の欠陥です。

プログラミングの観点でみると、LaTeXはTeX言語だけでなく、クラスファイルやスタイルファイルの定義にも問題が多いと感じます。
いくらTeXやLaTeXが古いからとはいえ、プログラミングの定石から外れたコード(マクロ定義)が多すぎるように見えます。
\section の定義はそれが顕著で、コードを共通化したいという気持ちは分かるものの、やり方がよくなくて、「タイトル上下のマージンの高さを変えたい」「フォントの大きさを変更したい」という程度の変更ですら「\section 定義の全体をコピーして必要な箇所を書き換える」ことが必要です(Open-Closed Pricipleに反してますよね)。
また \section\paragraph の定義は共通化する必要がないのに共通化してるし、逆に共通化できるはずの \section\chapter はしていないという、とてもちぐはぐな設計です。

LaTeXのパワーユーザーで『リファクタリング』を読んだ人って、実は少ない(というかほぼいない?)のではないでしょうか。
せめて『リーダブルコード』ぐらいは読んで、もうちょっとコード品質を気にかけてほしいです1
そうすれば「LaTeXに詳しくないと読めないコード」が減って、LaTeXに詳しくなくてもカスタマイズができるのに、と思います。

逆に言えば、クラスファイルやスタイルファイルの品質(LaTeX的な意味ではなくプログラミング的な意味での品質)を向上すれば、LaTeXはもっと使いやすくなるはずだと期待しています。
TeX言語の仕様は変えられないけど、クラスファイルやスタイルファイルは変えられるので。

おまけ

余計なお世話でしょうけど、jlreq.clsが200行を超えるようなマクロ定義を平気でしてるの、やめたほうがいいと思いますよ?(プログラミング的な意味で)


  1. もし読んでいてあのコード品質なら、読んでないのと同じ。 

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5