日文的一行里没有词间空格,可是用 LaTeX 排出来的日文段落两端却整整齐齐。奥妙在于一种叫 \kanjiskip 的胶:它的自然宽度为零,却可以伸展——在 jsarticle 十点下实测为 0.0pt plus 0.92473pt minus 0.0924pt。平时它根本不存在;一旦需要把行收紧或撑开,它就从每两个字之间一点点冒出来。西文排版交给词间空格去做的事,日文把它稀薄地摊到行内每一个字与字的接缝上。本页就沿着这套机制走一遍——日文胶、行首行尾的禁则、竖排与 ruby——这些都是 TeX 为日文增设、西文没有对应物的装置,并附上实测数字。
\kanjiskip 与 \xkanjiskip:你没打出来的那个空白
日文排版会自动插入两种胶。\kanjiskip 位于两个日文字符之间;\xkanjiskip 位于日文与拉丁文字的交界处。决定性的差别在自然宽度:jsarticle 十点下,\kanjiskip 是 0.0pt plus 0.92473pt minus 0.0924pt,\xkanjiskip 是 2.5pt plus 1.49994pt minus 0.59998pt。前者默认为零,只在必要时伸展——所以字面网格不乱,行却依然齐整。后者恒为 2.5pt——所以写下 日本語 and abc 时,虽然没有敲空格,英文词的两侧却自动张开。把这两者分清之后,关于日文字距的绝大多数争论都可以化约为一句"我们说的是哪一种胶"。
% measure it yourself: put this in the body and read the .log
\typeout{kanjiskip = \the\kanjiskip}
\typeout{xkanjiskip = \the\xkanjiskip}
\sbox0{日本語テキスト}\typeout{plain = \the\wd0}
\sbox0{日本語ABCテキスト}\typeout{mixed = \the\wd0}
\sbox0{ABC}\typeout{latin = \the\wd0}实测下来数字对得上。jsarticle 十点下,日本語テキスト 宽 64.72778pt,ABC 宽 21.8056pt。相加应得 86.53338pt,可是 日本語ABCテキスト 实测为 91.53339pt。相差的 5.00001pt,正是 2.5pt 的 \xkanjiskip 出现在两处:語 与 A 之间、C 与 テ 之间。为证,先声明 \noautoxspacing 再量同一个盒子,结果回到 86.53339pt,恰好是相加之值。还有一例:日本語 实测 27.74048pt,正好是单个汉字 9.24683pt 的三倍——字与字之间明明有 \kanjiskip,宽度却没有增加,因为它的自然宽度是零。
| 参数 | pTeX 系写法 | LuaTeX-ja 写法 | jsarticle 十点下的默认值 |
|---|---|---|---|
kanjiskip | \kanjiskip=... | \ltjsetparameter{kanjiskip=...} | 0.0pt plus 0.92473pt minus 0.0924pt |
xkanjiskip | \xkanjiskip=... | \ltjsetparameter{xkanjiskip=...} | 2.5pt plus 1.49994pt minus 0.59998pt |
prebreakpenalty | \prebreakpenalty 加字符 | \ltjsetparameter{prebreakpenalty={...}} | 标点与闭括号为 10000 |
postbreakpenalty | \postbreakpenalty 加字符 | \ltjsetparameter{postbreakpenalty={...}} | 开括号为 10000 |
jcharwidowpenalty | \jcharwidowpenalty=... | \ltjsetparameter{jcharwidowpenalty=...} | 500 |
inhibitxspcode | \inhibitxspcode 加日文字符 | \ltjsetparameter{jaxspmode={...}} | 全角开括号 2,闭括号与句号 1,汉字 3 |
xspcode | \xspcode 加西文字符 | \ltjsetparameter{alxspmode={...}} | A 为 3,. 为 2,( 为 1 |
这些默认值随文档类而变,而这正决定了版面的性格。在 jlreq 下做同样的测量,\kanjiskip 是 0.0pt plus 2.5pt,\xkanjiskip 是 2.5pt plus 2.5pt minus 1.25pt。由于 jlreq 十点下一个汉字正好 10pt,其伸展量就是全角的四分之一——是 jsclasses 那十分之一的 2.5 倍。也就是说,jlreq 的设计是以更大幅度撑开字距来对齐行,行末的收进与推出行为会有肉眼可见的差别。若换了文档类后觉得"排版气质变了",请先用 \typeout 把这两个值打出来比较。
为什么全角括号周围不加空隙:全角半角与 \inhibitxspcode
因为括号自己声明了"这一侧不要加空隙"。\inhibitxspcode 为每个日文字符指定一个 0 到 3 的值,控制 \xkanjiskip 如何附着;在 jsarticle 的默认里,全角开括号 ( 是 2,闭括号 ) 与句号 。 是 1,普通汉字是 3。一量便知:日本語(ABC)テキスト 实测 105.02704pt,正好是九个全角字符(9 × 9.24683pt)加上 ABC 的 21.8056pt——一处 \xkanjiskip 也没有。全角括号本身就在自己的方框里带着留白,再加 2.5pt 显然过头,所以由括号一方来禁止。遵守稿件惯例——英文字母与数字用半角,日文标点与括号用全角——不只是外观问题,更是让这套间距逻辑正确生效的前提。往日文里塞一个半角 (,接管的就是 \xspcode 一侧的规则(( 为 1),间距会完全不同。
% (u)pLaTeX: inspect and change how xkanjiskip attaches to one character
\typeout{open paren = \the\inhibitxspcode`(} % 2 in jsarticle
\typeout{close paren = \the\inhibitxspcode`)} % 1 in jsarticle
\inhibitxspcode`(=3 % allow the gap on both sides after all
% LuaLaTeX: same idea, other name -- and NO space after the comma
\ltjsetparameter{jaxspmode={`(,preonly}}这里藏着在 pLaTeX 与 LuaLaTeX 之间搬运设置时的陷阱:\inhibitxspcode 的 1 与 2,在 LuaTeX-ja 的 jaxspmode 里编号正好相反。 实测可证。在 pTeX 一侧把汉字 語 的 \inhibitxspcode 设为 1,A語 的空隙消失(19.24684pt → 16.74684pt),而 語A 保留。可是在 LuaTeX-ja 里把同一个字的 jaxspmode 设为 1,结果相反:空隙从 語A 消失(19.24713pt → 16.74713pt),却在 A語 保留。查 LuaTeX-ja 的源码,它给这些模式起了名字:1 是 preonly,2 是 postonly,含义是"只允许在该日文字符之前/之后加空隙"。pTeX 的 1 意为"只允许在其后"。两套体系在措辞上一致,在数字上相反。 机械地照抄设置,空隙就会落到相反的一侧。搬运时请写 preonly / postonly 这样的名字,而不是数字。但在 LuaTeX-ja 一侧要留意:逗号后面不能加空格。 一旦加了,TeX Live 2024 会报 ! Missing number, treated as zero.,随后仍按 0 生效——两侧的空隙都被抑制,而不只是一侧。
禁则处理:\prebreakpenalty 与 \postbreakpenalty
禁则就是不让特定字符出现在行首或行尾的规则。 标点 。、、闭括号 )」』 以及小假名 ゃ ぁ 不能出现在行首;开括号 (「『 不能出现在行尾。哪个字符讨厌哪一侧,由日文字体度量(JFM)与文档类共同给出,而 TeX 把它实现为惩罚值而非禁令。在句号 。 或闭括号 ) 之前断行,这个字符就会掉到下一行行首——所以 jsarticle 把 \prebreakpenalty。 与 \prebreakpenalty) 都设为 10000。反过来,在开括号 ( 之后断行,括号会被孤零零留在行尾,于是 \postbreakpenalty( 也是 10000。按 TeX 的惯例,10000 表示无穷大的惩罚,也就是彻底禁止。这样设计的好处是禁则可以分级:想"绝对避免"就写 10000,想"尽量避免"就写 200,断行算法会把它与其他因素一起权衡。西文排版根本没有禁则这个概念,这是 pTeX 为 TeX 增设的机制。
% read the shipped values, then relax one of them
\typeout{full stop = \the\prebreakpenalty`。} % 10000
\typeout{open paren = \the\postbreakpenalty`(} % 10000
\prebreakpenalty`〜=200 % discourage, do not forbid
\jcharwidowpenalty=1000 % dislike a lone character on the last line
% the LuaTeX-ja spelling of the same three settings
\ltjsetparameter{prebreakpenalty={`〜, 200}}
\ltjsetparameter{jcharwidowpenalty=1000}正因为写成惩罚值,日文排版才能长出一个西文完全没有对应物的参数:\jcharwidowpenalty,在 jsarticle 与 ltjsarticle 中默认都是 500。它是"段落最后一行只剩一个日文字符"——西文孤行的日文版——所受的权重。要点在于 500 是个中庸值:不是绝对禁止,而是"值得把其他行稍微收紧来避免,但不值得为此毁掉整个版面"。实务上,遇到怎么排都会剩下一个字的段落时,请先试 \jcharwidowpenalty=1000,再考虑改稿。若仍不见效,往往把文字增删一个字反而更快。
竖排怎么开启:tate 选项与 plext
如今是用文档类选项来切换。 \documentclass[tate]{jlreq} 在 uplatex 和 lualatex 下都能跑,\documentclass[tate]{ltjsarticle} 在 LuaLaTeX 下可用。pLaTeX 系还带有专用的竖排文档类 utarticle / utbook,直接用 uplatex 就能编译。竖排对 TeX 之所以特殊,不只是因为行进方向与字的堆叠方向互换,更因为同一个字要求不同的字形。标点与括号在竖排中必须重新摆位、旋转。所以 pTeX 从一开始就备有两套字体编码——横排用 JY 与竖排用 JT——在声明了 \tate 的盒子里会自动改用 JT 一侧。这一整块,西文排版里没有任何对应物。
% vertical writing, three ways that all compile on TeX Live 2024
\documentclass[tate,uplatex]{jlreq} % uplatex; also works under lualatex
\documentclass[tate]{ltjsarticle} % lualatex
\documentclass[uplatex]{utarticle} % uplatex, dedicated vertical class
% tate-chu-yoko: a short horizontal run inside vertical text
平成\tatechuyoko{31}年 % jlreq only -- under uplatex or lualatex
平成\rensuji{31}年 % plext (pLaTeX) and lltjext (LuaTeX-ja)
% zw is the width of one full-width character, zh its height;
% \begin{minipage}<t>{10zw} sets a vertical box ten characters wide在竖排文本中把两位数字或单位正立并横向排列,称为纵中横(tate-chu-yoko)。写法随路线而异。定义 \tatechuyoko{31} 的只有 jlreq,只要用 jlreq,在 uplatex 与 lualatex 下都能用;ltjsarticle 里没有它,在那里写 \tatechuyoko 会得到 ! Undefined control sequence.。LuaTeX-ja 一侧的写法是 lltjext(或竖排专用的 ltjtarticle、ltjtbook)提供的 \rensuji{31},pLaTeX 则用 plext 包的 \rensuji{31}。plext 本是 ASCII 为竖排准备的扩展,除 \rensuji 外还负责表格、盒子及其排版方向——它的标志性写法是尖括号参数,如 \begin{minipage}<t>{10zw},用来显式指定方向;要在竖排文档里插入横排图表,就靠它。LuaTeX-ja 一侧的对应物是 lltjext,无论命名还是参数写法都近乎照搬。竖排出问题时,先确认文档类是否接受 tate 选项,再确认是否加载了 plext / lltjext,是最快的排查路径。
Ruby(假名注音)与圈点:该选哪个宏包
当下的答案是:(u)pLaTeX 用 pxrubrica,LuaLaTeX 用 luatexja-ruby。二者写法都是 \ruby{...}{...},但 pxrubrica 区分 JIS X 4051 与 JLReq 所定义的两类 ruby,而且要你自己选。竖线是把读音按字切分的记号:\ruby{漢字}{かん|じ} 是单字 ruby(每个汉字各配一段读音)。组 ruby——整个词配一串连续假名——必须明确要求:\ruby[g]{漢字}{かんじ}。若给两字词一段不分割的读音,又不加选项,就会得到 ! Package pxrubrica Error: Group count mismatch between the ruby and the body (2 <> 1).。圈点(傍点)同样来自该宏包:\kenten{重要}。若只想轻量应付,jsclasses 附带的 okumacro 里的 \ruby 就够用——它一律是组 ruby,所以 \ruby{漢字}{かんじ} 直接可用。要留意的是,CTAN 上还另有一个名为 ruby 的宏包——那是 Werner Lemberg 为 CJK 宏包所写的 ruby(TeX Live 2024 内含 4.8.5 版)——出身完全不同的另一支。
| 宏包 | 路线 | 能做什么 |
|---|---|---|
pxrubrica | pLaTeX / upLaTeX / LuaLaTeX / XeLaTeX | 区分单字与组 ruby,控制悬挂方式,并用 \kenten 加圈点 |
luatexja-ruby | 仅 LuaLaTeX | 与 LuaTeX-ja 融为一体的 ruby,较少干扰断行 |
okumacro | pLaTeX / upLaTeX | 随 jsclasses 附带;快速的 \ruby 与 \kenten,没有分割语法 |
ruby | CJK 宏包一系(pdfLaTeX 等) | Werner Lemberg 的另一支;不属于日文 TeX 体系,请勿混用 |
\usepackage{pxrubrica}
...
\ruby{漢字}{かん|じ} % mono ruby: the bar splits the reading per character
\ruby[g]{漢字}{かんじ} % group ruby: one run over the word -- [g] is required
\kenten{重要} % emphasis dots beside each character这里介绍一个不报错却坏掉的事故。pxrubrica 与 okumacro 都定义了 \ruby,同时加载连一句警告都没有——后加载的那个会默默覆盖前者。在 pxrubrica 之后加载 okumacro,再排 \ruby{漢字}{かん|じ},かん|じ 里的竖线不再被当作单字 ruby 的分隔符,而是原样印成一条全角竖线(单用 pxrubrica 时读音会正确地分给两个汉字)。日志里什么也不会显示,只有看 PDF 才能发现。定义 \ruby 的宏包只能加载一个。 如果你用 jsclasses 并已加载 okumacro,改用 pxrubrica 时请把那一行删掉。
JLReq 是什么:W3C《日文排版需求》与 jlreq 文档类
JLReq 是 W3C 发布的《日文排版需求》(Requirements for Japanese Text Layout) 文档,把日文的版面设计、行内排版、禁则、ruby 与标点处理,以实现者可读的形式系统写下。在此之前,这些知识散落于印刷厂的经验之中,所以它的公开意义重大:从网页浏览器到电子书阅读器,如今都参照同一份文档。jlreq 文档类就是它在 LaTeX 上的实现,作者是 Noriyuki Abe。TeX Live 2024 内含的类文件开头明确写着所依据的版本:(based on JLReq 20200811)。因此选用 jlreq 不只是"换一个较新的文档类",而是把版面设计的依据放在一份公开文档上。
实务上的结果是:细部调整不必自己再写一遍。 竖排只需一个 tate 选项,\jlreqsetup 允许按名称设定行内排版参数,禁则与 ruby 的默认值也已按 JLReq 给定。反过来,把现有的 jsarticle 文档原样换成 jlreq,\kanjiskip 的伸展幅度变大,行的疏密随之改变,页数都可能变动。一条稳妥的界线是:新开的文档用 jlreq,要保持既有稿件外观就留在 jsclasses。 无论选哪一个,本页看过的 \kanjiskip、\xkanjiskip、\prebreakpenalty、\jcharwidowpenalty 都以同名存在,最后那点微调随时都在你手里。