《Lua in ConTeXt》09:時光機裏的 Lua 命名空間 碰釘子了 柳暗花明 新的時光機 結語

表格的製作,在 ConTeXt 裏需要很多命令,在 Lua 程序裏,只需要花括號和逗號。

例如,ConTeXt 裏的表格

\startxtable
  \startxrow
    \startxcell 1\stopxcell
    \startxcell 2\stopxcell
    \startxcell 3\stopxcell
  \stopxrow
\stopxtable

用 Lua 語言來表示同樣形式的表,只需寫成

{1, 2, 3}

用 Lua 表製作時光機,是個很誘人的想法。例如,寫一個 Lua 程序,將以下形式的 Lua 表

{{"成爲聖人", "\\no"},
 {"成爲狂人", "\\no"},
 {"隱居山野", "\\ok"}}

“翻譯”爲 ConTeXt 的表格

\startxtable[frame=off]
  \startxrow
      \startxcell[width=.0655\textwidth] \m{\cdot} \stopxcell
      \startxcell[width=0.8845\textwidth] 成爲聖人 \stopxcell
      \startxcell[width=.05\textwidth] \strut\no \stopxcell
  \stopxrow
  \startxrow
      \startxcell \m{\cdot} \stopxcell
      \startxcell 成爲狂人 \stopxcell
      \startxcell \strut\no \stopxcell
  \stopxrow
  \startxrow
      \startxcell \m{\cdot} \stopxcell
      \startxcell 隱居山野 \stopxcell
      \startxcell \strut\ok\stopxcell
  \stopxrow
\stopxtable

在物理學裏,這樣的程序稱爲槓桿。

命名空間

現在開始寫這個類似槓桿的程序。爲了避免我的 Lua 程序裏的一些元素(無非就是變量和函數)與 ConTeXt 裏的其他 Lua 程序的元素重名,有必要先確定我的 Lua 程序的命名空間。

命名空間 mingyi 本質上也是一個表,只不過它一開始是空表:

mingyi = {}

接下來,爲 mingyi 表增加一個元素,只不過這個元素是一個函數:

function mingyi.make_row(row, n)
    context([[\startxrow]])
    context([[\startxcell[width=.0655\textwidth]\m{\cdot}\stopxcell]])
    if n == 1 then
        context([[\startxcell[width=.8845\textwidth]%s\stopxcell]], row[1])
        context([[\startxcell[width=.05\textwidth]\strut %s\stopxcell]], row[2])
    else
        context([[\startxcell %s\stopxcell]], row[1])
        context([[\startxcell\strut %s\stopxcell]], row[2])
    end
    context([[\stopxrow]])
end

這個函數能做什麼,即使對 Lua 語言絲毫也不熟悉,但是結合上面給出的 ConTeXt 表格代碼,應當能猜出六七分。

代碼中的 [[...]] 是 Lua 的長字符串語法。至於 Lua 的短字符串語法,之前用過,是 "..." 形式。用長字符串語法的好處是,不需要對 TeX 命令的反斜線 \ 轉義。例如,\startxrow,若使用短字符串語法,需要寫爲 "\\startxrow"

下面,再爲 mingyi 這個表增加一個元素,依然是函數:

function mingyi.make_table(x)
    context([=[\startxtable[frame=off]]=]
    for i, v in ipairs(x) do
        mingyi.make_row(v, i)
    end
    context([[\stopxtable]])
end

其中,[=[...]=] 依然是 Lua 的長字串語法,與 [[...]] 等價。Lua 語法允許在兩個雙括號之間插入一個或多個 =,從而避免與字符串裏出現的 ]] 衝突。

碰釘子了

mingyi.make_table 接受的參數 x 應當是類似於下面的 Lua 表:

{{"成爲聖人", [[\no]]},
 {"成爲狂人", [[\no]]},
 {"隱居山野", [[\ok]]}}

在 ConTeXt 源文件裏,需要設法將該 Lua 表作爲參數,調用 mingyi.make_table 函數。試着定義一個宏:

\def\timemachine#1{\ctxlua{mingyi.make_table({#1})}}

然後通過 \timemachine 將 Lua 表傳給 mingyi.make_table 函數,例如

\timemachine{
  {"成爲聖人", [[\no]]},
  {"成爲狂人", [[\no]]},
  {"隱居山野", [[\ok]]}
}

可惜,不行。TeX 編譯器報錯:

...mtx/tex/texmf-context/tex/context/base/mkxl/buff-ini.lmt:495: invalid value (nil) at index 14 in table for 'concat'
... ... ...

調試了很久,最後發現,唯有放棄讓 mingyi.make_table 替我生成 \startxtable\stopxtable 語句:

function mingyi.make_table(x)
    for i, v in ipairs(x) do
        mingyi.make_row(v, i)
    end
end

然後在 ConTeXt 源文件裏像下面這樣調用 \timemachine

\startxtable[frame=off]
\timemachine{
  {"成爲聖人", [[\no]]},
  {"成爲狂人", [[\no]]},
  {"隱居山野", [[\ok]]}
}
\stopxtable

不要問我爲什麼……我也很想知道爲什麼。

柳暗花明

學會了使用一把錘子,以爲全天下的釘子都可以用這把錘子來敲。事實上,雖然

context([=[\startxtable[frame=off]]=]
... ... ...
context([[\stopxtable]])

行不通,但是 ConTeXt 裏的所有定義的宏,也許皆可以在 Lua 裏作爲函數調用,亦即

context.startxtable({frame = "off"})
... ... ...
context.stopxtable()

行得通。

同理,\blank\startxrow ...\stopxrow\startcell ... \stopcell……皆有 Lua 函數形式。

於是,我寫了以下 Lua 代碼:

mingyi = {}

local ctx = context
local dim = number.todimen
local w = tex.dimen.textwidth
local w1, w2, w3 = 0.0655 * w, nil, 0.05 * w; w2 = w - (w1 + w3)

function mingyi.make_row(row, n)
    ctx.startxrow()
    ctx.startxcell{width = dim(w1)}; ctx([[\m{\cdot}]]); ctx.stopxcell()
    if n == 1 then
        ctx.startxcell{width = dim(w2)}; ctx(row[1]); ctx.stopxcell()
        ctx.startxcell{width = dim(w3)}; ctx([[\strut ]] .. row[2]); ctx.stopxcell()
    else
        ctx.startxcell(); ctx(row[1]); ctx.stopxcell()
        ctx.startxcell(); ctx([[\strut ]] .. row[2]); ctx.stopxcell()        
    end
    ctx.stopxrow()
end

function mingyi.make_table(x)
    context.blank(halfline)
    context.startxtable{frame = "off"}
    for i, v in ipairs(x) do
        mingyi.make_row(v, i)
    end
    context.stopxtable()
    context.blank(halfline)
end

上述代碼裏,我用了 Lua 語言的一些特性。例如,將 ConTeXt 定義的一些 Lua 表保存到局部變量裏:

local ctx = context
local dim = number.todimen

這樣做,可以讓調用表裏的函數或變量的代碼變得更簡短,例如:

ctx.startxcell{width = dim(w2)}; ctx(row[1]); ctx.stopxcell()

此外,利用了 Lua 的語法糖:如果函數的參數是一個表,那麼它的 () 可省略,例如

ctx.startxcell{width = dim(w2)}

它與

ctx.startxcell({width = dim(w2)})

等價。

新的時光機

如果我不說,誰知道這是 Lua 生成的時光機呢?

下面,我給出 card.tex 的全部代碼:

\environment card-env
\starttext
\timestamp{2021 年 05 月 02 日}

\timemachine{
  {"洗上個冬天穿過的衣服", [[\ok]]},
  {"剃去在摩托車頭盔裏無限煩惱的頭髮", [[\ok]]},
  {"製造 Lua 時光機", [[\ok]]},
  {"去南河邊散步遛娃", [[\m{\cdots}]]}}

\stoptext

上文所寫的 Lua 代碼以及 \timemachine 宏的定義,我放在了 card-env.tex。以下是 card-env.tex 的全部內容:

\definepapersize[card][width=85.6mm,height=53.98mm]
\setuppapersize[card]
\setuplayout
  [backspace=.1\paperwidth,
    width=.8\paperwidth,
    topspace=.015\paperheight,
    height=.97\paperheight,
    leftmargin=.666\backspace,
    rightmargin=.666\cutspace,
    headerdistance=.025\makeupheight,
    footerdistance=.025\makeupheight,
    textheight=.95\makeupheight]
 
\setuppagenumbering[location=]
\setupfootertexts[margin][][\hfill\pagenumber\hfill]

\setuphead[title][align=middle]

\def\timestamp#1{%
  \setuptexttexts[margin]
                 []
                 [\hfill{\rotate[rotation=270]{#1}}\hfill]
}

% 段落
\setupindenting[first,always,2em]
\setupinterlinespace[line=1.5em]

\def\cangjie#1{%
  \lower.2ex\hbox{\externalfigure[#1][width=\bodyfontsize]}}
\def\ok{\cangjie{ok}}
\def\no{\cangjie{no}}
\def\mask{\cangjie{mask.png}}

\startluacode
mingyi = {}

local ctx = context
local dim = number.todimen
local w = tex.dimen.textwidth
local w1, w2, w3 = 0.0655 * w, nil, 0.05 * w; w2 = w - (w1 + w3)

function mingyi.make_row(row, n)
    ctx.startxrow()
    ctx.startxcell{width = dim(w1)}; ctx([[\m{\cdot}]]); ctx.stopxcell()
    if n == 1 then
        ctx.startxcell{width = dim(w2)}; ctx(row[1]); ctx.stopxcell()
        ctx.startxcell{width = dim(w3)}; ctx([[\strut ]] .. row[2]); ctx.stopxcell()
    else
        ctx.startxcell(); ctx(row[1]); ctx.stopxcell()
        ctx.startxcell(); ctx([[\strut ]] .. row[2]); ctx.stopxcell()        
    end
    ctx.stopxrow()
end

function mingyi.make_table(x)
    context.blank(halfline)
    context.startxtable{frame = "off"}
    for i, v in ipairs(x) do
        mingyi.make_row(v, i)
    end
    context.stopxtable()
    context.blank(halfline)
end
\stopluacode
\def\timemachine#1{\ctxlua{mingyi.make_table({#1})}}

\definefontfamily[myfont][serif][sourcehanserifcn]
\definefontfamily[myfont][math][xits]
\setscript[hanzi]
\setupbodyfont[myfont,7pt]

結語

似乎快要學會怎樣在 ConTeXt 裏擺弄 Lua 了。

我覺得將卡片的時間戳的位置靠頁眉放置,再將頁碼的位置提升到它上方的留白區域的底部,卡片的版面會更美觀一些。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章