En toi Pythmeni tes TeXnopoleos
[電脳世界の奥底にて] |
現在のサイトは、10月末を以て終了となります。
http://zrbabbler.sp.land.to/lualatexlua1.html
Lua 内で TeX コードの実行を完了させる
~LuaTeX で call/cc しない件について~
tex.print()
等で書き出した TeX 命令を
実行させた状態で以降の処理に進むという
処理を簡単に記述できる枠組みを構築する。
この実現には Lua のコルーチン機能を利用している。
問題の説明
「思わず Lua で LaTeX してみた(仮)」の「事例 4: 文字列を指定の字幅で切り捨てる」節で 扱った例題をここでも例に用いる。 (従って、ixbase0 パッケージの使用を前提とする。)
\usepackage{ixbase0}
%% 普通の Lua コード(になるはず)
\begin{execluacodeblock}
function truncate_to_width(str)
local wdt = ixbase.length.ttwTarget.width
local l, u = 0, str:len()
while l < u do
local m = math.ceil((l + u) / 2)
-- str:sub(1,m) を組版しその幅を \ttwMeasured に代入する
tex.print([[\settowidth{\ttwMeasured}{]]..str:sub(1, m)..[[}%]])
【前行の TeX コードの実行が完了した後で以降の処理に進みたい】
local wdm = ixbase.length.ttwMeasured.width
if wdm <= wdt then l = m
else u = m - 1
end
end
tex.print(str:sub(1, l))
end
\end{execluacodeblock}
%% 簡単な LaTeX コード
\newlength{\ttwTarget}
\newlength{\ttwMeasured}
\newcommand*{\truncatetowidth}[2]{% {width}{string}
\setlength{\ttwTarget}{#1}%
\directlua{
truncate_to_width("\luaescapestring{\detokenize{#2}}")
}%
}
ここでの問題は、上のソースの【】内に書いたように、
「それまでに tex.print
で書き出した TeX コードを
実行させた状態で次の処理に進みたい」ということである。
通常は、現在実行中の Lua コードブロック
(\directlua
の引数)の実行が終わらないと
TeX コードの実行は始まらない。
tangle ライブラリ(仮称)
この問題を解決するためのライブラリ tangle(仮称)を作成してみた。 (今のところ、ixbase0 パッケージに一緒に含めている。) これは次のような関数からなる。
tangle.execute(func, ...)
:...
を引数にして関数func
を呼び出す。 そのfunc
の実行の中の任意の時点で (そこから呼ばれた関数の実行中も服務)、 次項以降の機能を使うことができる。 重要な制限として、tangle.execute()
の呼び出しは\directlua
中で最後に実行される文で なければならない。tangle.run_tex()
: 一旦 Lua の実行コンテキストを抜け出し、それまでにtex.print()
等で書き出された TeX コードを実行した後、 直ちにrun_tex()
の次の文から Lua 実行を再開する。実際には、この関数は
\directlua{tangle.resume()}
をtex.print()
で書き出してからtangle.suspend()
を実行するという処理をしている。 そのため、以前にtex.print
で書いた TeX コードの 内容によっては正しく動作しない可能性がある。tangle.suspend()
: Lua コードの実行を中断して TeX に戻る。 中断されたコードがある時に新たにtangle.execute()
を実行するとエラーになる。tangle.resume()
:tangle.suspend()
により中断していた Lua コードの 実行を次の文から再開させる。 呼び出しの箇所について、tangle.execute()
と同じ制限がある。
つまり、基本的には、途中で tangle.run_tex()
を使いたいような Lua の実行について、その大元の呼び出しである
\directlua
が例えば
\directlua{ local x = A(); B(); C(x) }
であったら、それを
\directlua{ tangle.execute(function() local x = A(); B(); C(x) end) }
に変えればよい。 もしこの呼び出しが偶然単一の関数呼び出し、例えば
\directlua{ some_func(a, b, c) }
であある場合は
\directlua{ tangle.execute(some_func, a, b, c) }
という形に置き換えることもできる。
tangle を用いた実装
tangle を使って実現したのが以下のものである。 上掲の「構想」の記述と比べるとコードの変更が最小限になっていることがわかる。
% このファイルの文字コードは UTF-8 (日本語コメントを通すため) \documentclass[a4paper]{article} \usepackage{ixbase0} %% 普通の Lua コード \begin{execluacodeblock} function truncate_to_width(str) local wdt = ixbase.length.ttwTarget.width local l, u = 0, str:len() while l < u do local m = math.ceil((l + u) / 2) -- str:sub(1,m) を組版しその幅を \ttwMeasured に代入する tex.print([[\settowidth{\ttwMeasured}{]]..str:sub(1, m)..[[}%]]) tangle.run_tex() -- TeX の実行を完了させる local wdm = ixbase.length.ttwMeasured.width if wdm <= wdt then l = m else u = m - 1 end end tex.print(str:sub(1, l)) end \end{execluacodeblock} %% 簡単な LaTeX コード \newlength{\ttwTarget} \newlength{\ttwMeasured} \newcommand*{\truncatetowidth}[2]{% {width}{string} \setlength{\ttwTarget}{#1}% \directlua{ % ここで tangle.execute() を呼び出す tangle.execute(truncate_to_width, "\luaescapestring{\detokenize{#2}}") }% } \begin{document} \begin{itemize} \item \truncatetowidth{10pt}{Hello world!} \item \truncatetowidth{20pt}{Hello world!} \item \truncatetowidth{30pt}{Hello world!} \item \truncatetowidth{40pt}{Hello world!} \item \truncatetowidth{50pt}{Hello world!} \item \truncatetowidth{60pt}{Hello world!} \item \truncatetowidth{70pt}{Hello world!} \end{itemize} \end{document}
(出力は元の版と同じである。)
tangle の中身
実際の ixbase0 を簡略化したもの。 (簡略化の際にバグを入れているかも…。)
tangle = { -- 子スレッドの状態定数 _DONE = 0, -- 正常終了 _TEX = 1, -- run_tex() で中断 _STOP = 2 -- suspend() で中断 -- _current_co : 現在有効な子スレッド } -- 子スレッドを生成・起動 function tangle.execute(func, ...) if tangle._current_co then error("tangle is going now") end -- 子スレッド生成 local args = { ... } local co = coroutine.create(function() return tangle._DONE, func(unpack(args)) } end) tangle._current_co = co -- 子スレッド起動 return tangle._check(coroutine.resume(co)) -- execute() は Lua 呼出(親スレッド)の末尾と決められて -- いるので子スレッドが終了/中断すると _check 処理の後 -- 実行は TeX に戻る end -- 子スレッドを再開 function tangle.resume() if not tangle._current_co then error("tangle is not going") end return tangle._check(coroutine.resume(tangle._current_co)) -- execute() と同様、実行は TeX に戻る end -- (子スレッドが呼ぶ) TeX を実行させる function tangle.run_tex() -- この引数が中断時の「戻り値」となる coroutine.yield(tangle._TEX) end -- (子スレッドが呼ぶ) 実行を中断する function tangle.suspend() coroutine.yield(tangle._STOP) end -- 子スレッド終了/中断時の後処理 function tangle._check(costat, tstat, ...) -- この引数は execute()/resume() の戻り値である。 -- 子スレッドの戻り値は tstat 以降で、costat は Lua で -- 付加された、スレッドの正常終了を示すステータス値。 if not costat then -- 子スレッドでエラー発生 tangle._current_co = nil error(tstat) -- この場合、第2引数はメッセージ elseif tstat == tangle._DONE then -- 正常終了 tangle._current_co = nil -- もう子スレッドはない elseif tstat == tangle._TEX then -- TeX 戻り -- resume() を呼び出しを書き込む tex.print("\\directlua{tangle.resume()}\\relax") end -- _STOP(中断)の場合は処理なし return ... -- これは正常終了時の func の戻り値 end