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