跳转至

LuaTeX-LuaMetaTEX-CLD-TEX学习笔记

欢迎加入本人建的"LuaTeX ConTeXt 学习互助"群:431714622,互助学习LuaTeX ConTeXt LaTeX相关技术,以及通过Lua、Python实现格式化文本、数据的自动排版。

注意:为了避免本博客的jinja引擎误解析,已经在{+#之间,以及对应的}之前加入空格


LuaTeX#

本部分主要学习如下资料的笔记:

编译命令用:

lualatex test.tex
context --luatex test.mkiv
LuaTeX使用户可以从根基上控制TEX的行为,因此不只适用于学术排版、数学排版,而可作为任何排版系统的基础,比如数据排版speedata publisher几乎纯用LuaTeX,又如结合ConTeXt使用。

LuaTeX起源pdfTeX,但并未使用pdfTeX的代码,也不会随之更新。

LuaTeX引擎支持两种编程语言:传统的基于TeX的语言,比如LaTeX(LuaLaTeX)宏包,还有Lua脚本语言。

TEX主要关心的是生成段落和页。段落生成器(par builder)把字符列表分行,构成box,行间用glue、penalty等分隔。页面生成器(page builder)累积行,在适当的时机输出。TEX并不构造实际的页面,而是使用能够操纵box的基元(primitive,低级命令)。结果再推给一个文件(通常是pdf)。

TEX引擎是一个黑盒,LuaTEX打开了这个黑盒,让用户可以通过Lua访问TEX引擎约的320个基元和隐藏的部分(LuaTEX新加基元)。在处理过程中的几乎所有合理的节点上,LuaTEX引擎把钩子提供给Lua;也就是说,你可以重载TEX自身的基本行为。 当我们说“回调/回调函数”(callback)的时候,说的就是这些钩子。

lua结点表示#

node.types()返回类型总表(括号中的数字是类型id):

  1. hlist (0), vlist (1), rule (2), 1. ins (3), 1. mark (4), 1. adjust (5), boundary (6), disc (7), 1. whatsit (8), local_par (9), dir (10), 1. math (11), glue (12), kern(13), 1. penalty (14), unset (15), style (16), choice (17), noad (18), radical (19), fraction (20), accent (21), fence (22), math_char (23), sub_box (24), sub_mlist (25), math_text_char (26), delim (27), margin_kern (28), glyph (29), align_record (30), pseudo_file (31), pseudo_line(32), page_insert (33), split_insert (34), expr_stack (35), nested_list (36), span (37), attribute (38), glue_spec (39), attribute_list (40), temp (41), align_stack (42), movement_stack (43), if_stack (44), unhyphenated (45), hyphenated (46), delta (47), passive (48), shape (49).

结点视觉化lua库:

pgundlach/viznodelist.lua

主要文本结点#

开头的数字是类型id。

0 hlist

FIELD TYPE EXPLANATION
subtype number 0 = unknown, 1 = line, 2 = box, 3 = indent, 4 = alignment, 5 = cell,6 = equation, 7 = equationnumber, 8 = math, 9 = mathchar, 0 = hextensible, 11 = vextensible, 12 = hdelimiter, 13 = delimiter, 14 =overdelimiter, 15 = underdelimiter, 16 = numerator, 17 = enominator, 18 = limits, 19 = fraction, 20 = nucleus, 21 = sup, 2 = sub,23 = degree, 24 = scripts, 25 = over, 26 = under, 27 = ccent, 28 =radical
attr node 属性列表
width number 盒子的宽度
height number 盒子的高度
depth number 盒子的深度
shift number a displacement perpendicular to the character progression direction
glue_order number 数字范围[0, 4],表示胶的次序
glue_set number 计算后的胶的比率
glue_sign number 0 = 正常, 1 = 拉伸, 2 = 收缩
head/list node 本列表的第一个结点(两个名字等效)
dir string 盒子的方向,参看 8.2.15

1 vlist

与hlist相似。小类只有0,4,5。

3 rule

LuaTEX会使用界尺(rule)来存储可重用对象和图像,所以比tex的小类多;user结点不可见,可以通过回调截取。

FIELD TYPE EXPLANATION
subtype number 0 = normal, 1 = box, 2 = image, 3 = empty, 4 = user, 5 = over, 6 = under, 7 = fraction, 8 = radical, 9 = outline
attr node 属性列表
width number 界尺宽度,特殊值−1073741824用来处理胶的尺寸
height number 界尺高度 (可以是负值)
depth number 界尺深度 (可以是负值)
left number 左端移位 (宽度会减除它)
right number 右端移位 (宽度会减除它)
dir string 界尺方向,参看 8.2.15
index number 可用来引用的可选索引
transform number 私有值 (也用来指定轮廓宽度)

3 ins

与\insert基本命令相关。

FIELD TYPE EXPLANATION
subtype number 插入(insertion)类
attr node 属性列表
cost number 与本插入关联的惩罚(penalty)
height number 插入的及高度
depth number 插入的深度
head/list node 插入体的第一个结点

还有与关联的胶有关的字段:width, stretch, stretch_order, shrink and shrink_order. 都是数字。

4 mark

与\mark基本命令相关联。

FIELD TYPE EXPLANATION
subtype number 未使用
attr node 属性列表
class number mark类
mark table 表示token列表

5 adjust

与\vadjust基本命令相关联。

FIELD TYPE EXPLANATION
subtype number 0 = normal, 1 = pre
attr node 属性列表
head/list node 调整后的材料

7 disc

自由断字结点。与\discretionary and \-基本命令相关, 即字符-,也是断字机制也会生成这样的结点。

FIELD TYPE EXPLANATION
subtype number 0 = discretionary, 1 = explicit, 2 = automatic, 3 = regular, 4 = first, 5 = second
attr node 属性列表
pre node 指向断字前的文本
post node 指向断字后后文本
replace node 指向不断字的文本
penalty number 与断字相关联的惩罚,通常时 \hyphenpenalty 或 \exhyphenpenalty

11 math

属性结点表示数学公式(通常用$包裹)的边界。

12 glue

在传统的TEX中,skip(即glue)是唯一的复合值数据对象。TEX看到文本流中的空格时会插入skip;也可以通过\hskip和\vskip插入。表示一个skip的glue组件的结构叫glue_spec,有如下字段:

FIELD TYPE EXPLANATION
width number 水平或垂直占位
stretch number 额外的占位,或伸展量
stretch_order number 拉伸量的系数
shrink number 额外(负值)占位,或收缩量
shrink_order number 收缩量的系数

glue_spec(胶性)结点在注册器中存储一套胶值。

胶结点:

FIELD TYPE EXPLANATION
subtype number 0 = userskip, 1 = lineskip, 2 = baselineskip, 3 = parskip, 4 = abovedisplayskip, 5 = belowdisplayskip, 6 = abovedisplayshortskip, 7 = belowdisplayshortskip, 8 = leftskip, 9 = rightskip, 10 = topskip, 11 = splittopskip, 12 = tabskip, 13 = spaceskip, 14 = xspaceskip, 15 = parfillskip, 16 = mathskip, 17 = thinmuskip, 18 = medmuskip, 19 = thickmuskip, 98 = conditionalmathskip, 99 = muglue, 100 = leaders, 101 = cleaders, 102 = xleaders, 103 = gleaders
attr node 属性列表
leader node 指向起头的盒子或界尺

还有字段: width, stretch stretch_order, shrink, and shrink_order

水平胶和垂直胶都用width,这是TEX的传统。

通常的词语间空格也导致一个spaceskip小类(通常是0号的userskip)。

13 kern

由\kern命令、字体机制、数学机制生成的结点。

FIELD TYPE EXPLANATION
subtype number 0 = fontkern, 1 = userkern, 2 = accentkern, 3 = italiccorrection
attr node 属性列表
kern number 固定的水平或垂直提升量

14 penalty

由\penalty 命令生成。

FIELD TYPE EXPLANATION
subtype number 0 = userpenalty, 1 = linebreakpenalty, 2 = linepenalty, 3 = wordpenalty, 4 = finalpenalty, 5 = noadpenalty, 6 = beforedisplaypenalty, 7 = afterdisplaypenalty, 8 = equationnumberpenalty
attr node 属性列表中
penalty number 惩罚值

小类是非正式的,TEX并不使用它们。当你碰到linebreakpenalty时,你应该想到,它时club、widow(落单词)和其他相关惩罚的的累加。

29 glyph

字模结点,输入文本用。

FIELD TYPE EXPLANATION
subtype number 0=character, 1=ligature, 2=ghost, 3=left, 4=right
attr node 属性列表
char number 字符的字体中的索引
font number 字体标识
lang number 语言标识
left number 冻结的\lefthyphenmnin值
right number 冻结的\righthyphenmnin值
uchyph boolean 冻结的\uchyph值
components node 指向带子/连字符(ligature)组件
xoffset number 水平的虚拟占位
yoffset number 垂直的虚拟占位
width number 字符原始宽度(只读)
height number 字符原始高度(只读)
depth number 字符原始深度(只读)
expansion_factor number 要应用的扩展系数(段落构造时生成,供后端使用)
data number 给用户预留的通用数据空间

is_char函数用于检查结点是不是小类小于256的字符结点。 其变体is_glyph,不检查小类是不是小于256,返回字符值/nil和id。

uses_font函数输入一个结点和字体id,如果字符结点或断行结点引用了字体,则返回true。

跟节点有关的一些命令(基元):

COMMAND NODE EXPLANATION
\hbox hlist horizontal box
\vbox vlist vertical box with the baseline at the bottom
\vtop vlist vertical box with the baseline at the top
\hskip glue horizontal skip with optional stretch and shrink
\vskip glue vertical skip with optional stretch and shrink
\kern kern horizontal or vertical fixed skip
\discretionary disc hyphenation point (pre, post, replace)
\char glyph a character
\hrule rule a horizontal rule
\vrule rule a vertical rule
\textdir(ection) dir a change in text direction

打开所有基元命令:

\directlua { tex.enableprimitives('',tex.extraprimitives()) }

6 boundary

相关的基本命令是:\noboundary, \boundary, \protrusionboundary and \wordboundary

FIELD TYPE EXPLANATION
subtype number 0 = cancel, 1 = user, 2 = protrusion, 3 = word
attr node 属性列表
value number values 0–255 are reserved

9 local_par

段首插入的结点。一般不要改动。

FIELD TYPE EXPLANATION
attr node list of attributes
pen_inter number local interline penalty (from \localinterlinepenalty)
pen_broken number local broken penalty (from \localbrokenpenalty)
dir string the direction of this par. see 8.2.15
box_left node the \localleftbox
box_left_width number width of the \localleftbox
box_right node the \localrightbox
box_right_width number width of the \localrightbox

10 dir

方向结点,标记正在灌注、需要改变方向的文本的部分,由命令\textdir生成。

FIELD TYPE EXPLANATION
attr node list of attributes
dir string TLT, TRT, RTT, or LTL(3个字母依次表示段落、行、字符的起始方向)
level number nesting level of this direction whatsit

28 margin_kern

边缘字间是由突出(protrusion)引起的。

FIELD TYPE EXPLANATION
subtype number 0 = left, 1 = right
attr node list of attributes
width number the advance of the kern
glyph node the glyph to be used

PDF后端物件(whatsits)#

node库#

luatex手册 8.7

结点库中包含一些处理结点和结点列表的便利工具,可用于创建、更换、拷贝、删除和插入luatex结点对象——排版器(typesetter)的核心对象。

在lua中,结点表示为luatex.node元数据类型的用户数据。

所有结点都有以上三个字段,其余字段则根据类型(whatsits根据小类)而定:

{
    next:...,  -- 下一个结点
    id:...,  -- node type(数值,有些库函数可以接受字符串)
    subtype:..., --小类(数值),比如用于区分whatsits(东西、小玩意儿、那个什么)
}

对unset (alignment) 结点的支持比较特殊,可以查询和调整,但不能创建。

lua垃圾回收器看不到结点,用户必须字行调用结点释放函数。

结点构成互相连接的结点列表,但是没有引用计数,所以控制权交换luatex前,不要误删被引用的结点。一般情况下通过getter和setter来处理。还有一些可以定位结点内存的追踪、统计工具。

处理类型

  • t = node.is_node( item) --结点索引
  • t = node.types() --大类id:string的映射表(查表用node.type())
  • type = node.type( n) -- 类型id查类型名称;结点转字符串结点(string node)
  • t = node.whatsits() --小类id:string的映射表
  • id = node.id( type) --类型名称查类型id
  • subtype = node.subtype( type) -- 小类名称查id
  • 处理其余字段

    t = node.fields( id) --查某类型的有效字段,也接受名称
  • t = node.fields( id, subtype) --查某小类的有效字段,也接受名称
  • t = node.has_field( n, field) --判断字段有无
  • 新建结点

    • n =node.new( id)
    • n =node.new( id, subtype) --小东西

    也可以接收字符串名称。字段被初始化为nil或0。

    管理内存

    • next =node.free( n) -- 释放结点内存,返回后继结点
    • flush_node( n) --直接冲洗结点内存
    • node.flush_list( n) --直接冲洗结点n开头的整个列表

    请确保没有注册器、next字段等指向要释放、冲洗的节点和列表,以及列表中的所有元素。

    深拷贝

    • m =node.copy( n) --不会拷贝next字段
    • m =node.copy_list( n) --不会拷贝属性列表(通常也没有必要)
    • m =node.copy_list( n, m) --指定区间

    前导和后继

    • m =node.next( n)
    • m =node.prev( n)

    当前激活的属性列表

    • m =node.current_attr()

    返回实际属性列表的引用,也就是说,其后可能会改变(比如通过tex.setattribute)。

    local x1 = node.new("glyph")
    local x2 = node.new("glyph")
    local ca = node.current_attr()
    x1.attr = ca
    x2.attr = ca
    

    打横包

    把节点n开头的列表打包为横向列表hlist,放在hbox中。

    • h, b =node.hpack( n) --使用元素的自然宽度,返回b是新结点的badness
    • h, b =node.hpack( n, w, info) --info: 'additional'|'exactly',w: the additional (\hbox spread) or exact (\hbox to) width to be used
    • h, b =node.hpack( n, w, info, dir)

    打包后还会受到元素更新的影响;删除打包生成的节点也会删除其下的节点列表,除非事先把其结点列表断开(设置为nil)。

    打竖包

    • h, b =node.vpack( n)
    • h, b =node.vpack( n, w, info)
    • h, b =node.vpack( n, w, info, dir)

    把行间距加入行中

    • n, delta =node.prepend_prevdepth( n, prevdepth)

    行尺寸

    以结点n开头的行,从头到尾(或到结点t)的、经过缩放后的尺寸(pt,浮点数)。

    • w, h, d=node.dimensions( n)
    • w, h, d=node.dimensions( n, dir)
    • w, h, d=node.dimensions( n, t)
    • w, h, d=node.dimensions( n, t, dir)

    或增加胶参数:

    • w, h, d=node.dimensions( glue_set, glue_sign, glue_order, n)
    • w, h, d=node.dimensions( glue_set, glue_sign, glue_order, n, dir)
    • w, h, d=node.dimensions( glue_set, glue_sign, glue_order, n, t)
    • w, h, d=node.dimensions( glue_set, glue_sign, glue_order, n, t, dir)

    以上考虑了加胶的情况,特别适用于获取加过盒子的节点子列表的实际宽度,比如:

    \setbox0 = \hbox to 20pt {a b}
    \directlua{print (node.dimensions(
        tex.box[0].glue_set,
        tex.box[0].glue_sign,
        tex.box[0].glue_order,
        tex.box[0].head.next,
        node.tail(tex.box[0].head)
    )) }
    

    另有便利的查询【???】:

    • w, h, d=node.rangedimensions( parent, first)
    • w, h, d=node.rangedimensions( parent, first, last)

    数学列表转行列表

    h =node.mlist_to_hlist( n, display_type, penalties)

    跟回调函数mlist_to_hlist一样。

    尾结点

    返回结点n开头的节点列表的最后一个结点。

    m =node.tail( n)

    slide

    返回结点n开头的节点列表的最后一个结点。

    m =node.slide( n)

    会在结点之间生成前导各点的反向链条。

    长度和类型计数

    以结点n开头的结点列表的计数。

    • i =node.length( n)
    • i =node.length( n, m) --终止结点m不计算在内

    匹配类型id(字符串名称也可以)的版本:

    • i =node.count( id, n)
    • i =node.count( id, n, m)

    是不是字符、是不是字模

    判断字模结点(glyph node)的小类,看是不是已经转换为字符索引(character reference)。

    • b =node.is_char( n)
    • b =node.is_glyph( n)

    迭代结点列表

    • t, id, subtype =node.traverse( n)

    迭代从结点n开始的结点列表。

    -- 典型用法
    for n in node.traverse(head) do
        ...
    end
    
    -- 等价于
    do
        local n
        local function f (head,var)
            local t
            if var == nil then
                t = head
            else
                t = var.next
            end
            return t
        end
        while true do
            n = f (head, n)
            if n == nil then break end
            ...
        end
    end
    

    迭代结点列表中特定类型id的结点

    • t, subtype =node.traverse_id( id, n)

    即在node.traverse()基础上加入类型检查逻辑:

    ...
    while not t.id == id do
        t = t.next
    end
    ...
    

    迭代结点列表中的字模结点

    字模结点(glyph nodes):小类(subtype)小于256的结点。

    n, font, char =node.traverse_char( n)

    返回列表,过滤所有字模(glyph)【???】:

    n, font, char =node.traverse_glyph( n)

    迭代列表中的hlist结点和vlist结点

    n, id, subtype, list =node.traverse_list( n)

    列表中的第一个字模或自由断字(disc)结点

    n =node.has_glyph( n)

    disc是断字的点。

    返回列表的第一个字模结点

    字模结点:subtype为glyph的结点。

    • n =node.first_glyph( n)
    • n =node.first_glyph( n, m) --包含终点

    下一个数学结点

    t =node.end_of_math( start)

    可能是起点。

    从列表中移除当前结点

    head, current =node.remove( head, current)

    得自行确定是不是列表的一部分。返回新的head和当前结点(或nil)。

    在结点前、后插入结点

    head, new =node.insert_before( head, current, new) head, new =node.insert_after( head, current, new)

    head可以是nil。返回新的head。

    连字 ligaturing

    • h, t, success =node.ligaturing( n)
    • h, t, success =node.ligaturing( n, m)

    对指定结点列表应用TEX风格的连字功能。h,新头;t,新尾。

    调整字距 kerning

    • h, t, success =node.kerning( n)
    • h, t, success =node.kerning( n, m)

    对指定结点列表应用TEX风格的字距调整功能。h,新头;t,新尾(可能插入了kern node,词语边界可能有特殊的kerning)。

    字模与字符结点转换

    • node.unprotect_glyph( n)
    • node.unprotect_glyphs( n,[ n]) -- 指定终点

    把所有字符结点的小类值减去256。在结点处理过程中把字符转换成字模。

    • node.protect_glyph( n)
    • node.protect_glyphs( n,[ n])

    把所有字符结点的小类值加上256(如果值是1则只加255)。

    弹出当前列表最后一个结点

    n =node.last_node()

    在当前列表最后写入一个结点

    node.write( n)

    你应该保证当前是水平模式。

    结点能否跳过

    skippable =node.protrusion_skippable( n)

    为了在字符突出(character protrusion)开启时发现行边界,可能用到此功能。

    胶的处理#

    注胶

    • node.setglue( n)
    • node.setglue( n,width,stretch,shrink,stretch_order,shrink_order)
      • node.setglue(n,655360,false,65536),只调整 width and shrink.

    非数值等于0,即重置属性。

    查看胶值

    width, stretch, shrink, stretch_order, shrink_order = node.getglue( n)

    空值胶

    isglue =node.is_zero_glue( n)

    属性的处理#

    Fonts 字体#

    更多细节参看font manual(s)

    当前字体id fonts.currentid()

    当前字体数据 fonts.current()

    指定id的字体数据 local tfmdata = fonts.identifiers[number]

    字体数据表如下(有些是下级表的快捷方式):

    keys type 描述
    ascender number the height of a line conforming the font
    descender number the depth of a line conforming the font
    italicangle number the angle of the italic shapes (if present)
    designsize number the design size of the font (if known)
    size number the size in scaled points if the font instance
    factor number the multiplication factor for unscaled dimensions
    hfactor number the horizontal multiplication factor
    vfactor number the vertical multiplication factor
    extend number the horizontal scaling to be used by the backend
    slant number the slanting to be applied by the backend
    characters table the scaled character (glyph) information (tfm)
    descriptions table 未缩放的原始字模信息(otf, afm, tfm)
    indices table the mapping from unicode slot to glyph index
    unicodes table the mapoing from glyph names to unicode
    marks table a hash table with glyphs that are marks as entry
    parameters table the font parameters as TEX likes them
    mathconstants table the OPENTYPE math parameters
    mathparameters table a reference to the MathConstants table
    shared table a table with information shared between instances
    unique table a table with information unique for this instance
    unscaled table the unscaled (intermediate) table
    goodies table the CONTEXT specific extra font information
    fonts table the table with references to other fonts
    cidinfo table a table with special information for the backend
    filename string the full path of the loaded font
    fontname string the font name as specified in the font (limited in size)
    fullname string the complete font name as specified in the font
    name string the (short) name of the font
    psname string the (unique) name of the font as used by the backend
    hash string the hash that makes this instance unique

    执行lua代码#

    代码使用方式

    • 行内代码
    • 用\catcode、dofile()引入代码文件

    代码入口基元

    • \directlua{tex.print("Hi there")}
    • \latelua{}
    • \lateluafunction{}
    • \luaescapestring{}

    引用函数表与定义表:

    • \luafunction
    • \luafunctioncall
    • \luadef
    \directlua {
        local t = lua.get_functions_table()
        t[1] = function() tex.print("!") end
        t[2] = function() tex.print("?") end
    }
    \luafunction1
    \luafunction2
    

    \luabytecode and \luabytecodecall

    \directlua {
        lua.bytecode[9998] = function(s)
        tex.sprint(s*token.scan_int())
        end
        lua.bytecode[5555] = function(s)
        tex.sprint(s*token.scan_dimen())
        end
    }
    
    
    \luabytecode 9998 5
    \luabytecode 5555 5sp
    \luabytecodecall9998 5
    \luabytecodecall5555 5sp
    

    预先展开:\directlua代码执行机制#

    • category codes and TeX tokens: converting text to tokens and tokens to text;

    category codes and TeX tokens

    • TeX’s expansion process (and preventing expansion);

    TeX’s expansion process

    LuaTeX引擎会把代码预先交由TeX展开/求值,结果文本再作为lua代码交给lua引擎解释

    \def\aa{tex}
    \def\bb{.}
    \def\cc{print}
    \def\dd{("Hello")}
    \directlua{
        \aa\bb\cc\dd
    }
    % 等价于
    \directlua{tex.print("Hello")}
    

    展开实际上是LuaTeX的一个API,即Lua与TeX交换数据的方式

    \count75=1564 % TeX世界中的数据
    \directlua{
        local x=\number\count75 \space % 等号右边会展开,即把TeX数据传给Lua;\space是输入空格的命令,也可以省略
        tex.print("x= "..x)
        local y = (2*x-65)/5
        tex.print(" and y = "..y)
    }
    

    展开会生成的Lua代码:

    local x=1564 tex.print("x= "..x) local y = (2*x-65)/5 tex.print(" and y = "..y)
    

    在控制台打印:

    \directlua{
        local foo=[-[local x=\number\count75
        tex.print("x= "..x)
        local y = (2*x-65)/5
        tex.print(" and y = "..y)]-]
        print(foo)
    }
    

    Overleaf的控制台打印用texio.write(foo)

    \directlua预处理(scan_toks())中不展开的权标(token)#

    • 速记命令权标/shorthand command tokens: 此类命令权标源于用TeX基元\chardef, \mathchardef, \countdef, \dimendef, \skipdef, \muskipdef, \toksdef所定义的控制序列(下表的command,表示数值):
      • \chardef ⟨command⟩ = ⟨numeric value⟩
      • \mathchardef ⟨command⟩ = ⟨numeric value⟩
      • \countdef ⟨command⟩ = ⟨numeric value⟩
      • \dimendef ⟨command⟩ = ⟨numeric value⟩
      • \skipdef ⟨command⟩ = ⟨numeric value⟩
      • \muskipdef ⟨command⟩ = ⟨numeric value⟩
      • \toksdef ⟨command⟩ = ⟨numeric value⟩
    • 不展开的权标: 此类命令权标源于会展开的命令,但\directlua已经对它们:
      • 显式指令不予展开,比如\noexpand\unexpanded命令压制展开。
      • 通过处理序列予以注入权标,如\the\toks等。
    \chardef\mydollar=`\$ 
    \directlua{
        local x =[-[I paid \mydollar30.]-] 
        texio.write(x)
    }
    % I paid \mydollar 30
    

    注意: TeX中的\%, \&, \#, \$是用\chardef定义的,因此不可展开:

    \chardef\#=`\#
    \chardef\$=`\$
    \chardef\%=`\%
    \chardef\&=`\&
    % I paid \mydollar 30
    

    `\表示取得其后代码值的数字字符。但在LaTeX中这些都是宏,可展开。

    避免lua代码展开导致的问题#

    % TeX宏会吞掉后面的空格
    \directlua{
        myvar = \macro
      anothervar = 2
    }
    % 得到`yvar = 1anothervar = 2`
    
    % Lua注释的短路问题
    \directlua{
      myvar = 1
    --  anothervar = 2
      onelastvar = 3
    }
    % 得到`myvar = 1 -- anothervar = 2 onelastvar = 3`
    % 导致注释后的所有行无效
    
    % 以上续行问题可以用 `\endlinechar=10` 重新定义换行符
    

    更多

    • \noexpand⟨token⟩: 阻止单个⟨token⟩展开;
    • \unexpanded{⟨material⟩}: 阻止所有权标展开;
    • \protected: 宏定义前缀,阻止宏在某些环境,如\directlua, \write, \edef中展开。
    \directlua{local x= "\noexpand\TeX"}
    % 传给Lua的是:
    % local x= "\TeX "
    % **空格是\directlua的tokenlist_to_cstring()生成的**
    
    \def\macroA{"This unprotected macro contains a string"}
    \protected\def\macroB{"This protected macro also contains a string"}
    \directlua{
        local x=\macroA..\macroB
    }
    % local x="This unprotected macro contains a string"..\macroB
    
    \let\\\relax %重定义\\的意义以避免展开问题
    

    \directlua中的\the\toks#

    \toks指向权标注册器暂存的权标列表,列表是原样保存的,用\the\toks插入前不会预先展开。

    \def\mymacro{\TeX}
    \directlua{
        local x="\mymacro"
    }
    % 得到:
    % local x = "T\kern -.1667em\lower .5ex\hbox {E}\kern -.125emX"
    
    \toks100={\TeX}
    \directlua{
        local x="\the\toks100"
    }
    % 得到:
    % local x = "\TeX "
    

    其他展开技术#

    • \string
    • \detokenize{}, \string的多权标版本
    \directlua{
        local x="I will use \string\newcommand" 
        print(x)
    }
    % 有转义问题:
    % local x="I will use \newcommand" print(x)
    % I will use
    % ewcommand
    

    Lua转义序列#

    • 标准序列:\n (newline), \r (carriage return), \\ (backslash), \" (double quote), \t (horizontal tab), \v (vertical tab) and \' (single quote);
    • \xXX, XX是十六进制数;
    • \ddd, where ddd is a sequence of up to three decimal digits;
    • \u{XXX},用十六进制数表述的UTF-8字符

    长括号来救急:

    • "this is a string.\nI'll now start on a new line."
    • 长括号文本不会转义: [-[I am a long brackets\n string]-]
    \protected\def\newtest#1{The argument: #1}
    \directlua{
        tex.print([-[\newtest{Hello}]-])
    }
    % The argument: Hello
    

    以页面为单位的信息收集与统计#

    1. 通过标签与实际页码的关系确定信息位置 (需要多次编译才准确,并注意核对)
    2. 通过页面引用取得页码,不能使用在文档中不准确的\thepage
    3. 通过\refcount宏包的可展开命令\getrefnumber, \getpagerefnumber取得页码等可操作的数据,而不是通过\pageref(无法展开,仅文档中呈现,而无法作为值保存、处理)
    4. 埋设标签和收集信息可以包装成一个命令 (不能双重包装)
    \documentclass[a4paper]{ctexrep}%
    
    % 在页眉页脚中通过\thepage过滤、使用表中数据
    \usepackage{fancyhdr}
    \fancyfoot[C]{\thepage{}页共%
        \directlua{%
            sum = t[\thepage] or "??" % 设置默认值方便核对
            tex.print(sum) %
        }%
        题,做对\_\_\_\_%
    } %
    \pagestyle{fancy} % 应用页面样式
    %
    \usepackage{refcount}%
    \setrefcountdefault{??} % 未定义标签的默认值,方便核对
    % 定义表、计数器
    \directlua{ t = {} }
    %
    % 按页的标签计数
    \newcommand*{\countup}[1]{
        \directlua{
            t[#1] = t[#1] or 0;
            t[#1] = t[#1] + 1;
        }%
    }
    %
    \begin{document}%
    
    % 标签一般埋设在特定类别的条目,在一般文本行首、末可能会出错????
    这里有标签 \label{ 1 } 标签所在页码: \getpagerefnumber{ 1 }\pageref{ 1 }%
    
    % 收集  标签id,存入表中键值为页码的单元
    \directlua{t[\getpagerefnumber{ 1 }] = t[\getpagerefnumber{ 1 }] or 0; t[\getpagerefnumber{ 1 }] = t[\getpagerefnumber{ 1 }] + 1 }%
    % pylatex代码:
    % doc.append(NoEscape(fr'''\directlua{{
    %     t[\getpagerefnumber{{ {label_id} }}] = t[\getpagerefnumber{{ {label_id} }}] or 0;
    %     t[\getpagerefnumber{{ {label_id} }}] = t[\getpagerefnumber{{ {label_id} }}] + 1;
    %     }}'''))
    
    % 或包装成命令(不能装入\getpagerefnumber形成多重包装)
    % \countup{ \getpagerefnumber{ 1 } }%
    % pylatex代码:
    % doc.append(NoEscape(fr"\countup{{ \getpagerefnumber{{ {label_id} }} }}"))
    
    \par%
    
    \end{document}%
    

    \directlua包装成\command时要避免多重嵌套。 \command\directlua\command双重包装会引起第三遍编译(pdf能观察到): 第一遍编译,因引用变动而引起第二遍编译,此时正确取数; 但是又意外地引发第三次编译,无法取数,如下:

    \newcommand*{\countup}[1]{
        % 以下lua包装可以直接放在文档中,但不能再包装成命令
        \directlua{
            t[\getpagerefnumber{ #1 }] = t[\getpagerefnumber{ #1 }] or 0;
            t[\getpagerefnumber{ #1 }] = t[\getpagerefnumber{ #1 }] + 1;
        }%
    }
    % 这样使用
    \countup{ 1 }%
    

    可以这样:

    \newcommand*{\countup}[1]{
        \directlua{
            t[#1] = t[#1] or 0;
            t[#1] = t[#1] + 1;
        }%
    }
    % 这样使用:
    \countup{ \getpagerefnumber{ 1 } }%
    

    不可能在主文件中精确引用页码计数器#

    vid Carlisle:你几乎不可能在主文件中精确引用页码计数器(page counter),只有在输出进程(output routine,此时输出页面)中才能精确地知道。Luatex没有改变TeX的这种基本模式。

    可以通过埋设标签的方式实现,每处用一个\label{id}关联,第二轮运行时就可以通过\pageref{id}从Lua或TeX引用。

    \documentclass{article}
    \directlua{j=0}
    \newcommand\printluapage{%
        \directlua{
           for i = 1, 60 do
             j=j+1
             tex.print('\string\\pageref{foo' .. j .. '}  \string\\label{foo' .. j .. '}') 
             tex.print([-[\par]-])
           end
           }%
    }
    
    \begin{document}
    \printluapage
    
    New command
    
    \printluapage
    
    New command
    
    \printluapage
    \end{document}
    

    标点压缩临时方案#

    \usepackage{luacode}
    %%% 消除中文問號及驚嘆號後面多餘的空白。
    \def\exsp#1{\unskip#1\hspace{0em}}
    \begin{luacode}
    function dosub(s)
        s = string.gsub(s, '!', '\\exsp!')
        s = string.gsub(s, '?', '\\exsp?')
        return(s)
    end
    \end{luacode}
    % 对输入调用查找替换
    \AtBeginDocument{%
        \luaexec{luatexbase.add_to_callback("process_input_buffer", dosub, "dosub")}%
    }
    

    定义token、定义命令、定义宏#

    local t = token.create("relax")

    生成一个带有\relax基元属性的token。可用的token属性包括:

    NAME EXPLANATION
    command 一个代表内部命令的数字
    cmdname 命令的类型,比如catcode字符,确定内部处理的分类器
    csname 控制序列(如果可用)
    id token的id
    tok TEX中存储的完整的token数字
    active 布尔值,表示token的激活状态
    expandable 布尔值,表示表示token(宏)能否展开
    protected 布尔值,表示token(宏)受保护
    mode 数值,表示一个字符或实体
    index 数值,从0x0000到0xFFFF,表示一个TEX注册器索引

    可以通过getter get_ 获取token的这些属性。

    token.set_macro函数可以接受四个参数:

    token.set_macro("csname","content") token.set_macro("csname","content","global") token.set_macro("csname")

    第一个参数可以是catcodetable标识符:

    token.set_macro(catcodetable,"csname","content") token.set_macro(catcodetable,"csname","content","global") token.set_macro(catcodetable,"csname")

    等价于:

    \def\csname{content} \gdef\csname{content} \def\csname{}

    \directlua{token.set_macro('foo', 'GREEN')}
    

    等价于:

    \newcommand*\foo{GREEN}
    

    宏输出命令的转义:

    token.set_macro("mymacro",[-[\string\emph{content}]-])
    

    速度问题#

    使用本地变量而不是原始名字:

    nodetraverseid = node.traverse_id

    如果少一些LaTeX/宏展开,多一些lua,会快得多

    by svat on Sept 1, 2017

    The book A Gentle Introduction to TeX is 97 pages long (number of pages in the output). To produce the typeset output from gentle.tex, on my laptop,

    • tex takes 0.15 seconds
    • pdftex takes 0.37 seconds
    • luatex (1.0.4) takes 0.46 seconds

    All of these are reasonably fast, and I think getting a full book in less than half a second is acceptable. (My browser takes longer than that to render most things.)

    What's the catch? This book is written in plain TeX. The slowness comes when you use LaTeX: it's not unusual to have even single-page documents that take longer for LaTeX to generate. The whole LaTeX approach is to have a package for everything, load everything you may want, providing all the features, etc — and all of this implemented in TeX macro expansion in painful and hard-to-refactor ways.

    This is something LuaTeX will help with actually: LuaTeX makes TeX less monolithic by providing hooks (callbacks), and many things can now be implemented in easier-to-read Lua code as hooks into the right parts of TeX's execution, rather than setting everything up at the start so that after macros are expanded the right thing happens. If there's less use of LaTeX and elaborate macros, things should get faster.

    TeX_without_TeX_revised_and_expanded#

    拼音#

    \usepackage{luatexja-ruby}

    LuaMetaTEX(LMTX)#

    本部分主要学习:LuaMetaTEX Reference Manual

    引言#

    参见见官网LMTX

    LuaMetaTEX是更精瘦和干练的(leaner and meaner)LuaTEX,移除了大量的部件和依赖。主要为ConTeXt所用,可以说是新版的ConTeXt。目前兼容LuaTEX。

    LuaMetaTeX(lmtx)引擎整合了TEX文本渲染引擎、MetaPost图形引擎和Lua脚本解释器,或可联想Lua, MetaPost, TeX, XML。LuaMetaTeX是主要为ConTeXt创建的引擎,基于并兼容LuaTeX,而有优化:

    • 除了三个核心libc和libm运行时外,没有外部依赖。
    • 没有PDF后端代码,而由ConTeXt的Lua代码生成pdf。
    • 没有字体加载器lua界面库,(OpenType)字体加载由ConTeXt的Lua代码处理。
    • 没有(poppler-based)图像和pdf文件的lua界面库(后者用epdf代替)
    • 各种扩展已经做成lua界面库

    需要安装最新的ConTeXt实现CONTEXT LMTX很慢。在看到下载压缩包的时候,可以用下载工具手动下载,解压到相应的位置,再重启安装程序。 预计TeX Live 2022中会加入LuaMetaTeX。

    安装包中的设置系统路径的工具setpath.bat似乎无效,需要手动设置。

    查看版本:

    context --version && luametatex --version
    

    编译时,context test.mkiv 默认使用LuaMetaTeX。可指定使用luatex: context --luatex test.mkiv

    朝向与旋转(竖排、直书)#

    参见开发文档followingup的第6节,Directions。

    Lua in ConTeXt#

    本部分主要学习如下资料的笔记:

    引言#

    ConTeXt Lua 的两种运行方式:

    • 镶嵌在ConTeXt文档中执行
    • 用context直接执行.cld文档

    ConTeXt文档中Lua代码的主要问题是:代码先由TeX展开,再给Lua处理。这意味着,所有Lua代码,甚至分行和注释,首先必须在TeX中合法;其次,展开后要在Lua中合法!因此要注意:

    • 断行视作空格;
    • 特殊TeX字符,如&, #, $, {, }等,需要转义。
    \ctxlua
      {-- A Lua comment
       tex.print("This is not printed")}
    \ctxlua
      {% A Tex comment
       tex.print("This is printed")}
    
    % This doesn't work:
    %\ctxlua
    %  {local t = {1,2,3,4}
    %   tex.print("length " .. #t)}
    % should be:
    \ctxlua
      {local t = {1,2,3,4}
       tex.print("length " .. \string#t)}
    

    ConTeXt文档中的代码执行界面 Interfacing#

    • \startluacode...\stopluacode,模块式,如定义函数
    • \ctxlua,或\ctxluacode(更多准备),行内式,如执行函数

    以上都是在LuaTeX基元\directlua之上包装的;绝不要直接使用\directlua

    命名空间commands的快捷方式:

    • \ctxcommand{thisorthat("...")},等于\ctxlua{commands.thisorthat("...")}

    命名空间context的快捷方式:

    • \cldcontext{myfunction(5)}直接获得函数返回值,等价于\ctxlua{context(myfunction(5))}
    • \cldprocessfile#1{\directlua{context.runfile("#1")}}
    • \cldloadfile#1 {\directlua{context.loadfile("#1")}}
    • \cldcontext#1 {\directlua{context(#1)}}
    • \cldcommand#1 {\directlua{context.#1}}
    \startluacode
        -- The unknown command \undefined will cause this entire block to fail.
    
        -- Print a countdown '10, 8, ..., 0!'
        -- `..` is Lua for string concatenation
        for i = 10, 2, -2 do
            context(i .. ", ")
        end
        context("0!")
    
        -- \\par is equivalent to a blank line in the input
        -- (Notice the escaped backslash: TeX won't mind the above comment.)
        context.par()
    
        -- Look! we can use # and $ with impunity!
        context("Unless we print them, then we must \\#\\$\\& print the escape characters, too.")
    \stopluacode
    

    ctx函数与ctx宏/ctx定义:

    % ctx函数
    \startctxfunction MyFunctionA
        context(" A1 ")
    \stopctxfunction
    \ctxfunction{MyFunctionA}
    
    % ctx函数定义(宏)
    \startctxfunctiondefinition MyFunctionB
        context(" B2 ")
    \stopctxfunctiondefinition
    \MyFunctionB
    

    引入外部代码#

    \startluacode
    -- include the file my-lua-lib.lua
    require("my-lua-lib")
    \stopluacode
    

    使用命名空间#

    \startluacode
        -- if userdata doesn't exist yet, create it
        userdata = userdata or {}
        -- define a shorter synonym
        u = userdata
    
        -- create my custom function inside the userdata namespace
        function u.myfunction()
            -- do stuff
        end        
    \stopluacode
    
    • userdata = userdata or { } -- for users (e.g. functions etc)
    • documentdata = documentdata or { } -- for users (e.g. raw data)
    • moduledata = moduledata or { } -- only for development team
    • parametersets = parametersets or { } -- experimental for team
    • thirddata = thirddata or { } -- only for third party modules

    用子命名空间向用户暴露自己的数据:

    moduledata['mymodule'] = { }
    mm = moduledata.mymodule
    function mm.mainfunction()
        -- do stuff
    end
    

    #

    用表组织在一起的功能。

    Lua库:string, table, lpeg, math, io and os
    LUATEX库:node, tex and texio.

    输出文本#

    一般使用context(...),等价于tex.print(string.format(...))

    \startluacode
        name = "Jane"
        date = "today"
        context("Hello %s, how are you %s?", name, date)
        -- Becomes 'Hello Jane, how are you today?'
    \stopluacode
    

    更原始的:

    • tex.print()(多个参数,或一个表,每项一行)
    • tex.sprint()(多个参数,或一个表,所有项合成一个string)

    ConTeXt commands#

    大部分ConTeXt命令可以通过context.command的形式取得,方括号参数改用表来表示,花括号参数改用字符串。

    \startluacode
        context.chapter({first}, "Some title")
        context.startcolumns({n = 3, rule = "on"})
            context("Hello one")
        context.column()
            context("Hello two")
        context.column()
            context("Hello three")
        context.stopcolumns()
    \stopluacode
    

    等价于:

    \chapter[first]{Some title}
    \startcolumns[n=3, rule=on]
        Hello one
    \column
        Hello two
    \column
        Hello three
    \stopcolumns
    

    传递参数和缓存: ConTeXt命令给Lua提供的钩子#

    先定义Lua函数:

    \startluacode
        -- remember, using the userdata namespace prevents conflicts
        userdata = userdata or {}
    
        function userdata.surroundwithdashes(str)
            context("--" .. str .. "--")
        end
    \stopluacode
    

    然后定义同一个TeX命令来展开\ctxlua回调:

    \def\surroundwd#1%
        {\ctxlua{userdata.surroundwithdashes([==[#1]==])}}
    

    [==[#1]==]长字符串比引号更健壮。

    先定义Lua函数

    \startluacode
        userdata = userdata or {}
    
        function userdata.verynarrow(buffer)
            -- equivalent to \startnarrower[10em]
            context.startnarrower({"10em"})
                context(buffer)
            context.stopnarrower()
        end
    \stopluacode
    

    定义缓存的开始命令:

    \def\startverynarrow%
      {\dostartbuffer
        [verynarrow]      % buffer name
        [startverynarrow] % command where buffer starts
        [stopverynarrow]} % command where buffer ends
                          % also: command invoked when buffer stops
    

    定义结束命令\stopverynarrow,并当前完成的缓存传递给Lua函数verynarrow:

    \def\stopverynarrow
      {\ctxlua
         {userdata.verynarrow(buffers.getcontent('verynarrow'))}}
    

    用例#

    计算正弦

    \def\COSINE#1% {\ctxlua{context(math.cos(#12math.pi/360))}}

    $\pi = \ctxlua{context(math.pi)}$

    $\pi = \ctxlua{context("\letterpercent.6f", math.pi)}$

    \setupcolors[state=start] \setupTABLE[each][each][width=2em,height=2em,align={middle,middle}]
    \setupTABLE[r][1][background=color,backgroundcolor=gray]
    \setupTABLE[c][1][background=color,backgroundcolor=gray]

    循环而无需担忧展开:

    \startluacode context.bTABLE() context.bTR() context.bTD() context("$(+)$") context.eTD() for j=1,6 do context.bTD() context(j) context.eTD() end context.eTR() for i=1,6 do context.bTR() context.bTD() context(i) context.eTD() for j=1,6 do context.bTD() context(i+j) context.eTD() end context.eTR() end context.eTABLE() \stopluacode

    字符串操作#

    https://wiki.contextgarden.net/String_manipulation

    表格操作#

    https://wiki.contextgarden.net/Table_manipulation

    IO#

    https://wiki.contextgarden.net/Extensions_to_the_Lua_I/O_library

    在ConTeXt外部运行Lua(可使用ConTeXt环境)#

    https://wiki.contextgarden.net/Running_Lua_Code_Externally

    mtxrun --script dummy.lua

    CLD: ConTeXt Lua Documents#

    ConTeXt Lua Documents (CLD)是从Lua脚本中实用TeX的方式。可以实现纯Lua、不需要TeX代码的排版。适合自动排版。

    本部分主要学习如下资料的笔记:

    执行方式#

    极简文件test.cld

    context.starttext()
    context.chapter("Hello There!")
    context.stoptext()
    

    cld文件的执行:context test.cld,或自定后缀而加运行选项--forcecld。其余选项:

    • --forcecld
    • --forcelua
    • --forcexml
    • --forcemp

    常用函数#

    cld函数与文档命令往往一一对应:

    • context.chapter("Some title") 对应\chapter{Some title}
    • context.chapter({ "first" }, "Some title") 对应\chapter[first]{Some title}
    • context.startchapter({ title = "Some title", label = "first" }) 对应\startchapter[title={Some title},label=first]
    • context.pagenumber("pagenumber")

    文本被看作tex输入:

    • context.mathematics([-[\sqrt{2^3}]-])

    分行、分段:

    context.startlines()
    context("line 1") context(true)
    context("line 2") context(true)
    context("line 3") context(true)
    context("line 4\n")
    context.stoplines()
    -- 分段
    context.par()
    

    所有命令:

    https://source.contextgarden.net/tex/context/base/mkiv/cldf-ini.lua

    直接输出#

    仅在特殊情况下实用。

    仅仅是(给环境)传递参数,不做操作:context.direct("one","two"),等同于context(false,"one","two")

    特种码 Catcodes#

    输出文本$x=1$而不是数学公式:

    context.pushcatcodes("text")
    context("$x=1$")
    context.popcatcodes()
    

    catcode regimes:

    • ctx, ctxcatcodes, context: the normal CONTEXT catcode regime
    • prt, prtcatcodes, protect: the CONTEXT protected regime, used for modules
    • tex, texcatcodes, plain: the traditional (plain) TEX regime
    • txt, txtcatcodes, text: the CONTEXT regime but with less special characters
    • vrb, vrbcatcodes, verbatim: a regime specially meant for verbatim
    • xml, xmlcatcodes: a regime specially meant for XML processing

    以函数而不是语句作为参数#

    context.framed( { frame = "on", offset = "5mm", align = "middle" }, -- 不用context.input("knuth"),二用: function() context.input("knuth") end )

    尝试排版状态推出时转存缓存#

    Trial typesetting(比如处理天头、地脚时)退出时会清空缓存,可用return true转存

    function() context("whatever") return true end

    这样可能导致缓存过大,这是可以强制清空:context.restart()

    Steppers 从Lua临时进入TeX#

    未中断Lua

    context.stepwise (function()
        context.startitemize()
            context.startitem()
                context.step("BEFORE 1")
            context.stopitem()
                context.step("\\setbox0\\hbox{!!!!}")
            context.startitem()
                context.step("%p",tex.getbox(0).width)
            context.stopitem()
            -- ...
        context.stopitemize()
    end)
    

    Modes 模式匹配#

    processing time 测试:

    context.doifmodeelse( "screen",
        function()
            ... -- mode == screen
        end,
        function()
            ... -- mode ~= screen
        end
    )
    

    每个run只测试一次:

    if tex.modes["screen"] then
    ...
    else
    ...
    end
    

    常用模式:

    • tex.modes["mymode"]
    • tex.modes["*mymode"]
    • tex.systemmodes["mymode"]
    • tex.constants["someconstant'] 非公开,可能改变

    Token列表#

    TeX:\toks0 = {Don't get \inframed{framed}!}

    lua:context(tex.toks[0])

    实际上是以文本形式保存的:context("< %s >",tex.toks[0])

    但却不能反过来,存入文本形式的内容:tex.toks[0] = [-[\framed{oeps}]-],这样这能得到文本

    Node列表#

    TeX:\setbox0 = \hbox{Don't get \inframed{framed}!},然后用\box0输出,或用\copy0持有拷贝

    -- 引用、拷贝(结点必须free)
    context.direct(0)
    context.copy()
    context.copy(false,0)
    context(node.copy_list(tex.box[0]))
    -- 输出
    context(tex.box[0])
    context(tex.box[0].width)
    

    遍历node和任务挂载:

    \startluacode
    local hlist_code = nodes.nodecodes.hlist
    local glyphs = nodes.nodecodes.glyph
    function userdata.processmystuff(head)
        -- 遍历结点
        for n in node.traverse(head) do
            print(n.subtype)
        end
        -- 遍历glyphs结点id
        for n in node.traverse_id(glyphs,head) do
            print(n)
            --% nodes.tracers.colors.set(, "red")
       end
       return head, true
    end
    
    --把`userdata.processmystuff`函数挂载到processors回调的normalizers类别中。
    nodes.tasks.appendaction("processors", "after", "userdata.processmystuff")
    --停用
    nodes.tasks.disableaction("processors", "userdata.processmystuff")
    \stopluacode
    
    用例:
    
    \startluacode
    --启用
    nodes.tasks.enableaction("processors", "userdata.processmystuff")
    \stopluacode
    
    我和你。
    
    \startluacode
    --停用
    nodes.tasks.disableaction("processors", "userdata.processmystuff")
    \stopluacode
    

    TeX回调函数#

    参见官网回调函数

    和回调动作(Actions)

    几乎所有LuaTeX回调函数都可以用。详见14.2 Actions列表。与file handling, font definition and housekeeping相关的回调已经冻结,因为它们都涉及TEX目录结构。

    与 node list相关的回调函数仅开放这些:

    category callback arguments return value usage
    processors pre_linebreak_filter head, ... head, done 在段落分行之前回调
    hpack_filter 在水平盒子/行盒子构造前回调
    finalizers post_linebreak_filter head, ... head, done 在段落分行后回调
    shipouts no callback yet head head, done 应用到即将送出的盒子(或xform)
    mvlbuilders buildpage_filter done 有材料添加到主垂直列表后回调
    vboxbuilders vpack_filter head, ... head, done 有材料添加到垂直盒子后回调
    math mlist_to_hlist head, ... head, done 数学列表构造后、转为水平盒子列表前回调
    pagebuilders ??? head, ... head, done ???

    每类下还有一些小类,但用户只用beforeafter两个小类。

    示例:

    zhspuncs = zhspuncs or {} --或用third.mymodule命名空间
    
    function zhspuncs.my_linebreak_filter (head, is_display)
        compress_punc (head)
        return head, true
    end
    nodes.tasks.appendaction("processors","after","zhspuncs.my_linebreak_filter")
    
    function zhspuncs.align_left_puncs(head)
        -- do something
        return head, done
    end
    nodes.tasks.appendaction("finalizers", "after", "zhspuncs.align_left_puncs")
    

    TeX回调任务(Tasks)

    实际挂载nodes.tasks.appendaction()的任务列表见14.3 Tasks

    新建任务列表,挂载、删除任务:

    nodes.tasks.new("mytasks",{ "one", "two" })
    nodes.tasks.appendaction ("mytask","one","bla.alpha")
    nodes.tasks.appendaction ("mytask","one","bla.beta")
    nodes.tasks.prependaction("mytask","two","bla.gamma")
    nodes.tasks.prependaction("mytask","two","bla.delta")
    nodes.tasks.appendaction ("mytask","one","bla.whatever","bla.alpha")
    nodes.tasks.removeaction("mytask","one","bla.whatever")
    

    回调函数注册或用:

    callback.register("post_linebreak_filter",my_filter)

    latex中可用:

    • luatexbase.add_to_callback("post_linebreak_filter",my_filter,"a fancy new filter")
    • luatexbase.remove_from_callback("post_linebreak_filter","a fancy new filter")

    页面布局与框架布局#

    设置版面布局,随机生成图形:

    \startluacode
    --数字转尺寸
    local todimen, random = number.todimen, math.random
    
    -- 开始一个页面
    context.startTEXpage()
    
    --纸张尺寸
    local paperwidth = tex.dimen.paperwidth
    local paperheight = tex.dimen.paperheight
    local nofsteps = 25
    local firstcolor = "darkblue"
    local secondcolor = "white"
    
    --定义和设置整体框架布局
    context.definelayer({ "titlepage" })
    context.setuplayer(
        { "titlepage" },
        {
            width = todimen(paperwidth),
            height = todimen(paperheight),
    })
    context.setlayerframed(
        { "titlepage" },
        { offset = "-5pt" },
        {
            width = todimen(paperwidth),
            height = todimen(paperheight),
            background = "color",
            backgroundcolor = firstcolor,
            backgroundoffset = "10pt",
            frame = "off",
        },
        ""
    )
    --定义和设置小图框架布局
    local settings = {
        frame = "off",
        background = "color",
        backgroundcolor = secondcolor,
        foregroundcolor = firstcolor,
        foregroundstyle = "type",
    }
    for i=1, nofsteps do
        for j=1, nofsteps do
            context.setlayerframed(
                { "titlepage" },
                {
                    x = todimen((i-1) * paperwidth /nofsteps),
                    y = todimen((j-1) * paperheight/nofsteps),
                    --旋转角度
                    rotation = random(360),
                },
                settings,
                "CLD"
            )
        end
    end
    --???
    context.tightlayer({ "titlepage" })
    context.stopTEXpage()
    return true
    \stopluacode
    

    列表#

    context.startitemize { "a", "packed", "two" }
    for i=1,10 do
        context.startitem()
        context("this is item %i",i)
        context.stopitem()
    end
    context.stopitemize()
    

    表格#

    local one = {
        align = "middle",
        style = "type",
    }
    local two = {
        align = "middle",
        style = "type",
        background = "color",
        backgroundcolor = "darkblue",
        foregroundcolor = "white",
    }
    local random = math.random
    -- 表格开始
    context.bTABLE { framecolor = "darkblue" }
    for i=1,10 do
        -- 表行开始
        context.bTR()
        for i=1,20 do
            local r = random(99)
            -- 单元格开始
            context.bTD(r < 50 and one or two)
            context("%2i",r)
            context.eTD()
        end
        context.eTR()
    end
    context.eTABLE()
    

    放置图片#

    context.placefigure(
        "caption",
        --这里也可以生成表格,或表格中嵌入图像
        function() context.externalfigure( { "cow.pdf" } ) end
    )
    

    风格#

    • 粗体 context.bold("important")
    • 粗体加颜色 context.style( { style = "bold", color = "red" }, "important")
      • context.bold(function() context.color( { "red" }, "important") end)
      • context.bold(context.delayed.color( { "red" }, "important"))

    包装成命令:

    local function mycommands.important(str)
        context.style( { style = "bold", color = "red" }, str )
    end
    mycommands.important( "important")
    

    或设置、定义一种风格:

    context.setupstyle( { "important" }, { style = "bold", color = "red" } )
    context.style( { "important" }, "important")
    
    context.definestyle( { "important" }, { style = "bold", color = "red" } )
    context.important("important")
    context.startimportant()
    context.inframed("important")
    context.stopimportant()
    

    ForLorien 打印数学题的完整例子#

    Interfacing TeX界面和Unicode的处理#

    Formatters#

    Graphics#

    #

    可能有用的界面定义,效率不是最高。一般用TeX定义宏。

    TEX命令: \setupsomething[key=value] 等价于LUA调用: context.setupsomething { key = value }

    但Lua中的关键字需要先翻译成界面预设的相应的值,比如yes要转换成interfaces.variables.yes

    TeX的宏定义和使用:

    • \def\test#1#2{.. #1 .. #2 .. } \test{a}{b}
    • \def\test[#1]#2{.. #1 .. #2 .. } \test[a]{b}
    • \def\test(#1><#2){.. #1 .. #2 .. } \test(a><b) (定界符号可以灵活处理)

    In MkIV \define is like \unexpanded\def in TEX and \defineexpandable is like \def.

    \startluacode
    function test(opt_1, opt_2, arg_1)
        context.startnarrower()
        context("options 1: %s",interfaces.tolist(opt_1))
        context.par()
        context("options 2: %s",interfaces.tolist(opt_2))
        context.par()
        context("argument 1: %s",arg_1)
        context.stopnarrower()
    end
    
    interfaces.definecommand {
        name = "test",
        arguments = {
            { "option", "list" },
            { "option", "hash" },
            { "content", "string" },
        },
        macro = test,
    }
    \stopluacode
    

    使用test: \test[1][a=3]{whatever}

    在lua中操控宏:

    \def\TestA{A}
    \def\TestB{\def\TestC{c}}
    \def\TestC{C}
    
    \startluacode
        context(tokens.getters.macro("TestA"))
        context(tokens.getters.macro("TestB"))
        context(tokens.getters.macro("TestC"))
        tokens.setters.macro("TestA","a")
        context(tokens.getters.macro("TestA"))
        context(function()
            context(tokens.getters.macro("TestA"))
            context(tokens.getters.macro("TestB"))
            context(tokens.getters.macro("TestC"))
        end)
    \stopluacode
    
    ACaac
    
    \startluacode
    if tokens.getters.macro("fontstyle") == "rm" then
        context("serif")
    else
        context("unknown")
    end
    \stopluacode
    
    serif
    

    定义环境:

    \startluacode
    local function startmore(opt_1)
        context.startnarrower()
        context("start more, options: %s",interfaces.tolist(opt_1))
        context.startnarrower()
    end
    local function stopmore()
        context.stopnarrower()
        context("stop more")
        context.stopnarrower()
    end
    
    interfaces.definecommand ( "more", {
        environment = true,
        arguments = {
            { "option", "list" },
        },
        starter = startmore,
        stopper = stopmore,
    } )
    \stopluacode
    

    使用more: \startmore[1] one \startmore[2] two \stopmore one \stopmore

    Verbatim 字符字面值#

    获得公式:

    • $e=mc^2$
    • \mathematics{e=mc^2}
    • context.mathematics("e=mc^2")

    获得字面值:

    • context.verbatim("$e=mc^2$")
    • context.verbatim.bold("$e=mc^2$")
    • context.verbatim.inframed({ offset = "0pt" }, "$e=mc^2$")

    context.verbatimtype宏更方便、快、准确。

    利用缓存处理多行文本:

    • context.tobuffer("temp",str)
    • context.getbuffer("temp")

    利用虚拟文件处理多行文本:

    • context.tolines(str)
    • context.viafile(str)

    使用lpeg和visualizers完美打印:

    • spaces should honoured and can be visualized
    • newlines and empty lins need to be honoured as well
    • optionally lines have to be numbered but
    • wrapped around lines should not be numbered

    Logging#

    关闭日志:

    • context --silent
    • context --silent=structure,resolve,font*

    禁用控制台:

    • context --noconsole

    在CONTEXT使用指令:

    • \enabledirectives[logs.blocked=structure,resolve,font*]
    • \enabledirectives[logs.target=file]

    Lua界面:

    • local report_whatever = logs.reporter("modules","whatever")
    • report_whatever("not found: %s","this or that")

    TeX中的信息同步,还需要配置logs.messenger

    Lua 函数#

    查看内置函数:inspect(somevar)

    table#

    LuaTeX的函数:

    -- 表项拼接为文本
    local str = table.concat(t)
    local str = table.concat(t,separator)
    local str = table.concat(t,separator,first)
    local str = table.concat(t,separator,first,last)
    table.concat({"a","b","c","d","e"},"+")
    -- a+b+c+d+e
    table.concat({"a","b","c","d","e"},"+",2,3)
    -- b+c
    
    -- 插入和移除
    table.insert(t,value,position)
    value = table.remove(t,position)
    
    -- 解包
    local a, b, c = table.unpack(t)
    
    -- 排序
    table.sort(t)
    table.sort(t,comparefunction)
    t={"a","b","c"}
    table.sort(t,function(x,y) return x > y end)
    -- t={ "c", "b", "a" }
    

    内置函数:

    local a = table.sorted(b)
    
    -- 取键值
    local s = table.keys (t)
    local s = table.sortedkeys (t)
    local s = table.sortedhashkeys (t)
    
    -- 迭代器(与sortedpairs同义,以便与pairs、ipairs相对) 
    for key, value in table.sortedhash(t) do
        print(key,value)
    end
    
    table.serialize (root, name, reduce, noquotes, hexify)
    table.print (root, name, reduce, noquotes, hexify)
    table.tofile (filename, root, name, reduce, noquotes, hexify)
    table.tohandle (handle, root, name, reduce, noquotes, hexify)
    table.serialize({ a = 2, [3] = "b", [true] = "6" }, nil, true)
    -- t={[3]="b",["a"]=2,[true]="6",}
    
    -- 比较
    local b = table.identical (one, two)
    local b = table.are_equal (one, two)
    
    -- 哈希
    local hashed = table.tohash(indexed)
    local indexed = table.fromhash(hashed)
    table.tohash({ "a", "b", "c" })
    -- t={["a"]=true,["b"]=true,["c"]=true,}
    table.fromhash({ a = true, b = false, c = true })
    -- t={ "c", "a" }
    
    -- 
    local swapped = table.swapped(indexedtable)
    table.swapped({ "a", "b", "c" })
    -- t={["a"]=1,["b"]=2,["c"]=3,}
    
    -- 正反
    local reversed = table.reversed (indexedtable)
    local reverse = table.reverse(indexedtable)
    
    -- 镜像
    local mirrored = table.mirrored (hashedtable)
    table.mirrored({ a = "x", b = "y", c = "z" })
    -- t={["a"]="x",["b"]="y",["c"]="z",["x"]="a",["y"]="b",["z"]="c",}
    
    -- 增添
    table.append (one, two)
    table.prepend(one, two)
    
    -- 原位合并
    table.merge(one, two, ...)
    table.imerge(one, two, ...)
    -- 拷贝合并
    local merged = table.merged(one, two, ...)
    local merged = table.imerged (one, two, ...)
    
    -- 拷贝
    local copy = table.copy(t)
    local copy = table.fastcopy(t) --不检查循环引用
    
    -- 展平(一层)嵌套,用merge也可以做到
    local flattened = table.flatten(t)
    
    -- 键值小写
    local normalized = table.loweredkeys { a = "a", A = "b", b = "c" }
    
    -- 包含判断
    if table.contains(t, 5 ) then ... else ... end
    if table.contains(t,"5") then ... else ... end
    
    -- 去重、唯一化
    local t = table.unique { 1, 2, 3, 4, 3, 2, 5, 6 }
    
    -- 计算长度、元素个数(索引表用#t更快)
    local n = table.count(t)
    
    -- 序列化(比如供打印)
    print(table.sequenced(t, separator))
    

    math#

    除了内置函数,还提供:round, odd, even, div, mod, sind, cosd and tand

    boolean#

    -- 转0、1
    local state = boolean.tonumber(str)
    
    -- 文本转布尔(全局也可以用,或string.toboolean,如果tolerant=true,字符串true, yes, on, 1, t和数字1都转为布尔true)
    local b = toboolean(str)
    local b = toboolean(str,tolerant)
    
    -- 判断是布尔,或string.is_boolean
    if is_boolean(str)then ... end
    if is_boolean(str,default) then ... end
    

    string#

    Lua:

    string.byte("luatex") --108
    string.char(65,66,67) --ABC
    
    -- 切片
    local slice = string.sub(str,first,last)
    
    -- 替换
    local new, count = string.gsub(old,pattern,replacement,howoften)
    string.gsub("abcdef","[bc]",string.upper) --aBCdef
    
    -- 查找
    local first, last = find(str,pattern)
    if find("luatex","tex") then ... end
    
    -- 逐一匹配
    local a, b, c, ... = string.match(str,pattern)
    for s in string.gmatch(str,"([^,%s])+") do
        print(s)
    end
    
    -- 大写、小写
    string.lower("LOW")
    string.upper("upper")
    
    -- 模板格式化
    local s = format(format, str, ...)
    

    CONTEXT:

    -- 行修剪
    utilities.strings.striplines(str,"collapse")
    utilities.strings.striplines(str,"prune and collapse")
    utilities.strings.striplines(str,"prune and no empty")
    utilities.strings.striplines(str,"prune and to space")
    utilities.strings.striplines(str,"retain")
    
    -- 强大的格式化formatter(略)
    
    -- 修剪头尾
    local s = string.strip(str)
    
    -- 分切
    local t = string.split(str, pattern)
    local t = string.split(str, lpeg) --多个分切符号`lpeg.S(",.")`
    local t = string.checkedsplit (str, lpeg)
    local t = string.splitlines(str)
    
    -- 引号
    local q = string.quoted(str)
    local u = string.unquoted(str)
    
    -- 计数
    local n = count(str,pattern)
    string.count("test me", "e") -- 2
    
    -- 限制长度
    print(limit(str,max,sentinel)
    string.limit("too long", 6) --too...
    
    -- 判断为空
    if is_empty(str) then ... end
    
    -- 转义
    local e = escapedpattern(str, simple)
    local p = topattern(str, lowercase, strict)
    

    UTF#

    local b = utf.byte("å")
    local c = utf.char(0xE5)
    
    -- 切片
    utf.sub("123456àáâãäå",1,7)
    
    -- 长度
    local l = utf.len("ÀÁÂÃÄÅàáâãäå")
    
    -- 迭代
    for u in utf.values(str) do
        ... -- u is a number
    end
    for c in utf.characters(str) do
        ... -- c is a string
    end
    
    -- 转码
    utf.ustring("ù")
    -- U+000F9
    utf.xstring(0xE6)
    -- 0x000E6
    utf.xstring("à")
    -- 0x000E0
    utf.tocodes("òóö","+")
    -- 0x00F2+0x00F3+0x00F6
    
    -- 切分成表
    utf.split utf.splitlines utf.totable
    
    -- 计数
    utf.count("äáàa",lpeg.P("á") + lpeg.P("à")) --2
    
    -- 映射器、替换器remapper replacer substituter
    local remap = utf.remapper { a = 'd', b = "c", c = "b", d = "a" }
    print(remap("abcd 1234 abcd"))
    
    -- 判断值有效(主要是外部输入)
    if utf.is_valid(data) do ... end
    

    number#

    -- 转比特
    number.tobitstring(2013)
    
    -- 检查数字有效
    number.valid(12)
    number.valid("ab",56)
    

    LPEG patterns 格式#

    类似于正则表达式,细节见l-lpeg.lua

    local splitter = lpeg.splitat("*")
    local n, m = lpeg.match(splitter,"2*4")
    local n, m = lpeg.match(splitter,"2*4")
    

    IO#

    local f = io.open(filename)
    f:read(...)
    f:write(...)
    f:lines()
    f:close()
    
    io.loaddata(filename,textmode)
    io.savedata(filename,str)
    io.savedata(filename,tab,joiner)
    

    File、Dir、URL、OS#

    os.time()
    os.gettimeofday()
    os.runtime()
    

    LUA 界面代码 interface code#

    字体#

    Nodes#

    引擎提供的所有node助手都映射在nodes命名空间下。node详情参见 LUATEX manual

    由于历史原因,nodes下的一些助手与node下的近似。

    结点、列表追踪:

    \setbox0\hbox{我在这里。It's in \hbox{\bf all} those nodes.}
    
    \startluacode
    --输出显式
    context.copy(false,0)
    
    --结点追踪
    context.par()
    context(nodes.toutf(tex.box[0]))
    context.par()
    context(nodes.toutf(tex.box[0].list))
    
    --列表追踪
    context.par()
    context(nodes.listtoutf(tex.box[0]))
    
    context.par()
    context(nodes.listtoutf(tex.box[0].list))
    
    --显式glyphs结点信息
    context.par()
    context(nodes.tosequence(tex.box[0]))
    context.par()
    context(nodes.tosequence(tex.box[0].list))
    
    --汇总相同结点信息
    context.par()
    context(nodes.idstostring(tex.box[0]))
    context.par()
    context(nodes.idstostring(tex.box[0].list))
    
    --结点计数
    context.par()
    context(nodes.countall(tex.box[0]))
    context.par()
    context(nodes.countall(tex.box[0].list))
    
    --查看结点类型和id
    context.par()
    context(node.type(1))
    context.par()
    context(node.id("vlist"))
    

    结点类型的id与字符串映射表,大类:

    • nodes.nodecodes regular nodes, some fo them are sort of private to the engine. underscores:
      • nodes.nodecodes.glyph
      • nodes.nodecodes.kern
      • nodes.nodecodes.glue
    • nodes.noadcodes math nodes that later on are converted into regular nodes

    小类:

    • nodes.listcodes subtypes of hlist and vlist nodes
    • nodes.kerncodes subtypes of kern nodes
    • nodes.gluecodes subtypes of glue nodes (skips)
    • nodes.glyphcodes subtypes of glyph nodes, the subtype can change
    • nodes.mathcodes math specific subtypes
    • nodes.fillcodes these are not really subtypes but indicate the strength of the filler
    • nodes.whatsitcodes subtypes of a rather large group of extension nodes

    遍历并判断结点类型:

    -- 本地变量要比原变量快得多
    local glyph_code = nodes.nodecodes.glyph
    local kern_code = nodes.nodecodes.kern
    local glue_code = nodes.nodecodes.glue
    --for n in node.traverse_id(glyph_code,list) do
    for n in nodes.traverse(list) do
        local id == n.id
        if id == glyph_code then
            ...
        elseif id == kern_code then
            ...
        elseif id == glue_code then
            ...
        else
            ...
        end
    end
    

    新建结点、结点池操控函数:

    • node.new
    • local g = nodes.pool.glyph(fnt,chr) --根据字体id取字符

    Resolvers、math、grph、MetaPost、LuaTEX、Tracing#

    Scanners#

    定义直接与Lua交互的宏。

    实现一个界面:

    \startluacode
    interfaces.implement {
        name    = "MyMacroA",
        public    = true,
        permanent = false,
        arguments = "string",
        actions    = function(s)
            context("(%s)",s)
        end,
    }
    \stopluacode
    -- \def\MyMacro #1 {... \clf_MyMacroA{ #1 } ...}
    
    \startluacode
    interfaces.implement {
        name    = "MyMacroJ",
        public    = true,
        arguments = "box",
        actions    = function(b)
            context(b)
        end,
    }
    \stopluacode
    -- \MyMacroJ \hbox{\strut Test 1}
    -- \MyMacroJ \hbox to 4cm {\strut Test 2}
    
    \startluacode
        interfaces.implement {
        name    = "MyMacroL",
        public    = true,
        arguments = {
            "hbox",
            "vbox",
        },
        actions    = function(h,v)
            context(h)
            context(v)
        end,
    }
    \stopluacode
    -- \MyMacroL{\strut Test 1h} to 10mm {\vfill Test 1v\vfill}
    -- \MyMacroL spread 1cm {\strut Test 2h} to 15mm {\vfill Test 2v\vfill}
    

    控制台print中文乱码的问题#

    临时改变code page为utf8chcp 65001

    样例#

    hyphenation#

    导出luatex的断词信息。

    This will write to hyphens-\jobname.txt a dump of all the characters that luatex has been asked to add hyphenation points to from this point on in the source, with the output of each callback call on a single line.

    {\catcode`\#=12
    \directlua {
    local words = io.open('hyphens-' .. tex.jobname .. '.txt', 'w');
    local outchar = unicode.utf8.char
    local function dumphyphens (head)
       local data = {}
       for v in node.traverse(head) do
           if v.id == node.id('glyph') then
             data[#data+1] = outchar(v.char);
           elseif v.id == node.id('disc') then
              data[#data+1] = '-'
           elseif v.id == node.id('glue') then
             data[#data+1] = outchar(32)
           elseif v.id == node.id('hlist') then
             data[#data+1] = dumphyphens(v.list)
           end
       end
       return table.concat(data)
    end
    callback.register ('hyphenate', function (head,tail)
       lang.hyphenate(head, tail) 
       words:write (dumphyphens(head) .. outchar(10))
       end)
    }}
    
    \input knuth
    \bye
    

    TEX#

    本部分主要学习如下资料的笔记:

    • The TeXbook, by Donald E. Knuth, 2021
    • TeX by Topic, A TeXnicians Reference, by Victor Eijkhout, 2019
    • TeX Reference Manual, by David Bausum, 2002

    \relax#

    命令解析终止标记,以免有歧义(有些命令还能接收更多参数)。

    glue 胶#

    TEX中的内容都以盒子的形式存在,水平盒子之间由胶连接,还有其他的元素,构成水平列表;水平列表再如此构成垂直列表。

    Glue如同弹簧一样,有三个属性:自然空间(space);拉伸(stretch)能力;收缩(shrink)能力。只有当行、列版面糟糕的时候才会按比例拉伸和收缩。拉伸值最终可能被系统突破,而收缩值不会被突破。glue一旦设定(setting)后,就不变了。

    • \smallskip,等于\vskip3pt plus1pt minus1pt,或\hskip3pt plus1pt minus1pt
    • \medskip,2*\smallskip
    • \bigskip,2*\medskip
    • \vskip⟨glue⟩,glue:⟨dimen⟩ plus⟨dimen⟩ minus⟨dimen⟩,自然值必须指定,加减值默认为0
    • \hskip
      • \enskip,等于\hskip.5em\relax
    • \vfil,无限扩展胶,等于\vskip0pt plus \vsize
      • \centerline,居中,两端加无限制胶
    • \vfill、\vfilll,与\vfil共用时,拉伸强度不同
    • \vss、\hss,无限扩展和收缩胶
    • \hfil、\hfill、\hfilll
    • \hfilneg and \vfilneg取消\hfil and \vfil的效果
    • fil、fill、filll作为尺寸,\vskip 0pt plus 1fil minus 1fil

    标点后的glue有特殊规定。

    行间距:

    • \baselineskip=⟨glue⟩
    • \lineskip=⟨glue⟩
    • \lineskiplimit=⟨dimen⟩

    空白系数 space factor f

    水平列表开头的空白系数是1000,在一个非字符盒子或数学公式推送到列表上之后,也会再设置为1000。

    可以这样指定\spacefactor=⟨number⟩空白系数。不过,一般是在简单字符盒子(simple character box)加入列表时才设置为非1000的值。 每个字符都有空间系数代码g,进入列表时会设置为当前空间系数。但是,如果g是0,则f不变;如果f < 1000 < g,则空间系数将设置为1000。 (也就是说f不会从1000之下一步跳到1000之上。)空间指数最大是32767。

    重载字体词间距与伸缩:

    • \spaceskip,伸展f/1000倍,收缩1000/f倍
    • \xspaceskip,当前space factor f >= 2000时使用(space factor f 通常是1000)
    • \sfcode‘)=0 \sfcode‘.=3000,个别字符的空间系数指定
    • \frenchspacing重置为1000

    指定盒子的伸缩(相对于自然宽度):

    \hbox spread 5pt{‘‘Oh, oh!’’ ... laughed.}

    \kern 字间#

    字间类似glue,但不能伸缩,不会作为断行点。

    #

    \line{}生成宽度为\hsize的行。