2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

TeX で文字列を1文字ずつ処理する

Last updated at Posted at 2021-12-21

概要

世の中には,TeXについて,ちょっと数式が綺麗に書けるだけのワープロソフトの下位互換だと思っている人がいます。たぶん大勢います。今回は,TeXのプログラミング言語的側面を活用してこんなことができるよ,という話です。
具体的には,与えられた文字列に対し,前から順番に文字を取り出して,なんらかの処理をしていく,という話をします。

こんな人におすすめ

  • TeX でおもしろい再帰的な処理をしたい
  • 一歩進んだマクロ定義の方法を学びたい

はじめに

TeXで文書を書いていると,こんな文字列を出力したくなることがあります。

converge1.png

あるいは,こんなfancyな文字列を出力したくなることがあります。

gradation1.png

これらは,もちろん,

{\HUGE\Huge ん[中略] \scriptsize\tiny}

のようにしたり,

{\color{red!100!blue}\color{red!90!blue}色[中略]\color{red!0!blue}}

のようにして表現してやることもできますが,正直面倒すぎます。面倒すぎてワープロでいいやってなっちゃいます。
これをなんとかしてTeXプログラミングで実現できないか,というのが今回のテーマです。

方針

実現したいことを抽象化してやると,文字列を前から順番に徐々に変化させていきたい,と言えます。もう少しちゃんと考えると,定義するマクロのおおまかな挙動としては,文字列(トークン列)を引数として受け取り,前から1文字(トークン)ずつ変化させていく,というようになるでしょう。

また,このときの変化のさせ方としては,(最終的に出力文字列についてなんらかのパラメータをいじることになるわけなので,)パラメータを 等差数列的に or 等比数列的に 変化させるのが簡単ですが,収束させたいことを考えると等比数列的変化一択ですね。1

実装としては,

1. 文字列から最初の1文字を取る
2. 最初の1文字に処理を施して出力する
3. パラメータを更新する
4. 残りの文字列について 1. に戻る

という方針になります。

このうち,2. 以外は(文字サイズの変更やフォントカラーの変更といった)文字に対する処理内容に依らず,共通しているので,まずはこの共通部分を実装し,各処理はラッパーで実装して,2. の部分はラッパー内で処理内容を変更してから共通部分を呼び出すことにしてやればよさそうです。

共通部分の実装

ここはかなり頭の体操です。

まず,入力は,「スペースと改行を含んでも良いが,_を含まない記号と英数字からなる文字列」とします。(_は終端記号として特別な役割を持たせます。)2

また,「文字に対する処理」は,雑に言えば不可視でないもの,つまり「スペースと改行以外」に対して行っていくものとします。

したがって処理としては,入力をまず改行(段落)ごとに分解し,さらにスペースで分解して,単語にまで落とし込み,その単語中の(スペースと改行を含まない)各文字について行う,ということになります。
ただし,処理内容を噛ませるときはスペースと改行を除きますが,実際の出力時にはこれらも(処理を噛ませない状態で)出力してやらないと,人間に読みにくくなってしまいます。というよりも,そもそも元々の目標を達成できません。

このことを踏まえて,実装していきます。

トップレベル \processstr

まずトップレベル\processstrでは,入力を展開してやります。(\process@str)
そして,終端記号と\parを後に補って,\@processstrに引き渡します。(ここで\parが必要なのは,#1が元々\parを含んでいない場合に,そのままでは\@processstrが syntax error となってしまうためです。)

\long\def\processstr#1{%
  \edef\process@str{#1}%
  \expandafter\@processstr\process@str_\par\@processstr@end% This '\par' is important.
}

段落ごとへの分割 \@processstr

続く\@processstrでは,\processstrから引き渡された入力を改行で分割して段落へと落とし込んでいきます。すなわち,\parを発見したら,その前まで(#1)を一つの段落だとみなして,次の処理へと進んでいくのです。また,改行を出力した上で,発見した\parよりも後の部分(#2)に関して,再度自分自身を適用することで,空列になるまで段落ごとの処理へと分割して渡し続けていきます。(どちらについても,前段同様に終端記号と適切なトークンを後に補います。)

\long\def\@processstr#1\par#2\@processstr@end{%
  \def\@templine{#1}%
  \if\@templine_\relax% input done
  \else\@@processstr#1_ \@@processstr@end\par\@processstr#2_\par\@processstr@end\fi%% These ' ' and '\par' are important.
}

もう少し詳しく説明します。
TeXにおける処理のルールにより,\@processstrは後続の\@processstr@endとの間に挟まれた文字列中に登場する**最初の\par**の前を#1,その後の全てを#2として扱います。このことを踏まえて,動作例を用いて挙動を理解していきましょう。

動作例で説明

たとえば,Lorem\par ipsum dolor\par sit ametが入力であるとき,つまり

\@processstr Lorem\par ipsum dolor\par sit amet_\par\@processstr@end

においては,Lorem#1で,ipsum dolor\par sit amet_\par#2です。
#1は次の処理へと進みますが,#2は再び段落への分割処理を行うので,次のように進みます。

\@processstr ipsum dolor\par sit amet_\par_\par\@processstr@end

このときはipsum dolor#1で,sit amet_\par_\par#2です。
またしても#1は次の処理へと進みますが,#2は再び段落への分割処理を行うので,次のように進みます。

\@processstr sit amet_\par_\par_\par\@processstr@end

すると,sit amet_#1で,_\par_\par#2となります。
この#2はやはり再び段落への分割処理を行うので,次のように進みます。

\@processstr _\par_\par_\par\@processstr@end

_#1で,_\par_\par#2です。
しかしこのとき,(人間が読めばすぐわかりますが)もはや入力文字列は存在していません。実際,#1には終端を意味する記号が来ているので,ここで段落への分割作業は全て終了したということになります。

単語ごとへの分割 \@@processstr

\@@processstrには,既に\parを含まない状態で\@processstrから入力が引き渡されます。この入力に対し,スペースで分割して単語へと落とし込んでいくのがこのステージです。
すなわち, を発見したら,その前まで(#1)を一つの単語だとみなして,次の処理へと進んでいくのです。また,スペースを出力した上で,発見した よりも後の部分(#2)に関して,再度自分自身を適用することで,空列になるまで単語ごとの処理へと分割して渡し続けていきます。(後者については,前段同様に終端記号と適切なトークンを後に補います。)

\long\def\@@processstr#1 #2\@@processstr@end{% This ' ' is important.
  \def\@tempword{#1}%
  \if\@tempword_\relax% current paragraph done
  \else\@@@processstr#1_\@@@processstr@end\@processstr@space\@@processstr#2_ \@@processstr@end\fi%% This ' ' is important.
}

ただし,スペースを出力する部分では,以下のように定義した\@processstr@spaceを使っています。

\newcommand\@processstr@space{ }

動作例で説明

前段の動作例の続きを見ていきます。\@processstrからは,中身としてはLorem_~ipsum dolor_~sit amet__~ がこの順に送られてきます。ここでは,ipsum dolor_~の場合を見てみることにしましょう。

ただし,以下,~は全てスペースで読み替えてください。

\@@processstr ipsum dolor_ \@@processstr@end

では,ipsum#1dolor_~#2です。
#1については次の処理へと進みますが,#2は再び単語への分割処理を行うので,次のように進みます。

\@@processstr dolor_ _ \@@processstr@end

このとき,dolor_#1_~#2です。
またしても#1については次の処理へと進みますが,#2は再び単語への分割処理を行うので,次のように進みます。

\@@processstr _ _ \@@processstr@end

_#1で,_~#2です。
しかしこのとき,(人間が読めばすぐわかりますが)もはや入力文字列は存在していません。実際,#1には終端を意味する記号が来ているので,ここで単語への分割作業は全て終了したということになります。

文字ごとへの分割 \@@@processstr

最後の段です。
\@@@processstrには,既に\par も含まない状態で\@@processstrから入力が引き渡されます。この入力に対し,前から1文字ずつ取り出して,所定の処理をしていくのがこのステージです。
すなわち,与えられた最初のトークンを#1とし,そこから\@@@processstr@endの前までの全ての部分を#2とします。#1については,指定された処理\@processstr@mainを施します。#2については,再度自分自身を適用することで,空列になるまで繰り返し最初の1文字を取り出す操作をしていくわけです。(後者については,前段までと同様に終端記号を後に補います。)

\long\def\@@@processstr#1#2\@@@processstr@end{%
  \def\@tempchar{#1}%
  \if\@tempchar_\relax% current word done
  \else\@processstr@main{#1}\@@@processstr#2_\@@@processstr@end\fi%
}

共通部分の実装全体

以上をまとめると,共通部分は次のようになります。

なお,未定義を避けるため,\@processstr@mainのデフォルトの処理として各文字の前に,を置くようにしています。
また,\changeprocessstrについては後述します。

\long\def\@processstr@main#1{,#1}% by default put comma before each character
\long\def\changeprocessstr#1{\long\def\@processstr@main##1{#1{##1}}}

\newcommand\@processstr@space{ }

\long\def\processstr#1{%
  \edef\process@str{#1}%
  \expandafter\@processstr\process@str_\par\@processstr@end%% This `\par' is important.
}
\long\def\@processstr#1\par#2\@processstr@end{%
  \def\@templine{#1}%
  \if\@templine_\relax% input done
  \else\@@processstr#1_ \@@processstr@end\par\@processstr#2_\par\@processstr@end\fi%% These ` ' and `\par' are important.
}
\long\def\@@processstr#1 #2\@@processstr@end{% This ` ' is important.
  \def\@tempword{#1}%
  \if\@tempword_\relax% current paragraph done
  \else\@@@processstr#1_\@@@processstr@end\@processstr@space\@@processstr#2_ \@@processstr@end\fi%% These ` ' are important.
}
\long\def\@@@processstr#1#2\@@@processstr@end{%
  \def\@tempchar{#1}%
  \if\@tempchar_\relax% current word done
  \else\@processstr@main{#1}\@@@processstr#2_\@@@processstr@end\fi%
}

##1について

\changeprocessstrの中に登場する#1\changeprocessstrの第一引数なのはよいでしょう。
しかし,\changeprocessstrの中に登場する##1は,\changeprocessstrの第一引数ではありません。これは,\changeprocessstrが呼び出された時に#1と展開されて,この際に定義される\@processstr@mainの第一引数となります。

たとえば,

\def\putperiod#1{.#1}% put period before each character
\changeprocessstr{\putperiod}

とすると,

\long\def\@processstr@main#1{\putperiod{#1}}

が実行されるということになります。

実行例

\def\putperiod#1{.#1}% put period before each character
\changeprocessstr{\putperiod}
\processstr{Lorem ipsum dolor sit amet, consectetuer adipiscing elit.}

.L.o.r.e.m .i.p.s.u.m .d.o.l.o.r .s.i.t .a.m.e.t., .c.o.n.s.e.c.t.e.t.u.e.r .a.d.i.p.i.s.c.i.n.g
.e.l.i.t..

応用例

最初に実現したかったことを,実装してみます。

収束していく文字列 \converge

だんだん小さくなっていく文字列は,次のようにして実装できます。
(入力文字列の先頭だけは特別扱いしています。)

%% \converge[scale]{string}
\newcommand\converge{\@ifnextchar[{\@converge}{\@converge[.95]}}
\long\def\@converge[#1]#2{%
  \begingroup%
  \@converge@takethefirst#2\@converge@ttf@end%
  \def\@converge@scale{1}%
  \def\@processstr@main##1{%
    \FPeval\@converge@scale{\@converge@scale*#1}%
    \scalebox{\@converge@scale}{##1}%
  }%
  \expandafter\processstr\expandafter{\@converge@rest}%
  \endgroup%
}
\long\def\@converge@takethefirst#1#2\@converge@ttf@end{#1\def\@converge@rest{#2}}

#1についてはオプションなので,指定したい時だけ書けばよいです。

実行例

\converge{だんだん小さくなっていく〜}\par
\converge{Lorem ipsum dolor sit amet, consectetuer adipiscing elit.}\par
% より早く収束させることもできる
\converge[.9]{Lorem ipsum dolor sit amet, consectetuer adipiscing elit.}\par
% 発散させることもできる
{\small\converge[1.02]{Lorem ipsum dolor sit amet, consectetuer adipiscing elit.}}


>
![converge2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2412224/146bb40c-a37f-e919-c713-65e9c1753a52.png)



## グラデーションで色が変化していく文字列 \gradation

グラデーションで色が変化していく文字列は,次のようにして実装できます。

```tex
%% \gradation[color changing speed](starting color)<ending color>{string}
%%   #1: color changing speed. the smaller, the faster. (default: 0.995)
%%   #2: starting color. (default: black)
%%   #3: ending color. (default: white)
%%   #4: input string.
\newcommand\gradation{\@ifnextchar[{\@gradation}{\@gradation[.995]}}
\def\@gradation[#1]{\@ifnextchar({\@@gradation[#1]}{\@@gradation[#1](black)}}
\def\@@gradation[#1](#2){\@ifnextchar<{\@@@gradation[#1](#2)}{\@@@gradation[#1](#2)<white>}}
\long\def\@@@gradation[#1](#2)<#3>#4{%
  \begingroup%
  \edef\@gradation@str{#4}%
  \color{#2}%
  \expandafter\@gradation@takethefirst\@gradation@str\@gradation@ttf@end%
  \def\@gradation@ratio{100}%
  \def\@processstr@main##1{%
    \FPeval\@gradation@ratio{\@gradation@ratio*#1}%
    {\color{#2!\@gradation@ratio!#3}##1}%
  }%
  \expandafter\processstr\expandafter{\@gradation@rest}%
  \endgroup%
}
\long\def\@gradation@takethefirst#1#2\@gradation@ttf@end{#1\def\@gradation@rest{#2}}

#1#2#3についてはオプションなので,いずれも指定したい時だけ書けばよいです。

実行例

きれいなグラデーションにするためには,適宜収束率の調整が必要です。
また,色のオプションを省略すると,黒から白へのグラデーションとなるので,だんだん消えかかっていく文字列が表現できます。

\gradation[.87](red)<blue>{赤色から青色に変わっていく}\par
\gradation[.97](red)<blue>{Lorem ipsum dolor sit amet, consectetuer adipiscing elit.}\par
\gradation[.99](yellow)<green>{Lorem ipsum dolor sit amet, consectetuer adipiscing elit.}\par
\gradation[.98]{Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \par Ut purus elit, vestibulum ut, placerat
ac, adipiscing vitae, felis.}

gradation2.png

まとめ

結局,TeX でちょっとした lexer を書くって話ですね。まぁそう言ってしまえれば簡単なのですが,字句解析ってなんやねんという人にもわかるように書こうとしてみました。
おもしろいと思えたら,いいねしてみてください。

  1. 等差数列ではパラメータの初期値から一定の値を引いていくようなことを考えるわけですが,最終的にパラメータが0を割ってしまいうるのでいろいろ面倒です。

  2. あまりちゃんと検討してはいないですが,日常的な文章に対しては問題なく動作するはずです。(_は数式モードで用いられる記号であり,一般に通常の文章中では登場しない。)

2
3
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
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?