《Lua-in-ConTeXt》06:偽豎排

garfileo發表於2023-02-02
上一篇:時間戳

這次會用到 Lua,我保證。

頁碼

在我的淺薄的審美範疇裡,card.pdf 的頁碼沒有在頁尾(footer)的留白(Margin)區域居中,甚為不美。然而,card-env.tex 裡的

\setuppagenumbering[location={footer,inmargin}]

對此卻無能為力。既然如此,還要它作甚,破而後立吧。先將上述程式碼修改為

\setuppagenumbering[location=] % 關閉頁碼

然後使用 \setupfootertexts 在頁尾的留白區域安放頁碼:

\setupfootertexts[margin][][\hfill 1\hfill]

當然不可能所有頁面的頁碼都為 1,所以應當使用 \pagenumber 獲得每一頁對應的頁碼:

\setupfootertexts[margin][][\hfill\pagenumber\hfill]

現在將以下兩行程式碼新增到 card-env.tex 裡:

\setuppagenumbering[location=]
\setupfootertexts[margin][][\hfill\pagenumber\hfill]

時間戳

我想讓時間戳出現在版心(或正文區域)右側的留白區域,\setuptexttexts 可成就此事:

\setuptexttexts[margin][foo][bar]

可在正文區域的左側和右側的留白區域居中放置 foobar

\setuptexttexts[margin][\hfill foo\hfill][\hfill bar\hfill]

去掉 foo,將 bar 換成時間戳:

\setuptexttexts
  [margin]
  [][\hfill 2023 年 01 月 26 日 凌晨 4 時 44 分\hfill]

結果是時間戳文字大部分出界了。這在預料之中,留白區域太窄,時間戳太長。

使用 \rotate 可以根據指定角度逆時針旋轉文字,

\setuptexttexts
  [margin]
  [][\hfill\rotate[rotation=270]{2023 年 01 月 26 日 凌晨 4 時 44 分}\hfill]

在生成 PDF 檔案的過程中,上述程式碼會導致 context 命令報錯:

tex error       > tex error on line 1 in file ./card.tex: Argument of \rotate has an extra }

context 命令(嚴肅地說是 TeX 引擎)無法理解我在 \setuptexttexts 裡傳入的資訊是什麼,反而認為我傳入的是錯誤的資訊。可以用 {} 構造一個編組(Group),將 \rotate[...]{...} 語句囊括於其中,從而讓 context 命令認為傳入 \setuptexttexts 的是一段挺正常的文字:

\setuptexttexts
  [margin]
  [][\hfill{\rotate[rotation=270]{2023 年 01 月 26 日 凌晨 4 時 44 分}}\hfill]

現在已將時間戳完整地顯現於版心右側的留白區域,但其中每個字是躺著的,需要設法讓其中的漢字站立起來。

在進行下文之前, 需要給出上述最終排版結果對應的 card-env.tex 和 card.tex。

card.tex:

\environment card-env
\setuptexttexts
  [margin]
  [][\hfill{\rotate[rotation=270]{2023 年 01 月 26 日 凌晨 4 時 44 分}}\hfill]
\showframe
  
\starttext
看版心右側 $\rightarrow$
\stoptext

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]

% 字型
\definefontfamily[myfont][serif][sourcehanserifcn]
\setscript[hanzi]
\setupbodyfont[myfont,7pt]

% 頁碼
\setuppagenumbering[location=]
\setupfootertexts[margin][][\hfill\pagenumber\hfill]

% 標題
\setuphead[title][align=middle]

TeX 宏

接下來,焦點是 card.tex 檔案中的

\setuptexttexts
  [margin]
  []
  [\hfill{\rotate[rotation=270]{2023 年 01 月 26 日 凌晨 4 時 44 分}}\hfill]

可以定義一個宏,用於簡化 \setuptexttexts 語句。例如

\def\timestamp{\rotate[rotation=270]{2023 年 01 月 26 日 凌晨 4 時 44 分}}
\setuptexttexts
  [margin]
  [][\hfill\timestamp\hfill]

我試驗過了,排版結果依然與上一節最後給出的排版結果相同。\timestamp 是一個宏。它在 \stemptexttexts 語句裡出現的時候,可稱為它被呼叫了。宏呼叫的結果便是它的定義。

最簡單的宏定義,形式如下

\def\foo{...宏定義...}

宏又了定義之後,TeX 遇到 \foo,就會用它的定義替換它,這個過程稱為宏的展開。例如

\environment card-env
\starttext
\def\hello{漢字!} % 宏定義
\hello % 宏呼叫,\hello 會被 TeX 編譯器替換為「漢字!」
\stoptext

宏可以接受引數,例如:

\def\timestamp#1{\rotate[rotation=270]{#1}}

其中 #1 表示 \timestamp 的第一個引數。帶引數的宏,用法通常是

\宏{引數}

例如,

\timestamp{2023 年 01 月 26 日 凌晨 4 時 44 分}

展開結果為:

\rotate[rotation=270]{2023 年 01 月 26 日 凌晨 4 時 44 分}

僅需要知道這些知識,便可進入 Lua 的世界。

Hello,Lua!

我要用 Lua 語言對 \timestamp 所接受的引數裡的每個漢字逆時針旋轉 90 度角。對於 Lua 語言,完成此事的關鍵在於遍歷一個字串裡的每個字元,稍微有些難度的是,這個字串裡含有漢字,這需要 Lua 支援文字的 UTF-8 編碼。不需要解釋太多,漢字雖然在計算機軟體技術裡也是疼痛了很久,但現在是 UTF-8 的時代。

假設有 Lua 字串變數 x

x = "我喜歡漢字"

Lua 語言已經不需要我們再為它做什麼額外的工作,它能夠理解 UTF-8。可遍歷 x 的每個字元的程式碼如下:

for _, c in utf8.codes(x) do
    print(utf8.char(c))
end

變數 c 的值是 Unicode 碼位(codepoint),需要使用 Lua 語言的 utf8 庫提供的 utf8.char 函式將其轉換為 UTF-8 編碼,然後方能被 print 之類的函式視為字串而輸出至程式外部——終端或文字檔案。 之所以從 UTF-8 編碼的字串裡獲得 Unicode 碼位,再將 Unicode 碼位轉化為 UTF-8 編碼,字串裡每個字元的編碼長度並不固定,先將字串轉化為固定長度的 Unicode 碼位序列,對字串的解析會更方便。

倘若系統裡並未安裝 Lua 直譯器,沒有關係,因為 ConTeXt 的 TeX 直譯器裡內嵌了 Lua 直譯器,因此可將上述 Lua 程式碼存入 .lua 檔案,例如 foo.lua,然後在在終端執行以下命令:

$ context --noconsole foo.lua

可得到以下輸出:

resolvers  ... ... ...
我
喜
歡
漢
字
system          | total runtime: 0.489 seconds of 0.539 seconds

雖然 context 命令輸出了很多它覺得有必要輸出的資訊,但是也輸出了我想看到的資訊。

要旋轉字串裡的每個字元,只需對上述的字串遍歷程式碼略作修改,例如

for _, c in utf8.codes(x) do
    print(string.format("\\rotate[rotation=90]{%s}", utf8.char(c)))
end

可在終端輸出

\rotate[rotation=90]{我}
\rotate[rotation=90]{喜}
\rotate[rotation=90]{歡}
\rotate[rotation=90]{漢}
\rotate[rotation=90]{字}

string.format 是 Lua 的字串格式化函式,在上述程式碼裡,它可將 utf8.char(c) 生成的漢字資訊作為字串嵌入

"\\rotate[rotation=90]{%s}"

中的 %s 位置,並取代 %s。這就是所謂的字串格式化。不使用字串格式化函式也能產生與上述程式碼等價的輸出,只需使用字串連線符號 ..,例如:

print("\\rotate[rotation=90]{" .. utf8.char(c) .. "}"))

至於上述程式碼裡,為何輸出 \\rotate... 需要用兩個反斜線符號 \,因為在 Lua 語言裡,\ 符號用於對一些特殊符號進行轉義,而 \ 自身也是此類特殊符號。

至此,關鍵技術已然解決,但如何將上述的 Lua 程式碼嵌入 ConTeXt 原始檔呢?可使用 \ctxlua

\ctxlua

\ctxlua 說,看我的!

\def\timestamp#1{\rotate[rotation=270]{\ctxlua{%
  for _, c in utf8.codes(x) do
    context("\\rotate[rotation=90]{%s}", utf8.char(c))
  end}}

將上一節給出的字串遍歷程式碼嵌入 \timestamp 的定義之後,變動的僅僅是將 Lua 函式 print 替換為函式 context,因為後者可將資訊輸出到 PDF 檔案裡,而前者僅能將資訊輸出到終端。此外,string.format 也不需要了,因為 context 函式自身支援字串格式化。

現在,對一下 card.tex 吧……

\environment card-env
\def\timestamp#1{\rotate[rotation=270]{\ctxlua{%
  x = "#1"
  for _, c in utf8.codes(x) do
    context("\\rotate[rotation=90]{%s}", utf8.char(c))
  end}}
\setuptexttexts
  [margin]
  []
  [\hfill\timestamp{我喜歡漢字}\hfill]

\starttext
向右看 $\rightarrow$
\stoptext

ConTeXt 的 TeX 編譯器(LuaTeX)在處理 card.tex 時,會報錯:

tex error       > tex error on line 13 in file ./card.tex: The file ended when scanning a definition.

這個錯誤讓我一整天徘徊不前,且百思不得其解。直到我去 ConTeXt 的 Wiki 上查閱了 \ctxlua 的文件:

https://wiki.contextgarden.ne...

文件裡說:

Use this command to quickly execute some Lua code. TeX expands the argument before Lua receives it. Advantage: you can pass the contents of macro parameters like #1 to Lua. Disadvantage: everything after a percent sign is ignored, and once the comments are processed out the linebreaks are stripped, too.

意思時,字串格式化裡的 % 被 TeX 編譯器誤以為是 TeX 原始檔裡的註釋符 %。此問題無解。避開方法是用字串連線符 .. 代替字串格式化:

\def\timestamp#1{\rotate[rotation=270]{\ctxlua{%
  x = "#1"
  for _, c in utf8.codes(x) do
    context("\\rotate[rotation=90]{" .. utf8.char(c) .. "}")
  end}}}

經過上述修正,card.tex 可透過 TeX 編譯器,順利轉化為 card.pdf。

站立起來的這幾個漢字,其間距過於緊密,可透過 TeX 命令 \kern 構造指定寬度的空白空間予以調解:

\def\timestamp#1{\rotate[rotation=270]{\ctxlua{%
  x = "#1"
  pad = "\\kern.125em"
  for _, c in utf8.codes(x) do
    context(pad .. "\\rotate[rotation=90]{" .. utf8.char(c) .. "}" .. pad)
  end}}}

\ctxlua 雖然能解決問題,但是讓 \timestamp 宏的定義甚為醜陋。倘若在 \startluacode ... \stopluacode 裡定義一個 rotate 函式,便可讓 \timestamp 的定義大幅簡化。

試試看,

\startluacode
my = {}
function my.rotate(x, a)
    pad = "\\kern.125em"
    for _, c in utf8.codes(x) do
        context("%s\\rotate[rotation=%d]{%s}%s", pad, a, utf8.char(c), pad)
    end
end
\stopluacode
\def\timestamp#1{\rotate[rotation=270]{\ctxlua{my.rotate("#1", 90)}}}

我試過了,沒問題,依然能生成上一節最後的排版結果。順便解釋一下,my 是我為 rotate 的名稱空間。使用名稱空間的好處是,可避免函式同名而造成一些誤會。

豎排的時間戳

好了,現在我們可以再對一下 card.tex 了。

\environment card-env
\startluacode
my = {}
function my.rotate(x, a)
    pad = "\\kern.125em"
    for _, c in utf8.codes(x) do
        context("%s\\rotate[rotation=%d]{%s}%s", pad, a, utf8.char(c), pad)
    end
end
\stopluacode
\def\timestamp#1{\rotate[rotation=270]{\ctxlua{my.rotate("#1", 90)}}}
\setuptexttexts
  [margin]
  []
  [\hfill{\timestamp{2023 年 01 月 26 日 凌晨 04 時 44 分}}\hfill]

\starttext
向右看 $\rightarrow$
\stoptext

結果很醜:

如果僅僅讓漢字豎立,其它符號保持躺平,結果會美觀一些。要實現該想法,需要對 \timestamp 的引數裡的漢字進行識別。下面定義一個函式,使之能夠基於漢字的 Unicode 碼區識別漢字:

function my.is_cjk_char(c)
    if c >= 0x3400 and c <= 0x4db5
        or c >= 0x4e00 and c <= 0x9fa5
        or c >= 0x9fa6 and c <= 0x9fbb
        or c >= 0xf900 and c <= 0xfa2d
        or c >= 0xfa30 and c <= 0xfa6a
        or c >= 0xfa70 and c <= 0xfad9
        or c >= 0x20000 and c <= 0x2a6d6
        or c >= 0x2f800 and c <= 0x2fa1d
        or c >= 0xff00 and c <= 0xffef
        or c >= 0x2e80 and c <= 0x2eff
        or c >= 0x3000 and c <= 0x303f
        or c >= 0x31c0 and c <= 0x31ef then
        return true;
    else
        return false;
    end
end

修改 my.rotate 函式:

function my.rotate(x, a)
    pad = "\\kern.125em"
    for _, c in utf8.codes(x) do
        if my.is_cjk_char(c) then
            context("%s{\\rotate[rotation=%d]{%s}}%s", pad, a, utf8.char(c), pad)
        else
            context(utf8.char(c))
        end
    end
end

結果如下:

該結果依然不盡人意,數字和漢字沒有豎直居中對齊。若要解決這個問題,需要整些暴力手段(希望以後能找到更為簡單的方法):

function my.rotate(x, a)
    pad = "\\kern.125em"
    for _, c in utf8.codes(x) do
        if my.is_cjk_char(c) then
            context("%s{\\rotate[rotation=%d]{%s}}%s", pad, a, utf8.char(c), pad)
        else
            context("{\\raise.5\\maxdepth\\hbox{%s}}", utf8.char(c))
        end
    end
end

TeX 宏\hbox 可將字元包圍在一個水平對齊的盒子裡。在 card.tex 檔案裡,\raise.5\maxdepth\hbox{...} 可將豎排的每個非 CJK 字元向右偏置 0.5 倍的 \maxdepth

TeX 宏 \raise\lower 可將字元向上(或向下)提升指定距離,但是將其與 ConTeXt 宏\rotate 宏在豎排時配合使用時,用途便會變為向左或向右微調字元位置。

\maxdepth 是 ConTeXt 排版時為一行文字定義的最大深度值,其含義可參考:

完整的 card.tex 內容如下:

\environment card-env
\startluacode
my = {}
function my.is_cjk_char(c)
    if c >= 0x3400 and c <= 0x4db5
        or c >= 0x4e00 and c <= 0x9fa5
        or c >= 0x9fa6 and c <= 0x9fbb
        or c >= 0xf900 and c <= 0xfa2d
        or c >= 0xfa30 and c <= 0xfa6a
        or c >= 0xfa70 and c <= 0xfad9
        or c >= 0x20000 and c <= 0x2a6d6
        or c >= 0x2f800 and c <= 0x2fa1d
        or c >= 0xff00 and c <= 0xffef
        or c >= 0x2e80 and c <= 0x2eff
        or c >= 0x3000 and c <= 0x303f
        or c >= 0x31c0 and c <= 0x31ef then
        return true;
    else
        return false;
    end
end
function my.rotate(x, a)
    pad = "\\kern.125em"
    for _, c in utf8.codes(x) do
        if my.is_cjk_char(c) then
            context("%s{\\rotate[rotation=%d]{%s}}%s", pad, a, utf8.char(c), pad)
        else
            context("{\\raise.5\\maxdepth\\hbox{%s}}", utf8.char(c))
        end
    end
end
\stopluacode
\def\timestamp#1{\rotate[rotation=270]{\ctxlua{my.rotate("#1", 90)}}}
\setuptexttexts
  [margin]
  [][\hfill{\timestamp{2023 年 01 月 26 日 凌晨 04 時 44 分}}\hfill]
\showframe

\starttext
向右看 $\rightarrow$
\stoptext

可將 \environment card-env\setuptexttexts 之間的內容移動到 card-env.tex 檔案。

結語

若需要用 ConTeXt 實現真正的豎排,推薦黃復雄的 vertical-typesetting 模組。

下一篇:時間管理

參考

相關文章