マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

もっと新しくなったbxtexlogoの話(v0.8)

前回のbxtexlogoの記事は0.4版のときのものだったので、それより後の改修点について解説する。

ロゴがもっと増えた件

追加されたやつ

※上の4つ(OpTeXCSTUGHiTeXTeXXeT)は**による一括インポートの対象となる。それ以外は一括インポートの対象外で、個別にインポートする必要がある。
1TeXOneTeXと等価である。\bxtexlogo命令で使う用途で用意した。
XeT以下の6つはロゴを構成する部品である。loweは「LaTeX2e」のロゴに現れる「下がったε」の部分。
YukidarumaTeXは素敵っぽいが何を指すかは不明である(ざんねん🙃)

さっそく新しいロゴを使ってみた文書の例。

% upLaTeX+dvipdfmx文書
\documentclass[uplatex,dvipdfmx,a4paper]{jsarticle}
\usepackage{graphicx,bxtexlogo}
% "Prote"等は一括インポートの対象外なので個別に指定する
\bxtexlogoimport{*,**,Prote,lowe,One,Yukidaruma}
\begin{document}
\begin{itemize}
\item 現在の{\HiTeX}{\eTeX}拡張をもつ。\\{\Prote}の実装を利用しているらしい。\\
  ※ただし{\TeXXeT}拡張は除外されている。
\item {\LaTeX}3{\lowe}(非存在)
\item sc{\One}Typstしましょう!{\Yukidaruma}
\end{itemize}
\end{document}

hologoのロゴが全部使える件

「一括インポート対象外のロゴ」という概念を導入したついでに、今まで省いていたものも含めて全てのhologo提供のロゴをbxtexlogoでも使用可能にした。以下に示すのはその中の一部である。

追加されたhologoのやつ(抜粋)

新たに追加したものは一括インポート(***)の対象外なので、個別にインポートするか、または後述の\bxtexlogo命令を1使う必要がある。

% upLaTeX+dvipdfmx文書
\documentclass[uplatex,dvipdfmx,a4paper]{jsarticle}
\usepackage{graphicx,bxtexlogo}
% やはり個別にインポートする
\bxtexlogoimport{La,PiC}
\newcommand\+{\hspace{0pt}}% 和欧文間空白を消すトリック
\begin{document}
\begin{itemize}
\item アレ/{\La}アレ Advent Calendar
  % ちなみに"\TeX"は(カーネル定義済なので)常に使える
\item あたま{\TeX}\+ァ\+{\TeX}\+ァ さえて{\PiC}\+ァ\+{\PiC}\+ァ(違う)
\end{itemize}
\end{document}

\bxtexlogo: インポートせずに使える件

\hologo{‹名前›}と書く必要のあるhologoのロゴを単一命令にして使いやすくする」のが本パッケージの当初の目的であったが、本パッケージ独自のロゴが増えてきたので、逆に「独自の(稀に使う)ロゴをわざわざインポートせずに\hologoみたいな命令で直接書きたい」という要望が想定できる。そこで、名前を引数に指定してロゴを出力する命令である\bxtexlogo命令を用意した。

※命令としてのインポートの有無にかかわらず\bxtexlogo命令は常に使用可能である。

% upLaTeX+dvipdfmx文書
\documentclass[uplatex,dvipdfmx,a4paper]{jsarticle}
\usepackage{graphicx,bxtexlogo}
\bxtexlogoimport{LuaTeX}% "\LuaTeX"をインポートした
\begin{document}
{\LuaTeX}とか% インポートしたので使える
\bxtexlogo{LuaTeX}とか% "\bxtexlogo"は無条件に使える
\bxtexlogo{LuahbTeX}とか。% やはり無条件に使える
\end{document}

今回新たに追加したロゴの中には1TeXや(hologoの)(La)TeXのように非英字を含む名前のものがある。これらはインポートして命令にする使い方はできない2が、\bxtexlogoで使うことが想定されている。

% upLaTeX+dvipdfmx文書
\documentclass[uplatex,dvipdfmx,a4paper]{jsarticle}
\usepackage{graphicx,bxtexlogo}
\begin{document}
\bxtexlogo{OneTeX}とか
\bxtexlogo{1TeX}とか% 数字が入っても問題無し
\bxtexlogo{(La)TeX}とか。% hologo提供のロゴ("TeXLaTeX"と同じ)
\end{document}

dvipsとかdvisvgmでマトモに1TeXできる話

一般的に、TeX関連のロゴを斜体(イタリックも含む)の書体の中で用いる場合はロゴの文字も斜体で書く。しかしロゴで使われる特殊な記号の中には斜体のフォントが用意できないものもあり、本パッケージ提供のロゴでは、そういう部品について「斜体変形をかけた文字」(合成斜体)が使われている。例えば、「1TeX」(OneTeX)の「1」や「スヤァTeX」(SuyahTeX)の「スヤァ」の部分が該当する。

% pdfLaTeX文書
\documentclass[a4paper]{article}
\usepackage{graphicx,bxtexlogo}% ここではgraphicxは必須
\bxtexlogoimport{OneTeX,SuyahTeX,YukidarumaTeX}
\begin{document}
\itshape % 全体をイタリックに印字
\begin{itemize}
\item I like {\OneTeX} better than {\SuyahTeX}.
\item Do you know what {\YukidarumaTeX} is?
\end{itemize}
\end{document}

※「合成斜体」を使ったロゴを出力する場合はgraphicxパッケージの読込が必要である。これを忘れると警告が出る。

ところが諸般の事情(=手抜き🙃)のため、従来はこの「合成斜体」の処理がpdfTeX系とdvipdfmx系のドライバにおいてしか実装されてなかった。このため、例えば[dvisvgm]を使った場合は「合成斜体」が無効になりロゴの外見がイマイチなことになっていた。

0.8版においてdvipsおよびdvisvgmについても「合成斜体」の処理が実装されたので、それを使ったロゴの出力がマトモになる。

dvisvgmを使う場合は、(dvipdfmxの時と同じく)グローバルオプションにdvisvgmを指定する。これでgraphicxのドライバがdvisvgm用になり、bxtexlogoはgraphicxの設定に追随するので正しくdvisvgm用の処理が使われるようになる。

% 欧文LaTeX+dvisvgm文書
\documentclass[dvisvgm,a4paper]{article}% ドライバオプション指定
\usepackage{graphicx,bxtexlogo}% ここではgraphicxは必須
\bxtexlogoimport{OneTeX,SuyahTeX,YukidarumaTeX}
\begin{document}
\itshape % 全体をイタリックに印字
\begin{itemize}
\item I like {\OneTeX} better than {\SuyahTeX}.
\item Do you know what {\YukidarumaTeX} is?
\end{itemize}
\end{document}

※先のpdfLaTeX用のソースコードと異なるのはドライバオプション(dvisvgm)の追加の部分だけである。

余談であるが、Yukidarumaの“ロゴ”は、本来は(実態が不明な)「☃TeX」のロゴの部品として用意されているが、ロゴとは無関係に「☃」の文字を出す用途に使える。「斜体に対応した☃の文字」を出す手段は貴重であるので、本質的な語句を多用する人は記憶に留めておくといいかもしれない。

\bxtexlogosetup: パラメタ設定する件

key-value形式でパラメタ設定を行う命令が新設された。

\bxtexlogosetup{‹パラメタ›=‹値›,…}

現状で有効なパラメタを以降で紹介する。

ghost: ゴーストする件

TeX関連のロゴは基本的に欧文なので、和文の文書中で用いる場合は周りの和文文字との間に和欧文間空白が適切に入ることが望ましい。しかしロゴのような「凝った加工」を施した文字があると自動の和欧文間空白挿入が失敗することが多い。これへの回避策となるのがbxghostパッケージによる「ゴースト挿入」である。

bxtexlogoではロゴの周りに自動的にゴーストを入れる機能を用意して、ghostパラメタで有効化できる。

  • ghost=‹真偽値›(既定=偽): 自動ゴースト挿入を有効にするか。
    trueにする場合はbxghostが読み込まれている必要がある。

すなわち、以下の手順3で自動ゴースト挿入が有効になる。

  1. bxghostパッケージ4を読み込む。
  2. bxtexlogoのパラメタghosttrueに設定する。

ゴースト挿入の効果をみるための例を示す。

% upLaTeX+dvipdfmx文書
\documentclass[uplatex,dvipdfmx,a4paper]{jsarticle}
\usepackage{bxghost,bxtexlogo}% bxghostを読み込む
\bxtexlogoimport{SATySFi}
\begin{document}
% 比較のため, ghost無効の状態で出力する
お客様の中に{\SATySFi}芸人はおられますか。

% ghostを有効にして出力する
\bxtexlogosetup{ghost=true}% ghostする
お客様の中に{\SATySFi}芸人はおられますか。
\end{document}

※この文書ではゴースト挿入が有効と無効の状態を比較するために敢えて本文の途中でghosttrueにしているが、もちろん実際にはプリアンブルで設定することが普通であろう。

ghostを有効にするとSATySFiロゴの両側に正しく和欧文間空白が入っていることがわかる😊

one-font: 1を太くしたい話

「1TeX」のロゴの「1」の文字はいわゆる「黒板太字」で書かれる。「黒板太字」であればどの書体であっても「正しいロゴ」と見なされるわけだが、1TeXの作者(某ZR氏🙃)は「dsrom」という書体(doublestrokeパッケージ)を使っている。このため、bxtexlogoの既定の動作では「1」のフォントとしてdsromが優先的に5使っている。

dsromはカッコイイ書体(多分🙃)であるが「太字版がない」という欠点(ええっ😲6)がある。だから太字の書体の中で1TeXのロゴが現れる場面においては既定動作ではバランスが悪く見えるかもしれない。

太字版がある黒板太字のフォントというと「bboldx」というものが存在する。そこで、bxtexlogoではこのフォントに切り替えるためのパラメタを用意している。

  • one-font=‹値›: 「1TeX」ロゴの「1」の書体を指定する。
    • normal(既定): 既定の動作。最優先でdsromを使う。
    • bboldx: bboldxを使う。

実際に「1」の書体をbboldxに切り替える例を示す。

% pdfLaTeX文書
\documentclass[a4paper]{article}
\usepackage{graphicx,bxtexlogo}
\bxtexlogoimport{OneTeX}
\bxtexlogosetup{one-font=bboldx}
\begin{document}
I love {\OneTeX}! % 中字
\textbf{Yes, I love {\OneTeX}!!} % 太字
\end{document}

これで良くなったかは微妙だが、取りあえず太字のロゴとしての整合性は取れているだろう🙃

その他諸々の話

他のパラメタについては使用する機会が乏しいと思われるので詳細は省略する。

  • smallcaps=‹値›: スモールキャップの文字を出力する場合に、実際のフォントの指定を行うか疑似出力(小さいサイズの大文字を出す)を行うか。

    • auto(既定): 「CM Sans Serif」と「LM Sans Serif」のときだけ(スモールキャップがないことが判っているので)疑似にする。
    • real: 常に実際のフォントの指定を行う。書体にスモールキャップがない場合はフォールバックが起こるので不正確な出力がになりうる。
    • fake: 常に疑似にする。
  • hologo-for-basic=‹真偽値›(既定=偽): 本パッケージ提供のロゴの中の「TeX」「LaTeX」「LaTeX2e」の部分の出力にhologoの該当のロゴを使うか。偽の場合は現在定義されている\TeX\LaTeX\LaTeXe命令を使う。

まとめ

というわけで皆さん、新しいbxtexlogoパッケージでドンドン「1TeX」ロゴを書いていきましょう!💁(えっ😲)


  1. もっとも、hologo提供のロゴなら\hologo命令でも使えるので\bxtexlogoを使うメリットはないのであるが。
  2. ただしTeXのレベルで考えると、実は\bxtexlogoimport{1TeX}により1TeXという名前の制御綴は実際に定義される。
  3. 将来、一定の条件下でghostの既定値を有効にする改修を行うかもしれない。
  4. bxghostの代わりにbxghost-libを読み込んでもよい(知っている人向け情報)。
  5. 「dsrom→bbm→bbold」の順で最初に見つかったものを使い、どれもない場合は通常の太字の「CM Roman BoldExtended」にフォールバックする。
  6. 元々数式用の「黒板太字」なのだから太字版がないのはアタリマエである。

dvisvgmのフォントマップ指定がよくわからない件

dvisvgmの「文書中のspecial命令によるフォントマップ指定」の機能がよくわからないので、チョット調べてみた。

公式ドキュメントの説明では

公式ドキュメントdvisvgm.man1.pdfには次のように書かれている。

pdf:mapfile and pdf:mapline allow for modifying the font map tree while processing the DVI file. They are used by CTeX, for example. dvisvgm supports both, the dvips and dvipdfm font map format.

  • dvisvgmはdvipdfmxのpdf:maplinepdf:mapfileのspecial命令をサポートする。
    • dvipdfmxと同様に、dvipsとdvipdfmの両方のマップ行形式をサポートする。

実際の動作では

  • マップ行の修飾子(+-)はサポートしていない
    • 例えば「+foo …」というマップ行を書くと「+fooというTFMに対するマップ指定」と見なされる。
  • 既に行われたマップ指定を変更する操作はサポートしていない😢😢😢
    • 「修飾子無し」のマップ行はdvipdfmxでは「古い設定を置換」になるはずだが、実際には無視されてしまう。
  • dvipdfmx形式のマップ行の-lオプションはサポートしていない
    • pdf:mapline: invalid option: -l」という警告が出る。

和文フォントに対する動作は

dvisvgmはdvipdfmxの和文用のマップ行指定(第2項にCMapを指定する)に対応しているようだ。

dvipdfmxで有効な設定パターンを用いて出力がどうなるかを試してみると以下のようになった。

※「TTF」は「2004JIS字形が既定であるTrueTypeグリフのフォント」、「CFF」は「CFFグリフのフォント」を物理フォント(第3項)に指定した場合を指す1。欄中の「2000JIS」は「2000JIS字形で正常に文字が出力された」ことを表す。
※「TTF」ではCIDでのグリフアクセスができないので、必ず既定のグリフ(つまり2004JIS字形)が使われるのが正しい動作である。

[pTeX・JIS系の和文TFM使用時]

Cmap指定 TTF CFF
H ⭕2004JIS ⚠️2004JIS
2004-H ⭕2004JIS ⭕2004JIS

[upTeX・Unicode系の和文TFM使用時]

Cmap指定 TTF CFF
UniJIS-UTF16-H ❌文字化け ⭕2000JIS
UniJIS2004-UTF16-H ❌文字化け ⭕2004JIS
unicode ⭕2004JIS ⭕2004JIS
  • HとCFF」の組み合わせで2004JISになるのが不可解である🤔
    • 一見、CMapを無視して独自で「JIS→Unicode」で変換しているように思えるが、実際はそうではなく、HのCMapファイルの内容は正しく解釈されているようだ2
    • dvipdfmxではTTFの場合(CIDアクセス不可)に「CMapでCIDに変換した後、ToUnicodeマップ(Adobe-Japan1-UCS2)でさらにUnicodeに変換する」という処理が行われるが、dvisvgmはこれをCIDアクセス可能なCFFの時にも適用しているように思える。
    • 元がUnicodeである場合はこれと正反対で、TTFとCFFのどちらの場合も「さらにUnicodeに変換」を適用していないと思われる。このため、CFFでは正しい字形が選ばれるが、TTFでは文字化けになってしまう。

まとめ


  1. 具体的に使用したのは「TTF」が「IPA明朝」(ipam.ttf)、「CFF」が「原ノ味明朝 Bold」(HaranoAjiMincho-Bold.otf)。
  2. 「CMapファイルの内容をわざと改変する」等の方法で確認した。

LuaTeXで“relax化しないcsname”をもっとつくる件

前回の記事で「\relaxせずに\csnameする」クイズについて、Luaコードを利用して実装する正解例を示した。

ところで、例のクイズの補足説明を改めて見ると、少し気になる内容がある。

※もちろんLuaコードを使ってよい。
※ただしLuaコードを使わない回答ができればもっとよい🙃

実はこの問題はLuaコードを使わない(ただしLuaTeXの機能は利用する)解決法が存在する。本記事ではその方法を解説してみる。

必要な前提知識

  • フツーのTeX言語🤮の知識。

TeXの常識、LuaTeXの非常識

前回の問題の考察において、「“relax”化が起こった場合に代入操作で再度未定義の状態に戻す」という案を考えた。

\csnameによる“relax化”が避けられないなら、その後でもう一度その制御綴に未定義を代入すればよい。しかし今回の問題では完全展開可能で実装する必要があるため、代入操作は使えない。

完全展開可能の実装で代入は使えないというのは、TeX言語の常識である。……といいたいところだが、実はLuaTeXでは違う😲😲😲 LuaTeX拡張には「展開操作で代入を行うためのプリミティブ」が用意されている1のである。

  • \immediateassignment‹代入文›: [展開可能] 空トークン列に展開されるが、その副作用として‹代入文›で指示された代入が行われる。

ただしここで指示できる代入文には制限があるようである。例えばボックス代入は許されていない。

代入が可能であるのなら、以下のような素直な手順で要件が実現できるはずである。

  • まず\ifcsnameで当該制御綴が定義済かを判定する。
  • 判定が済んだ2ので普通に\csnameで制御綴を生成する。判定結果が真(定義済)だった場合はここで終了。
  • 判定結果が偽(未定義)だった場合、既に制御綴が手元にあるので、\immediateassignment付きの代入で制御綴を未定義に戻してから制御綴を置く。

とりあえず書いてみる

先の方針を素直にコードで表すと以下のようになる。

\def\myMakeCS#1{%
  % まず判定する
  \ifcsname#1\endcsname
    % 定義済の場合は簡単
    \csname#1\endcsname
  \else
    % 未定義の場合.
    % ここで`\csname`を1回展開して制御綴を作る(relax化発生)
    \expandafter\my@make@cs@undef\csname#1\endcsname
  \fi
}
\def\my@make@cs@undef#1{% #1は目的の制御綴
  % 制御綴を未定義に戻す
  \immediateassignment\let#1\my@undefined
  % 展開結果
  #1%
}

全体の動作だけみると、この実装で既に想定通りになっている。

% 完全展開可能性の確認のため \message 中で実行.
% \futurelet は展開不能なのでこれが最終結果になる.
\message{\myMakeCS{futurelet}}
%==>「\futurelet」と表示
% 未定義の制御綴の場合, (relax化せず)未定義エラーが出る.
*\message{\myMakeCS{duckduck}}
%==> \duckduck の箇所でエラー「! Undefined control sequence.」

しかしこの実装は要件を満たしていない。まず問題なのは「生成された制御綴よりも後方に\fiが残るせいで(完全展開可能ではあるが)先頭完全展開可能になっていない」ことである。仮に“後方の部分”を無視したとしても、「if文が真の場合と偽の場合で制御綴に到達するまでの展開回数が異なる」という問題が残っている。

これらの問題を解決するために「展開の加速」という技法を利用することにする。展開の加速については以前に別の記事で紹介した。

ここでは\expandedを利用して完全展開可能な(しかし先頭完全展開可能とは限らない)マクロを「2回展開」に加速する手法3を用いる。これによって(マクロが常に2回展開になるので)先述の2つの問題が同時に解決できてしまう。

では早速マクロを修正してみる。\expandedによる加速を適用するのは簡単で、マクロの定義本体を\expanded{…}で囲うだけでよい。

\def\myMakeCS#1{%
  \expanded{% 加速する
    \ifcsname#1\endcsname
      \csname#1\endcsname
    \else
      \expandafter\my@make@cs@undef\csname#1\endcsname
    \fi
  }%
}

ただ今の要件には少し厄介な点があり、それは「制御綴が現れたらそこで展開を止める必要がある」ことである。これは\unexpandedプリミティブ4を使えば解決できる。例えば\my@make@cs@undefマクロの最後の#1(=所望の制御綴)はそれ以上展開してほしくないので\unexpandedで囲えばよい。

\def\my@make@cs@undef#1{% #1は目的の制御綴
  \immediateassignment\let#1\my@undefined
  % これが展開結果なので \unexpanded で展開を止める
  \unexpanded{#1}%
}

\myMakeCS中の\csname#1\endcsnameは、それを1回展開してそこで止まってほしい。これは\unexpanded\expandafterと組み合わせる5ことで解決できる。

      \unexpanded\expandafter{\csname#1\endcsname}%

これで要件通り(n=2)に動作するプログラムが完成したことになる🙂

正解例(Luaしないやつ)

最終的なプログラムは以下のようになった。

%% \myMakeCS{‹文字トークン列›}: 2回展開すると`‹文字トークン列›`を
% 名前とする制御綴になる.
\def\myMakeCS#1{%
  \expanded{%
    \ifcsname#1\endcsname
      \unexpanded\expandafter{\csname#1\endcsname}%
    \else
      \expandafter\my@make@cs@undef\csname#1\endcsname
    \fi
  }%
}
\def\my@make@cs@undef#1{% #1は制御綴
  % 制御綴を未定義に戻す
  \immediateassignment\let#1\my@undefined
  \unexpanded{#1}%
}

前回と同じ例を試してみる。こちらも期待通りに動いているようだ😌

\let\XA\expandafter
\XA\XA\XA\show\myMakeCS{noexpand} % 定義済の制御綴
%==>「\noexpand=\noexpand.」と表示
\XA\XA\XA\show\myMakeCS{duck?duck!} % 未定義の制御綴
%==>「\duck?duck!=undefined.」と表示
\XA\XA\XA\show\myMakeCS{"アレ\string\?} %引数は「"アレ\?」
%==>「\"アレ\?=undefined.」と表示

おまけ(Luaしない別のやつ)

この投稿をしたときに用意していたプログラムは以下のものだった。

\def\myMakeCS#1{%
  \romannumeral-`>\ifcsname#1\endcsname
    \expandafter\my@make@cs@a\expandafter\space
  \else \expandafter\my@make@cs@a\expandafter\my@make@cs@b
  \fi{#1}}
\def\my@make@cs@a#1#2{%
  \expandafter#1\csname#2\endcsname}
\def\my@make@cs@b#1{%
  \immediateassignment\let#1\@undefined
  \space#1}

このプログラムでは\expandedの代わりに\romannumeralトリックによる先頭完全展開を利用している6

まとめ

というわけで、キャンペーン🌸🍀期間内でも期間外でもとにかく皆さん、ドンドンTeX言語🤮しましょう!💁


  1. なお「LuaコードでTeXパラメタへの代入を行う」ことでも実質的に「展開操作で代入」を行える。しかし今回の問題については「制御綴に“未定義”を代入する」をLuaコード上で行う方法が自分には発見できなかった😢
  2. 判定が済む前に\csnameを使ってしまうと元々未定義だったのか\relaxだったのかが区別できなくなり失敗する。
  3. 参考記事にあるように「Luaしない」前提であれば加速は「2回展開」までが限界である。前回の正解例は実質的にLuaで実装されているので、参考記事の技法を使えば「1回展開」に加速することが可能である。
  4. \expanded{…}で完全展開しているトークン列の中に\unexpanded{‹何か›}があった場合、その展開結果は‹何か›になりそれ以上展開されない
  5. \unexpandedはグループを引数にとるプリミティブなので、中を1回展開したい場合に\unexpandedの前に\expandafterを付ける必要はない。
  6. \romannumeralトリックによる先頭完全展開を途中で止めたい場合は、トークン列の先頭にわざと空白トークンを生じさせればよい。

LuaTeXで“relax化しないcsname”をつくる件

先日、TeX言語GW特別キャンペーン🍀なので、例によってクイズを出してみた。

その結果、例によって誰も解かなかったので(ざんねん🙃)本記事では正解例を発表することにする。

必要な前提知識

  • フツーのTeX言語🤮の知識。
  • フツーにLuaTeXでLuaするための知識。
 

本記事で掲載するプログラムコードはplainまたはLaTeXの\makeatletterの状態を前提とする。

その問題

以下の仕様を満たす命令\myMakeCSをLuaTeXで実装せよ。
\myMakeCS{‹文字トークン列›}をn回展開すると引数の文字列を名前とする制御綴のトークンになるが、\csname~\endcsnameと異なり、当該の制御綴が元々未定義の場合に“relax化”が起こらない。」

※nは引数非依存の定数で好きに決めてよい。
※もちろんLuaコードを使ってよい。
※ただしLuaコードを使わない回答ができればもっとよい🙃

この問題で難しい点は「​“relax化”が起こらない」という条件である。文字トークン列から制御綴を生成するには普通は\csname~\endcsnameを使うが、これは“relax化”(当該の制御綴の意味が未定義だった場合に意味が\relaxプリミティブに変更される)が起こってしまい、もちろんLuaTeXでもその仕様は変わらない。

一方で「当該の制御綴が未定義であるか」の判定\ifcsnameを使って(“relax化”を起こさずに)行える。定義済なら普通に\csnameが使えるので、以下では未定義である場合の対処を考えてみる。

以前の記事で紹介した通り、LuaTeXには\begincsnameという\csnameの変種があり、これは「“relax化”を起こさない」ので一見使えそうであるが、未定義時に制御綴を作らずに空に展開されてしまうので、この問題では実は役に立たない。TeX言語で「制御綴の生成」をしたいなら結局\csnameを使うしかないのである。

少し視点を変えてみる。\csnameによる“relax化”が避けられないなら、その後でもう一度その制御綴に未定義を代入1すればよい。しかし今回の問題では完全展開可能で実装する必要があるため、代入操作は使えない。“relax化”は局所的定義であることを利用して「\csnameの展開をグループ内で行う」という手段も考えられる2が、「グループに出入りする」のは実行操作であるため、完全展開可能の実装ではこの手段も使えない。

その正解例(Luaするやつ)

TeX言語だけで考えると行き詰ってしまうが、しかしこの問題はLuaTeXが前提なので、Luaコードが使える。

Luaコードではtex.sprint()関数でTeXコードの文字列3をプログラム的に組み立てることができる。この性質を利用すれば\csnameを使わずに容易に制御綴を「生成」できてしまう。\myMakeCSの引数(#1)の文字列をLuaで受け取って、「その前に\を付けた文字列をtex.sprint()する」だけでよい。このアイデアを素直に実装すると以下のようになる。

% \myMakeCS{‹文字トークン列›}: 2回展開すると`‹文字トークン列›`を
% 名前とする制御綴になる.
\def\myMakeCS#1{%
  \directlua{
    tex.sprint("\string\\#1")
  }%
}

※1回展開で\directlua{…}になり、さらに展開するとLuaの実行結果になる。常に2回展開で所望の制御綴が得られるので、問題の要件を「n=2」で満たしたことになる。
\string\\を完全展開した結果はthe-文字列の\\なので、Lua文字列リテラルの中で\の文字を表す。

この素朴な実装は引数が「カテゴリコード11の文字」のみからなる場合には実際に問題の要件を満たす。

% \expandafterの3重連で"\mymakeCS"を2回展開する.
% ("\show\duckduck"になるはず.)
\expandafter\expandafter\expandafter\show\myMakeCS{duckduck}
%==>"\duckduck=undefined."と表示(期待通り)

そうなると残っている課題は「引数がカテゴリコード11以外の文字を含む」場合である。この場合は「引数の文字列の前に\を付けたもの」が制御綴として字句解釈されないので、このままでは動作しない。しかしtex.sprint()は「カテゴリコードを予め指定した状態でTeXコード文字列を出力する」ことができる。だから、「全ての文字のカテゴリコードが11である状態」を用意すればこの問題は解決する。(ただし先頭に付ける\はエスケープ文字として働く必要があるので、取りあえず\のカテゴリコードだけは0にする。)

「カテゴリコードの状態」は実際にはLuaTeXの「カテゴリコードテーブル」の機能を用いて管理する。本記事ではカテゴリコードテーブルについての解説は割愛するので、プログラムコード内のコメントを見て概略を把握してほしい(ざんねん🙃)

%% 事前準備
% ※カテゴリコードテーブルの番号は(一旦)固定値100にする
% カテゴリコードテーブルを生成(初期化)する
\initcatcodetable100
% カテゴリコードテーブルの設定はTeXでもできるが面倒なので
% Luaコードで実行する.
\directlua{
  % カテゴリコードを全て11に変更する
  for c = 0, 0x10FFFF do
    % カテゴリコードテーブル100の文字コードcのコードを11に設定
    tex.setcatcode('global', 100, c, 11)
  end
  % ただし'\'(U+005C)は0にする
  tex.setcatcode('global', 100, 0x5C, 0)
}

%% \myMakeCS{‹文字トークン列›}: 2回展開すると`‹文字トークン列›`を
% 名前とする制御綴になる.
\def\myMakeCS#1{%
  \directlua{
    % カテゴリコードテーブル100の状態でTeXコードを出力する
    tex.sprint(100, "\string\\#1")
  }%
}

これで「英字以外を含む名前」でも要件通りに動作するようになる。

\expandafter\expandafter\expandafter\show\myMakeCS{duck?duck!}
%==>"\duck?duck!=undefined."と表示(期待通り)

あとは、実用のコードで使用しても問題ないように調整を施す。

  • カテゴリコードテーブルの番号を固定値(100)にしていたのを\newcatcodetableによる動的確保に変える。
    \newcatcodetableで得られた番号をLuaコード中で参照するにはluatexbase.registernumber()関数を利用する。
    ※plainで\newcatcodetableluatexbase.registernumber()を使うにはltluatex.texというファイルを読み込む必要がある。
  • 引数文字列に\を含められるようにするため、エスケープ文字を\からU+FDD1(Not-a-Charの符号位置の1つ4)に変更する。
  • TeXコードの引数をLuaに渡すときに\luaescapestringを使う。

最終的なプログラムは以下のようになった。

% plainではltluatex.texを読み込む
\unless\ifdefined\newcatcodetable
  \input{ltluatex}
\fi

%% 事前準備
% \my@cc@table: 利用するカテゴリコードテーブルの番号
\newcatcodetable\my@cc@table
\initcatcodetable\my@cc@table
\directlua{
  local escape_code = 0xFDD1 % エスケープ用の文字
  local escape = utf8.char(escape_code)
  %% 所望のカテゴリコードテーブルを用意する
  local cctable = luatexbase.registernumber("my@cc@table")
  % カテゴリコードを全て11に変更する
  for c = 0, 0x10FFFF do
    tex.setcatcode('global', cctable, c, 11)
  end
  % ただしエスケープ用の文字は0にする
  tex.setcatcode('global', cctable, escape_code, 0)
  %% '\myMakeCS'のLua部分の実装
  function my_make_cs(name)
    tex.sprint(cctable, escape..name)
  end
}

%% \myMakeCS{‹文字トークン列›}: 2回展開すると`‹文字トークン列›`を
% 名前とする制御綴になる.
\def\myMakeCS#1{%
  \directlua{my_make_cs("\luaescapestring{#1}")}%
}

どうやら期待通りに動作しているようだ😍

\let\XA\expandafter
\XA\XA\XA\show\myMakeCS{noexpand} % 定義済の制御綴
%==>「\noexpand=\noexpand.」と表示
\XA\XA\XA\show\myMakeCS{duck?duck!} % 未定義の制御綴
%==>「\duck?duck!=undefined.」と表示
\XA\XA\XA\show\myMakeCS{"アレ\string\?} %引数は「"アレ\?」
%==>「\"アレ\?=undefined.」と表示

まとめ

というわけで、TeX言語GW特別キャンペーン☘️も今日でおしまいです。皆さんのTeX言語活動の進捗が満足のいくものであったことを願います😃


  1. つまり、制御綴が\fooなら\let\foo\@undefinedとすればよい。
  2. この技法は実用のプログラムでも時々使われている。
  3. TeX言語ではトークン列を組み替える処理しかできないので、例えばfooという文字トークン列を組み替えて\fooという制御綴を作り出すことはできない。それをするためには\csnameという「機能」が必要なのである。
  4. つまり「まさかNot-a-Charの文字は使わないよね😊」と仮定している。

完全解説! TeX言語のプリミティブな制御記号全部まとめ

せっかくの「TeX言語GW特別キャンペーン🍀」であるが、誰も「完全解説! TeX言語のプリミティブな制御記号全部まとめ」の記事を書かないので、仕方がなく自分で書くことにした(ざんねん🙃)

必要な前提知識

  • TeX言語の「プリミティブ」「制御綴」とは何かを知っている。

「プリミティブな制御記号」の定義

制御記号(control symbol)というのは「英字(カテゴリコード11)以外の文字1つからなる名前1の制御綴」のことであり、従って「プリミティブな制御記号」とは「制御記号で表されるプリミティブ」を指すことになるだろうが、そもそもカテゴリコードの設定や制御綴の意味は可変であるためこれをそのまま定義とするのは都合が悪い。INIモードの初期状態を前提にするのも一つの手であるが、LuaTeXのように「初期状態で制御綴に割り当てられていないプリミティブがある」ようなエンジンもある。

そこで、まず条件を緩めて文字の種類は問わないことにする。さらに「制御綴名」の代わりに「プリミティブ名」を考えることにする。「プリミティブ名」というのは「意味がプリミティブであるトークンに\meaningを適用して得られる文字列2」のことであり、例えば「\expandafterプリミティブ」などと説明する場合の「expandafter」がプリミティブ名である。この名前はプリミティブごとに決まっていて不変である。そこで、「プリミティブな制御記号」の定義を「プリミティブ名が1文字のプリミティブ」と定めることにしよう。

「プリミティブな制御記号」の一覧

というわけで、TeXの「名前が1文字のプリミティブ」を全て列挙すると以下のようになる。

プリミティブ説明
\␣(U+0020)通常の欧文空白
\-任意(discretionary)のハイフン
\/イタリック補正

この結果は(メジャーエンジンの範囲3では)どのエンジンでも変わらない。ここで現れる文字(空白、- /)はどれも(一般的なフォーマットで)カテゴリコードが11以外であるので、結果的に「名前が1文字」のプリミティブは制御記号的なものしかないことがわかる。

「プリミティブな制御記号」の解説

本記事の目的は恐らく「一覧がどうなるか」であったはずで、そうすると記事はもう完成していることになるが、せっかくなので各プリミティブについて「具体的な動作の詳細」を解説をしてみる。

なおこの3つのプリミティブは「そのまま4LaTeXカーネルの命令になっている」くらい有名なので、基本的な使い方については周知であるとする(ざんねん🙃)

\␣:通常の欧文空白

名前がASCII空白1文字であるプリミティブ5は「段落に欧文空白としての水平空きを出力する」機能を持つ。

  • 現在が垂直モードである場合は水平モードに移行する6
  • 現在の「欧文空白グルー値」を以下の要領で取得する:
    • グルー値パラメタ\spaceskipの値がゼロ(0pt)である場合は現在のフォントのfontdimenを調べる。「‹fontdimen2の値› plus ‹fontdimen3の値› minus ‹fontdimen4の値›」が「欧文空白グルー値」である。
    • \spaceskipが非ゼロの場合はその値が「欧文空白グルー値」である。
  • 大きさが「欧文空白グルー値」の水平空きを出力する。

\-:任意のハイフン

\-は「普段は何も出ないが、行分割時には現在のhyphencharの文字を行末に出す」ようなdiscretionaryを出力する。

  • \hyphenchar\fontの値を取得し「hyphencharの値」とする。
  • \discretionary{\char‹hyphencharの値›}{}{}を行う。
    • ただし「hyphencharの値」が有効な文字コードの範囲7の外であれば第1引数を空にする。

ただしLuaTeXでは手順が少し異なる。

  • 現在の言語の\prehyphencharの値を取得し「prehyphencharの値」とする。
  • 現在の言語の\posthyphencharの値を取得し「posthyphencharの値」とする。
  • \discretionary{\char‹prehyphencharの値›}{\char‹posthyphencharの値›}{}を行う。
    • ただし「prehyphencharの値」が有効な文字コードの範囲外であれば第1引数を空にし、「posthyphencharの値」が有効な文字コードの範囲外であれば第2引数を空にする。

\/:イタリック補正

\/は直前の文字に対するイタリック補正の水平カーンを出力する。

  • 垂直モードではエラーになる。
  • 数式モードでは常にゼロの水平カーンを出力する。
    ※数式モードでは文字のイタリック補正の分のカーンが原則常に出力される8のでそれ以上空ける必要がない。
  • 水平モードの場合は実際にイタリック補正のカーンを出力する:
    • 直前に出力したものが文字でない場合は何もせず終了。
    • 直前に出力した(フォントの)文字のイタリック補正の値を取得する。
      ※TFMに記録されている9
    • 取得した値の水平カーンをイタリック補正として出力する。

まとめ

というわけで皆さん、もっともっとTeX言語🤮の記事を書きましょう!💁


  1. 制御綴の「名前」にはエスケープ文字は含めない。
  2. ここで得られる文字列自体は\expandafterのような制御綴の形の文字列であり、正確にはここからエスケープ文字を除いたものをプリミティブ名とする。なお、少なくとも元祖TeXではINIモードの初期状態において「全てのプリミティブについて、それと同名の制御綴にプリミティブが紐づけられている」(例えば\expandafter制御綴の意味はexpandafterプリミティブである)が成立している。
  3. 元祖TeX、pdfTeX、XeTeX、LuaTeX、(e-)(u)pTeX。
  4. \-についてはLaTeXカーネルで再定義が行われていて少し挙動が異なる。
  5. ちなみに「名前がASCII空白1文字である制御綴」のことは「制御空白(control space)」と呼ばれる。
  6. ちなみに「空白トークンを実行した」場合は、現在が水平モード以外(垂直モードまたは数式モード)である場合は何も起こらない。
  7. LuaTeXでは1~0x10FFFF(0は無効扱い)、XeTeXでは0~0x10FFFF、イマドキのe-upTeXでは0~0x2E7F、それ以外は0~255。
  8. OpenTypeの数式フォントを利用する場合は「数式イタリック補正」のデータに基づいて自動的に(常に)補正が行われる。
  9. XeTeX・LuaTeXでOpenTypeを使う場合もそれに相当するデータが存在する。

徹底解説! TeXエンジンがTectonicであるかを判定する方法

Tectonic、スゴイですね!(多分🙃)

というわけで、本記事では「今動いているTeXエンジンがTectonicであるかを判別する方法」について解説します。

必要な前提知識

  • Tectonicが何か知っている。
  • そもそも「エンジンを判別する」とはどういうことか解っている。
  • 一般的な「エンジンを判別する」ための方針について知っている。

いきなり説明

エンジン判別の一番基本的な方法は「そのエンジンだけの特有の拡張プリミティブ」に注目して、それが定義済であるかを\ifxなり\ifdefinedなりで判断するというものです。

では「Tectonicだけの特有の拡張プリミティブ」はあるのかというと、現状ではただ1つだけ存在します。それは\TectonicCodaTokensというプリミティブです1。トークン列レジスタの一種のようですが、この機能自体はどうでもよくて、要するに「これが定義されていること」が重要です。\TectonicCodaTokensという制御綴が定義済かどうかを調べることで「Tectonicであるか」を判定できます。

% \ifx で判定する方法
\ifx\TectonicCodaTokens\@undefined\else
  % Tectonicである
\fi

% \ifdefined で判定する方法
\ifdefined\TectonicCodaTokens
  % Tectonicである
\fi

もちろん、もっと厳密に判定したいならプリミティブ判定をすることもできます。\ifpdfprimitiveを使うのが簡単ですが、ここで「TectonicはXeTeXの一種である」ことに注意が必要です。XeTeX(とLuaTeX)では\ifpdfprimitive\ifprimitiveに改名されているので、「まずXeTeXであることを判別してから\ifprimitiveを使う」ようにします。

% if-均衡の維持のため少し複雑...
\let\ifTemp\iffalse
\def\next{\let\ifTemp\ifprimitive}
\ifdefined\XeTeXversion \next \fi
\ifTemp\TectonicCodaTokens
  % Tectonicである
\fi

もちろん、“string-meaningテスト”を使うこともできます2

プリミティブ判定の方法については別の記事で詳しく解説しました。

まとめ

ジェミニ氏もチャッピ~氏もデタラメな答えを返してきたのでムシャクシャして書いた(めでたしめでたし🙂)


  1. \TectonicCodaTokensはTectonicのかなり初期の0.1.6版[2017-07-10]から存在するようです。
  2. \TectonicCodaTokens\meaningを適用して得られる文字列は普通に\TectonicCodaTokensです。

スゴイLuaTeXの新プリミティブ \begincsname を徹底紹介

LuaTeX、スゴイですね!😍

というわけで、本記事ではLuaTeXのスゴイ(多分🙃)拡張プリミティブである\begincsnameについて解説します。

必要な前提知識

  • \csnameが何か知っていること。

いきなり説明

  • \begincsname‹トークン列›\endcsname[展開可能]: 「\csname‹トークン列›\endcsname」と同様に、トークン列を完全展開して得られる文字トークン列1を名前とする制御綴に展開される。ただしその制御綴の意味が未定義である場合は代わりに空に展開され、この場合に「\relax化」が発生しない2

つまり従来は

\ifcsname‹トークン列›\endcsname
  \csname‹トークン列›\endcsname
\fi

のように書かないといけなかった(しかもこれは先頭完全展開可能でない)のが\begincsnameを使うと一回展開で済む形になります。

まとめ

スゴイ(多分🙃)


  1. 完全展開した結果に制御綴が混じっている場合はエラーになります(\csnameと同様)。
  2. さらに「文字列プールの消費」も発生しません。この辺りの仕様は\ifcsnameと同じです。