《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 了。

我觉得将卡片的时间戳的位置靠页眉放置,再将页码的位置提升到它上方的留白区域的底部,卡片的版面会更美观一些。

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