上一篇:時間戳
這次會用到 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]
可在正文區域的左側和右側的留白區域居中放置 foo
和 bar
:
\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 模組。
下一篇:時間管理