2019年12月15日

クラスファイルを作ろう(4)

見出し命令(\sectionなど)の定義をします.articleクラスに習い,\part\section\subsection\subsubsection\paragraph\subparagraphを定義することにしましょう.

なお,以下では「1.1 はじめに」というような見出しに対して,「1.1」をラベル,「はじめに」を見出し文字列と呼ぶことにします.

レベルとカウンタ

各々の見出しにはレベルという値を定めなければなりません.これはsecnumdepthカウンタと比較され,レベルがsecnumdepthカウンタの値以下の時にのみラベルが出力されます.上位の見出しに小さい値を指定します.ここでは,順に0,1,…と割り当てることにしましょう.例えば\subsectionのレベルは2とします.この値は後の実際の実装で使います.

見出しの番号を格納するカウンタも必要です.この段階で宣言をしておきます.

\newcounter{part}
\newcounter{section}
% sectionカウンタをsubsectionカウンタの親とする.sectionカウンタが増えると,subsectionカウンタが0に戻る.
\newcounter{subsection}[section]
\newcounter{subsubsection}[subsection]
\newcounter{paragraph}[subsubsection]
\newcounter{subparagraph}[paragraph]

\the<カウンタ名>を再定義することで,ラベルの出力書式を変更しておきます.

\renewcommand{\thepart}{\Roman{part}}
\renewcommand{\thesection}{\arabic{section}}
\renewcommand{\thesubsection}{\thesection .\arabic{subsection}}% 1.1みたいになる.
\renewcommand{\thesubsubsection}{\thesubsection .\arabic{subsubsection}}
\renewcommand{\theparagraph}{\thesubsubsection .\arabic{paragraph}}
\renewcommand{\thesubparagraph}{\theparagraph .\arabic{subparagraph}}

\@startsection

凝らない見出し命令は,\@startsectionで作ることができます.その書式は

\@startsection{<見出し命令名>}
   {<見出しレベル>}% secnumdepthと比較に使われます.このレベルがsecnumdepthカウンタ以下の時にのみラベルが出力されます.上位の見出しに小さい値を指定します.
   {<見出しのインデント量>}
   {<見出し直前の行送り方向空き量>}% 負の値を指定した場合は,その絶対値が使われ,見出し直後のインデントが抑制される.
   {<見出し直後の行送り方向空き量>}% 負の値を指定した場合は,見出し後の改行が抑制され,ここでの値の絶対値が文字送り方向の空き量となる
   {<フォント設定命令>}
    *[<別見出し>]{<見出し文字列>}% この三つは\sectionとかの引数と同じ
です.通常見出し命令内で第六引数まで指定しておきます.これにより,見出し命令への引数の処理を\@startsectionに任せます.さてこの命令を使って,\part以外を一気に設定してしまいましょう.
\newcommand{\section}{\@startsection{section}{1}{0pt}{\baselineskip}{.5\baselineskip}{\normalfont\Large\bfseries\gtfamily}}
\newcommand{\subsection}{\@startsection{subsection}{2}{0pt}{\baselineskip}{.5\baselineskip}{\normalfont\large\bfseries\gtfamily}}
\newcommand{\subsubsection}{\@startsection{subsubsection}{3}{0pt}{\baselineskip}{0pt}{\normalfont\normalsize\bfseries\gtfamily}}
% ここから見出し直後に改行しないようにする(第五引数を負にする).いわゆる同行見出し.
\newcommand{\paragraph}{\@startsection{paragraph}{4}{0pt}{0.5\baselineskip}{-1zw}{\normalfont\normalsize\bfseries\gtfamily}}
\newcommand{\subparagraph}{\@startsection{subparagraph}{5}{0pt}{0pt}{-1zw}{\normalfont\normalsize\bfseries\gtfamily}}

ラベルを出力する際に\@startsectionの内部で\@seccntformat\@seccntformat{<見出し命令名>}という形で呼び出されます.ラベルの出力を制御するにはこれを上書きします.見出し命令名での分岐が必要になります.マクロへの格納+TeXのプリミティブ\ifxでの比較でもよいですが*1,ここではifthenパッケージを導入して\ifthenelseで処理することにしてみます.

\RequirePackage{ifthen}% パッケージロード.クラスファイルの頭に置いてもよい.
\renewcommand{\@seccntformat}[1]{%
  \ifthenelse{\equal{#1}{section}}{\S \thesection}% \sectionの時は§を頭につける
    {\@nameuse{the#1}}% それ以外の時はそのまま
  \quad % 空き
}

\part

\partはもう少し込み入った定義をしたいので,\@startsectionを使わず直接定義します.引数に関する処理をちょっと楽にするために,\secdefという命令をLaTeXは用意しています.これは\secdef\@part\@spartのように使います.\partだと\@partが,\part*だと\@spartが呼び出されます.

ラベルを出力するかを決定するために,見出しレベルとsecnumdepthカウンタの比較が必要です.TeXのプリミティブ\ifnumを使ってもよいのですがやはり\ifthenelseを使いましょう.\partのレベルは0とします.というわけで以下のようになります.「改行のコメントアウト」もお忘れなく.

\newcommand{\part}{% 
  \if@noskipsec\leavevmode\fi % この直前に見出し命令があった時にその見出しを出力するための定型処理です.後でもう少し説明をします.
  \par % 水平モード移行
  \addvspace{4ex}% 縦方向空き
  \@afterindenttrue % 直後にインデントをする.しない場合は\@afterindentfalse
  \secdef\@part\@spart % *があるかないかで分岐.
}
% *なし命令
% #1 = 別見出し,#2 = 見出し文字列
\newcommand{\@part}[2][]{% 実際には\secdefの処理によりオプションなしで渡されることはない.\def\@part[#1]#2{でもよい
  \ifthenelse{\value{secnumdepth} < 0}{% secnumdepthが\partのレベルである0未満なので,ラベルを出力せず,カウンタも出力しない.
    \addcontentsline{toc}{part}{#1}% 目次に追加
  }{%
    \refstepcounter{part}% partカウンタを1増加
    \addcontentsline{toc}{part}{第\thepart 部\hspace{1zw}#1}%
  }%
  \partmark{#1}% 柱の設定.\partmarkは後で定義する.
  {%
    \parindent=0pt % 改段落後のインデントを0にします.見出しが終わったら戻すためにグルーピングで囲みます. 
    \raggedright
    % 段落内における改ページのペナルティを最大値にする.これで見出し内で改ページが起こらなくなる.
    \interlinepenalty=10000
    \normalfont
    \ifthenelse{\value{secnumdepth} < 0}{}{% レベル<=secnumdepthの時のみラベルを出力
      \Large\bfseries\gtfamily 第\thepart 部\par\nobreak
    }%
    \huge\bfseries\gtfamily #2%
    \par
  }%
  \nobreak
  \vspace{3ex}%
  \@afterheading % 見出し直後の処理をLaTeXに委ねる
}
% *あり命令.ラベルの出力が常にない以外は\@partの後半部分と同じ.
\newcommand{\@spart}[1]{%
  {%
    \parindent=0pt
    \raggedright
    \interlinepenalty=10000
    \normalfont
    \huge\bfseries\gtfamily #1%
    \par
  }%
  \nobreak
  \vspace{3ex}%
  \@afterheading
}  

secnumdepthカウンタ

適当な値に初期化しておきます.

\setcounter{secnumdepth}{3}

同行見出し

見出し文字列後に改行せずに本文が続く,いわゆる同行見出しは,上記の通り\@startsectionの第五引数を負にすることで実現できます.\@startsectionはさらにペナルティの設定を行うので,この見出しがページの最終行に来ることはありません.この挙動が気に入らない場合*2は,\@startsectionを使わずに定義することになります.さて,どのように実装すればよいでしょうか.

  • 見出し命令を水平モードのままで終了させる.こうすると,
    \paragraph{パラグラフ}
    
    本文開始……
    
    のように書かれた場合に同行見出しになりません.
  • 垂直モードに移行して終了させる.見出し直後の本文は,見出し分だけ右にずれていなければなりませんが,どうすればよいでしょうか?

このように,なかなか難しいことがわかります.\@startsectionでは,\everyparを使って見出し文字の出力処理を段落開始時まで遅延することで解決しています.次のような手順です.

  1. \everyparに見出し出力処理が格納されていることを示すフラグを立てるために,\@noskipsectrueを実行する.
  2. \everyparに見出し出力処理を仕込む.この中では,\@noskipsecfalseを実行と,\@noskipsecfalseの時は\@everyparをリセットするようなコードを入れておく.

このような機構がLaTeXにおいては採用されているので,独自で定義した見出し命令の中では\@noskipsecをチェックし,もしこれがtrueならば\leavevmodeを実行して\everyparにある処理を実行する必要があります.これが上述の\partにあった\if@noskipsec\leavevmode\fiです.

さて,この機構に基づいて同行見出しを定義するならば,次のようになるでしょうか.

\newcommand{\paragraph}{%
  \if@noskipsec\leavevmode\fi
  \global\@noskipsectrue
  \par
  \secdef\@paragraph\@sparagraph
}
\newcommand{\@paragraph}[2][]{%
  \everypar{% everyparに出力を仕込む
    \ifthenelse{\boolean{@noskipsec}}{% \if@noskipsecでもよい
      {\setbox0\lastbox}% \parindentで入った空きを消す.
      \global\@noskipsecfalse % 見出しを出力したのでこれ以上出力する必要はない
      \paragraphmark{#1}%
      {%
        \ifthenelse{\value{secnumdepth} < 4}{%
          \addcontentsline{toc}{paragraph}{#1}%
        }{%
          \refstepcounter{paragraph}%
          \addcontentsline{toc}{paragraph}{\theparagraph\hspace{1zw}#1}%
        }%
        \normalsize\bfseries\gtfamily
        \ifthenelse{\value{secnumdepth} < 4}{}{\theparagraph}%
        \normalsize\normalfont\bfseries\gtfamily #2%
        \hspace{1zw}%
      }%
    }{%
      \everypar{}% \everyparを戻す.
    }%
  }%
  \ignorespaces % 直後の空白を無視させる
}
\newcommand{\@sparagraph}[1]{%
  \everypar{%
    \ifthenelse{\boolean{@noskipsec}}{%
      {\setbox0\lastbox}%
      \global\@noskipsecfalse 
      {%
        \normalsize\normalfont\bfseries\gtfamily #1%
        \hspace{1zw}%
      }%
    }{%
      \everypar{}%
    }%
  }%
  \ignorespaces
}
*1
こっちを使っているクラスファイルの方が多いかと思います.
*2
例えば日本語組版処理の要件では最終行に同行見出しが来ることを許しています.

2 件のコメント:

  1. 「別行見出し」という用語、意図通りでしょうか?
    「別行見出し」といえば見出しだけ独立した別の行になるように思えますが、
    そこで説明されているのは、見出しの後に改行せずに本文が続くようなケースのようです。
    これだと、見出しが別の行になっているようには見えないと思うのですが。

    (追記)コメントを投稿しようとするとロボットよけに写真が表示されますが、
    「~の部分を選べ」というような指示が一切ないので、
    何に注目して選べばよいのかわかりません。
    当方Windows10でChromeブラウザを使って閲覧しています。
    適当に選んでみようとは思いますが。

    返信削除
  2. ありがとうございます.修正しました.

    返信削除

コメントの追加にはサードパーティーCookieの許可が必要です