これは「TeX & LaTeX Advent Calendar 2021」の 13 日目の記事です.12 日目は yukishita さん,14 日目は Daiji さん です.
今年の重点テーマが「TeX言語(とか)しましょう!」なので,ここでは「WEB 言語で e-(u)pTeX をいじってみた」話をします.
TeX Live 2022 の pTeX 系列 (p4.0.0) には\ptexlineendmode という名前の内部整数が追加されますが,本ページ「ネタ2」で説明している \epTeXlineendmode とは全く異なるものです.混同しないでください.
WEB 言語とは
この記事で扱っている WEB とは,World Wide Web (Wikipedia) のことではありません.ここでの WEB は Knuth 教授が提唱している「文芸的プログラミング」の実装であり,
- Pascal で書かれたプログラムとそれの TeX ソースによる「解説文書」を 1 つのファイル(たとえば
sample.web
)に記述する -
sample.web
をtangle
というプログラムで処理することで Pascal ソースsample.p
を得る -
sample.web
をweave
というプログラムで処理することで「解説文書」の TeX ソースsample.tex
を得る
という流れになっています.TeX 自身がこの WEB 言語で記述されています (tex.web
).
tex.web
に対する変更は Knuth 教授しか行えず,機能拡張や実装依存の部分は WEB 言語の change file という形式でパッチされることになっています.たとえば,TeX Live 2021 における e-pTeX バイナリは,tex.web
と 25 個程度の change file(そして C 言語による各種ライブラリ)から作られています.
e-TeX, (e-)(u)pTeX, Omega, Aleph は 以上で述べたように tex.web に大量の change file を積み重ねる方式をとっていますが,pdfTeX, XeTeX はそれぞれ pdftex.web, xetex.web からスタートしています.また,LuaTeX は WEB ではなく,C 言語で記述されています.
ネタ 1:LuaTeX の \suppresslongerror
プリミティブ等を実装してみた
これは,LuaTeX に実装されている,各種エラー抑止のためのプリミティブ(いずれも内部整数)
\suppresslongerror
,\suppressoutererror
,\suppressmathparerror
,\suppressfontnotfounderror
,\suppressifcsnameerror
,\suppressprimitiveerror
のうち,前者 3 つのプリミティブを実装した,というものです.
-
\suppresslongerror
:0 でない場合,\long
なしマクロの引数に\par
が含まれた場合のエラーを抑止する -
\suppressoutererror
:0 でない場合,\outer
付きマクロが禁止されている状況で\outer
付きマクロが来た場合のエラーを抑止する -
\suppressmathparerror
0 でない場合,数式モード中に\par
が来た場合のエラーを抑止する
初期値はいずれも 0(つまり従来どおりの動作)です.
実装自体は LuaTeX ソースの該当部分の変更を e-(u)pTeX 用に書き直すだけなので単純です.tex-jp-build リポジトリの 個人フォークの suppress_errors ブランチ におきました.
ここで追加した 3 つのプリミティブは「エラーが出なくなる」という意味では便利なものですが,「早期に書き忘れ・書き間違いを防止する」目的で決められた TeX の仕様を破るものです.安易に次の 3 行をプリアンブルに書くのはやめたほうが良いでしょう.
\suppresslongerror=1
\suppressoutererror=1
\suppressmathparerror=1
このネタは TeX Live 開発版に r61239 (2021-12-07) として入りました.TeX Live 2022 の e-(u)pTeX バイナリでは利用できることになるでしょう.
他の 3 つのプリミティブを見送った理由は……
-
\suppressfontnotfounderror
:フォント (tfm) が見つからない場合,どっちみち外部プログラムmktextfm
が走ることには変わりがないため,必要性が薄い -
\suppressifcsnameerror
:実装(とくにたくさんの change file 間の同期)が面倒そう -
\suppressprimitiveerror
:e-(u)pTeX, pdfTeX, XeTeX では必要性はない(\(pdf)primitive
でエラーが出るのは LuaTeX だけ)
\suppresslongerror
について
このプリミティブは LuaTeX beta-0.36.0 で導入されたものですが,\long
についてここで説明しておきます.
\long
の意味
TeX において,標準ではマクロの引数内に \par
や空行を含んでいてはならない,ということはよく知られています.例を用いて説明します.
% plain TeX
\def\hoge#1{#1}
\hoge{a\par b}
\bye
2 行目で定義された\hoge
は引数を 1 つとるマクロですが,\long
なしで定義されているため 引数に \par
が含まれることは許容されていません.しかし 3 行目の \hoge
の引数には a\par b
と明示的な \par
が含まれているので,
! Paragraph ended before \hoge was complete.
<to be read again>
\par
l.3 \hoge{a\par
b}
?
とエラーが発生します.
また,TeX ソース中の空行は改段落を引き起こしますが,これは「空行があると,\par
が自動的に挿入される」という TeX の規則によるものです.したがって,
\hoge{a
b}
も同様のエラーを引き起こします.
このように「標準ではマクロの引数内に \par や空行を含んでいてはならない」という仕様は,引数を閉じる } の書き忘れを早期に検出するためのものと言えます.
マクロ定義の際に「このマクロでは引数に \par
が含まれても良い」ことを明示するフラグが \long
です.たとえば
\long\def\hoge#1{#1}
と \long
付きでマクロを定義すると,
\hoge{a\par b}
\hoge{a
b}
のどちらでもエラーは起こりません.
……とここまで書いてきましたが,[r60054](https://www.tug.org/svn/texlive?view=revision&revision=60054) (2021-07-25) で状況が変わりました.
\partokenname\MYPAR
\def\MYPAR{!!\par}
\def\hoge#1{(#1)}
a
b % 空行によって \par ではなく \MYPAR が挿入される
\hoge{x\par y} % ==> no error
\hoge{x\MYPAR y} % ==> error "! Paragraph ended before"
\partokencontext
は,\par
(あるいは\partokenname
で別のものに置き換わっているかも)が自動挿入される場所を制御します.
LaTeX での現象
LaTeX の世界に話を移します.たとえば次の LaTeX ソースを考えます.
\documentclass{article}
\usepackage{lipsum} % \lipsum 命令のため
\begin{document}
ABC%
\smash{\rlap{\footnotesize\sffamily
\begin{minipage}[t]{30em}
\lipsum[1]
\lipsum[2]
\end{minipage}}}%
\lipsum[3]
\end{document}
「\smash{\rlap{}}
の中に minipage 環境を入れる」という記述がどうなのかという点は無視して,まあ書きそうなソースです.minipage が専有するスペースを無視して本文が続く,以下のような出力が期待されます:
しかし,lipsum.tex
をタイプセットすると,以下のエラーが出て止まってしまいます:
Runaway argument?
{\rlap {\footnotesize \sffamily \begin {minipage}[t]{30em} \lipsum [1\ETC.
! Paragraph ended before \makesm@sh was complete.
<to be read again>
\par
l.8
?
ひとことでいえば,これは \smash
が \long
付きで定義されていないためです.
この文はごまかしがあります.正確に言うと……
\DeclareRobustCommand\smash{%
\relax % \relax, in case this comes first in \halign
\ifmmode
\expandafter\mathpalette\expandafter\mathsm@sh
\else
\expandafter\makesm@sh
\fi}
\def\makesm@sh#1{%
\setbox\z@\hbox{\color@begingroup#1\color@endgroup}\finsm@sh}
\def\mathsm@sh#1#2{%
\setbox\z@\hbox{$\m@th#1{#2}$}\finsm@sh}
\def\finsm@sh{\ht\z@\z@ \dp\z@\z@ \leavevmode@ifvmode\box\z@}
なお,zr_tex8r さんの記事「完全攻略! LaTeX のマクロ定義」で述べられているように,\newcommand
(や \renewcommand
)で定義されたマクロは,標準では引数に \par
を許容(つまり \long
付きで定義)します.アスタリスク付きの\newcommand*
(や \renewcommand*
)で定義した場合は \par
を許容しなくなります.
もともと \par は TeX のプリミティブです.しかし LaTeX2e 2021-06-01 以降では段落頭・終端でのフックを実現するため,\par, \endgraf はプリミティブではなくなりました.
伝統的な回避法
\long
なしのマクロが引数中に禁止しているのは \par
という名前の制御綴です.したがって,\par
の中身を別の制御綴に \let
してあげればエラーは起きないことになります.plain TeX や LaTeX では \par
の別名として既に \endgraf
が定義されているので,これを使えば良いことになります.
\documentclass{article}
\usepackage{lipsum}
\begin{document}
ABC%
\smash{\rlap{\footnotesize\sffamily
\begin{minipage}[t]{30em}
\lipsum[1]
\endgraf
\lipsum[2]
\end{minipage}}}%
\lipsum[3]
\end{document}
\suppresslongerror
を使った回避法
\suppresslongerror
を使うと,次のように書くことが出来ます.
\documentclass{article}
\usepackage{lipsum} % \lipsum 命令のため
\begin{document}
ABC%
{\suppresslongerror=1
\smash{\rlap{\footnotesize\sffamily
\begin{minipage}[t]{30em}
\lipsum[1]
\lipsum[2]
\end{minipage}}}}%
\lipsum[3]
\end{document}
原理的には \suppresslongerror=1
をプリアンブルに書いてしまってもよいのですが,それだと「引数を閉じる }
の書き忘れ」が起こった場合にファイル終端でようやくエラーが出されることになります.
グルーピングしておけば,影響を最小限にとどめることができます.
! Paragraph ended before ... のエラーは \smash の引数を取得する部分で発生しているので,\smash{\suppresslongerror=1 \rlap{... と記述するのは効果がありません.
\suppressoutererror
について
「\long
なし」のようにマクロに関する制限として,もう一つあるのが \outer
です.
\outer
付きで定義されたマクロは,他のマクロの内部や条件文 \if... \else...\fi
などで使えないという制限があります.例を以下に示します.
% plain TeX
\outer\def\F{hoge}
\def\G{piyo\F piyo} % ==> error
\immediate\write-1{\F} % ==> error
\message{\F} % ==> error
\iftrue a\F \else b\F \fi % ==> error
\halign{#\F&#\cr a&d\cr} % ==> error
2 行目で \outer
付きでマクロ \F
が定義されていますが,3 行目以降の各行で ! Forbidden control sequence found while scanning ...
エラーが発生します.
\suppressoutererror
が 0 でない場合は,この ! Forbidden control sequence found while scanning ...
エラーがどれも出ないようになります.
LaTeX 本体や LaTeX 用の各種パッケージでは,\outer 付きマクロはほとんど使われません.ただ,まったくないわけではありません(bm パッケージや fancyvrb パッケージなど).
\suppressmathparerror
について
数式モード中に \par
(もちろん,\par
を生み出す「空行」も)を使うことは出来ません.
$ x+1 =
ab$
という入力からは,空行由来の \par
から ! Missing $ inserted.
というエラーが発生します.
この仕様も「$
の書き忘れを早期に検出する」という意味はありますが,次のように「何を書こうか迷ってとりあえず空行のまま放置した」場合にエラーが発生して嫌な気分になったことはあるかもしれません.
\documentclass{article}
\begin{document}
\[
\ frac 1n \sum_{k=0}^{n-1} \left(\frac{k}{n}\right)^2=\int_0^1 x^2\,dx
\]% 何を書こうか迷っている
\end{document}
このような場合,\suppressmathparerror
を 0 以外の値にすると,! Missing $ inserted.
エラーが発生せずにタイプセットすることができます.
amsmath パッケージの提供する各種環境(align 環境など)では,\suppressmathparerror, \suppresslongerror の両方に非ゼロの値を設定しないとエラーは消えません.
ネタ 2:「次の行が和文文字から始まる」場合に行末由来のスペースを挿入しない
こちらが本来のネタ(TeX Live に取り込んでもらう気はない,という意味で)です.
冒頭でも注意しましたが,ここで述べている話題は TeX Live 2022 の pTeX 系列 (p4.0.0) に追加される内部整数 \ptexlineendmode とは全く異なるものです.混同しないでください.
きちんと追ったわけではありませんが,CSS における議論 も関連しそうです.
背景
(p)TeX における行末(改行文字)の扱いでは,次の点が有名です:
- 欧文文字直後の改行では空白文字を発生させる
- 和文文字直後の改行は何も発生させない
詳細
- コントロールワードや「
\
」の直後の改行は何も発生しない - 「
\
」以外のコントロールシンボルの直後の改行は空白文字を発生させる - 和文文字→グループ境界 (
{
.}
) が有限個という状態で行が終わった場合は,何も発生しない
があります.個人的には最後の点はやや不思議ですが,
このルールに従うと,例えば
この
alphabet
は
と入力した場合,
- 1 行目は和文文字「の」で終わっているので,空白は発生しない
- 2 行目は欧文文字「t」で終わっているので,空白が発生する
ということになり,結局次の入力と同じことになります:
このalphabet は
さて,「の」「a」の間には標準の和欧文間空白 (\xkanjiskip
) が,「t」「は」の間には欧文空白が挿入されることになります.pLaTeX の標準では前者は 2.40553pt plus 1.0pt minus 1.0pt
,後者は 3.33333pt plus 1.66666pt minus 1.11111pt
(cmr10) ですから,結果的に**「alphabet」の前後で入る空白量が異なる**ということになります.
前後の不統一が発生するのは,上で説明したように行末の扱いがそれより前にのみ依存するからです.
では,「先読みを行って,行末の前後の文字からその行末の扱いを決定できないか」というのが今回のネタになります.
作ってみた
例によって tex-jp-build リポジトリの 個人フォークの kitagawa_lineend_1 ブランチ におきました.
行末由来の空白をどういう場合に挿入するか制御する \epTeXlineendmode
という内部整数を追加しています.この値によって
値 | 欧文→欧文 | 欧文→和文 | 和文→欧文 | 和文→和文 | 先読み | 備考 |
---|---|---|---|---|---|---|
0 | 空白 | 空白 | なし | なし | なし | pTeX 標準 |
1 | 空白 | なし | なし | なし | あり | |
2 | 空白 | 空白 | 空白 | 空白 | なし | |
3 | 空白 | 空白 | 空白 | なし | あり |
と変わります.
- ファイル終端では,「欧文文字が続く」ものとして扱います.
- 端末からの入力の場合,「次の行の入力」があるかどうかはわからないので,先読み処理をせずに「次の行は欧文文字から始まる」と想定します.
- 先読み処理は単純に「空白以外で先頭にくる文字が和文文字か否か」のみで行っています.行の先頭が catcode 9 (ignore) とか ^^ 記法だった場合の考慮はしていません.
実装方針
TeX では,「次のトークンを取得する」処理は get_next
プロシージャにまとまって記述されています.その中の「外部ファイルから読む」部分の処理は,大雑把に言って
switch: if (現在の行が終了していない) then
begin (現在位置の文字を取得,cur_cmd をその文字の catcode にセット)
(cur_cmd の値と入力プロセッサの内部状態に合わせて分岐)
end
else begin
(次の行に移動する準備)
goto switch;
end;
のようになっています.後半の else 節では次の行のトークンを取得する準備だけをしている段階であることに注目すると,「先読みをして行末由来の空白を挿入するか調べる」ことを示す変数 pending_space
を準備して
switch: if (現在の行が終了していない) then
begin (現在位置の文字を取得,cur_cmd をその文字の catcode にセット)
(cur_cmd の値と入力プロセッサの内部状態に合わせて分岐.
\epTeXlineendmode の値などに応じて pending_space を設定)
end
else begin
(次の行に移動する準備)
if pending_space then begin
pending_space:=false;
(catcode が 10 でないような最初の文字が欧文文字なら,空白文字を戻り値にする.
和文文字なら,goto switch)
end;
goto switch;
end;
のようにすれば一応の形になるというわけです.
行頭にある catcode 10 の文字(空白の役割)たちは無視されるので,この先読み部分でも無視しています.
おわりに
WEB 言語しましょう!