% \iffalse meta-comment % % File: LuaCJK.dtx % ----------------------------------------------------------------------- % Copyright (C) 2026 by Mingyu Xia * % ----------------------------------------------------------------------- % This work may be distributed and/or modified under the conditions * % of the LaTeX Project Public License (LPPL), either version 1.3c of * % this license or (at your option) any later version. * % The latest version of this license is in * % * % http://www.latex-project.org/lppl.txt * % * % and version 1.3c or later is part of all distributions of LaTeX * % version 2008 or later. * % * % This work has the LPPL maintenance status `maintained'. * % * % The Current Maintainer of this work is Mingyu Xia. * % ----------------------------------------------------------------------- % This work consists of the files LuaCJK.dtx, * % LuaCJK.ins, * % the derived files LuaCJK.sty, * % LuaCJKfntef.sty, * % LuaCJK.lua, * % LuaCJK-core.lua, * % the documentation files LuaCJK.pdf, * % and README.md, * % and LICENSE. * % ----------------------------------------------------------------------- % % Any modification of this file should ensure that the copyright and * % license information is placed in the derived files. * % * % ----------------------------------------------------------------------- % %<*internal> \iffalse % % %<*readme> [![CTAN Version](https://img.shields.io/ctan/v/luacjk)](https://ctan.org/pkg/luacjk) [![GitHub Release](https://img.shields.io/github/v/release/myhsia/LuaCJK)](https://github.com/myhsia/LuaCJK/releases/latest) [![GitHub Last Commit](https://img.shields.io/github/last-commit/myhsia/LuaCJK)](https://github.com/myhsia/LuaCJK/commits) [![Actions Status](https://github.com/myhsia/LuaCJK/workflows/Automated%20testing/badge.svg)](https://github.com/myhsia/LuaCJK/actions) [![GitHub Repo stars](https://img.shields.io/github/stars/myhsia/LuaCJK)](https://github.com/myhsia/LuaCJK) # LuaCJK LuaCJK is a LuaTeX package for Chinese typesetting. The package provides a CJK font family layer, CJK/CJK and CJK/Latin spacing, the `quanjiao`, `plain`, `banjiao`, `kaiming`, `CCT`, and `hangmobanjiao` punctuation styles with line-breaking prohibitions, and fallback fonts, all on top of LuaTeX node callbacks. The companion package `LuaCJKfntef` adds underlining, emphasis dots, and strike-through through `lua-ul`. `LuaCJK-listings` keeps CJK text in place and two columns wide inside a `listings` block. ## Loading ```tex \usepackage{LuaCJK} \setCJKmainfont{FandolSong-Regular.otf} ``` LuaCJK requires the LuaTeX engine. It must not be loaded together with `luatexja`. ## Tagged PDF Declare `\DocumentMetadata{tagging = on}` before `\documentclass` to produce a tagged PDF. The ink of a decoration is an artifact, a decoration symbol is left out of the text a reader copies, and `LuaCJKfntef`'s `selectable` key keeps a symbol as text. Under `l3doc`, load `latex-lab-testphase-l3doc` as well. The manual is built with tagging enabled. ## Using it with ctex Pass `engine = luacjk` to the `ctex` class or package: ```tex \documentclass[engine = luacjk]{ctexart} ``` `ctex` then inputs `ctex-engine-luacjk.def` instead of its own LuaTeX backend, and that file loads LuaCJK, so the package is not loaded separately. ## License This work may be distributed and/or modified under the conditions of the LaTeX Project Public License, version 1.3c or later. % % %<*package|fntef|listings|ctex> \NeedsTeXFormat{LaTeX2e}[2026/06/01] \RequirePackage { expl3 } \GetIdInfo $Id: LuaCJK.dtx v0.0.2 2026-11-15 myhsia$ % {A LuaTeX CJK typesetting package} %\ProvidesExplPackage {\ExplFileName} % {Font effects for LuaCJK (CTEX)} %\ProvidesExplPackage {LuaCJKfntef} % {CJK support for listings} %\ProvidesPackage {LuaCJK-listings} % {LuaTeX adapter for ctex (LuaCJK)} %\ProvidesExplFile {ctex-engine-luacjk.def} % {\ExplFileDate} {\ExplFileVersion} {\ExplFileDescription} % {\ExplFileDate} {\ExplFileVersion} {\ExplFileDescription} % {\ExplFileDate} {\ExplFileVersion} {\ExplFileDescription} % [\ExplFileDate\space\ExplFileVersion\space\ExplFileDescription] %\edef \DocFileDate {\ExplFileDate} %\edef \DocFileVersion {\ExplFileVersion} %\edef \DocFileDescription {\ExplFileDescription} %\tl_greplace_all:Nnn \DocFileDate { / } { - } % % %<*internal> \fi % % %<*driver> \DocumentMetadata{tagging = on, pdfversion = 1.7, lang = zh-CN} \documentclass[11pt, letterpaper]{l3doc} \setlength{\emergencystretch}{3em} \usepackage{latex-lab-testphase-l3doc} \providecommand*\LuaLaTeX{\hologo{LuaLaTeX}} \providecommand*\XeLaTeX {\hologo{XeLaTeX}} \usepackage{xurl} \usepackage[color = 00498F, ratio = .67]{docext} \hypersetup{ pdfstartview = FitH, unicode = true, pdfauthor = Mingyu Xia } \newcommand \mail[1] {\href{mailto:#1}{\ttfamily #1}} \usepackage[engine = luacjk, fontset = fandol, linespread = 1]{ctex} \setmainfont{texgyrepagella}[ Extension = .otf, UprightFont = *-regular, BoldFont = *-bold, ItalicFont = *-italic, BoldItalicFont = *-bolditalic] \setsansfont{texgyreheros}[ Extension = .otf, UprightFont = *-regular, BoldFont = *-bold, ItalicFont = *-italic, BoldItalicFont = *-bolditalic] \setmonofont{cmun}[ Extension = .otf, UprightFont = *btl, BoldFont = *tb, ItalicFont = *bto, BoldItalicFont = *tx, HyphenChar = None] \usepackage{unicode-math} \setmathfont{texgyrepagella-math.otf} \NewDocumentCommand \LuaCJKdocsetup { } { \LuaCJKsetup{AutoFakeBold = false, AutoFakeSlant = false} \setCJKmainfont[ Language = Chinese Simplified, ItalicFont = LXGWWenKaiGBLite-Regular.ttf ]{Noto Serif CJK SC} \setCJKsansfont[Language = Chinese Simplified]{Noto Sans CJK SC} \setCJKmonofont[Language = Chinese Simplified] {LXGWZhuqueFangsong-Regular.ttf} \setCJKfamilyfont{zhsong}[Language = Chinese Simplified] {Noto Serif CJK SC} \setCJKfamilyfont{zhhei}[Language = Chinese Simplified] {Noto Sans CJK SC} \setCJKfamilyfont{zhkai}[Language = Chinese Simplified] {LXGWWenKaiGBLite-Regular.ttf} \setCJKfamilyfont{zhfs}[Language = Chinese Simplified] {LXGWZhuqueFangsong-Regular.ttf} } \LuaCJKdocsetup \AddToHook{env/codemo/after}{\LuaCJKdocsetup} \usepackage{LuaCJKfntef} \LuaCJKsetup{CJKmath = true, LatinPunct = true} \setCJKmathfont[Language = Chinese Simplified]{Noto Serif CJK SC} \renewcommand \contentsname {\texorpdfstring{目录\quad Contents}{目录 Contents}} \renewcommand \abstractname {摘要 \texttt\textbar\ Abstract} \ExplSyntaxOn \tl_new:N \LuaCJKdoclang_tl \tl_set:Nn \LuaCJKdoclang_tl { en } \NewDocumentCommand \LuaCJKdoclang { m } { \tl_set:Nn \LuaCJKdoclang_tl {#1} \cs_if_exist:NT \tagpdfsetup { \str_case:nn {#1} { { zh } { \tagpdfsetup { text / lang = zh-CN } } { en } { \tagpdfsetup { text / lang = en-US } } } } } \cs_set:Npn \theHpart { \tl_use:N \LuaCJKdoclang_tl } \cs_set_protected:Npn \__codedoc_get_hyper_target:nN #1#2 { \__kernel_tl_set:Nx #2 { \tl_to_str:n {#1} } \tl_replace_all:NVn #2 \c_underscore_str { / } \tl_remove_all:NV #2 \c_backslash_str \tl_put_left:Nx #2 { doc / function / \tl_use:N \LuaCJKdoclang_tl / } } \ExplSyntaxOff \begin{document} \DocInput{\jobname.dtx} \clearpage \PrintChanges \PrintIndex \end{document} % % \fi % % \changes{v0.0.1}{2026/11/11}{Provide the CJK font family interface % (\cs{setCJKmainfont}, \cs{setCJKsansfont}, \cs{setCJKmonofont}, % \cs{setCJKfamilyfont}, \cs{CJKfamily}, \cs{newCJKfontfamily}, % \cs{CJKfontspec}); an undeclared family warns once.} % \changes{v0.0.1}{2026/11/11}{Provide \pkg{LuaCJK-listings}, which keeps CJK % characters in place inside a \pkg{listings} block and counts each of them % as two columns; \pkg{LuaCJK} loads it automatically with \pkg{listings}.} % \changes{v0.0.1}{2026/11/11}{Ship \file{ctex-engine-luacjk.def} as the % \LuaTeX\ backend for \pkg{ctex}, selected with the option |engine = % luacjk|; like \pkg{ctex}'s own backend it refuses \pkg{CJKfntef} and % points at \pkg{LuaCJKfntef}.} % \changes{v0.0.1}{2026/11/11}{Refuse to load with \pkg{luatexja}.} % \title{\bfseries % \pkg{LuaCJK} 宏包\\ % The \pkg{LuaCJK} package\thanks{^^A % \url{https://ctan.org/pkg/luacjk},\ % \url{https://github.com/myhsia/LuaCJK}^^A % }^^A % } % \author{Mingyu Xia\thanks{\mail{xiamingyu@westlake.edu.cn}}} % \date{Released \DocFileDate\quad \texttt{\DocFileVersion}} % \maketitle % % \begin{abstract} % \pkg{LuaCJK} 为 \LuaTeX\ 提供中文排版。 % 它把当前的 CJK 字体记录在一个 \LuaTeX\ 属性里, % 并在断行与盒子打包之前替换 CJK 字形节点。 % \setlength \parindent {1.5em}\par % \pkg{LuaCJK} provides Chinese typesetting for \LuaTeX. % It records the current CJK font in a \LuaTeX\ attribute % and replaces CJK glyph nodes before line breaking and box packing. % \end{abstract} % % \tableofcontents % % \LuaCJKdoclang{zh} % \clearpage % % \begin{documentation} % % \section{简介} % % \pkg{LuaCJK} 是一个只用于 \LuaTeX\ 的中文排版宏包。它的主要接口是 % \cs{setCJKmainfont}、\cs{setCJKsansfont}、\cs{setCJKmonofont}、 % \cs{setCJKfamilyfont}、\cs{CJKfamily} 与 \cs{LuaCJKsetup}。 % % 当前的 CJK 字体记录在一个 \LuaTeX\ 属性里, % 并在断行与盒子打包之前替换 CJK 字形节点。 % % \section{加载} % % \begin{quote} % \cs{usepackage}\oarg{options}"{LuaCJK}" % \end{quote} % % \pkg{LuaCJK} 需要 \LuaTeX\ 引擎。它与 \pkg{luatexja} 冲突: % 无论先后加载哪一个,都会报错。 % % \begin{function}{\LuaCJKversion} % 打印 \pkg{LuaCJK} 的版本。 % \end{function} % % \begin{function}{\LuaCJKsetup} % \begin{syntax} % \tn{LuaCJKsetup} \marg{key-value list} % \end{syntax} % 配置 \pkg{LuaCJK}。 % 这些键就是随后各节中记录的那些键。 % \end{function} % % 这些键分为两组。 % % 描述文字如何排版的设置只作用于当前分组, % 并且记录在字符本身上, % 所以分组中的文字即使在该分组已经结束之后才断行,也仍然保留它的设置: % \begin{quote} % |\vbox{\begingroup\LuaCJKsetup{CJKglue = 10pt}中中\endgroup\par}| % \end{quote} % 用的就是 10pt 的胶,尽管分组在 |\par| 之前已经关闭。 % 这一组包含 % \begin{itemize} % \item \keys{CJKglue, CJKecglue, CJKspace} 与 \keys{xCJKecglue}; % \item \keys{PunctStyle, PunctKinsoku, AllowBreakBetweenPuncts, PunctWidth} % 与 \keys{PunctBoundWidth}; % \item \keys{LatinPunct} 与 \keys{CJKmath}; % \item \keys{CheckSingle, WidowPenalty} 与 \keys{CJLineBreak}; % \item \keys{AutoFallBack} 与 \keys{Verb}; % \item 两个命令 \cs{LuaCJKCancelSubCJKBlock} 与 % \cs{LuaCJKRestoreSubCJKBlock}。 % \end{itemize} % % 其余的键是全局的,或者在加载宏包时起作用: % \begin{itemize} % \item \keys{KaiMingPunct, MiddlePunct, LongPunct} % 与 \keys{NoBreakLongPunct} 修改字符集合; % \item \keys{AutoFakeBold, AutoFakeSlant, EmboldenFactor} % 与 \keys{SlantFactor} 在注册字族时起作用; % \item \keys{LoadFandol} 与 \keys{LocalConfig} 在加载宏包时起作用。 % \end{itemize} % 声明 \cs{LuaCJKDeclareCharClass}、\cs{LuaCJKDeclarePunctClass}、 % \cs{LuaCJKDeclareSubCJKBlock}、\cs{LuaCJKsetwidth} 与 \cs{LuaCJKsetkern} % 同样是全局的,这一点在各自的说明里都提到了。 % % 同样的键也可以作为宏包选项给出: % \begin{quote} % |\usepackage[PunctStyle = banjiao]{LuaCJK}| % \end{quote} % % \begin{keyval} % \item[\keys{LocalConfig}]\val{\TTF, name} % 从配置文件读取本地设置。 % \texttt{true} 加载 \file{luacjk.cfg}, % \meta{name} 加载 \file{luacjk-}\meta{name}\file{.cfg}, % 而 \texttt{false}(初始值)什么都不加载。 % 文件在宏包末尾读取,所以它可以使用 \pkg{LuaCJK} 定义的每一个命令。 % 文件不存在时只给出警告。 % \end{keyval} % % 要在 \pkg{ctex} 中使用 \pkg{LuaCJK},把它的 \keys{engine} 选项设为 % \val{luacjk}: % \begin{quote} % |\documentclass[engine = luacjk]{ctexart}| % \end{quote} % \pkg{ctex} 于是载入 \pkg{LuaCJK} 的 |ctex-engine-luacjk.def| 而不是它自己的 % \LuaTeX\ 后端,这个文件同时载入宏包,\pkg{LuaCJK} 不必再单独载入。 % % \pkg{LuaCJK} 与 \LaTeX\ 的标记 PDF(tagged PDF)项目协同工作。 % 文档开头声明 % \begin{quote} % |\DocumentMetadata{tagging = on}| % \end{quote} % 时,装饰的墨迹标记为 \texttt{artifact},装饰符号不会出现在复制和选择 % 的文本中;\keys{selectable = true} 保留符号的文字。 % 在 \cls{l3doc} 文档中还要载入 \pkg{latex-lab-testphase-l3doc}, % 本手册就是这样生成的。 % 选区的高度由 PDF 阅读器按字体的 \texttt{hhea} 上伸部和下伸部决定, % 对 \texttt{Noto Serif CJK SC} 是 1.437em,所以选区比 1em 的方块更高; % 这不是 \pkg{LuaCJK} 能改变的,也不影响字形本身。 % % \section{字体} % % 主要的字体命令接受一个可选的 \pkg{fontspec} 选项列表。 % 选项既可以写在字体名之前,也可以写在字体名之后。 % % \begin{function}{\setCJKmainfont,\setCJKsansfont,\setCJKmonofont} % \begin{syntax} % \tn{setCJKmainfont} \oarg{options} \marg{font} \oarg{options} % \end{syntax} % 为罗马、无衬线与等宽字族设置 CJK 字体。 % \end{function} % % \iffalse %<*example> % \fi \begin{codemo} \setCJKmainfont[Scale = 1.2] {FandolSong-Regular.otf} English 中文字体\\ \setCJKsansfont[Scale = 1.2] {FandolKai-Regular.otf} \sffamily English 中文字体 \end{codemo} % \iffalse % % \fi % % \begin{function}{\setCJKfamilyfont} % \begin{syntax} % \tn{setCJKfamilyfont}\marg{family}\oarg{options}\marg{font}\oarg{options} % \end{syntax} % 定义一个具名的 CJK 字族。 % \end{function} % % \begin{function}{\CJKfamily} % \begin{syntax} % \tn{CJKfamily} \marg{family} % \end{syntax} % 切换到具名的 CJK 字族。 % \end{function} % % \iffalse %<*example> % \fi \begin{codemo} \CJKfamily{zhsong}English 中文字体\\ \CJKfamily{zhhei}English 中文字体\\ \CJKfamily{zhkai}English 中文字体\\ \CJKfamily{zhfs}English 中文字体 \end{codemo} % \iffalse % % \fi % % \begin{function}{\newCJKfontfamily} % \begin{syntax} % \tn{newCJKfontfamily}\oarg{family}\meta{command} % \ \oarg{options}\marg{font}\oarg{options} % \end{syntax} % 定义一个切换到新声明字族的命令。 % 与 \cs{newfontfamily} 一样,该命令只作用于当前分组。 % 选项可以写在字体名之前,也可以写在之后。 % \end{function} % % \begin{function}{\CJKfontspec} % \begin{syntax} % \tn{CJKfontspec} \oarg{options} \marg{font} \oarg{options} % \end{syntax} % 为当前分组选择一个 CJK 字体。 % 选项可以写在字体名之前,也可以写在之后。 % \end{function} % % \begin{function}{\songti, \heiti, \fangsong, \kaishu, \lishu, \youyuan} % 名为 \texttt{zhsong}、\texttt{zhhei}、\texttt{zhfs}、\texttt{zhkai}、 % \texttt{zhli} 与 \texttt{zhyou} 的字族的快捷方式。 % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \newCJKfontfamily[kai]\mykai {FandolKai-Regular.otf} \mykai English 中文字体\\ \songti English 中文字体 \end{codemo} \endgroup % \iffalse % % \fi % % \begin{variable} % {\CJKrmdefault, \CJKsfdefault, \CJKttdefault, % \CJKfamilydefault} 由 \tn{rmfamily}、\tn{sffamily} 与 \tn{ttfamily} % 选择的 CJK 字族,以及由 \tn{normalfont} 选择的字族。 % \cs{CJKfamilydefault} 默认跟随 \cs{CJKrmdefault}。 % \end{variable} % % \subsection{子区块} % % 一种 CJK 字体很少覆盖 \textsc{Unicode} 的每一个区块。 % 子区块让一个范围之内的字符在一个字族里拥有自己的字体; % 这个区块随后成为字体命令的一个选项。 % % \begin{function}{\LuaCJKDeclareSubCJKBlock} % \begin{syntax} % \tn{LuaCJKDeclareSubCJKBlock} \marg{block} \marg{range list} % \end{syntax} % 用给定的范围声明 \meta{block},写法与 \cs{LuaCJKDeclareCharClass} 相同。 % 这个名字随后成为一个字体选项,正如下例所示: % 设置了它的字族对该区块的字符使用第二个字体, % 没有设置它的字族则把这些字符留在主字体里。 % 重叠的区块按声明顺序解决。 % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \LuaCJKDeclareSubCJKBlock{Kana}{3040 -> 30FF} \setCJKmainfont[Kana = FandolKai-Regular.otf] {FandolSong-Regular.otf} 汉字 かんじ \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\LuaCJKCancelSubCJKBlock, \LuaCJKRestoreSubCJKBlock} % \begin{syntax} % \tn{LuaCJKCancelSubCJKBlock} \marg{block list} \\ % \tn{LuaCJKRestoreSubCJKBlock} \marg{block list} % \end{syntax} % 在当前分组的剩余部分关闭或重新打开列出的区块。 % 因为字体的选择是按字体标识符记录的, % 所以提供同一字体的两个字族共享同一个设置: % 最后选择该字体的字族说了算。 % \end{function} % % \subsection{后备字体} % % 一种 CJK 字体很少覆盖需要的每一个字符。 % 当所选的 CJK 字体没有某个字符的字形时,后备字体按声明的顺序依次尝试。 % % \begin{function}{\setCJKfallbackfamilyfont} % \begin{syntax} % \tn{setCJKfallbackfamilyfont} \marg{family} \oarg{options} \\ % \ \marg{font} \oarg{options} % \end{syntax} % 把 \meta{font} 加入 \meta{family} 的后备列表。字族可以用 % \cs{CJKrmdefault} 之类的名字给出。多次调用该命令可以建立一条链; % 第一个拥有该字符的字体胜出。 % 这条链按文字的字重、字形与字号解析, % 所以粗体里的字符取自后备字体的粗体字面, % 放大后的字符取自对应字号的字面。 % \end{function} % % \begin{keyval} % \item[\keys{AutoFallBack}]\val{\TFF} % 是否使用声明过的后备字体。这个开关像间距的那些键一样跟随文字, % 所以可以只在一个分组里打开它。 % \end{keyval} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \setCJKmainfont{FandolSong-Regular.otf} \setCJKfallbackfamilyfont{\CJKrmdefault} {FandolKai-Regular.otf} \LuaCJKsetup{AutoFallBack = true}中あ \end{codemo} \endgroup % \iffalse % % \fi % % \subsection{基线调整} % % 两种 CJK 字体很少是为彼此设计的: % 混排在同一行里,一种字体的字总显得偏高或偏低。 % 一种字体可以带一个 \keys{Baseline} 值, % 把它的字整体向上或向下移动, % 使两种字体立在同一条基线上。 % 这个值属于字体而不属于文字, % 所以它是声明这个字体的那条命令的一个选项。 % % \begin{keyval} % \item[\keys{Baseline}]\meta{dimension} % 把这个字体的字向上移动 \meta{dimension},负值则向下移动。 % 它和其他字体选项一样可以写在后备字体的声明里, % 这也是它最常见的用处:让后备字体贴住主字体的基线。 % 单位可以用任何 \TeX\ 长度单位,|em| 随字号一起变化。 % 两条声明给出同一个字体时,共享后一条声明的值, % 因为字的位置是按字体记录的。 % \end{keyval} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \setCJKfamilyfont{high}[Baseline = .3em] {FandolKai-Regular.otf} \songti 基线\quad\CJKfamily{high}基线 \end{codemo} \endgroup % \iffalse % % \fi % % \subsection{加载 Fandol} % % \begin{keyval} % \item[\keys{LoadFandol}]\val{\TTF} % 当文档没有声明任何 CJK 字体时,在 |\begin{document}| 处加载 % 四款 Fandol 字体:宋体作为主字体, % 黑体作为无衬线字体,仿宋作为等宽字体, % 并且把宋、黑、楷、仿宋全部注册为具名字族。 % \end{keyval} % % \subsection{伪粗体与伪斜体} % % 一种 CJK 字族常常没有粗体或斜体。下面这些键让 \pkg{LuaCJK} 请求 % \pkg{fontspec} 加粗或倾斜常规字体,而不去使用粗体或斜体。 % 它们作用于此后注册的字族,并且在字族自己设置了 \texttt{BoldFont} 或 % \texttt{ItalicFont} 时不生效。它们是全局的: % 分组不会改变一个已经注册的字族所用的设置。 % % \begin{keyval} % \item[\keys{AutoFakeBold}]\val{\TFF, number} % 在请求粗体形状时加粗常规字体。 % 给出一个数字时同时设置 \texttt{EmboldenFactor}。 % \item[\keys{AutoFakeSlant}]\val{\TFF, number} % 在请求斜体形状时倾斜常规字体。 % 给出一个数字时同时设置 \texttt{SlantFactor}。 % \item[\keys{EmboldenFactor}]\val{number}: 加粗的量。 % 初始值是 \texttt{4}。 % \item[\keys{SlantFactor}]\val{number}: 倾斜的量,介于 % $-0.999$ 与 $0.999$ 之间。初始值是 \texttt{0.167}。 % \end{keyval} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \LuaCJKsetup{AutoFakeBold = true} \setCJKmainfont{FandolSong-Regular.otf} \textbf{English 中文字体}\\ English 中文字体 \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\defaultCJKfontfeatures} % \begin{syntax} % \tn{defaultCJKfontfeatures} \marg{font features} % \end{syntax} % 设置前置到此后注册的每一个 CJK 字族的特性。 % 初始值是 |Script = CJK|。 % 在调用之前注册的字族保留它们当时得到的特性。 % \end{function} % % \begin{function}{\addCJKfontfeatures} % \begin{syntax} % \tn{addCJKfontfeatures} \oarg{ignored} \marg{font features} % \end{syntax} % 为当前 CJK 字族添加特性,直到分组结束, % 例如 |\addCJKfontfeatures{Scale = 1.2}|。 % 开头的一个 \texttt{*} 也可以接受;两种形式作用于同一个字体。 % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \defaultCJKfontfeatures{Scale = 1.5} \setCJKfamilyfont{big} {FandolSong-Regular.otf} \CJKfamily{big}中文字体\\ \CJKfamily{zhsong}中文字体\\ \addCJKfontfeatures{Scale = 1.2} \CJKfamily{zhsong}中文字体 \end{codemo} \endgroup % \iffalse % % \fi % % \subsection{数学模式中的汉字} % % \begin{keyval} % \item[\keys{CJKmath}]\val{\TFF}: 是否允许在数学模式里直接输入汉字。 % 打开时,这样的字符用它自己的一个数学字族的 CJK 字体排出, % 而不是到数学字体里去找, % 所以上标或分数里的字符会取那个数学样式的尺寸。 % 可以用 |\LuaCJKsetup{CJKmath = true}| 打开它,也可以用宏包选项 % |CJKmath = true|。 % 此时字符取数学版本(见 \tn{mathversion})的字体: % 在 \texttt{bold} 版本里用字族的粗体,字族没有粗体时保持正体; % 字形取字族的正体,不随周围的正文。 % 不打开它时,数学模式里的汉字会到数学字体里去找,通常就此消失, % 所以这种情况下 \pkg{LuaCJK} 给出一次性的警告。 % \end{keyval} % % \begin{function}{\setCJKmathfont} % \begin{syntax} % \tn{setCJKmathfont} \oarg{options} \marg{font} \oarg{options} % \end{syntax} % 选择数学模式里的字符所用的 CJK 字体。 % 不用这个命令时,使用 CJK 主字体。 % 本手册在前导区打开 \keys{CJKmath} 并设置了这个字体。 % 与其他的字体命令一样,这个选择只作用于当前分组: % 分组里的公式使用该分组里选择的字族。 % \end{function} % % 数学模式里出现的每一个 CJK 字族都被注册成一个独立的数学字族, % 这样 \TeX\ 就能取它的文本、脚本或脚脚本尺寸。 % 字族按数学版本选字体:\texttt{bold} 版本用 \cs{bfdefault},其余版本用 % \cs{mddefault},字形的形状取 \cs{shapedefault}。 % 所以 \cs{boldmath} 会把公式里的汉字换成粗体,而且只作用于当前分组; % 字族没有粗体时保持正体,也不报警告。 % 字族号取自 240--255 这一段,是 \LuaTeX\ 允许的 256 个字族里的最后十六个, % 正常文档不会用到那里;一个文档如果在数学里用到十六个以上的 CJK 字族, % 就会报错停止。 % 脚本尺寸是当前正文尺寸的百分之七十五, % 也就是标准尺寸表里的比例。 % 数学参数不取自 CJK 字体,因为它没有 % \texttt{MathConstants} 表:这些参数仍然是文档数学字体的, % 这就是为什么两个汉字组成的分数用的是周围数学的横线与间距。 % % \iffalse %<*example> % \fi \begin{codemo} $ 天地玄黄 $ and 天地玄黄 \end{codemo} % \iffalse % % \fi % % \section{文本排版} % % \subsection{间距} % % \begin{keyval} % \item[\keys{CJKglue}]\val{glue}: 插入相邻 CJK 字符之间的胶。 % 初始值是 \texttt{0pt plus 0.08\cs{baselineskip}},在每次选择字体时重新计算, % 所以它跟随字号;文档给出的值按原样使用,不随字号变化, % 连同它的伸缩阶(例如 |plus 1filll|)一起保留。 % \item[\keys{CJKecglue}]\val{glue}: 插入 CJK 与拉丁字符之间的胶。 % 标点两侧的胶没有宽度:标点的进距本身就带有间距。 % 初始值是 \texttt{0.3333em plus 0.1667em minus 0.1111em}, % 它同样跟随字号。 % \item[\keys{CJKspace}]\val{\TFF}: 是否保留源文件中 CJK 字符之间的空格。 % 不保留时,空格换成字符之间的胶,断行与伸缩都随之保留。 % \item[\keys{xCJKecglue}]\val{\TFF}: 是否把源文件中 CJK 与拉丁字符之间的 % 空格替换为 \texttt{CJKecglue}。 % \end{keyval} % % \iffalse %<*example> % \fi \begin{codemo} 中文 English 中文 \end{codemo} % \iffalse % % \fi % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \LuaCJKsetup{CJKspace = true}天 地 玄 黄 宇 宙 \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\normalspacedchars} % \begin{syntax} % \tn{normalspacedchars} \marg{character list} % \end{syntax} % 列出的字符两侧都不加胶, % 这样拉丁文字中的连字符或斜线不会把周围的文字拉开。 % 初始列表是 |/|、|\| 与 |-|。这个设置是全局的。 % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} 中x中\par \normalspacedchars{x}中x中 \end{codemo} \normalspacedchars{/,\,,-} \endgroup % \iffalse % % \fi % % 只用于控制文字、本身没有进距的字符 % (U+200B、U+200C、U+200D、U+2060、U+180E、U+FEFF 以及各个变体选择符) % 在输入时就已删除。因此它们既不占胶,也不把两个 CJK 字符分开: % 零宽空格不会加上本来会在它两侧出现的两个 \keys{CJKecglue}, % 而它想提供的断行机会在周围的胶上本来就存在。 % 用 \tn{char} 请求的字符不会被删除,因为 \tn{char} 要的就是字形本身。 % % \begin{environment}{CJKfilltwosides} % 把各行排进一个竖直盒子里, % 盒中的行两端对齐到 \meta{width},各行之间用 |\\| 分开。 % 它的 \meta{position} 参数决定盒子在基线上的位置: % \texttt{t} 表示顶部,\texttt{c} 表示居中(默认),\texttt{b} 表示底部。 % 宽度为零或为负时沿用周围的行宽。 % 带星号的形式行为相同;它不与内容的自然宽度作比较。 % \begin{syntax} % \tn{begin}\{CJKfilltwosides\}\oarg{position}\marg{width} % \ \meta{lines} \\ % \tn{end}\{CJKfilltwosides\} % \end{syntax} % \end{environment} % % \iffalse %<*example> % \fi \begin{codemo} \begin{CJKfilltwosides}{6em} 瞻彼阕者,\\虚室生白 \end{CJKfilltwosides} \end{codemo} % \iffalse % % \fi % % \iffalse %<*example> % \fi \begin{codemo} \begin{CJKfilltwosides*}{0pt} 瞻彼阕者,\\虚室生白 \end{CJKfilltwosides*} \end{codemo} % \iffalse % % \fi % % \subsection{标点} % % \begin{keyval} % \item[\keys{PunctStyle}]\val{style}: 选择一种标点样式。 % 已经实现的样式有 \texttt{quanjiao}(默认)、 % \texttt{plain}、\texttt{banjiao}、\texttt{kaiming}、\texttt{CCT} % 与 \texttt{hangmobanjiao}。 % 未知的名字给出警告并回退到 \texttt{plain}。 % \item[\keys{PunctWidth}]\val{length, false}: 不管样式怎么说, % 给每个标点同样的进距。 % 用 \cs{LuaCJKsetwidth} 为某个标点设置的宽度仍然优先。 % \item[\keys{PunctBoundWidth}]\val{length, false}: 标点出现在行尾或行首时, % 它的进距中有多少留在行内; % 其余的墨迹悬挂到页边,或者悬挂到段落开头的缩进里。 % 默认悬挂标点自带的空白,所以它的墨迹正好落在版心边上; % 断行时这一份悬挂已经算进去,行尾的标点因此能随它所在的行一起放下。 % \texttt{false} 恢复这个默认值。 % \item[\keys{LatinPunct}]\val{\TFF}: 把引号 % (U+2018、U+2019、U+201C、U+201D)、间隔号(U+00B7) % 与省略号(U+2026、U+2027、U+22EF)当作拉丁标点: % 它们留在当前的拉丁字体里,也不被压缩。 % 这样英文单词里的撇号就不会变成一个全角形式。破折号不受影响。 % \item[\keys{PunctFamily}]\val{family, false}: 为标点使用 % 另一个 CJK 字族。 % 该字族必须用 \cs{setCJKfamilyfont} 声明;\texttt{false} % (初始值)让标点重新跟随文字。 % \item[\keys{AllowBreakBetweenPuncts}]\val{\TFF}: 两个相邻的标点是否可以 % 被断行分开。 % 破折号与省略号不在此列,一串连续的破折号或省略号由 % \texttt{NoBreakLongPunct} 控制。 % 收尾的或中间的标点不能出现在行首、开头的标点不能出现在行尾, % 这两条禁则不由本选项放开,所以一个收尾的引号不会被单独排在行首。 % 标点仍然会被压缩。 % \item[\keys{PunctKinsoku}]\val{\TTF}: 是否对标点施加断行禁则。 % 收尾的或中间的标点不能出现在行首,开头的标点不能出现在行尾, % 两个相邻的标点不会被分开,一串连续的破折号或省略号保持在一起。 % 盒子边缘的标点同样参与判断。 % 这些禁则与 \texttt{PunctStyle} 无关, % 所以 |PunctStyle = plain| 时它们仍然有效。 % \end{keyval} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} 不亦说乎,不亦乐乎\\ \LuaCJKsetup{PunctFamily = zhkai} 不亦说乎,不亦乐乎 \end{codemo} \endgroup % \iffalse % % \fi % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \LuaCJKsetup{LatinPunct = false}% a’a\quad 不亦说乎!\\ \LuaCJKsetup{LatinPunct, PunctWidth = .8em} a’a\quad 不亦说乎! \end{codemo} \endgroup % \iffalse % % \fi % % \iffalse %<*example> % \fi \begingroup \begin{codemo} 列位看官。\\ \LuaCJKsetup{PunctBoundWidth = .5em} 列位看官。\\ (列位看官 \end{codemo} \endgroup % \iffalse % % \fi % % 接下来是标点代码所用的字符集合。每个集合的键都有三种形式: % 不带符号的形式替换整个集合,带 \texttt{+} 的形式添加字符, % 带 \texttt{-} 的形式去掉字符。范围的写法与 \cs{LuaCJKDeclareCharClass} 相同。 % 这些集合是全局的:分组不会把先前的成员带回来。 % % \begin{keyval} % \item[\keys{KaiMingPunct<+->}]\val{char list}: % 在 \texttt{kaiming} 样式里保持全角宽度的标点。 % 初始为 \texttt{。}、\texttt{.}、\texttt{?}、\texttt{!}。 % \item[\keys{MiddlePunct<+->}]\val{char list}: % 居中的标点,放在自己盒子的两半之间。居中的标点不能出现在行首。 % \item[\keys{LongPunct<+->}]\val{char list}: % 彼此之间从不压缩的标点,例如破折号与省略号。 % \item[\keys{NoBreakLongPunct<+->}]\val{char list}: % 不能被断行分开的长标点。初始与 \texttt{LongPunct} 相同。 % \end{keyval} % % \begin{function}{\LuaCJKDeclareCharClass} % \begin{syntax} % \tn{LuaCJKDeclareCharClass} \marg{class} \marg{range list} % \end{syntax} % 改写一段码位的类别。 % 范围用十六进制书写,前导的 |"| 可以省略, % |"START -> "END| 表示一段范围; % 列表用逗号分隔。 % 类别有:\texttt{CJK} 与 \texttt{Default} 让这段范围表现得像 CJK 或拉丁文字; % \texttt{FullLeft} 与 \texttt{HalfLeft} 把它当作开头的标点, % \texttt{FullRight} 与 \texttt{HalfRight} 把它当作收尾的标点, % 于是断行禁则适用; % \texttt{NormalSpace} 把这段范围移出 CJK/拉丁胶, % 就像 \cs{normalspacedchars} 那样; % \texttt{CJStarter}、\texttt{CM} 与 \texttt{Boundary} 当作 \texttt{CJK} 处理, % 因为 \LuaTeX\ 没有组合标记或文字段边界的模型。 % \end{function} % % \begin{function}{\LuaCJKDeclarePunctClass} % \begin{syntax} % \tn{LuaCJKDeclarePunctClass} \marg{group} \marg{range list} % \end{syntax} % 改写一段范围的标点组。组有 \texttt{open}、\texttt{close}、 % \texttt{middle}、\texttt{dash} 与 \texttt{ellipsis}; % \texttt{none} 去掉标点处理,于是这些字符不再被压缩或悬挂。 % \end{function} % % \begin{function}{\LuaCJKResetCharClass} % 丢弃迄今声明的每一个类别与标点改写, % 并把正常间距集合恢复为 \cs{normalspacedchars} 的字符。 % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} 中ABC中\par \LuaCJKDeclareCharClass{CJK}{0041 -> 005A} 中ABC中\par \LuaCJKDeclarePunctClass{none}{FF0C}% 字,,字\par \LuaCJKResetCharClass 中ABC中 \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\LuaCJKEditPunctStyle} % \begin{syntax} % \tn{LuaCJKEditPunctStyle} \marg{style} \marg{key-value list} % \end{syntax} % 修改一个已有样式的参数。 % 键是 \keys{compress-adjacent, adjacent-ratio, keep-open-close}; % 没有给出的键保持它原来的值。 % \end{function} % % \begin{function}{\LuaCJKsetwidth} % \begin{syntax} % \tn{LuaCJKsetwidth} \marg{punctuation list} \marg{length} % \end{syntax} % 给列出的标点一个固定的进距, % 覆盖所选的 \keys{PunctStyle} 与 \keys{PunctWidth}。 % 这个设置是全局的:分组不会恢复先前的宽度, % 后一次调用是在表中添加,而不是替换。 % 开头的一个 \texttt{*} 可以接受;它会在行首或行尾设置的宽度没有实现, % 星号只警告一次。 % \end{function} % % \begin{function}{\LuaCJKsetkern} % \begin{syntax} % \tn{LuaCJKsetkern} \marg{before} \marg{after} \marg{length} % \end{syntax} % 设置两个相邻标点之间的空隙,覆盖样式的设置。 % 每个参数只使用第一个字符。 % 与 \cs{LuaCJKsetwidth} 一样,这个设置是全局的。 % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \LuaCJKsetwidth{「}{1.5em}字「字\\ \LuaCJKsetkern{「}{」}{.3em}字「」字 \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\LuaCJKDeclarePunctStyle} % \begin{syntax} % \tn{LuaCJKDeclarePunctStyle} \marg{name} \marg{key-value list} % \end{syntax} % 定义一种标点样式。 % \end{function} % % \begin{keyval} % \item[\keys{compress-adjacent}]\val{\TTF}: 是否压缩相邻的标点。 % \item[\keys{adjacent-ratio}]\val{number}: 压缩的比例, % 相对于被压缩标点的宽度。 % \item[\keys{keep-open-close}]\val{\TTF}: 开标点与闭标点相邻时 % 是否保持不压缩。 % \end{keyval} % % \iffalse %<*example> % \fi \begin{codemo} \LuaCJKDeclarePunctStyle{mine} {adjacent-ratio = .25} 字,,字\par \LuaCJKsetup{PunctStyle = mine}字,,字\par \LuaCJKEditPunctStyle{mine} {adjacent-ratio = .75} 字,,字 \end{codemo} % \iffalse % % \fi % % \subsection{断行} % % 一串破折号或省略号(U+2013、U+2014、U+2015、U+FF0D、U+2026 % 与 U+22EF)是一个整体:这些标点之间不加胶,所以两端对齐不会把一个 % 两字宽的破折号拆开,它们之间也不提供断行机会。 % 键 \keys{LongPunct} 替换这个集合。 % % \begin{keyval} % \item[\keys{CheckSingle}]\val{\TFF} % 打开时,末字符是 CJK 的段落的最后两个断行点被设为不可断开, % 这样单个字符就不会孤零零地落在最后一行上。初始值是 \meta{false}。 % \item[\keys{WidowPenalty}]\val{penalty}: \texttt{CheckSingle} 使用的惩罚值。 % 初始值是 \texttt{10000}; % 较小的值在值得这个代价时允许断开。 % \item[\keys{CJLineBreak}]\val{normal, strict}: 在 \texttt{strict} % 模式下,Unicode 的 \texttt{Line\_Break} 属性为 % \texttt{CJ} 的字符(小假名之类)不能出现在行首。 % 这个模式只增加禁则,不取消任何禁则。 % 初始值是 \texttt{normal},它把这些字符当作其他 CJK 字符。 % 这个集合从 \pkg{unicode-data} 宏包集的 \file{LineBreak.txt} 读取, % 所以严格模式需要这个宏包集。 % \end{keyval} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \parbox{3em}{天地玄黄宇宙洪荒\par}\quad \LuaCJKsetup{CheckSingle = true}% \parbox{3em}{天地玄黄宇宙洪荒\par} \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\LuaCJKnobreak} % \begin{syntax} % \tn{LuaCJKnobreak} \marg{text} % \end{syntax} % 把 \meta{text} 保持在一行之内。参数的节点被打上标记, % 断行的处理不在它们的任何边界处断开。标记随当前分组结束。 % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \parbox{3em}{天地玄黄}\quad \parbox{3em}{\LuaCJKnobreak{天地玄黄}} \end{codemo} \endgroup % \iffalse % % \fi % % \subsection{暂停与钩子} % % \begin{function}{\LuaCJKdisable, \LuaCJKenable} % \tn{LuaCJKdisable} 停止排版调整:不再插入 \keys{CJKglue} % 或 \keys{CJKecglue},标点不再压缩, % 不再添加断行禁则,标点不再悬挂, % 数学模式里的 CJK 字符也不再处理。 % CJK 字符仍然用 CJK 字体排出,所以文字依然可读。 % 这个开关只作用于当前分组;\tn{LuaCJKenable} 重新打开这些调整。 % \end{function} % % 这就是抄录类环境使用的钩子, % 在测量原始节点列表时也用得上。 % % \iffalse %<*example> % \fi \begingroup \begin{codemo} 精致甲版 红楼梦\\ \LuaCJKdisable 精致甲版 红楼梦 \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\LuaCJKShipoutHook} % \begin{syntax} % \tn{LuaCJKShipoutHook} \marg{code} % \end{syntax} % 在每一页输出之前运行 \meta{code}。代码被加入 % \texttt{shipout/before} 钩子,所以它遵守通常的钩子顺序。 % \end{function} % % \subsection{抄录与 \pkg{listings}} % % \begin{function}{\LuaCJKVerbAddon, \LuaCJKOffVerbAddon} % 让一个 CJK 字符和当前等宽字体的两个空格一样宽, % 这样代码列表里含有 CJK 文字时各列仍然对齐。 % 空隙以 \keys{CJKglue} 的形式加入,所以 CJK 字体本身并没有被缩放; % 负的空隙只给出警告,什么也不加。 % CJK 字符之间的断行被禁止,直到分组结束,与拉丁文的抄录文字一样。 % % 这条命令在最外层的分组里不起作用, % 所以它应当在需要它的环境内部使用, % 例如 \pkg{fancyvrb} 的 \texttt{formatcom} 选项里。 % \tn{LuaCJKOffVerbAddon} 在同一个分组里再次取消它。 % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \ttfamily abc汉字abc\\ \ttfamily\LuaCJKVerbAddon abc汉字abc \end{codemo} \endgroup % \iffalse % % \fi % % \begin{keyval} % \item[\keys{Verb}]\val{\TTF, env, env+} % \tn{verb} 与抄录类环境(\env{verbatim}、\env{Verbatim}、\env{lstlisting} % 以及它们的近亲)里做什么。\texttt{true} 在两者中都暂停调整; % \texttt{env} 在环境里保留调整,但在 \tn{verb} 里不保留, % 这样代码里含有 CJK 文字时各列仍然对齐; % \texttt{env+} 在所有地方都保留调整;\texttt{false} 完全不碰抄录材料。 % 既然 \texttt{env+} 与 \texttt{false} 都保留调整, % 它们在 \pkg{LuaCJK} 里行为相同。 % 暂停只去掉间距:紧挨着被暂停材料的一个标点, % 仍然保留它后面的断行机会。 % \end{keyval} % % \pkg{listings} 一次处理一个字符,并且在读到字符的那一刻就把 % 字符表之外的字符排出来。CJK 字符在它那张表之外, % 于是会跑到周围拉丁文字的前面。配套的 \pkg{LuaCJK-listings} 修正了这一点: % 它把 CJK 字符加入 \pkg{listings} 的字符表,所以它们留在原处, % 把每个字符算作两列,并在列表内部关掉间距调整。 % 使用 \pkg{listings} 时 \pkg{LuaCJK} 自动加载它,所以不需要额外的命令。 % % \tn{lstinline} 以改变了的类别码读取参数; % 补丁会破坏那个扫描器,所以行内代码保持不动,按通常的方式处理。 % % \subsection{装饰} % % 配套的 \pkg{LuaCJKfntef} 提供下划线、着重号与删除线。 % 它建立在 \pkg{lua-ul} 之上, % 所以被装饰的文字可以包含任意命令、CJK 与拉丁文字, % 也仍然可以被断字并跨行断开。 % % \begin{function}{\CJKunderline} % \begin{syntax} % \tn{CJKunderline} \oarg{options} \marg{text} % \end{syntax} % 给 \meta{text} 加下划线。初始几何是 \texttt{depth = 0.2em} % 与 \texttt{thickness = 0.4pt}。 % 开头的一个 \texttt{*} 或 \texttt{-} 可以接受,并且被忽略。 % \end{function} % % 所有装饰命令共用下面这组键:与某个命令无关的键照样接受,只是不起作用。 % 只给一个颜色名而不写键名,等价于 \texttt{format = }\meta{color}。 % 键 \texttt{top} 与 \texttt{bottom} 被传给 \pkg{lua-ul}, % 因此只对 \cs{CJKunderline} 有效,并覆盖计算出来的几何。 % 画出的图案从不改变被装饰行的高度或深度,所以行距不会变大。 % % \begin{keyval} % \item[\keys{thickness}]\val{length}: 横线的粗细,初始 \texttt{0.4pt}。 % \item[\keys{depth}]\val{length}: 从基线到横线顶部的距离, % 初始 \texttt{0.2em}。 % \item[\keys{textformat}]\val{code}: 作用于文字的代码,例如 % |textformat = \bfseries|。 % \item[\keys{format}]\val{color name}: 横线的颜色,例如 % |format = red|;它只影响横线,不影响文字。 % \item[\keys{color}]\val{color name}: \texttt{format} 的另一个名字。 % \item[\keys{subtract}]\val{length}: 在横线两端各去掉的长度, % 初始关闭;\texttt{true} 表示半字宽, % 这样相邻的两个命令不会连在一起。 % \item[\keys{skip}]\val{\TFF}: 把标点排除在横线之外。 % \item[\keys{hidden}]\val{\TFF}: 隐藏文字而保留横线, % 横线取隐藏文字的宽度、高度与深度。 % \item[\keys{gap}]\val{length}: 双线两条横线之间的距离, % 初始 \texttt{1.1pt}。 % \item[\keys{height}]\val{length}: 删除线中心到基线的距离, % 初始 \texttt{0.3em}。 % \item[\keys{symbol}]\val{code}: 替代波浪或圆点的符号。 % \item[\keys{selectable}]\val{\TFF}: 装饰符号是否保留为文字, % 初始 \texttt{false};\texttt{true} 时 PDF 阅读器可以复制与选择它。 % \end{keyval} % % 不给 \keys{format} 时,横线使用它所装饰的文字的颜色, % 例如 \cs{textcolor} 设定的颜色。 % % \keys{sep} 为了兼容而接受,但没有作用。 % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \CJKunderline[thickness = 1pt]{有涯}\\ \CJKunderline[format = red]{有涯}\\ \CJKunderline[textformat = \bfseries] {有涯}\\ \CJKunderline[hidden]{有涯} \end{codemo} \endgroup % \iffalse % % \fi % % \iffalse %<*example> % \fi \begingroup \begin{codemo} 今天是 \CJKunderline{\hspace*{3em}} 年\\ 第 \CJKunderline{\hspace*{2em}} 题 \end{codemo} \endgroup % \iffalse % % \fi % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \CJKunderline{有涯}\CJKunderline{有涯}\\ \CJKunderline[subtract]{吾生也有涯}\\ \CJKunderline[skip]{有涯,无涯} \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\CJKunderdblline} % \begin{syntax} % \tn{CJKunderdblline} \oarg{options} \marg{text} % \end{syntax} % 在 \meta{text} 下面画两条平行的横线,键见上面的键列表。 % 两条横线都使用 \keys{thickness},上面那条从 \keys{depth} 开始, % \keys{gap} 是两条横线之间的距离。 % 开头的一个 \texttt{*} 或 \texttt{-} 可以接受,并且被忽略。 % \end{function} % % \begin{function}{\CJKunderwave} % \begin{syntax} % \tn{CJKunderwave} \oarg{options} \marg{text} % \end{syntax} % 在 \meta{text} 下面画一条波浪线。 % 波浪线用 \pkg{l3draw} 画出,并用 \tn{leaders} 重复, % 所以在一行的各部分之间相位相同。一个周期是半字宽, % 整个图案随字号缩放;与其他画出的图案一样, % 它不改变行的高度与深度。 % \keys{symbol} 用来把波浪线换成别的符号: % 每字宽一个符号,居中,符号顶部在基线之下半字宽处。 % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \CJKunderwave{吾生也有涯}\\ \CJKunderwave[symbol = \textbullet] {吾生也有涯}\\ \CJKunderdot{吾生也有涯}\\ \CJKunderdot[symbol = \textbullet] {吾生也有涯} \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\CJKxout} % \begin{syntax} % \tn{CJKxout} \oarg{options} \marg{text} % \end{syntax} % 用重复的斜线划掉 \meta{text},像 \pkg{ulem} 的 \tn{xout} 那样。 % 斜线从基线之上 \texttt{0.8em} 延伸到基线之下 \texttt{-0.15em}, % 所以它穿过一个 CJK 字形的整个字身。 % \end{function} % % \begin{function}{\LuaCJKemdescent} % 当前字体 em 盒的深度,可以当作长度使用。 % CJK 的 em 盒是一个 em 的正方形,所以把高度为 \texttt{1em} 的对象 % 放在这个深度上,就正好覆盖一个汉字所占的区域; % 字体没有 OpenType 度量时退回 \texttt{.12em}。 % \end{function} % % \begin{function}{\CJKunderanyline} % \begin{syntax} % \tn{CJKunderanyline} \marg{depth} \marg{object} \marg{text} % \end{syntax} % 把 \meta{object} 放在 \meta{text} 下面,并用 \tn{leaders} 重复。 % \meta{object} 的底部放在基线之下 \meta{depth} 处, % 所以对于一个(本身没有深度的)\cs{rule} 来说, % 横线从基线之下 \meta{depth} 处向上占据。 % 要覆盖一个 CJK 字形,就用字体的 em 盒:\meta{depth} 取 % \cs{LuaCJKemdescent},对象高度取 \texttt{1em},正如下例所做的那样。 % 对象填满的是 em 盒,也就是 PDF 阅读器划出的选区: % 横向是字身宽度,纵向是 em 盒的高度, % 字体墨迹比 em 盒窄时,四周会各露出一条边距。 % 类型会被缓存,所以相同的参数复用一个下划线类型。 % \end{function} % % \begin{function}{\CJKunderanysymbol} % \begin{syntax} % \tn{CJKunderanysymbol}\oarg{options}\marg{depth}\marg{symbol}\marg{text} % \end{syntax} % 用法相同,只是每字宽放一个 \meta{symbol}: % 符号在一个和一个 CJK 字符一样宽的格子里居中, % 所以一排符号与它们上方的字符对齐。 % \meta{depth} 是符号顶部到\emph{em 盒底部}的距离, % 所以 |0pt| 留下的间隙正与字符两侧的边距相同; % 字体没有 OpenType 度量时按 |.12em| 计算。 % 为了兼容,接受选项 \keys{sep},但它没有作用, % 因为格子固定为一个字宽。 % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \CJKunderanyline{\LuaCJKemdescent} {\color{yellow}\rule{1pt}{1em}}{有涯}\\ \CJKunderanysymbol{0pt}{\tiny$\triangle$}{有涯} \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\CJKunderdot} % \begin{syntax} % \tn{CJKunderdot} \oarg{options} \marg{text} % \end{syntax} % 在 \meta{text} 下面加着重号。 % 用 |symbol = |\meta{symbol} 时,一个符号取代着重号, % 放置方式与 \cs{CJKunderanysymbol} 的符号相同: % 每字宽一个,居中,符号顶部在 em 盒底部之下 \texttt{0.3em} 处。 % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \CJKunderline{虚室生白,\CJKunderdot{吉祥}止止, 虚室生白} \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\CJKsout} % \begin{syntax} % \tn{CJKsout} \oarg{options} \marg{text} % \end{syntax} % 给 \meta{text} 加删除线,键见上面的键列表。 % 删除线中心在基线上方,穿过一个 CJK 字形的中部, % 比拉丁字母 x 高度的中部要高; % 对于完全是拉丁文的文字,可以用 |height = 0.5ex|。 % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \CJKsout[thickness = 2pt, format = red] {吾生也有涯}\\ \CJKsout[height = .5ex]{吾生也有涯} \end{codemo} \endgroup % \iffalse % % \fi % % \iffalse %<*example> % \fi \begin{codemo} \CJKunderline {吾生也有涯}\\ \CJKunderdblline{吾生也有涯}\\ \CJKunderwave {吾生也有涯}\\ \CJKunderdot {吾生也有涯}\\ \CJKsout {吾生也有涯}\\ \CJKxout {吾生也有涯} \end{codemo} % \iffalse % % \fi % % \section{与 \pkg{xeCJK} 的比较} % % \pkg{LuaCJK} 只要两个引擎允许,就沿用 \pkg{xeCJK} 的接口, % 所以一个已经在用 \pkg{xeCJK} 的文档大多不需要改动。 % 这里只列出不同之处。 % % \subsection{行为差异} % % \begin{itemize} % \item \keys{Verb}:\texttt{env+} 的行为与 \texttt{false} 一样, % 因为两者都保留调整。 % \item 标点几何。样式把标点的尺寸给成一半,或者一半但句末标点保持全角, % 或者 0.7 倍,并且墨迹按类别随进距移动: % 开头的标点移动整个量,中间的标点移动一半,收尾的标点完全不移动。 % \pkg{xeCJK} 则是测量每个字形的真实侧边距。 % \item \cs{LuaCJKDeclarePunctStyle} 接受三个键 % \keys{compress-adjacent}、\keys{adjacent-ratio} 与 % \keys{keep-open-close};\pkg{xeCJK} 的三十个几何键描述它的侧边距模型, % 在这里没有对应物。 % \item \texttt{CJKfilltwosides*} 不与自然宽度作比较。 % \item CJK 字体是在文字用当前字体定形之后才应用到字形节点上的, % 所以 CJK 字体的 OpenType 特性(连字、\texttt{locl}、\texttt{palt}、 % 变体序列)不生效:间距、标点进距与禁则都来自 \pkg{LuaCJK} 自己。 % \item 在 \pkg{LuaCJKfntef} 里,\keys{format} 接受颜色名而不是代码, % 而且装饰由 \pkg{lua-ul} 画出,所以 \texttt{ulem} 的选项列表不存在, % \tn{underLine} 直接配置。 % \end{itemize} % % \subsection{未提供的功能} % % \begin{itemize} % \item \texttt{NewLineCS}、\texttt{EnvCS}、\texttt{InlineEnv} 与 % \texttt{PlainEquation}:它们的存在是为了让 \pkg{xeCJK} 在扫描记号时 % 认出 \TeX\ 构造,而一个已经完成的节点列表不需要提示。 % \item \texttt{PoZheHaoLigature},理由同上:字体的两字宽破折号连字 % 在 \pkg{LuaCJK} 应用 CJK 字体之前就已经定了。 % \item \texttt{RubberPunctSkip}:标点的进距是一个 kern, % 它既不是断行机会,也不参与两端对齐, % 而正是这一点让固定宽度的样式保持稳定。 % \item \texttt{CheckFullRight}:它需要标点之后的控制序列, % 节点列表并不显示这个;请改用 \cs{LuaCJKnobreak}。 % \end{itemize} % % \end{documentation} % % \setcounter{section}{0} % \LuaCJKdoclang{en} % \clearpage % % \begin{documentation} % % \section{Introduction} % % \pkg{LuaCJK} is a \LuaTeX-only package for Chinese typesetting. Its main % interface is \cs{setCJKmainfont}, \cs{setCJKsansfont}, \cs{setCJKmonofont}, % \cs{setCJKfamilyfont}, \cs{CJKfamily}, and \cs{LuaCJKsetup}. % % The current CJK font is recorded in a \LuaTeX\ attribute, % and CJK glyph nodes are replaced before line breaking and box packing. % % \section{Loading} % % \begin{quote} % \cs{usepackage}\oarg{options}"{LuaCJK}" % \end{quote} % % \pkg{LuaCJK} requires the \LuaTeX\ engine. It conflicts with \pkg{luatexja}: % loading either package before or after the other raises an error. % % \begin{function}{\LuaCJKversion} % Print the version of \pkg{LuaCJK}. % \end{function} % % \begin{function}{\LuaCJKsetup} % \begin{syntax} % \tn{LuaCJKsetup} \marg{key-value list} % \end{syntax} % Configure \pkg{LuaCJK}. % The keys are the ones documented in the following sections. % \end{function} % % The keys fall into two groups. % % The settings that describe how text is set are local to the current group, % and they are recorded on the characters themselves, % so text inside a group keeps its settings even when the group has already % ended by the time the paragraph is broken: % \begin{quote} % |\vbox{\begingroup\LuaCJKsetup{CJKglue = 10pt}中中\endgroup\par}| % \end{quote} % uses the 10pt glue, although the group is closed before |\par|. % This group contains % \begin{itemize} % \item \keys{CJKglue, CJKecglue, CJKspace}, and \keys{xCJKecglue}; % \item \keys{PunctStyle, PunctKinsoku, AllowBreakBetweenPuncts, PunctWidth}, % and \keys{PunctBoundWidth}; % \item \keys{LatinPunct} and \keys{CJKmath}; % \item \keys{CheckSingle, WidowPenalty}, and \keys{CJLineBreak}; % \item \keys{AutoFallBack} and \keys{Verb}; % \item the two commands \cs{LuaCJKCancelSubCJKBlock} and % \cs{LuaCJKRestoreSubCJKBlock}. % \end{itemize} % % The other keys are global or act while the package is loaded: % \begin{itemize} % \item \keys{KaiMingPunct, MiddlePunct, LongPunct}, % and \keys{NoBreakLongPunct} edit character sets; % \item \keys{AutoFakeBold, AutoFakeSlant, EmboldenFactor}, % and \keys{SlantFactor} act when a family is registered; % \item \keys{LoadFandol} and \keys{LocalConfig} act while the package % is loaded. % \end{itemize} % The declarations \cs{LuaCJKDeclareCharClass}, % \cs{LuaCJKDeclarePunctClass}, \cs{LuaCJKDeclareSubCJKBlock}, % \cs{LuaCJKsetwidth}, and \cs{LuaCJKsetkern} are global as well, % as noted with each of them. % % The same keys may be given as package options: % \begin{quote} % |\usepackage[PunctStyle = banjiao]{LuaCJK}| % \end{quote} % % \begin{keyval} % \item[\keys{LocalConfig}]\val{\TTF, name} % Read local settings from a configuration file. % \texttt{true} loads \file{luacjk.cfg}, % \meta{name} loads \file{luacjk-}\meta{name}\file{.cfg}, % and \texttt{false} (the initial value) loads nothing. % The file is read at the end of the package, so it may use every % command \pkg{LuaCJK} defines. A missing file only warns. % \end{keyval} % % To use \pkg{LuaCJK} with \pkg{ctex}, pass the \keys{engine} option with the % value \val{luacjk}: % \begin{quote} % |\documentclass[engine = luacjk]{ctexart}| % \end{quote} % \pkg{ctex} then inputs \pkg{LuaCJK}'s |ctex-engine-luacjk.def| instead of % its own \LuaTeX\ backend, and that file loads the package, so \pkg{LuaCJK} % is not loaded separately. % % \pkg{LuaCJK} cooperates with the \LaTeX\ tagged-PDF project. % A document that declares % \begin{quote} % |\DocumentMetadata{tagging = on}| % \end{quote} % marks the ink of a decoration as \texttt{artifact}, and a decoration symbol % is left out of the text a reader copies or selects; % \keys{selectable = true} keeps the symbol as text. % Under the \cls{l3doc} class, load \pkg{latex-lab-testphase-l3doc} as well; % this manual is built that way. % A PDF reader derives the height of a selection from the font's % \texttt{hhea} ascent and descent, which is 1.437em for % \texttt{Noto Serif CJK SC}; the selection is therefore taller than the 1em % square. \pkg{LuaCJK} cannot change that, and it does not affect the glyph. % % \section{Fonts} % % The main font commands take an optional list of \pkg{fontspec} options. % Options may be written before or after the font name. % % \begin{function}{\setCJKmainfont,\setCJKsansfont,\setCJKmonofont} % \begin{syntax} % \tn{setCJKmainfont} \oarg{options} \marg{font} \oarg{options} % \end{syntax} % Set the CJK font for the roman, sans serif, and monospaced families. % \end{function} % % \iffalse %<*example> % \fi \begin{codemo} \setCJKmainfont[Scale = 1.2] {FandolSong-Regular.otf} English 中文字体\\ \setCJKsansfont[Scale = 1.2] {FandolKai-Regular.otf} \sffamily English 中文字体 \end{codemo} % \iffalse % % \fi % % \begin{function}{\setCJKfamilyfont} % \begin{syntax} % \tn{setCJKfamilyfont}\marg{family}\oarg{options}\marg{font}\oarg{options} % \end{syntax} % Define a named CJK family. % \end{function} % % \begin{function}{\CJKfamily} % \begin{syntax} % \tn{CJKfamily} \marg{family} % \end{syntax} % Switch to a named CJK family. % \end{function} % % \iffalse %<*example> % \fi \begin{codemo} \CJKfamily{zhsong}English 中文字体\\ \CJKfamily{zhhei}English 中文字体\\ \CJKfamily{zhkai}English 中文字体\\ \CJKfamily{zhfs}English 中文字体 \end{codemo} % \iffalse % % \fi % % \begin{function}{\newCJKfontfamily} % \begin{syntax} % \tn{newCJKfontfamily}\oarg{family}\meta{command} % \ \oarg{options}\marg{font}\oarg{options} % \end{syntax} % Define a command that switches to a newly declared family. % The command is local to the current group, like \cs{newfontfamily}. % The options may be given before or after the font name. % \end{function} % % \begin{function}{\CJKfontspec} % \begin{syntax} % \tn{CJKfontspec} \oarg{options} \marg{font} \oarg{options} % \end{syntax} % Select a CJK font for the current group. % The options may be given before or after the font name. % \end{function} % % \begin{function}{\songti, \heiti, \fangsong, \kaishu, \lishu, \youyuan} % Shortcuts for the families named \texttt{zhsong}, \texttt{zhhei}, % \texttt{zhfs}, \texttt{zhkai}, \texttt{zhli}, and \texttt{zhyou}. % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \newCJKfontfamily[kai]\mykai {FandolKai-Regular.otf} \mykai English 中文字体\\ \songti English 中文字体 \end{codemo} \endgroup % \iffalse % % \fi % % \begin{variable} % {\CJKrmdefault, \CJKsfdefault, \CJKttdefault, % \CJKfamilydefault} The CJK families selected by \tn{rmfamily}, % \tn{sffamily}, and \tn{ttfamily}, and the family selected by % \tn{normalfont}. \cs{CJKfamilydefault} follows \cs{CJKrmdefault} % by default. % \end{variable} % \subsection{Sub-blocks} % % One CJK font rarely covers every block of \textsc{Unicode}. % A sub-block gives a range of characters its own font inside a family; % the block then becomes an option of the font commands. % % \begin{function}{\LuaCJKDeclareSubCJKBlock} % \begin{syntax} % \tn{LuaCJKDeclareSubCJKBlock} \marg{block} \marg{range list} % \end{syntax} % Declare \meta{block} with the given ranges, in the same spelling % as \cs{LuaCJKDeclareCharClass}. The name becomes a font option, % as the example below shows: a family that sets it uses the second font % for the characters of the block, and a family that does not leaves them % in the main font. Overlapping blocks are resolved in declaration order. % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \LuaCJKDeclareSubCJKBlock{Kana}{3040 -> 30FF} \setCJKmainfont[Kana = FandolKai-Regular.otf] {FandolSong-Regular.otf} 汉字 かんじ \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\LuaCJKCancelSubCJKBlock, \LuaCJKRestoreSubCJKBlock} % \begin{syntax} % \tn{LuaCJKCancelSubCJKBlock} \marg{block list} \\ % \tn{LuaCJKRestoreSubCJKBlock} \marg{block list} % \end{syntax} % Turn the listed blocks off or on again for the rest of the group. % Because the choice of font is recorded per font identifier, % two families that supply the same font share one setting: % the family that selected the font last decides. % \end{function} % % \subsection{Fallback fonts} % % A CJK font rarely covers every character one needs. % Fallback fonts are tried in the order they were declared when % the selected CJK font has no glyph for a character. % % \begin{function}{\setCJKfallbackfamilyfont} % \begin{syntax} % \tn{setCJKfallbackfamilyfont} \marg{family} \oarg{options} \\ % \ \marg{font} \oarg{options} % \end{syntax} % Add \meta{font} to the fallback list of \meta{family}. The family may % be given as \cs{CJKrmdefault} and friends. Call the command several % times to build a chain; the first font that has the character wins. % The chain is resolved in the weight, shape, and size of the text, so a % character in bold takes the bold face of the fallback font. % \end{function} % % \begin{keyval} % \item[\keys{AutoFallBack}]\val{\TFF} % Whether to use the declared fallback fonts. The switch follows the text % like the spacing keys, so it can be turned on for one group. % \end{keyval} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \setCJKmainfont{FandolSong-Regular.otf} \setCJKfallbackfamilyfont{\CJKrmdefault} {FandolKai-Regular.otf} \LuaCJKsetup{AutoFallBack = true}中あ \end{codemo} \endgroup % \iffalse % % \fi % % \subsection{Baseline shift} % % Two CJK fonts are rarely designed for each other: mixed in one line, the % characters of one font sit too high or too low. % A font may carry a \keys{Baseline} value that moves its characters up or % down, so that the two fonts stand on one baseline. % The value belongs to the font rather than to the text, which is why it is % an option of the command that declares the font. % % \begin{keyval} % \item[\keys{Baseline}]\meta{dimension} % Raise the characters of this font by \meta{dimension}; a negative value % lowers them. It is an option like the others, so a fallback declaration % may carry it as well, and that is its usual use: it makes a fallback font % stand on the baseline of the main font. Any \TeX\ unit works, and |em| % follows the size. Two declarations that supply the same font share the % value of the last one, because a position is recorded per font. % \end{keyval} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \setCJKfamilyfont{high}[Baseline = .3em] {FandolKai-Regular.otf} \songti 基线\quad\CJKfamily{high}基线 \end{codemo} \endgroup % \iffalse % % \fi % % \subsection{Loading Fandol} % % \begin{keyval} % \item[\keys{LoadFandol}]\val{\TTF} % When the document does not declare any CJK font, load the four Fandol % faces at |\begin{document}|: Song as the main font, % Hei as the sans serif font, Fang as the monospaced font, % and all of Song, Hei, Kai, and Fang as the named families. % \end{keyval} % % \subsection{Fake bold and slant} % % A CJK family often has no bold or italic face. The following keys make % \pkg{LuaCJK} ask \pkg{fontspec} to embolden or slant the regular face % instead. % They apply to families registered afterwards, and are not used when the % family itself sets \texttt{BoldFont} or \texttt{ItalicFont}. They are % global: a group does not change what an already registered family uses. % % \begin{keyval} % \item[\keys{AutoFakeBold}]\val{\TFF, number} % Embolden the regular face when the bold shape is requested. % A number also sets \texttt{EmboldenFactor}. % \item[\keys{AutoFakeSlant}]\val{\TFF, number} % Slant the regular face when the italic shape is requested. % A number also sets \texttt{SlantFactor}. % \item[\keys{EmboldenFactor}]\val{number}: The amount of emboldening. % The initial value is \texttt{4}. % \item[\keys{SlantFactor}]\val{number}: The amount of slanting, between % $-0.999$ and $0.999$. The initial value is \texttt{0.167}. % \end{keyval} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \LuaCJKsetup{AutoFakeBold = true} \setCJKmainfont{FandolSong-Regular.otf} \textbf{English 中文字体}\\ English 中文字体 \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\defaultCJKfontfeatures} % \begin{syntax} % \tn{defaultCJKfontfeatures} \marg{font features} % \end{syntax} % Set the features prepended to every CJK family registered afterwards. % The initial value is |Script = CJK|. % Families registered before the call keep the features they were given. % \end{function} % % \begin{function}{\addCJKfontfeatures} % \begin{syntax} % \tn{addCJKfontfeatures} \oarg{ignored} \marg{font features} % \end{syntax} % Add features to the current CJK family for the rest of the group, % for example |\addCJKfontfeatures{Scale = 1.2}|. % A leading \texttt{*} is accepted; both forms act on the same font. % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \defaultCJKfontfeatures{Scale = 1.5} \setCJKfamilyfont{big} {FandolSong-Regular.otf} \CJKfamily{big}中文字体\\ \CJKfamily{zhsong}中文字体\\ \addCJKfontfeatures{Scale = 1.2} \CJKfamily{zhsong}中文字体 \end{codemo} \endgroup % \iffalse % % \fi % % \subsection{CJK in mathematics} % % \begin{keyval} % \item[\keys{CJKmath}]\val{\TFF}: Whether CJK characters may be typed % directly in math mode. % When it is on, such a character is set in the CJK font of a math family % of its own instead of being looked up in the math font, so a character in % a superscript or a fraction takes the size of that math style. % Enable it with |\LuaCJKsetup{CJKmath = true}| or with the package option % |CJKmath = true|. % Such a character takes the font of the math version, as \tn{mathversion} % sets it: the bold face of the family in the \texttt{bold} version, its % regular face when it has no bold one, and the upright shape of the family % rather than the shape of the text. % Without it a CJK character in math mode is looked up in the math font % and usually disappears, so \pkg{LuaCJK} issues a one-time warning in that % case. % \end{keyval} % % \begin{function}{\setCJKmathfont} % \begin{syntax} % \tn{setCJKmathfont} \oarg{options} \marg{font} \oarg{options} % \end{syntax} % Select the CJK font used for characters in math mode. % Without this command the CJK main font is used. % This manual enables \keys{CJKmath} and sets this font in its preamble. % The selection is local to the current group, like the other font commands: % a formula in a group uses the family selected in that group. % \end{function} % % Each CJK family that appears in math is registered as a math family of its % own, so that \TeX\ can pick the text, script, or scriptscript size of it. % The family takes its font from the math version: \cs{bfdefault} in the % \texttt{bold} version and \cs{mddefault} in the others, with the shape % \cs{shapedefault}. \cs{boldmath} therefore bolds the CJK characters of a % formula, within the group that asks for it, and a family without a bold face % keeps its regular one without a warning. % The family number comes from the range 240--255, the last sixteen of the 256 % that \LaTeXe\ allocates under \LuaTeX, which an ordinary document never % reaches; more than sixteen CJK families in math stops with an error. % The script sizes are seven and five tenths of the current text size, the % ratio of the standard size tables. % The math parameters are not taken from the CJK font, which has no % \texttt{MathConstants} table: they stay those of the document's math font, % which is why a fraction of two CJK characters has the rule and the gaps of % the surrounding math. % % \iffalse %<*example> % \fi \begin{codemo} $ 天地玄黄 $ and 天地玄黄 \end{codemo} % \iffalse % % \fi % % \section{Text layout} % % \subsection{Spacing} % % \begin{keyval} % \item[\keys{CJKglue}]\val{glue}: Glue inserted between adjacent CJK % characters. % The initial value is \texttt{0pt plus 0.08\cs{baselineskip}}, recomputed % at every font selection, so it follows the size; a value given % by the document is used as written and does not change with the size, % and its stretch order (for example |plus 1filll|) is kept as well. % \item[\keys{CJKecglue}]\val{glue}: Glue inserted between CJK and Latin % characters. % Beside a punctuation mark the glue has no advance: the mark's own advance % already carries the spacing. % The initial value is \texttt{0.3333em plus 0.1667em minus 0.1111em}, % which follows the size in the same way. % \item[\keys{CJKspace}]\val{\TFF}: Whether to keep source spaces between % CJK characters. When they are not kept, the space becomes the % inter-character glue, so the break opportunity and the stretch survive. % \item[\keys{xCJKecglue}]\val{\TFF}: Whether to replace source spaces % between CJK and Latin characters with \texttt{CJKecglue}. % \end{keyval} % % \iffalse %<*example> % \fi \begin{codemo} 中文 English 中文 \end{codemo} % \iffalse % % \fi % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \LuaCJKsetup{CJKspace = true}天 地 玄 黄 宇 宙 \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\normalspacedchars} % \begin{syntax} % \tn{normalspacedchars} \marg{character list} % \end{syntax} % The listed characters get no glue on either side, so that a hyphen % or a slash inside Latin text does not pull the surrounding text apart. % The initial list is |/|, |\|, and |-|. The setting is global. % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} 中x中\par \normalspacedchars{x}中x中 \end{codemo} \normalspacedchars{/,\,,-} \endgroup % \iffalse % % \fi % % Characters that only control the text and have no advance of their own % (U+200B, U+200C, U+200D, U+2060, U+180E, U+FEFF, and the variation % selectors) are removed at the input. They therefore neither take glue nor % separate two CJK characters: a zero-width space does not add the two % \keys{CJKecglue} that would otherwise surround it, and the break % opportunity it was meant to create already exists at the surrounding glue. % A character requested with \tn{char} is not removed, because \tn{char} % asks for the glyph itself. % % \begin{environment}{CJKfilltwosides} % Set the lines in a vertical box whose lines are justified % to \meta{width}, separated by |\\|. Its \meta{position} argument puts % the box on the baseline: \texttt{t} for top, \texttt{c} for centered % (the default), or \texttt{b} for bottom. A width of zero or less keeps % the surrounding line width. The starred form behaves the same; % it does not compare against the natural width of the content. % \begin{syntax} % \tn{begin}\{CJKfilltwosides\}\oarg{position}\marg{width} % \ \meta{lines} \\ % \tn{end}\{CJKfilltwosides\} % \end{syntax} % \end{environment} % % \iffalse %<*example> % \fi \begin{codemo} \begin{CJKfilltwosides}{6em} 瞻彼阕者,\\虚室生白 \end{CJKfilltwosides} \end{codemo} % \iffalse % % \fi % % \iffalse %<*example> % \fi \begin{codemo} \begin{CJKfilltwosides*}{0pt} 瞻彼阕者,\\虚室生白 \end{CJKfilltwosides*} \end{codemo} % \iffalse % % \fi % % \subsection{Punctuation} % % \begin{keyval} % \item[\keys{PunctStyle}]\val{style}: Select a punctuation style. % The implemented styles are \texttt{quanjiao} (the default), % \texttt{plain}, \texttt{banjiao}, \texttt{kaiming}, \texttt{CCT}, % and \texttt{hangmobanjiao}. % An unknown name issues a warning and falls back to \texttt{plain}. % \item[\keys{PunctWidth}]\val{length, false}: Give % every punctuation mark the same advance, whatever the style says. % A width set with \cs{LuaCJKsetwidth} still wins for that mark. % \item[\keys{PunctBoundWidth}]\val{length, false}: How much of a mark's % advance stays inside the line when the mark ends or starts the line; % the rest of the ink stands in the margin, or in the indentation at the % start of a paragraph. % The default leaves the mark's own blank out, so its ink stands on the % measure, and line breaking counts that hang, so a mark can end the line the % natural width would refuse. \texttt{false} returns to that default. % \item[\keys{LatinPunct}]\val{\TFF}: Treat the quotation marks % (U+2018, U+2019, U+201C, U+201D), the middle dot (U+00B7), % and the ellipses (U+2026, U+2027, U+22EF) as Latin punctuation: % they stay in the current Latin font and are not compressed. % This keeps an apostrophe inside an English word from turning into % a full-width form. The em dash is not affected. % \item[\keys{PunctFamily}]\val{family, false}: Use a % different CJK family for the punctuation marks. % The family must be declared with \cs{setCJKfamilyfont}; \texttt{false} % (the initial value) makes the marks follow the text again. % \item[\keys{AllowBreakBetweenPuncts}]\val{\TFF}: Whether two adjacent % punctuation marks may be separated by a line break. % A dash or an ellipsis is not affected: a run of them is governed by % \texttt{NoBreakLongPunct}. % The prohibitions that keep a closing or middle mark out of the line start % and an opening mark out of the line end are not lifted, so a closing % quotation mark is never set at the start of a line. % The marks are still compressed. % \item[\keys{PunctKinsoku}]\val{\TTF}: Whether to apply line-breaking % prohibitions for punctuation. % A closing or middle punctuation cannot start a line, an opening punctuation % cannot end a line, two adjacent marks are not separated, and a run of long % punctuation is kept together. % A mark at the edge of a box takes part as well. % The prohibitions are independent of \texttt{PunctStyle}, % so they stay active with |PunctStyle = plain|. % \end{keyval} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} 不亦说乎,不亦乐乎\\ \LuaCJKsetup{PunctFamily = zhkai} 不亦说乎,不亦乐乎 \end{codemo} \endgroup % \iffalse % % \fi % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \LuaCJKsetup{LatinPunct = false}% a’a\quad 不亦说乎!\\ \LuaCJKsetup{LatinPunct, PunctWidth = .8em} a’a\quad 不亦说乎! \end{codemo} \endgroup % \iffalse % % \fi % % \iffalse %<*example> % \fi \begingroup \begin{codemo} 列位看官。\\ \LuaCJKsetup{PunctBoundWidth = .5em} 列位看官。\\ (列位看官 \end{codemo} \endgroup % \iffalse % % \fi % % The character sets that the punctuation code works with come next. Each % of their keys has three forms: the plain one replaces the set, the form % with \texttt{+} adds characters, and the form with \texttt{-} removes % them. Ranges use the same spelling as \cs{LuaCJKDeclareCharClass}. % The sets are global: a group does not bring the previous members back. % % \begin{keyval} % \item[\keys{KaiMingPunct<+->}]\val{char list}: % The marks that keep a full width in the \texttt{kaiming} style. % Initially \texttt{。}、\texttt{.}、\texttt{?}、\texttt{!}. % \item[\keys{MiddlePunct<+->}]\val{char list}: % The marks centered between the two halves of their box. % A middle mark cannot start a line. % \item[\keys{LongPunct<+->}]\val{char list}: The % marks that are never compressed against each other, % such as dashes and ellipses. % \item[\keys{NoBreakLongPunct<+->}] % \val{char list}: The long marks that must not be separated by a line % break. Initially the same as \texttt{LongPunct}. % \end{keyval} % % \begin{function}{\LuaCJKDeclareCharClass} % \begin{syntax} % \tn{LuaCJKDeclareCharClass} \marg{class} \marg{range list} % \end{syntax} % Override the class of a code-point range. % Ranges are written in hexadecimal and the leading |"| may be % omitted; |"START -> "END| gives a range, and the list is comma separated. % % The classes are: \texttt{CJK} and \texttt{Default} make the range % behave like CJK or Latin text; % \texttt{FullLeft} and \texttt{HalfLeft} treat it as an opening mark, % \texttt{FullRight} and \texttt{HalfRight} as a closing mark, % so the line-breaking prohibitions apply; % \texttt{NormalSpace} takes the range out of the CJK/Latin glue, % like \cs{normalspacedchars}; \texttt{CJStarter}, \texttt{CM}, % and \texttt{Boundary} are treated as \texttt{CJK}, % because \LuaTeX\ has no model of combining marks or run boundaries. % \end{function} % % \begin{function}{\LuaCJKDeclarePunctClass} % \begin{syntax} % \tn{LuaCJKDeclarePunctClass} \marg{group} \marg{range list} % \end{syntax} % Override the punctuation group of a range. The groups % are \texttt{open}, \texttt{close}, \texttt{middle}, \texttt{dash}, % and \texttt{ellipsis}; \texttt{none} removes the punctuation % treatment, so the characters are no longer compressed or hung. % \end{function} % % \begin{function}{\LuaCJKResetCharClass} % Drop every class and punctuation override declared so far, % and put the normal-spaced set back to the characters % of \cs{normalspacedchars}. % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} 中ABC中\par \LuaCJKDeclareCharClass{CJK}{0041 -> 005A} 中ABC中\par \LuaCJKDeclarePunctClass{none}{FF0C}% 字,,字\par \LuaCJKResetCharClass 中ABC中 \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\LuaCJKEditPunctStyle} % \begin{syntax} % \tn{LuaCJKEditPunctStyle} \marg{style} \marg{key-value list} % \end{syntax} % Change the parameters of an existing style. % The keys are \keys{compress-adjacent, adjacent-ratio, keep-open-close}; % a key that is not given keeps its value. % \end{function} % % \begin{function}{\LuaCJKsetwidth} % \begin{syntax} % \tn{LuaCJKsetwidth} \marg{punctuation list} \marg{length} % \end{syntax} % Give the listed punctuation marks a fixed advance, % overriding the selected \keys{PunctStyle} and \keys{PunctWidth}. % The setting is global: a group does not restore the previous width, % and a later call adds to the table rather than replacing it. % A leading \texttt{*} is accepted; the width it would set at the start % or end of a line is not implemented, and the star warns once. % \end{function} % % \begin{function}{\LuaCJKsetkern} % \begin{syntax} % \tn{LuaCJKsetkern} \marg{before} \marg{after} \marg{length} % \end{syntax} % Set the gap between two adjacent punctuation marks, overriding % the style. Only the first character of each argument is used. % Like \cs{LuaCJKsetwidth}, the setting is global. % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \LuaCJKsetwidth{「}{1.5em}字「字\\ \LuaCJKsetkern{「}{」}{.3em}字「」字 \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\LuaCJKDeclarePunctStyle} % \begin{syntax} % \tn{LuaCJKDeclarePunctStyle} \marg{name} \marg{key-value list} % \end{syntax} % Define a punctuation style. % \end{function} % % \begin{keyval} % \item[\keys{compress-adjacent}]\val{\TTF}: whether two adjacent % punctuation marks are compressed. % \item[\keys{adjacent-ratio}]\val{number}: the ratio of the compression, % relative to the width of the mark that is compressed. % \item[\keys{keep-open-close}]\val{\TTF}: whether an opening mark next % to a closing one stays uncompressed. % \end{keyval} % % \iffalse %<*example> % \fi \begin{codemo} \LuaCJKDeclarePunctStyle{mine} {adjacent-ratio = .25} 字,,字\par \LuaCJKsetup{PunctStyle = mine}字,,字\par \LuaCJKEditPunctStyle{mine} {adjacent-ratio = .75} 字,,字 \end{codemo} % \iffalse % % \fi % % \subsection{Line breaking} % % A run of dashes or ellipses (U+2013, U+2014, U+2015, U+FF0D, U+2026, % and U+22EF) is one unit: the marks are joined without glue, so % justification cannot pull a two-em dash apart, and no break is offered % between them. The key \keys{LongPunct} replaces the set. % % \begin{keyval} % \item[\keys{CheckSingle}]\val{\TFF} % When on, the last two break points of a paragraph whose final character % is CJK are made unbreakable, so a single character cannot end up alone % on the last line. The initial value is \meta{false}. % \item[\keys{WidowPenalty}]\val{penalty}: The penalty used by % \texttt{CheckSingle}. The initial value is \texttt{10000}; % a lower value allows the break when it is worth the cost. % \item[\keys{CJLineBreak}]\val{normal, strict}: In the \texttt{strict} % mode a character whose Unicode \texttt{Line\_Break} property is % \texttt{CJ} (the small kana and the like) may not start a line. % The mode only adds prohibitions; it does not remove any. % The initial value is \texttt{normal}, which treats those characters % like other CJK characters. % The set is read from \file{LineBreak.txt} of the \pkg{unicode-data} % bundle, so the package needs that bundle for the strict mode. % \end{keyval} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \parbox{3em}{天地玄黄宇宙洪荒\par}\quad \LuaCJKsetup{CheckSingle = true}% \parbox{3em}{天地玄黄宇宙洪荒\par} \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\LuaCJKnobreak} % \begin{syntax} % \tn{LuaCJKnobreak} \marg{text} % \end{syntax} % Keep \meta{text} on one line. The nodes of the argument are marked % and the line-breaking pass forbids a break at any of their boundaries. % The marking ends with the current group. % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \parbox{3em}{天地玄黄}\quad \parbox{3em}{\LuaCJKnobreak{天地玄黄}} \end{codemo} \endgroup % \iffalse % % \fi % % \subsection{Suspending and hooks} % % \begin{function}{\LuaCJKdisable, \LuaCJKenable} % \tn{LuaCJKdisable} stops the typographic adjustments: no \keys{CJKglue} % or \keys{CJKecglue} is inserted, punctuation is not compressed, % no line-breaking prohibitions are added, punctuation is not hung, and % CJK characters in math mode are left alone. CJK characters are still % typeset in the CJK font, so text stays readable. The switch is local % to the current group; \tn{LuaCJKenable} turns the adjustments back on. % \end{function} % % This is the hook verbatim-like environments use, % and it is also useful when measuring raw node lists. % % \iffalse %<*example> % \fi \begingroup \begin{codemo} 精致甲版 红楼梦\\ \LuaCJKdisable 精致甲版 红楼梦 \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\LuaCJKShipoutHook} % \begin{syntax} % \tn{LuaCJKShipoutHook} \marg{code} % \end{syntax} % Run \meta{code} before every page is shipped out. The code is added to % the \texttt{shipout/before} hook, so it obeys the usual hook ordering. % \end{function} % % \subsection{Verbatim and listings} % % \begin{function}{\LuaCJKVerbAddon, \LuaCJKOffVerbAddon} % Make one CJK character as wide as two spaces of the current monospaced % font, which keeps columns aligned when a code listing contains CJK text. % The gap is added as \keys{CJKglue}, so the CJK font itself % is not scaled; a negative gap only raises a warning and adds nothing. % Breaks between CJK characters are forbidden until the group ends, % as in Latin verbatim text. % % The command does nothing at the outermost group level, % so it is meant to be used inside the environment that needs it, % for example in the \texttt{formatcom} option of \pkg{fancyvrb}. % \tn{LuaCJKOffVerbAddon} cancels it again inside the same group. % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \ttfamily abc汉字abc\\ \ttfamily\LuaCJKVerbAddon abc汉字abc \end{codemo} \endgroup % \iffalse % % \fi % % \begin{keyval} % \item[\keys{Verb}]\val{\TTF, env, env+} % What to do inside \tn{verb} and the verbatim-like environments % (\env{verbatim}, \env{Verbatim}, \env{lstlisting}, % and their relatives). \texttt{true} suspends the adjustments in both; % \texttt{env} keeps them inside the environments but not in \tn{verb}, % which preserves column alignment when the code contains CJK text; % \texttt{env+} keeps them everywhere; \texttt{false} leaves verbatim % material entirely alone. Since \texttt{env+} and \texttt{false} both % leave the adjustments on, they behave the same in \pkg{LuaCJK}. % Suspension removes the spacing only: a punctuation mark next to suspended % material still keeps the break opportunity that follows it. % \end{keyval} % % \pkg{listings} processes one character at a time and typesets a character % that is outside its character table at the moment it is read. % CJK characters are outside that table, so they end up in front of the % surrounding Latin text. The companion package \pkg{LuaCJK-listings} fixes % this: it adds the CJK characters to the character table of \pkg{listings}, % so they stay in place, counts each of them as two columns, % and turns the spacing adjustments off inside the listing. % \pkg{LuaCJK} loads it automatically when \pkg{listings} is used, % so no extra command is needed. % % \tn{lstinline} reads its argument with changed category codes; % the patch would break that scanner, % so inline code is left alone and takes the normal handling. % % \subsection{Decorations} % % The companion package \pkg{LuaCJKfntef} provides underlining, % emphasis dots, and strike-through. It builds on \pkg{lua-ul}, % so the decorated text may contain arbitrary commands, CJK and Latin text, % and may still be hyphenated and broken between lines. % % \begin{function}{\CJKunderline} % \begin{syntax} % \tn{CJKunderline} \oarg{options} \marg{text} % \end{syntax} % Underline \meta{text}. The initial geometry is \texttt{depth = 0.2em} % and \texttt{thickness = 0.4pt}. % A leading \texttt{*} or \texttt{-} is accepted and ignored. % \end{function} % % Every decoration takes the keys below; a key that a command does not use % is accepted and has no effect. A bare color name with no key means % \texttt{format = }\meta{color}. The keys \texttt{top} and \texttt{bottom} % are passed to \pkg{lua-ul}, so they apply to \cs{CJKunderline} alone and % override the computed geometry. A drawn pattern never changes the height % or depth of the line it decorates, so the leading does not grow. % % \begin{keyval} % \item[\keys{thickness}]\val{length}: the thickness of the rule, % initially \texttt{0.4pt}. % \item[\keys{depth}]\val{length}: the distance from the baseline to the % top of the rule, initially \texttt{0.2em}. % \item[\keys{textformat}]\val{code}: code applied to the text, for % example |textformat = \bfseries|. % \item[\keys{format}]\val{color name}: the color of the rule, for % example |format = red|; it affects the rule only, not the text. % \item[\keys{color}]\val{color name}: another name for \texttt{format}. % \item[\keys{subtract}]\val{length}: taken off the rule at both ends, % off by default; \texttt{true} means half an em, so two neighbouring % commands do not run together. % \item[\keys{skip}]\val{\TFF}: leaves the punctuation out of the rule. % \item[\keys{hidden}]\val{\TFF}: hides the text and keeps the rule, % with the width, height, and depth of the hidden text. % \item[\keys{gap}]\val{length}: the distance between the two rules of % the double line, initially \texttt{1.1pt}. % \item[\keys{height}]\val{length}: the distance from the baseline to the % center of the strike-out rule, initially \texttt{0.3em}. % \item[\keys{symbol}]\val{code}: a symbol replacing the wave or the dots. % \item[\keys{selectable}]\val{\TFF}: whether the decoration symbol stays % as text, initially \texttt{false}; with \texttt{true} a PDF reader can % copy and select it. % \end{keyval} % % Without \keys{format} the rule takes the color of the text it decorates, % for example the color set by \cs{textcolor}. % % \keys{sep} is accepted for compatibility and has no effect. % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \CJKunderline[thickness = 1pt]{有涯}\\ \CJKunderline[format = red]{有涯}\\ \CJKunderline[textformat = \bfseries] {有涯}\\ \CJKunderline[hidden]{有涯} \end{codemo} \endgroup % \iffalse % % \fi % % \iffalse %<*example> % \fi \begingroup \begin{codemo} 今天是 \CJKunderline{\hspace*{3em}} 年\\ 第 \CJKunderline{\hspace*{2em}} 题 \end{codemo} \endgroup % \iffalse % % \fi % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \CJKunderline{有涯}\CJKunderline{有涯}\\ \CJKunderline[subtract]{吾生也有涯}\\ \CJKunderline[skip]{有涯,无涯} \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\CJKunderdblline} % \begin{syntax} % \tn{CJKunderdblline} \oarg{options} \marg{text} % \end{syntax} % Put two parallel rules under \meta{text}, with the keys listed above. % Both rules use \keys{thickness}, the top rule starts at \keys{depth}, % and \keys{gap} is the distance between them. % A leading \texttt{*} or \texttt{-} is accepted and ignored. % \end{function} % % \begin{function}{\CJKunderwave} % \begin{syntax} % \tn{CJKunderwave} \oarg{options} \marg{text} % \end{syntax} % Put a wave under \meta{text}. % The wave is drawn with \pkg{l3draw} and repeated with \tn{leaders}, % so it has the same phase across the parts of one line. One period % is half an em and the whole pattern scales with the font size; like the % other drawn patterns it leaves the height and depth of the line alone. % \keys{symbol} replaces the wave with another symbol: % one symbol per em, centered, its top half an em below the baseline. % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \CJKunderwave{吾生也有涯}\\ \CJKunderwave[symbol = \textbullet] {吾生也有涯}\\ \CJKunderdot{吾生也有涯}\\ \CJKunderdot[symbol = \textbullet] {吾生也有涯} \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\CJKxout} % \begin{syntax} % \tn{CJKxout} \oarg{options} \marg{text} % \end{syntax} % Strike \meta{text} out with a repeated slash, like \pkg{ulem}'s % \tn{xout}. The slash runs from \texttt{-0.15em} to \texttt{0.8em} % relative to the baseline, so it crosses the whole body of a CJK glyph. % \end{function} % % \begin{function}{\LuaCJKemdescent} % The depth of the em box of the current font, usable as a length. % A CJK em box is one em square, so an object of height \texttt{1em} % placed at this depth covers the area a CJK character occupies; a font % without OpenType metrics falls back to \texttt{.12em}. % \end{function} % % \begin{function}{\CJKunderanyline} % \begin{syntax} % \tn{CJKunderanyline} \marg{depth} \marg{object} \marg{text} % \end{syntax} % Put \meta{object} under \meta{text}, repeated by \tn{leaders}. % The bottom of \meta{object} is placed \meta{depth} below % the baseline, so for a \cs{rule} (which has no depth of its own) % the rule occupies \meta{depth} below the baseline upwards. % To cover a CJK character, use the em box of the font: % \meta{depth} is \cs{LuaCJKemdescent} and the object is \texttt{1em} % high, as the example below does. The object fills the em box, which is % the area a PDF reader selects: the advance across, the em box down, so a % font whose ink is smaller shows its side bearing all round. The type is % cached, so the same arguments reuse one underline type. % \end{function} % % \begin{function}{\CJKunderanysymbol} % \begin{syntax} % \tn{CJKunderanysymbol}\oarg{options}\marg{depth}\marg{symbol}\marg{text} % \end{syntax} % The same, with one \meta{symbol} per em: the symbol is centered in a cell % as wide as a CJK character, so a row of symbols lines up with the % characters above them. \meta{depth} is the distance from the top of the % symbol to the \emph{bottom of the em box}, so |0pt| leaves the same gap % below the character as the side bearings leave beside it; a font without % OpenType metrics is computed at |.12em|. The option \keys{sep} is % accepted for compatibility but has no effect, because the cell is fixed % at one em. % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \CJKunderanyline{\LuaCJKemdescent} {\color{yellow}\rule{1pt}{1em}}{有涯}\\ \CJKunderanysymbol{0pt}{\tiny$\triangle$}{有涯} \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\CJKunderdot} % \begin{syntax} % \tn{CJKunderdot} \oarg{options} \marg{text} % \end{syntax} % Put emphasis dots under \meta{text}. % With |symbol = |\meta{symbol} a symbol takes the place of the dots % and is placed like the symbol of \cs{CJKunderanysymbol}: one per em, % centered, its top \texttt{0.3em} under the bottom of the em box. % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \CJKunderline{虚室生白,\CJKunderdot{吉祥}止止, 虚室生白} \end{codemo} \endgroup % \iffalse % % \fi % % \begin{function}{\CJKsout} % \begin{syntax} % \tn{CJKsout} \oarg{options} \marg{text} % \end{syntax} % Strike \meta{text} through, with the keys listed above. % The initial height passes through the middle of a CJK glyph, % which sits higher than the middle of the Latin x-height; % use |height = 0.5ex| for text that is entirely Latin. % \end{function} % % \iffalse %<*example> % \fi \begingroup \begin{codemo} \CJKsout[thickness = 2pt, format = red] {吾生也有涯}\\ \CJKsout[height = .5ex]{吾生也有涯} \end{codemo} \endgroup % \iffalse % % \fi % % \iffalse %<*example> % \fi \begin{codemo} \CJKunderline {吾生也有涯}\\ \CJKunderdblline{吾生也有涯}\\ \CJKunderwave {吾生也有涯}\\ \CJKunderdot {吾生也有涯}\\ \CJKsout {吾生也有涯}\\ \CJKxout {吾生也有涯} \end{codemo} % \iffalse % % \fi % % \section{Comparison with \pkg{xeCJK}} % % \pkg{LuaCJK} follows the \pkg{xeCJK} interface where the two engines allow % it, so a document that already uses \pkg{xeCJK} mostly needs no change. % Only the differences are listed. % % \subsection{Different behavior} % % \begin{itemize} % \item \keys{Verb}: \texttt{env+} behaves like \texttt{false}, because both % leave the adjustments on. % \item Punctuation geometry. A style gives a mark half the size, half with % the sentence-ending marks kept full, or 0.7 of it, and the ink moves with % the advance by class: an opening mark by the whole amount, a middle mark by % half, a closing mark not at all. % \pkg{xeCJK} measures the real side bearings of each glyph instead. % \item \cs{LuaCJKDeclarePunctStyle} takes the three keys % \keys{compress-adjacent}, \keys{adjacent-ratio}, and % \keys{keep-open-close}; the thirty geometry keys of \pkg{xeCJK} describe % its side-bearing model and have no counterpart here. % \item \texttt{CJKfilltwosides*} does not compare with the natural width. % \item The CJK font is applied to the glyph node after the text has been % shaped with the current font, so the OpenType features of the CJK font % (ligatures, \texttt{locl}, \texttt{palt}, variation sequences) do not take % effect: the spacing, the punctuation advance, and the prohibitions come % from \pkg{LuaCJK} itself. % \item In \pkg{LuaCJKfntef}, \keys{format} takes a color name rather than % code, and the decoration is drawn by \pkg{lua-ul}, so the \texttt{ulem} % option list does not exist and \tn{underLine} is configured directly. % \end{itemize} % % \subsection{Not available} % % \begin{itemize} % \item \texttt{NewLineCS}, \texttt{EnvCS}, \texttt{InlineEnv}, and % \texttt{PlainEquation}: they exist so that \pkg{xeCJK} can recognize % \TeX\ constructs while it scans tokens, and a finished node list needs no % hint. % \item \texttt{PoZheHaoLigature}, for the reason given above: the font's % two-em dash ligature is decided before \pkg{LuaCJK} applies the CJK font. % \item \texttt{RubberPunctSkip}: the advance of a mark is a kern, which is % neither a break opportunity nor part of justification, and that is what % keeps a fixed-width style stable. % \item \texttt{CheckFullRight}: it needs the control sequence after the % mark, which a node list does not show; use \cs{LuaCJKnobreak}. % \end{itemize} % % \end{documentation} % % \StopEventually{\PrintIndex} % \clearpage \appendix % % \begin{implementation} % % \section{Implementation} % % \begin{macrocode} %<*package> % \end{macrocode} % % \begin{macrocode} %<@@=LuaCJK> % \end{macrocode} % % \subsection{Loading and conflicts} % % \pkg{LuaCJK} only works with \LuaTeX. The engine test runs before any other % package is loaded, so that a wrong engine stops with a clear message. % \begin{macrocode} \sys_if_engine_luatex:F { \msg_new:nnn { LuaCJK } { engine } { LuaCJK ~ requires ~ LuaTeX. } \msg_critical:nn { LuaCJK } { engine } \tex_endinput:D } \RequirePackage { fontspec } % \end{macrocode} % % \begin{macro}{\LuaCJKversion} % \changes{v0.0.1}{2026/11/11}{Provide \cs{LuaCJKversion}, which prints the % version of the package.} % The version of the package. % \begin{variable}[int]{\c_@@_version_tl} % The version string, taken from the package metadata. % \begin{macrocode} \tl_const:Nx \c_@@_version_tl { \ExplFileVersion } % \end{macrocode} % \end{variable} % \begin{macrocode} \cs_new:Npn \LuaCJKversion { \tl_use:N \c_@@_version_tl } % \end{macrocode} % \end{macro} % % \pkg{luatexja} installs its own CJK font and spacing layer and works % on the same callbacks as \pkg{LuaCJK}, so the two are rejected % whatever the loading order. \pkg{xeCJK} needs no check here: % it stops by itself on any engine other than \XeTeX. % \begin{macrocode} \msg_new:nnn { LuaCJK } { luatexja-conflict } { LuaCJK ~ and ~ LuaTeX-ja ~ must ~ not ~ be ~ loaded ~ together.\\ Load ~ only ~ one ~ of ~ them. } \cs_if_exist:cT { ver@luatexja.sty } { \msg_critical:nn { LuaCJK } { luatexja-conflict } } \hook_gput_code:nnn { package / luatexja / before } { LuaCJK } { \msg_critical:nn { LuaCJK } { luatexja-conflict } } % \end{macrocode} % % \subsection{Registers, styles, and options} % % The runtime state lives in attributes and registers, % which the options below fill in. % % \begin{variable}[int] % { % \g_@@_font_attr, \c_@@_font_attr_int, % \g_@@_cjk_space_attr, \c_@@_cjk_space_attr_int, % \g_@@_xecglue_attr, \c_@@_xecglue_attr_int, % \g_@@_inserted_attr, \c_@@_inserted_attr_int, % \g_@@_math_font_attr, \c_@@_math_font_attr_int, % \g_@@_disabled_attr, \c_@@_disabled_attr_int, % \g_@@_punct_font_attr, \c_@@_punct_font_attr_int, % \g_@@_settings_attr, \c_@@_settings_attr_int, % \g_@@_nobreak_attr, \c_@@_nobreak_attr_int % } % The attributes record, per node, the CJK font, the space and glue % policies, whether the node was inserted by \pkg{LuaCJK}, the CJK math font, % and whether the adjustments are suspended. The integer constants hold % the allocated attribute numbers for the Lua side. % \begin{macrocode} \clist_map_inline:nn { font, cjk_space, xecglue, inserted, math_font, disabled, punct_font, settings, nobreak } { \exp_args:Nc \newattribute { g_@@_#1_attr } \int_const:cn { c_@@_#1_attr_int } { \int_use:N \allocationnumber } } \g_@@_cjk_space_attr = 0 \g_@@_xecglue_attr = 0 \g_@@_math_font_attr = 0 \g_@@_punct_font_attr = 0 \g_@@_settings_attr = 0 % \end{macrocode} % \end{variable} % % \begin{variable}[int]{\l_@@_ccglue_skip, \l_@@_ecglue_skip} % The glue used between CJK characters and between CJK and Latin text. % \begin{macrocode} \skip_new:N \l_@@_ccglue_skip \skip_new:N \l_@@_ecglue_skip \bool_new:N \l_@@_ccglue_touched_bool \bool_new:N \l_@@_ecglue_touched_bool \skip_set:Nn \l_@@_ccglue_skip { 0pt plus 0.08 \tex_baselineskip:D } \skip_set:Nn \l_@@_ecglue_skip { 0.3333em plus 0.1667em minus 0.1111em } % \end{macrocode} % \end{variable} % % \begin{macro}[int]{\@@_set_ccglue:n, \@@_set_ecglue:n} % The two glues keep their default relative to the current size, so they % are recomputed on every font selection unless the document set them. % A value given by the document is taken as it is. % \begin{macrocode} \cs_new_protected:Npn \@@_set_ccglue:n #1 { \skip_set:Nn \l_@@_ccglue_skip {#1} \bool_set_true:N \l_@@_ccglue_touched_bool } \cs_new_protected:Npn \@@_set_ecglue:n #1 { \skip_set:Nn \l_@@_ecglue_skip {#1} \bool_set_true:N \l_@@_ecglue_touched_bool } \cs_new_protected:Npn \@@_refresh_glues: { \bool_if:NF \l_@@_ccglue_touched_bool { \skip_set:Nn \l_@@_ccglue_skip { 0pt plus 0.08 \tex_baselineskip:D } } \bool_if:NF \l_@@_ecglue_touched_bool { \skip_set:Nn \l_@@_ecglue_skip { 0.3333em plus 0.1667em minus 0.1111em } } } % \end{macrocode} % \end{macro} % % A punctuation style is a property list, % read through the settings of each node. % % \begin{variable}[int]{\g_@@_punct_style_prop} % The names of the styles \pkg{LuaCJK} knows about, % including those defined by \cs{LuaCJKDeclarePunctStyle}. % \begin{macrocode} \prop_new:N \g_@@_punct_style_prop \prop_gput:Nnn \g_@@_punct_style_prop { quanjiao } { } \prop_gput:Nnn \g_@@_punct_style_prop { plain } { } \prop_gput:Nnn \g_@@_punct_style_prop { banjiao } { } \prop_gput:Nnn \g_@@_punct_style_prop { kaiming } { } \prop_gput:Nnn \g_@@_punct_style_prop { CCT } { } \prop_gput:Nnn \g_@@_punct_style_prop { hangmobanjiao } { } % \end{macrocode} % \end{variable} % % \begin{macro}[int]{\@@_set_punct_style:n} % \changes{v0.0.1}{2026/11/11}{Provide the \keys{PunctStyle} key with the % \texttt{quanjiao}, \texttt{plain}, \texttt{banjiao}, \texttt{kaiming}, % \texttt{CCT}, and \texttt{hangmobanjiao} styles. Under a style with a % fixed width the advance of an opening or middle mark moves its ink with % it.} % Store the selected style, % or warn and fall back to \texttt{plain} when the name is unknown. % \begin{variable}[int]{\l_@@_punct_style_tl} % The style name read by the Lua side. % \begin{macrocode} \tl_new:N \l_@@_punct_style_tl \tl_set:Nn \l_@@_punct_style_tl { quanjiao } % \end{macrocode} % \end{variable} % \begin{macrocode} \msg_new:nnn { LuaCJK } { punct-style-unknown } { PunctStyle ~ `#1' ~ is ~ not ~ implemented.\\ Using ~ `plain' ~ instead. } \cs_new_protected:Npn \@@_set_punct_style:n #1 { \prop_if_in:NnTF \g_@@_punct_style_prop {#1} { \tl_set:Nn \l_@@_punct_style_tl {#1} } { \msg_warning:nnn { LuaCJK } { punct-style-unknown } {#1} \tl_set:Nn \l_@@_punct_style_tl { plain } } } % \end{macrocode} % \end{macro} % % \begin{macro}{\LuaCJKEditPunctStyle} % \changes{v0.0.1}{2026/11/11}{Provide \cs{LuaCJKEditPunctStyle} to edit the % punctuation sets of the current style, and \keys{AllowBreakBetweenPuncts}.} % Change the parameters of an existing style. A key that is not given % keeps its current value, which is why the three ``given'' switches % are tracked here instead of starting from the defaults. % \begin{variable}[int] % { % \l_@@_punct_compress_given_bool, % \l_@@_punct_ratio_given_bool, % \l_@@_punct_keep_given_bool % } % Whether the corresponding key was part of the edit. % \begin{macrocode} \bool_new:N \l_@@_punct_compress_given_bool \bool_new:N \l_@@_punct_ratio_given_bool \bool_new:N \l_@@_punct_keep_given_bool \tl_new:N \l_@@_punct_compress_value_tl \tl_new:N \l_@@_punct_keep_value_tl % \end{macrocode} % \end{variable} % \begin{macrocode} \keys_define:nn { LuaCJK / punct-edit } { compress-adjacent .code:n = { \bool_set_true:N \l_@@_punct_compress_given_bool \tl_set:Nn \l_@@_punct_compress_value_tl {#1} }, adjacent-ratio .code:n = { \bool_set_true:N \l_@@_punct_ratio_given_bool \fp_set:Nn \l_@@_punct_ratio_fp {#1} }, keep-open-close .code:n = { \bool_set_true:N \l_@@_punct_keep_given_bool \tl_set:Nn \l_@@_punct_keep_value_tl {#1} }, } \NewDocumentCommand \LuaCJKEditPunctStyle { m m } { \bool_set_false:N \l_@@_punct_compress_given_bool \bool_set_false:N \l_@@_punct_ratio_given_bool \bool_set_false:N \l_@@_punct_keep_given_bool \keys_set:nn { LuaCJK / punct-edit } {#2} \lua_now:e { LuaCJK.edit_punct_style ( "\lua_escape:e {#1}" , \bool_if:NTF \l_@@_punct_compress_given_bool { \l_@@_punct_compress_value_tl } { nil }, \bool_if:NTF \l_@@_punct_ratio_given_bool { \fp_use:N \l_@@_punct_ratio_fp } { nil }, \bool_if:NTF \l_@@_punct_keep_given_bool { \l_@@_punct_keep_value_tl } { nil } ) } } % \end{macrocode} % \end{macro} % % \begin{macro}{\LuaCJKsetwidth} % \changes{v0.0.1}{2026/11/11}{Provide \cs{LuaCJKsetwidth} and % \cs{LuaCJKsetkern} for per-mark widths and per-pair gaps that override the % selected punctuation style.} % Give specific punctuation marks a fixed advance, overriding the % selected style. The star form of \pkg{xeCJK}, which sets the width used % at the start or end of a line, has no counterpart yet and only warns. % \begin{variable}[int]{\l_@@_bound_width_warned_bool} % Guards the one-time warning for the star form. % \begin{macrocode} \bool_new:N \l_@@_bound_width_warned_bool \msg_new:nnn { LuaCJK } { bound-width-unsupported } { The ~ star ~ form ~ of ~ \token_to_str:N \LuaCJKsetwidth, ~ which ~ sets ~ the ~ width ~ at ~ the ~ start ~ or ~ end ~ of ~ a ~ line, ~ is ~ not ~ implemented.\\ The ~ width ~ is ~ applied ~ to ~ every ~ position ~ instead. } % \end{macrocode} % \end{variable} % \begin{macrocode} \NewDocumentCommand \LuaCJKsetwidth { s m m } { \IfBooleanT {#1} { \bool_if:NF \l_@@_bound_width_warned_bool { \bool_set_true:N \l_@@_bound_width_warned_bool \msg_warning:nn { LuaCJK } { bound-width-unsupported } } } \lua_now:e { LuaCJK.set_punct_width ( "\lua_escape:e {#2}" , \dim_to_decimal_in_sp:n {#3} ) } } % \end{macrocode} % \end{macro} % % \begin{macro}{\LuaCJKsetkern} % Set the gap between two adjacent punctuation marks, overriding the % selected style. Only the first character of each argument is used. % \begin{macrocode} \NewDocumentCommand \LuaCJKsetkern { m m m } { \lua_now:e { LuaCJK.set_punct_kern ( "\lua_escape:e {#1}" , "\lua_escape:e {#2}" , \dim_to_decimal_in_sp:n {#3} ) } } % \end{macrocode} % \end{macro} % % A single CJK font rarely covers every block of \textsc{Unicode}. % A sub-block gives a range of characters its own font inside a family, % named by an option of the font commands. % % \begin{macro}{\LuaCJKDeclareSubCJKBlock} % \changes{v0.0.1}{2026/11/11}{Provide \cs{LuaCJKDeclareSubCJKBlock} with % \cs{LuaCJKCancelSubCJKBlock} and \cs{LuaCJKRestoreSubCJKBlock}, so a range % of characters can use its own font inside a family.} % \begin{variable}[int] % {\g_@@_block_prop, \g_@@_block_names_clist} % The ranges of each block, and the declaration order that decides % which block wins when the ranges overlap. % \begin{macrocode} \prop_new:N \g_@@_block_prop \clist_new:N \g_@@_block_names_clist \NewDocumentCommand \LuaCJKDeclareSubCJKBlock { m m } { \@@_build_ranges:n {#2} \prop_gput:NnV \g_@@_block_prop {#1} \l_@@_ranges_tl \clist_if_in:NnF \g_@@_block_names_clist {#1} { \clist_gput_right:Nn \g_@@_block_names_clist {#1} } \lua_now:e { LuaCJK.declare_block ( "\lua_escape:e {#1}" , { \l_@@_ranges_tl } ) } } % \end{macrocode} % \end{variable} % \end{macro} % % \begin{macro}{\LuaCJKCancelSubCJKBlock, \LuaCJKRestoreSubCJKBlock} % Turn a block off or on again. The list of canceled blocks is part % of the settings of the text, so the change ends with the group; the % fonts of the current family are refreshed so it takes effect at once. % \begin{macrocode} \clist_new:N \l_@@_blocks_off_clist \cs_new_protected:Npn \@@_set_block_enabled:nn #1#2 { \clist_map_inline:nn {#1} { \str_if_eq:nnTF {#2} { false } { \clist_if_in:NnF \l_@@_blocks_off_clist {##1} { \clist_put_right:Nn \l_@@_blocks_off_clist {##1} } } { \clist_remove_all:Nn \l_@@_blocks_off_clist {##1} } } \@@_settings_push: \@@_update_font: } \NewDocumentCommand \LuaCJKCancelSubCJKBlock { m } { \@@_set_block_enabled:nn {#1} { false } } \NewDocumentCommand \LuaCJKRestoreSubCJKBlock { m } { \@@_set_block_enabled:nn {#1} { true } } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_extract_font_keys:nn, \@@_parse_font_options:n} % \changes{v0.0.2}{2026/11/15}{\keys{Baseline} shifts the glyphs of the font % that declares it, so two CJK fonts whose baselines do not match can be set % side by side.} % Pull the keys that \pkg{LuaCJK} keeps for itself out of a font option list: % the block names, which name a font of their own, and \keys{Baseline}. % The remaining options go into \cs{l_@@_features_tl}, which is what % \pkg{fontspec} is given, and \keys{Baseline} goes into % \cs{l_@@_shift_tl}. % A list that names neither is handed to \pkg{fontspec} exactly as it was % written, because the parse would only respell it. % \begin{variable}[int] % { % \g_@@_family_block_prop, \g_@@_pending_block_prop, % \l_@@_features_tl, \l_@@_block_key_tl, % \l_@@_option_key_tl, \l_@@_option_seq, % \l_@@_shift_tl % } % The block font of each family and block, the blocks found in the % option list being processed, the filtered options, the vertical offset % \keys{Baseline} asks for, and scratch variables for splitting one option % at the equals sign. % \begin{macrocode} \prop_new:N \g_@@_family_block_prop \prop_new:N \g_@@_pending_block_prop \tl_new:N \l_@@_features_tl \tl_new:N \l_@@_block_key_tl \tl_new:N \l_@@_option_key_tl \seq_new:N \l_@@_option_seq \bool_new:N \l_@@_is_block_bool \tl_new:N \l_@@_shift_tl \cs_new_protected:Npn \@@_put_feature:n #1 { \tl_if_empty:NTF \l_@@_features_tl { \tl_set:Nn \l_@@_features_tl {#1} } { \tl_put_right:Nn \l_@@_features_tl { , ~ #1 } } } \cs_new_protected:Npn \@@_font_option:nn #1#2 { \tl_set:Nn \l_@@_option_key_tl {#1} \tl_trim_spaces:N \l_@@_option_key_tl \str_if_eq:eeTF { \l_@@_option_key_tl } { Baseline } { \tl_set:Ne \l_@@_shift_tl {#2} } { \bool_set_false:N \l_@@_is_block_bool \clist_map_inline:Nn \g_@@_block_names_clist { \str_if_eq:eeT {##1} { \l_@@_option_key_tl } { \bool_set_true:N \l_@@_is_block_bool } } \bool_if:NTF \l_@@_is_block_bool { \prop_gput:NVn \g_@@_pending_block_prop \l_@@_option_key_tl {#2} } { \@@_put_feature:n { #1 = {#2} } } } } \cs_new_protected:Npn \@@_font_option:n #1 { \@@_put_feature:n {#1} } \cs_new_protected:Npn \@@_parse_font_options:n #1 { \tl_clear:N \l_@@_features_tl \tl_clear:N \l_@@_shift_tl \keyval_parse:NNn \@@_font_option:n \@@_font_option:nn {#1} } \cs_new_protected:Npn \@@_extract_font_keys:nn #1#2 { \prop_gclear:N \g_@@_pending_block_prop \@@_parse_font_options:n {#2} \prop_gput:NnV \g_@@_shift_decl_prop {#1} \l_@@_shift_tl \prop_if_empty:NTF \g_@@_pending_block_prop { \tl_if_empty:NT \l_@@_shift_tl { \tl_set:Nn \l_@@_features_tl {#2} } } { \@@_register_block_fonts:n {#1} } } \cs_new_protected:Npn \@@_register_block_fonts:n #1 { \prop_map_inline:Nn \g_@@_pending_block_prop { \group_begin: \fontspec_gset_family:Nnn \l_@@_tmpa_tl { } {##2} \tl_set:Ne \l_@@_block_key_tl { #1 | ##1 } \prop_gput:NVV \g_@@_family_block_prop \l_@@_block_key_tl \l_@@_tmpa_tl \group_end: } } % \end{macrocode} % \end{variable} % \end{macro} % % \begin{macro}{\LuaCJKDeclareCharClass} % \changes{v0.0.1}{2026/11/11}{Provide \cs{LuaCJKDeclareCharClass}, % \cs{LuaCJKDeclarePunctClass}, and \cs{LuaCJKResetCharClass} to override % the class of a code-point range. The classes carry the names of % \pkg{xeCJK} (\texttt{FullLeft}, \texttt{FullRight}, \texttt{HalfLeft}, % \texttt{HalfRight}, \texttt{NormalSpace}, \texttt{CJStarter}, \texttt{CM}, % \texttt{Boundary}), the reset also restores the normal-spaced set, and the % built-in table covers the CJK blocks of Unicode 15 and 16.} % Override the class of a code-point range. % The range list uses the \pkg{xeCJK} spelling: hexadecimal numbers % with a leading |"|, and |"START -> "END| for a range. % \begin{variable}[int] % { % \l_@@_ranges_tl, \l_@@_range_seq, % \l_@@_range_pair_tl, \l_@@_hex_tl, % \l_@@_hex_int_tl % } % The Lua table being built, the two halves of one range, % and the current hexadecimal number and its value. % \begin{macrocode} \tl_new:N \l_@@_ranges_tl \seq_new:N \l_@@_range_seq \tl_new:N \l_@@_range_pair_tl \tl_new:N \l_@@_hex_tl \tl_new:N \l_@@_hex_int_tl % \end{macrocode} % \end{variable} % \begin{macrocode} \cs_new_protected:Npn \@@_hex_store:n #1 { \tl_set:Nn \l_@@_hex_tl {#1} \tl_replace_all:Nnn \l_@@_hex_tl { " } { } \tl_trim_spaces:N \l_@@_hex_tl \tl_set:Ne \l_@@_hex_int_tl { \exp_args:NV \int_from_hex:n \l_@@_hex_tl } } \cs_new_protected:Npn \@@_range_entry:n #1 { \seq_set_split:Nnn \l_@@_range_seq { -> } {#1} \tl_clear:N \l_@@_range_pair_tl \seq_map_inline:Nn \l_@@_range_seq { \@@_hex_store:n {##1} \tl_put_right:Nx \l_@@_range_pair_tl { \exp_not:V \l_@@_hex_int_tl , } } \tl_put_right:Nx \l_@@_ranges_tl { { \exp_not:V \l_@@_range_pair_tl } , } } \cs_new_protected:Npn \@@_build_ranges:n #1 { \tl_clear:N \l_@@_ranges_tl \clist_map_inline:nn {#1} { \@@_range_entry:n {##1} } } \msg_new:nnn { LuaCJK } { unknown-char-class } { Unknown ~ character ~ class ~ `#1' ~ is ~ being ~ ignored. } \msg_new:nnn { LuaCJK } { unknown-punct-class } { Unknown ~ punctuation ~ class ~ `#1' ~ is ~ being ~ ignored. } \NewDocumentCommand \LuaCJKDeclareCharClass { m m } { \str_case:nnF {#1} { { CJK } { \@@_push_class:nn { true } {#2} } { Default } { \@@_push_class:nn { false } {#2} } { FullLeft } { \@@_push_punct:nn { "open" } {#2} } { HalfLeft } { \@@_push_punct:nn { "open" } {#2} } { FullRight } { \@@_push_punct:nn { "close" } {#2} } { HalfRight } { \@@_push_punct:nn { "close" } {#2} } { NormalSpace } { \@@_push_normal_space:n {#2} } { CJStarter } { \@@_push_class:nn { true } {#2} } { CM } { \@@_push_class:nn { true } {#2} } { Boundary } { \@@_push_class:nn { true } {#2} } } { \msg_warning:nnn { LuaCJK } { unknown-char-class } {#1} } } \cs_new_protected:Npn \@@_push_normal_space:n #1 { \@@_build_ranges:n {#1} \lua_now:e { LuaCJK.add_normal_spaced ( { \l_@@_ranges_tl } ) } } \cs_new_protected:Npn \@@_push_class:nn #1#2 { \@@_build_ranges:n {#2} \lua_now:e { LuaCJK.declare_class ( { \l_@@_ranges_tl } , #1 ) } } % \end{macrocode} % \end{macro} % % \begin{macro}{\LuaCJKDeclarePunctClass} % Override the punctuation group of a range, % or with \texttt{none} remove the punctuation treatment from it. % \begin{macrocode} \NewDocumentCommand \LuaCJKDeclarePunctClass { m m } { \str_case:nnF {#1} { { open } { \@@_push_punct:nn { "open" } {#2} } { close } { \@@_push_punct:nn { "close" } {#2} } { middle } { \@@_push_punct:nn { "middle" } {#2} } { dash } { \@@_push_punct:nn { "dash" } {#2} } { ellipsis } { \@@_push_punct:nn { "ellipsis" } {#2} } { none } { \@@_push_punct:nn { false } {#2} } } { \msg_warning:nnn { LuaCJK } { unknown-punct-class } {#1} } } \cs_new_protected:Npn \@@_push_punct:nn #1#2 { \@@_build_ranges:n {#2} \lua_now:e { LuaCJK.declare_punct ( { \l_@@_ranges_tl } , #1 ) } } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_set_charset:nnn} % Replace, extend, or reduce one of the punctuation character sets. The % ranges are converted the same way as for \cs{LuaCJKDeclareCharClass}. % \begin{macrocode} \cs_new_protected:Npn \@@_set_charset:nnn #1#2#3 { \@@_build_ranges:n {#3} \lua_now:e { LuaCJK.set_charset ( "#1" , "#2" , { \l_@@_ranges_tl } ) } } % \end{macrocode} % \end{macro} % % \begin{macro}{\LuaCJKResetCharClass} % Drop every class and punctuation override declared so far, % and put the normal-spaced set back to the characters % of \cs{normalspacedchars}. % \begin{macrocode} \NewDocumentCommand \LuaCJKResetCharClass { } { \lua_now:n { LuaCJK.reset_classes() } \@@_set_normal_spaced_chars: } % \end{macrocode} % \end{macro} % % \begin{macro}{\LuaCJKDeclarePunctStyle} % \changes{v0.0.1}{2026/11/11}{Provide \cs{LuaCJKDeclarePunctStyle} for a % punctuation style of your own, with the keys \keys{compress-adjacent}, % \keys{adjacent-ratio}, and \keys{keep-open-close}.} % Define a style from a small set of keys and hand the result to Lua. % \begin{variable}[int] % { % \l_@@_punct_compress_bool, \l_@@_punct_ratio_fp, % \l_@@_punct_keep_bool % } % The values collected from the keys, % with the defaults used when a key is not given. % \begin{macrocode} \bool_new:N \l_@@_punct_compress_bool \fp_new:N \l_@@_punct_ratio_fp \bool_new:N \l_@@_punct_keep_bool % \end{macrocode} % \end{variable} % \begin{macrocode} \keys_define:nn { LuaCJK / punct-style } { compress-adjacent .bool_set:N = \l_@@_punct_compress_bool, adjacent-ratio .fp_set:N = \l_@@_punct_ratio_fp, keep-open-close .bool_set:N = \l_@@_punct_keep_bool, } \NewDocumentCommand \LuaCJKDeclarePunctStyle { m m } { \bool_set_true:N \l_@@_punct_compress_bool \fp_set:Nn \l_@@_punct_ratio_fp { 0.5 } \bool_set_true:N \l_@@_punct_keep_bool \keys_set:nn { LuaCJK / punct-style } {#2} \prop_gput:Nnn \g_@@_punct_style_prop {#1} { } \lua_now:e { LuaCJK.define_punct_style( "\lua_escape:e {#1}", \bool_if:NTF \l_@@_punct_compress_bool { true } { false }, \fp_use:N \l_@@_punct_ratio_fp, \bool_if:NTF \l_@@_punct_keep_bool { true } { false } ) } } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_set_autofake:nn} % \changes{v0.0.1}{2026/11/11}{Provide \keys{AutoFakeBold}, % \keys{AutoFakeSlant}, \keys{EmboldenFactor}, and \keys{SlantFactor}; a % family that sets \keys{BoldFont} or \keys{ItalicFont} is not emboldened or % slanted.} % Turn fake bold or slant on or off. A number both enables the feature % and sets the factor, as in \pkg{xeCJK}. % \begin{macrocode} \cs_new_protected:Npn \@@_set_autofake:nn #1#2 { \str_case:nnF {#2} { { true } { \str_if_eq:nnTF {#1} { bold } { \tl_gset_eq:NN \g_@@_autofake_bold_tl \g_@@_embolden_factor_tl } { \tl_gset_eq:NN \g_@@_autofake_slant_tl \g_@@_slant_factor_tl } } { false } { \str_if_eq:nnTF {#1} { bold } { \tl_gclear:N \g_@@_autofake_bold_tl } { \tl_gclear:N \g_@@_autofake_slant_tl } } } { \str_if_eq:nnTF {#1} { bold } { \tl_gset:Nn \g_@@_embolden_factor_tl {#2} \tl_gset:Nn \g_@@_autofake_bold_tl {#2} } { \tl_gset:Nn \g_@@_slant_factor_tl {#2} \tl_gset:Nn \g_@@_autofake_slant_tl {#2} } } } % \end{macrocode} % \end{macro} % % \begin{macro}{\LuaCJKsetup} % \changes{v0.0.1}{2026/11/11}{Provide \cs{LuaCJKsetup}, and accept the same % keys as package options, for example \cs{usepackage}|[PunctStyle = % banjiao]{LuaCJK}|; an unknown key warns.} % The same key path serves \cs{LuaCJKsetup} and the package options. % \begin{variable}[int] % { % \l_@@_punct_kinsoku_tl, \l_@@_cjk_math_tl, % \l_@@_cj_strict_tl, % \l_@@_verb_mode_tl, \l_@@_local_config_tl, % \l_@@_check_single_tl, \l_@@_widow_penalty_int, % \l_@@_punct_width_dim, \l_@@_punct_bound_width_dim, % \l_@@_break_between_puncts_bool, % \g_@@_autofake_bold_tl, \g_@@_autofake_slant_tl, % \g_@@_embolden_factor_tl, \g_@@_slant_factor_tl, % \g_@@_fallback_prop, \l_@@_auto_fallback_bool, % \g_@@_load_fandol_bool, % \l_@@_fallback_family_tl, \l_@@_fallback_ids_tl, \l_@@_fontid_tl, % \g_@@_shift_decl_prop, \g_@@_font_shift_prop, % \g_@@_shift_pushed_prop, \l_@@_shift_dim_tl, % \l_@@_shift_fid_tl, % \l_@@_shift_family_tl % } % The switches read by the Lua side, the \keys{Verb} setting, % the configuration file name, the fake bold and slant parameters, % and the fallback fonts. % \begin{macrocode} \tl_new:N \l_@@_punct_kinsoku_tl \tl_set:Nn \l_@@_punct_kinsoku_tl { 1 } \tl_new:N \l_@@_cj_strict_tl \tl_set:Nn \l_@@_cj_strict_tl { 0 } \tl_new:N \l_@@_cjk_math_tl \tl_set:Nn \l_@@_cjk_math_tl { 0 } \tl_new:N \l_@@_latin_punct_tl \tl_set:Nn \l_@@_latin_punct_tl { 0 } \tl_new:N \l_@@_verb_mode_tl \tl_set:Nn \l_@@_verb_mode_tl { true } \bool_new:N \l_@@_break_between_puncts_bool \dim_new:N \l_@@_punct_width_dim \dim_set:Nn \l_@@_punct_width_dim { 0pt } \dim_new:N \l_@@_punct_bound_width_dim \dim_set:Nn \l_@@_punct_bound_width_dim { 0pt } \tl_new:N \l_@@_check_single_tl \tl_set:Nn \l_@@_check_single_tl { 0 } \int_new:N \l_@@_widow_penalty_int \int_set:Nn \l_@@_widow_penalty_int { 10000 } \tl_new:N \l_@@_local_config_tl \tl_new:N \g_@@_autofake_bold_tl \tl_new:N \g_@@_autofake_slant_tl \tl_new:N \g_@@_embolden_factor_tl \tl_gset:Nn \g_@@_embolden_factor_tl { 4 } \tl_new:N \g_@@_slant_factor_tl \tl_gset:Nn \g_@@_slant_factor_tl { 0.167 } \prop_new:N \g_@@_fallback_prop \bool_new:N \l_@@_auto_fallback_bool \bool_new:N \g_@@_load_fandol_bool \tl_new:N \l_@@_fallback_family_tl \tl_new:N \l_@@_fallback_key_tl \prop_new:N \g_@@_shift_decl_prop \prop_new:N \g_@@_font_shift_prop \prop_new:N \g_@@_shift_pushed_prop \tl_new:N \l_@@_shift_dim_tl \tl_new:N \l_@@_shift_fid_tl \tl_new:N \l_@@_shift_family_tl \cs_generate_variant:Nn \prop_get:NnNF { NVNF } \cs_generate_variant:Nn \prop_gput:Nnn { NVV , NnV } \tl_new:N \l_@@_fallback_ids_tl \tl_new:N \l_@@_fontid_tl % \end{macrocode} % \end{variable} % \begin{macrocode} \keys_define:nn { LuaCJK / options } { CJKglue .code:n = { \@@_set_ccglue:n {#1} }, CJKecglue .code:n = { \@@_set_ecglue:n {#1} }, CJKspace .choice:, CJKspace / true .code:n = { \g_@@_cjk_space_attr = 1 }, CJKspace / false .code:n = { \g_@@_cjk_space_attr = 0 }, CJKspace .default:n = { true }, CJKspace .initial:n = { false }, xCJKecglue .choice:, xCJKecglue / true .code:n = { \g_@@_xecglue_attr = 1 }, xCJKecglue / false .code:n = { \g_@@_xecglue_attr = 0 }, xCJKecglue .default:n = { true }, xCJKecglue .initial:n = { false }, PunctStyle .code:n = { \@@_set_punct_style:n {#1} }, PunctStyle .default:n = { quanjiao }, PunctKinsoku .choice:, PunctKinsoku / true .code:n = { \tl_set:Nn \l_@@_punct_kinsoku_tl { 1 } }, PunctKinsoku / false.code:n = { \tl_set:Nn \l_@@_punct_kinsoku_tl { 0 } }, PunctKinsoku .default:n = { true }, CJLineBreak .choice:, CJLineBreak / normal.code:n = { \tl_set:Nn \l_@@_cj_strict_tl { 0 } }, CJLineBreak / strict.code:n = { \tl_set:Nn \l_@@_cj_strict_tl { 1 } \@@_load_cj: }, CJLineBreak .default:n = { strict }, CJKmath .choice:, CJKmath / true .code:n = { \tl_set:Nn \l_@@_cjk_math_tl { 1 } \@@_update_font: }, CJKmath / false .code:n = { \tl_set:Nn \l_@@_cjk_math_tl { 0 } \@@_update_font: }, CJKmath .default:n = { true }, Verb .choice:, Verb / true .code:n = { \tl_set:Nn \l_@@_verb_mode_tl { true } }, Verb / env .code:n = { \tl_set:Nn \l_@@_verb_mode_tl { env } }, Verb / env+ .code:n = { \tl_set:Nn \l_@@_verb_mode_tl { env+ } }, Verb / false .code:n = { \tl_set:Nn \l_@@_verb_mode_tl { false } }, Verb .default:n = { true }, KaiMingPunct .code:n = { \@@_set_charset:nnn { kaiming } { set } {#1} }, KaiMingPunct+ .code:n = { \@@_set_charset:nnn { kaiming } { add } {#1} }, KaiMingPunct- .code:n = { \@@_set_charset:nnn { kaiming } { remove } {#1} }, MiddlePunct .code:n = { \@@_set_charset:nnn { middle } { set } {#1} }, MiddlePunct+ .code:n = { \@@_set_charset:nnn { middle } { add } {#1} }, MiddlePunct- .code:n = { \@@_set_charset:nnn { middle } { remove } {#1} }, LongPunct .code:n = { \@@_set_charset:nnn { long } { set } {#1} }, LongPunct+ .code:n = { \@@_set_charset:nnn { long } { add } {#1} }, LongPunct- .code:n = { \@@_set_charset:nnn { long } { remove } {#1} }, NoBreakLongPunct .code:n = { \@@_set_charset:nnn { nobreak } { set } {#1} }, NoBreakLongPunct+ .code:n = { \@@_set_charset:nnn { nobreak } { add } {#1} }, NoBreakLongPunct- .code:n = { \@@_set_charset:nnn { nobreak } { remove } {#1} }, PunctWidth .code:n = { \str_if_eq:nnTF {#1} { false } { \dim_zero:N \l_@@_punct_width_dim } { \dim_set:Nn \l_@@_punct_width_dim {#1} } }, PunctWidth .default:n = { false }, PunctFamily .code:n = { \str_if_eq:nnTF {#1} { false } { \tl_clear:N \l_@@_punct_family_tl } { \tl_set:Nn \l_@@_punct_family_tl {#1} } \@@_update_font: }, PunctFamily .default:n = { false }, PunctBoundWidth .code:n = { \str_if_eq:nnTF {#1} { false } { \dim_zero:N \l_@@_punct_bound_width_dim } { \dim_set:Nn \l_@@_punct_bound_width_dim {#1} } }, PunctBoundWidth .default:n = { false }, LatinPunct .choice:, LatinPunct / true .code:n = { \tl_set:Nn \l_@@_latin_punct_tl { 1 } }, LatinPunct / false .code:n = { \tl_set:Nn \l_@@_latin_punct_tl { 0 } }, LatinPunct .default:n = { true }, LatinPunct .initial:n = { false }, AllowBreakBetweenPuncts .choice:, AllowBreakBetweenPuncts / true .code:n = { \bool_set_true:N \l_@@_break_between_puncts_bool }, AllowBreakBetweenPuncts / false .code:n = { \bool_set_false:N \l_@@_break_between_puncts_bool }, AllowBreakBetweenPuncts .default:n = { true }, CheckSingle .choice:, CheckSingle / true .code:n = { \tl_set:Nn \l_@@_check_single_tl { 1 } }, CheckSingle / false .code:n = { \tl_set:Nn \l_@@_check_single_tl { 0 } }, CheckSingle .default:n = { true }, WidowPenalty .code:n = { \int_set:Nn \l_@@_widow_penalty_int {#1} }, LocalConfig .code:n = { \str_case:nnF {#1} { { true } { \tl_set:Nn \l_@@_local_config_tl { luacjk.cfg } } { false } { \tl_clear:N \l_@@_local_config_tl } } { \tl_set:Nn \l_@@_local_config_tl { luacjk-#1.cfg } } }, LocalConfig .default:n = { true }, AutoFakeBold .code:n = { \@@_set_autofake:nn { bold } {#1} }, AutoFakeBold .default:n = { true }, AutoFakeBold .initial:n = { false }, AutoFakeSlant .code:n = { \@@_set_autofake:nn { slant } {#1} }, AutoFakeSlant .default:n = { true }, AutoFakeSlant .initial:n = { false }, EmboldenFactor .code:n = { \tl_gset:Nn \g_@@_embolden_factor_tl {#1} \tl_if_empty:NF \g_@@_autofake_bold_tl { \tl_gset:Nn \g_@@_autofake_bold_tl {#1} } }, AutoFallBack .choice:, AutoFallBack / true .code:n = { \bool_set_true:N \l_@@_auto_fallback_bool \@@_refresh_fallbacks: }, AutoFallBack / false.code:n = { \bool_set_false:N \l_@@_auto_fallback_bool }, AutoFallBack .default:n = { true }, LoadFandol .choice:, LoadFandol / true .code:n = { \bool_gset_true:N \g_@@_load_fandol_bool }, LoadFandol / false .code:n = { \bool_gset_false:N \g_@@_load_fandol_bool }, LoadFandol .default:n = { true }, SlantFactor .code:n = { \tl_gset:Nn \g_@@_slant_factor_tl {#1} \tl_if_empty:NF \g_@@_autofake_slant_tl { \tl_gset:Nn \g_@@_autofake_slant_tl {#1} } }, unknown .code:n = { \msg_warning:nne { LuaCJK } { unknown-option } { \l_keys_key_str } }, } \msg_new:nnn { LuaCJK } { unknown-option } { Unknown ~ option ~ `#1' ~ is ~ being ~ ignored. } % \end{macrocode} % \begin{macrocode} \NewDocumentCommand \LuaCJKsetup { m } { \keys_set:nn { LuaCJK / options } {#1} \@@_settings_push: } \ProcessKeyOptions [ LuaCJK / options ] % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_settings_push:} % \changes{v0.0.1}{2026/11/11}{Every setting travels with the characters: a % snapshot of the options is recorded on the glyphs, so a group that ends % before its paragraph decides how its own text is set.} % Hand the current options to Lua, % which stores them once and selects the stored copy with an attribute. % Called after \cs{LuaCJKsetup} and after every font selection, % because the default glues and the size-dependent distances have % to be recomputed for the new size. The attribute is what makes a group % that ends before the paragraph still decide how its own text is set. % \begin{macrocode} \cs_new:Npn \@@_bool_js:N #1 { \str_if_eq:VnTF #1 { 1 } { true } { false } } \cs_new_protected:Npn \@@_settings_push: { \bool_if:NT \g_@@_lua_loaded_bool { \lua_now:e { LuaCJK.new_settings ( "\lua_escape:e { \l_@@_punct_style_tl }" , \@@_bool_js:N \l_@@_punct_kinsoku_tl , \@@_bool_js:N \l_@@_cj_strict_tl , \@@_bool_js:N \l_@@_latin_punct_tl , \@@_bool_js:N \l_@@_cjk_math_tl , \dim_to_decimal_in_sp:n { \l_@@_punct_width_dim } , \dim_to_decimal_in_sp:n { \l_@@_punct_bound_width_dim } , \@@_bool_js:N \l_@@_check_single_tl , \int_use:N \l_@@_widow_penalty_int , \bool_if:NTF \l_@@_break_between_puncts_bool { true } { false } , \bool_if:NTF \l_@@_auto_fallback_bool { true } { false } , "\lua_escape:e { \l_@@_blocks_off_clist }" ) } } } % \end{macrocode} % \end{macro} % % \begin{macro}{\LuaCJKdisable, \LuaCJKenable} % \changes{v0.0.1}{2026/11/11}{Provide \cs{LuaCJKdisable} and % \cs{LuaCJKenable} to suspend the typographic adjustments while keeping CJK % font selection; the state is recorded per node.} % Set the node attribute that makes the Lua callbacks skip % the adjustments. The attribute follows the \TeX\ grouping. % \begin{macrocode} \NewDocumentCommand \LuaCJKdisable { } { \g_@@_disabled_attr = 1 } \NewDocumentCommand \LuaCJKenable { } { \g_@@_disabled_attr = 0 } % \end{macrocode} % \end{macro} % % \begin{macro}{\LuaCJKShipoutHook} % \changes{v0.0.1}{2026/11/11}{Provide \cs{LuaCJKShipoutHook}, which runs code % through the standard \texttt{shipout/before} hook.} % Run code just before every page is shipped out. This is the place % for work that must see the finished page; \pkg{xeCJK} calls a macro % at the same moment, so the name is kept, but the code goes into % the \texttt{shipout/before} hook and therefore follows its rules. % \begin{macrocode} \NewDocumentCommand \LuaCJKShipoutHook { m } { \hook_gput_code:nnn { shipout / before } { LuaCJK } {#1} } % \end{macrocode} % \end{macro} % % \begin{macro}{\LuaCJKnobreak} % \changes{v0.0.1}{2026/11/11}{Provide \cs{LuaCJKnobreak} and the % \keys{PunctWidth} option; neither this prohibition nor that of % \cs{LuaCJKVerbAddon} depends on \keys{PunctKinsoku}, which controls only % the punctuation rules.} % \changes{v0.0.2}{2026/11/15}{\cs{LuaCJKnobreak} also forbids the break at a % \texttt{discretionary}, so an explicit hyphen or an automatic hyphenation % point inside the argument no longer breaks the line.} % Keep the argument on one line. The nodes of the argument are marked, % and the line-breaking pass forbids every break opportunity that involves a % marked node, whether it is a glue, a kern, or a discretionary. % \begin{macrocode} \NewDocumentCommand \LuaCJKnobreak { +m } { \group_begin: \g_@@_nobreak_attr = 1 #1 \group_end: } % \end{macrocode} % \end{macro} % % \begin{macro}{\LuaCJKVerbAddon, \LuaCJKOffVerbAddon} % \changes{v0.0.1}{2026/11/11}{Provide \cs{LuaCJKVerbAddon} and % \cs{LuaCJKOffVerbAddon}, which make one CJK character as wide as two % spaces of the current monospaced font and forbid breaks between CJK % characters, so columns in code listings stay aligned.} % Align CJK text in a monospaced context: one CJK character plus the glue % that follows it should take the width of two spaces. % The gap therefore is twice the space of the current font minus % the width of one CJK character. The width of the character % is measured after switching to the CJK family in a group, % because the current font is the monospaced one. A proportional font % has no fixed space to align to, so the command adds nothing there. % \begin{variable}[int] % { % \l_@@_verb_space_dim, \l_@@_verb_cjk_dim, % \l_@@_verb_gap_dim, \l_@@_verb_saved_ccglue_skip, % \l_@@_verb_saved_ecglue_skip, % \l_@@_verb_saved_ccglue_touched_bool, % \l_@@_verb_saved_ecglue_touched_bool, % \l_@@_verb_addon_bool, \l_@@_verb_box % } % The width of two spaces, the width of one CJK character, % the gap between them, the saved glues, whether the addon is on, % and the box used to measure the character. % \begin{macrocode} \dim_new:N \l_@@_verb_space_dim \dim_new:N \l_@@_verb_cjk_dim \dim_new:N \l_@@_verb_gap_dim \skip_new:N \l_@@_verb_saved_ccglue_skip \skip_new:N \l_@@_verb_saved_ecglue_skip \bool_new:N \l_@@_verb_saved_ccglue_touched_bool \bool_new:N \l_@@_verb_saved_ecglue_touched_bool \bool_new:N \l_@@_verb_addon_bool \box_new:N \l_@@_verb_box % \end{macrocode} % \end{variable} % \begin{macrocode} \msg_new:nnn { LuaCJK } { verb-addon-narrow } { The ~ monospaced ~ font ~ is ~ too ~ narrow ~ for ~ one ~ CJK ~ character.\\ `\token_to_str:N \LuaCJKVerbAddon' ~ adds ~ no ~ space. } \cs_new_protected:Npn \@@_verb_addon_compute: { \dim_set:Nn \l_@@_verb_space_dim { 2 \tex_fontdimen:D 2 \tex_font:D } \dim_compare:nNnTF { \tex_fontdimen:D 2 \tex_font:D } = { \tex_fontcharwd:D \tex_font:D 77 } { \dim_set_eq:NN \l_@@_verb_cjk_dim \l_@@_verb_space_dim \group_begin: \prop_get:NVNT \g_@@_family_prop \l_@@_family_tl \l_@@_tmpa_tl { \exp_args:NV \fontfamily \l_@@_tmpa_tl \selectfont \hbox_set:Nn \l_@@_verb_box { 一 } \dim_gset:Nn \l_@@_verb_cjk_dim { \box_wd:N \l_@@_verb_box } } \group_end: \dim_set:Nn \l_@@_verb_gap_dim { \l_@@_verb_space_dim - \l_@@_verb_cjk_dim } \dim_compare:nNnTF { \l_@@_verb_gap_dim } < { 0pt } { \msg_warning:nn { LuaCJK } { verb-addon-narrow } \LuaCJKsetup { CJKglue = 0pt, CJKecglue = 0pt } } { \LuaCJKsetup { CJKglue = \dim_use:N \l_@@_verb_gap_dim , CJKecglue = \dim_eval:n { \l_@@_verb_gap_dim / 2 } } } } { \LuaCJKsetup { CJKglue = 0pt, CJKecglue = 0pt } } } \NewDocumentCommand \LuaCJKVerbAddon { } { \int_compare:nNnF \tex_currentgrouplevel:D = \c_zero_int { \skip_set_eq:NN \l_@@_verb_saved_ccglue_skip \l_@@_ccglue_skip \skip_set_eq:NN \l_@@_verb_saved_ecglue_skip \l_@@_ecglue_skip \bool_set_eq:NN \l_@@_verb_saved_ccglue_touched_bool \l_@@_ccglue_touched_bool \bool_set_eq:NN \l_@@_verb_saved_ecglue_touched_bool \l_@@_ecglue_touched_bool \@@_verb_addon_compute: \g_@@_nobreak_attr = 1 \bool_set_true:N \l_@@_verb_addon_bool } } \NewDocumentCommand \LuaCJKOffVerbAddon { } { \bool_if:NT \l_@@_verb_addon_bool { \skip_set_eq:NN \l_@@_ccglue_skip \l_@@_verb_saved_ccglue_skip \skip_set_eq:NN \l_@@_ecglue_skip \l_@@_verb_saved_ecglue_skip \bool_set_eq:NN \l_@@_ccglue_touched_bool \l_@@_verb_saved_ccglue_touched_bool \bool_set_eq:NN \l_@@_ecglue_touched_bool \l_@@_verb_saved_ecglue_touched_bool \@@_settings_push: \g_@@_nobreak_attr = 0 \bool_set_false:N \l_@@_verb_addon_bool } } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_verbatim@font_hook:} % Verbatim-like material must usually be left alone. % \tn{verb} opens its own group and selects \tn{verbatim@font} inside % it, so extending that command covers \tn{verb} on its own. % The hook consults the current \keys{Verb} setting, so that % \cs{LuaCJKsetup} can change the behavior after the package is loaded. % \begin{macrocode} \cs_new_protected:Npn \@@_verbatim@font_hook: { \tl_if_eq:VnTF \l_@@_verb_mode_tl { true } { \LuaCJKdisable } { \tl_if_eq:VnT \l_@@_verb_mode_tl { env } { \LuaCJKdisable } } } \cs_if_exist:NT \verbatim@font { \cs_gset_protected:Npx \verbatim@font { \exp_not:o { \verbatim@font } \exp_not:N \@@_verbatim@font_hook: } } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_verbatim_env_begin:, \@@_verbatim_env_end:} % \changes{v0.0.1}{2026/11/11}{Suspend the adjustments inside \cs{verb} and % the verbatim-like environments (\env{verbatim}, \pkg{fancyvrb}, % \pkg{listings}); the \keys{Verb} option (\texttt{true}, \texttt{env}, % \texttt{env+}, \texttt{false}) controls what is suspended there.} % Environments that do not go through \tn{verbatim@font} need their own % hooks. Only the \texttt{true} setting suspends the adjustments here: % \texttt{env} and \texttt{env+} keep them, which is what keeps code % columns aligned when CJK text is used inside the code. % \begin{macrocode} \cs_new_protected:Npn \@@_verbatim_env_begin: { \tl_if_eq:VnT \l_@@_verb_mode_tl { true } { \LuaCJKdisable } } \cs_new_protected:Npn \@@_verbatim_env_end: { \tl_if_eq:VnT \l_@@_verb_mode_tl { true } { \LuaCJKenable } } \clist_map_inline:nn { verbatim, Verbatim, BVerbatim, verbatim*, Verbatim*, LVerbatim, lstlisting } { \hook_gput_code:nnn { env / #1 / begin } { LuaCJK } { \@@_verbatim_env_begin: } \hook_gput_code:nnn { env / #1 / end } { LuaCJK } { \@@_verbatim_env_end: } } % \end{macrocode} % \end{macro} % % \subsection{Fonts} % % \begin{macro}[int]{\@@_set_family:nnn} % Define a family through \pkg{fontspec} and remember the resulting % family name. The family is only registered; % the switch happens in \cs{@@_switch_family:n}. % \begin{variable}[int]{\g_@@_family_prop, \l_@@_family_tl} % The registered families, and the family name currently selected. % \begin{macrocode} \prop_new:N \g_@@_family_prop \tl_new:N \l_@@_family_tl \tl_set:Nn \l_@@_family_tl { zhsong } % \end{macrocode} % \end{variable} % \begin{variable}[int]{\l_@@_fontspec_tl, \l_@@_family_name_tl, \l_@@_tmpa_tl} % Scratch variables holding the generated \pkg{fontspec} family name, % the family name looked up in the property list, % and a temporary value. % \begin{macrocode} \tl_new:N \l_@@_fontspec_tl \tl_new:N \l_@@_family_name_tl \tl_new:N \l_@@_tmpa_tl \tl_new:N \l_@@_tmpb_tl \int_new:N \l_@@_tmpa_int \tl_new:N \l_@@_registered_name_tl % \end{macrocode} % \end{variable} % \begin{variable}[int] % { % \g_@@_font_name_prop, % \g_@@_font_feature_prop, % \l_@@_default_feature_tl % } % The font name and the explicit options of each family, % kept so that \cs{addCJKfontfeatures} can register the family again, % and the features prepended to every family. % \begin{variable}[int] % { % \l_@@_autofake_bold_given_bool, % \l_@@_autofake_slant_given_bool, % \l_@@_bold_font_given_bool, % \l_@@_italic_font_given_bool % } % Whether one family's options already mention \keys{AutoFakeBold}, % \keys{AutoFakeSlant}, \texttt{BoldFont}, or \texttt{ItalicFont}. % \begin{macrocode} \bool_new:N \l_@@_autofake_bold_given_bool \bool_new:N \l_@@_autofake_slant_given_bool \bool_new:N \l_@@_bold_font_given_bool \bool_new:N \l_@@_italic_font_given_bool % \end{macrocode} % \end{variable} % \begin{macrocode} \prop_new:N \g_@@_font_name_prop \prop_new:N \g_@@_font_feature_prop \tl_new:N \l_@@_default_feature_tl \tl_set:Nn \l_@@_default_feature_tl { Script = CJK } \tl_new:N \l_@@_feature_tl \cs_generate_variant:Nn \fontspec_gset_family:Nnn { NVV , NVn } % \end{macrocode} % \end{variable} % \begin{macrocode} \cs_new_protected:Npn \@@_set_family:nnn #1#2#3 { \@@_build_features:n {#2} \prop_gput:NnV \g_@@_font_feature_prop {#1} \l_@@_feature_tl \prop_gput:Nnn \g_@@_font_name_prop {#1} {#3} \@@_register_family:n {#1} } % \end{macrocode} % \begin{macro}[int]{\@@_build_features:n} % Combine the default features, the options given for this family, % and the automatic fake bold and slant, % unless the family sets them itself. % \begin{macrocode} \cs_new_protected:Npn \@@_note_feature_key:nn #1#2 { \@@_note_feature_key:n {#1} } \cs_new_protected:Npn \@@_note_feature_key:n #1 { \str_case:en { \tl_trim_spaces:n {#1} } { { AutoFakeBold } { \bool_set_true:N \l_@@_autofake_bold_given_bool } { AutoFakeSlant } { \bool_set_true:N \l_@@_autofake_slant_given_bool } { BoldFont } { \bool_set_true:N \l_@@_bold_font_given_bool } { ItalicFont } { \bool_set_true:N \l_@@_italic_font_given_bool } } } \cs_new_protected:Npn \@@_build_features:n #1 { \clist_map_inline:nn { autofake_bold, autofake_slant, bold_font, italic_font } { \exp_args:Nc \bool_set_false:N { l_@@_##1_given_bool } } \keyval_parse:NNn \@@_note_feature_key:n \@@_note_feature_key:nn {#1} \tl_set_eq:NN \l_@@_feature_tl \l_@@_default_feature_tl \tl_if_blank:nF {#1} { \tl_put_right:Nn \l_@@_feature_tl { , ~ #1 } } \tl_if_empty:NF \g_@@_autofake_bold_tl { \bool_lazy_or:nnF { \l_@@_autofake_bold_given_bool } { \l_@@_bold_font_given_bool } { \tl_put_right:Nx \l_@@_feature_tl { , ~ AutoFakeBold = \exp_not:V \g_@@_autofake_bold_tl } } } \tl_if_empty:NF \g_@@_autofake_slant_tl { \bool_lazy_or:nnF { \l_@@_autofake_slant_given_bool } { \l_@@_italic_font_given_bool } { \tl_put_right:Nx \l_@@_feature_tl { , ~ AutoFakeSlant = \exp_not:V \g_@@_autofake_slant_tl } } } } % \end{macrocode} % \end{macro} % \begin{macro}[int]{\@@_register_family:n} % Build the \pkg{fontspec} family from the stored font name % and features, and re-select it if it is the current one. % \begin{macrocode} \cs_new_protected:Npn \@@_register_family:n #1 { \prop_get:NnNF \g_@@_font_name_prop {#1} \l_@@_registered_name_tl { \tl_clear:N \l_@@_registered_name_tl } \prop_get:NnNF \g_@@_font_feature_prop {#1} \l_@@_feature_tl { \tl_clear:N \l_@@_feature_tl } \prop_get:NnNF \g_@@_shift_decl_prop {#1} \l_@@_shift_dim_tl { \tl_clear:N \l_@@_shift_dim_tl } \group_begin: \fontspec_gset_family:NVV \l_@@_fontspec_tl \l_@@_feature_tl \l_@@_registered_name_tl \prop_gput:NnV \g_@@_family_prop {#1} \l_@@_fontspec_tl \tl_if_empty:NF \l_@@_shift_dim_tl { \prop_gput:NVV \g_@@_font_shift_prop \l_@@_fontspec_tl \l_@@_shift_dim_tl } \group_end: \prop_if_in:NVT \g_@@_family_prop \l_@@_family_tl { \selectfont } } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_push_fallback:, \@@_refresh_fallbacks:} % Resolve the fallback families of the current CJK family to font % identifiers at the current size, series, and shape, and hand the list to % Lua together with the identifier of the family itself. % A font identifier belongs to one instance of a font, so the list has to be % resolved again whenever a font is selected: the identifier of the bold or % enlarged main font is not the one the upright size was resolved at. % \begin{variable}[int] % {\g_@@_fallback_pushed_prop, \g_@@_lua_loaded_bool} % The CJK family whose list was last handed to Lua for each font identifier, % which makes a selection that resolves to a known identifier cost one % property lookup, and whether the Lua module has been loaded. % The guard of \cs{@@_update_font:} is raised around the resolution, so the % temporary selections it makes do not change the identifier they are % resolved for. % \begin{macrocode} \prop_new:N \g_@@_fallback_pushed_prop \bool_new:N \g_@@_lua_loaded_bool \cs_new_protected:Npn \@@_push_fallback: { \int_compare:nNnT { \g_@@_fontid_int } > { 0 } { \tl_set:Ne \l_@@_fontid_tl { \int_use:N \g_@@_fontid_int } \prop_get:NVNT \g_@@_fallback_prop \l_@@_family_tl \l_@@_tmpa_tl { \prop_get:NVNF \g_@@_fallback_pushed_prop \l_@@_fontid_tl \l_@@_tmpb_tl { \tl_clear:N \l_@@_tmpb_tl } \tl_if_eq:NNF \l_@@_tmpb_tl \l_@@_family_tl { \exp_args:NV \@@_push_fallback_aux:n \l_@@_tmpa_tl } } } } \cs_new_protected:Npn \@@_push_fallback_aux:n #1 { \group_begin: \tl_clear:N \l_@@_fallback_ids_tl \clist_map_inline:nn {#1} { \fontfamily {##1} \selectfont \tl_set:Ne \l_@@_tmpb_tl { \int_eval:n { \tex_fontid:D \tex_font:D } } \tl_put_right:NV \l_@@_fallback_ids_tl \l_@@_tmpb_tl \tl_put_right:Nn \l_@@_fallback_ids_tl { , } \@@_push_shift:nn { \l_@@_tmpb_tl } {##1} } \bool_if:NT \g_@@_lua_loaded_bool { \tl_if_empty:NF \l_@@_fallback_ids_tl { \lua_now:e { LuaCJK.set_fallback ( \int_use:N \g_@@_fontid_int , { \l_@@_fallback_ids_tl } ) } \prop_gput:NVV \g_@@_fallback_pushed_prop \l_@@_fontid_tl \l_@@_family_tl } } \group_end: } \cs_new_protected:Npn \@@_refresh_fallbacks: { \prop_gclear:N \g_@@_fallback_pushed_prop \bool_set_true:N \l_@@_selectfont_bool \@@_push_fallback: \bool_set_false:N \l_@@_selectfont_bool } % \end{macrocode} % \end{variable} % \end{macro} % % \begin{macro}[int]{\@@_push_shift:nn} % Hand the vertical offset of one font to Lua, once per font identifier. % \begin{variable}[int] % { % \g_@@_font_shift_prop, \g_@@_shift_pushed_prop, % \l_@@_shift_dim_tl, \l_@@_shift_fid_tl, \l_@@_shift_family_tl % } % The \keys{Baseline} value of each registered font, keyed by the family % \pkg{fontspec} returned for it; the identifiers whose offset Lua already % knows; and scratch variables for the identifier and the offset, which is % evaluated at the size in force so that a value in |em| follows the text. % \begin{macrocode} \cs_new_protected:Npn \@@_push_shift:nn #1#2 { \tl_set:Ne \l_@@_shift_fid_tl {#1} \tl_set:Ne \l_@@_shift_family_tl {#2} \prop_get:NVNT \g_@@_font_shift_prop \l_@@_shift_family_tl \l_@@_shift_dim_tl { \tl_if_empty:NF \l_@@_shift_dim_tl { \prop_if_in:NVF \g_@@_shift_pushed_prop \l_@@_shift_fid_tl { \prop_gput:NVn \g_@@_shift_pushed_prop \l_@@_shift_fid_tl { } \lua_now:e { LuaCJK.set_yoffset ( \l_@@_shift_fid_tl , \dim_to_decimal_in_sp:n { \dim_eval:n { \l_@@_shift_dim_tl } } ) } } } } } % \end{macrocode} % \end{variable} % \end{macro} % \end{macro} % % \begin{macro}[int]{\@@_switch_family:n} % Select a registered family and warn once about an unknown name. % \begin{macrocode} \cs_new_protected:Npn \@@_switch_family:n #1 { \prop_if_in:NnTF \g_@@_family_prop {#1} { \tl_set:Nn \l_@@_family_tl {#1} \selectfont } { \seq_if_in:NnF \g_@@_unknown_family_seq {#1} { \seq_gput_right:Nn \g_@@_unknown_family_seq {#1} \msg_warning:nnn { LuaCJK } { family-unknown } {#1} } } } \seq_new:N \g_@@_unknown_family_seq \msg_new:nnn { LuaCJK } { family-unknown } { Unknown ~ CJK ~ family ~ `#1' ~ is ~ being ~ ignored. } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_reset_family:} % \changes{v0.0.2}{2026/11/15}{Restore \cs{CJKfamilydefault} when % \cs{normalfont} or \cs{textnormal} runs, so that the default font command % overrides a family chosen earlier in the group; \cs{CJKfamilydefault} % follows \cs{CJKrmdefault} until the document sets it.} % Reset the CJK family to \cs{CJKfamilydefault}. % The name is expanded first, because the default is written as % \cs{CJKrmdefault} until the document chooses a family of its own. % A name that no family carries is passed over in silence: % \tn{normalfont} also runs when the document starts, and a document that % has declared no font yet has nothing to switch to. % \begin{macrocode} \cs_new_protected:Npn \@@_reset_family: { \tl_set:Ne \l_@@_tmpa_tl { \CJKfamilydefault } \prop_if_in:NVT \g_@@_family_prop \l_@@_tmpa_tl { \exp_args:NV \@@_switch_family:n \l_@@_tmpa_tl } } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_math_font_update:} % Register the CJK family used by the current settings as a math family, or % clear the math font attribute when \keys{CJKmath} is off or the family is % not known. % \cs{@@_update_font:} runs it at every font selection, and % \cs{@@_math_version_refresh:} runs it again when the math version changes, % which is why it stands on its own. % \begin{macrocode} \cs_new_protected:Npn \@@_math_font_update: { \tl_if_eq:NnTF \l_@@_cjk_math_tl { 1 } { \tl_if_empty:NTF \l_@@_math_family_tl { \tl_set_eq:NN \l_@@_tmpa_tl \l_@@_family_tl } { \tl_set_eq:NN \l_@@_tmpa_tl \l_@@_math_family_tl } \prop_get:NVNTF \g_@@_family_prop \l_@@_tmpa_tl \l_@@_tmpb_tl { \exp_args:NV \@@_register_math_family:n \l_@@_tmpb_tl } { \g_@@_math_font_attr = \c_zero_int } } { \g_@@_math_font_attr = \c_zero_int } } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_update_font:} % \changes{v0.0.1}{2026/11/11}{Provide \keys{PunctFamily}, which sets a % separate CJK family for the punctuation marks.} % On every \tn{selectfont}, resolve the current family, its fallback % families, and the math family to font identifiers and store them in the % attributes. % \begin{variable}[int]{\l_@@_selectfont_bool, \g_@@_fontid_int} % A guard against re-entering the update while \tn{selectfont} runs, % and the font identifier of the current CJK family. % \begin{macrocode} \bool_new:N \l_@@_selectfont_bool \int_new:N \g_@@_fontid_int % \end{macrocode} % \end{variable} % \begin{variable}[int] % { % \l_@@_math_family_tl, \l_@@_punct_family_tl, % \g_@@_punct_fontid_int % } % The math family registered by \cs{setCJKmathfont}, the family chosen by % \keys{PunctFamily}, and the font identifier resolved for the latter. % \begin{macrocode} \tl_new:N \l_@@_math_family_tl \tl_new:N \l_@@_punct_family_tl \int_new:N \g_@@_punct_fontid_int % \end{macrocode} % \end{variable} % \begin{variable}[int] % { % \g_@@_math_family_int, \g_@@_math_family_prop, % \c_@@_math_family_max_int % } % The last math family number handed out, the map from a CJK family name to % its number, and the upper bound. % A CJK character in a math list needs a math family of its own, because the % family is what selects the font \emph{and} its size for the three math % styles. \LaTeXe\ allocates the 256 families that \LuaTeX\ allows from zero % upwards, so the numbers start at 240; a document with more than sixteen CJK % families is out of luck and gets an error. % \begin{macrocode} \int_new:N \g_@@_math_family_int \int_gset:Nn \g_@@_math_family_int { 239 } \int_const:Nn \c_@@_math_family_max_int { 255 } \prop_new:N \g_@@_math_family_prop % \end{macrocode} % \end{variable} % \begin{macrocode} \cs_new_protected:Npn \@@_update_font: { \bool_if:NF \l_@@_selectfont_bool { \bool_set_true:N \l_@@_selectfont_bool \prop_get:NVNTF \g_@@_family_prop \l_@@_family_tl \l_@@_family_name_tl { \group_begin: \exp_args:NV \fontfamily \l_@@_family_name_tl \selectfont \int_gset:Nn \g_@@_fontid_int { \tex_fontid:D \tex_font:D } \@@_push_shift:nn { \int_use:N \g_@@_fontid_int } { \l_@@_family_name_tl } \group_end: \g_@@_font_attr = \g_@@_fontid_int } { \g_@@_font_attr = \c_zero_int \tl_clear:N \l_@@_family_name_tl } \@@_math_font_update: \@@_push_fallback: \@@_push_block_fonts: \tl_if_empty:NTF \l_@@_punct_family_tl { \g_@@_punct_font_attr = \c_zero_int } { \prop_get:NVNTF \g_@@_family_prop \l_@@_punct_family_tl \l_@@_tmpa_tl { \group_begin: \exp_args:NV \fontfamily \l_@@_tmpa_tl \selectfont \int_gset:Nn \g_@@_punct_fontid_int { \tex_fontid:D \tex_font:D } \group_end: \g_@@_punct_font_attr = \g_@@_punct_fontid_int } { \g_@@_punct_font_attr = \c_zero_int } } \bool_set_false:N \l_@@_selectfont_bool } } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_register_math_family:n} % Give a CJK family a math family number and fill the three style fonts of % that number from the family at the current size. % A CJK character in a math list needs a family rather than a box, because the % family is what selects the font \emph{and} its size for text, script, and % scriptscript style; a box is built once and keeps the size it was built at. % The script sizes are seven and five tenths of the text size, which is the % ratio of the standard \LaTeX\ size tables. The family is selected inside a % group, so the caller's font is not disturbed, and the assignments are global % because a math font belongs to a family, not to a group. % The series and the shape come from the math version, not from the text % around the formula: the family is selected at \cs{mddefault} and % \cs{shapedefault}, and, in the \texttt{bold} math version, at % \cs{bfdefault} as well when the family declares that shape. The regular % face is what loads the shape list the test reads, and a family without a % bold face keeps its regular one in silence. A formula therefore follows % \cs{boldmath}, and a \cs{bfseries} that happens to precede it no longer % leaks into it. % \begin{macrocode} \msg_new:nnn { LuaCJK } { math-family-exhausted } { Too ~ many ~ CJK ~ families ~ for ~ the ~ math ~ families ~ 240--255. } \cs_new_protected:Npn \@@_math_family:n #1 { \prop_if_in:NnF \g_@@_math_family_prop {#1} { \int_gincr:N \g_@@_math_family_int \int_compare:nNnT \g_@@_math_family_int > \c_@@_math_family_max_int { \msg_error:nn { LuaCJK } { math-family-exhausted } } \prop_gput:NnV \g_@@_math_family_prop {#1} \g_@@_math_family_int } } \cs_new_protected:Npn \@@_register_math_family:n #1 { \@@_math_family:n {#1} \int_set:Nn \l_@@_tmpa_int { \prop_item:Nn \g_@@_math_family_prop {#1} } \group_begin: \fontfamily {#1} \fontseries { \mddefault } \fontshape { \shapedefault } \fontsize { \use:c { f@size } pt } { \baselineskip } \selectfont \bool_lazy_and:nnT { \str_if_eq_p:ee { \math@version } { bold } } { \cs_if_exist_p:c { \use:c { f@encoding } / #1 / \bfdefault / \shapedefault } } { \fontseries { \bfdefault } \selectfont } \global \textfont \l_@@_tmpa_int = \font \fontsize { \dim_eval:n { \use:c { f@size } pt * 7 / 10 } } { \baselineskip } \selectfont \global \scriptfont \l_@@_tmpa_int = \font \fontsize { \dim_eval:n { \use:c { f@size } pt * 5 / 10 } } { \baselineskip } \selectfont \global \scriptscriptfont \l_@@_tmpa_int = \font \group_end: \g_@@_math_font_attr = \l_@@_tmpa_int } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_math_version_refresh:} % Register the math family again when the math version changes. % \LaTeXe\ knows the numbers of its own math families only, so \tn{mathversion} % cannot reach the fonts of a CJK math family: the version is applied by hand. % \cs{glb@settings} is where a version change and a text-size change are % applied, and it runs \tn{every@math@size} afterwards, so that register is % where the refresh belongs. It runs there also when the group that changed % the version ends and the version in force before the group is applied again, % which is what keeps |{\boldmath $中$}| local. The guard stops the font % selections of the registration from running the whole update again. % \begin{macrocode} \cs_new_protected:Npn \@@_math_version_refresh: { \bool_if:NF \l_@@_selectfont_bool { \bool_set_true:N \l_@@_selectfont_bool \@@_math_font_update: \bool_set_false:N \l_@@_selectfont_bool } } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_toks_put_right:Nn} % Append tokens to a token register such as \tn{every@math@size}. % The register holds tokens rather than a token list variable, and % \cs{tl_put_right:Nn} would put the name of the register into its own value, % so the assignment is written out with \tn{the}. % \begin{macrocode} \cs_new_protected:Npn \@@_toks_put_right:Nn #1#2 { #1 \expandafter { \tex_the:D #1 #2 } } \exp_args:Nc \@@_toks_put_right:Nn { every@math@size } { \@@_math_version_refresh: } % \end{macrocode} % \end{macro} % % % \begin{macro}[int]{\@@_push_block_fonts:} % Resolve the block fonts of the current family at the current size % and hand them to Lua, keyed by the font identifier of the family. % Font identifiers depend on the size, % so this runs again whenever a font is selected. % \begin{macrocode} \int_new:N \g_@@_block_fontid_int \cs_new_protected:Npn \@@_push_block_fonts: { \int_compare:nNnT { \g_@@_fontid_int } > { 0 } { \clist_map_inline:Nn \g_@@_block_names_clist { \tl_set:Ne \l_@@_block_key_tl { \l_@@_family_tl | ##1 } \int_gzero:N \g_@@_block_fontid_int \prop_get:NVNT \g_@@_family_block_prop \l_@@_block_key_tl \l_@@_tmpa_tl { \group_begin: \exp_args:NV \fontfamily \l_@@_tmpa_tl \selectfont \int_gset:Nn \g_@@_block_fontid_int { \tex_fontid:D \tex_font:D } \group_end: } \lua_now:e { LuaCJK.set_block_font ( \int_use:N \g_@@_fontid_int , "\lua_escape:e {##1}" , \int_use:N \g_@@_block_fontid_int ) } } } } % \end{macrocode} % \end{macro} % % The glues and the settings belong to the size that is being selected, % but \cs{baselineskip} has not changed yet when the \texttt{selectfont} % hook runs, so they are refreshed after the font selection. % The guard keeps the temporary font selections inside \cs{@@_update_font:} % from doing the same twice. % \begin{macrocode} \hook_gput_code:nnn { selectfont } { LuaCJK } { \@@_update_font: } \hook_gput_code:nnn { cmd / selectfont / after } { LuaCJK } { \bool_if:NF \l_@@_selectfont_bool { \@@_refresh_glues: \@@_settings_push: } } % \end{macrocode} % % \begin{variable}[int] % {\CJKrmdefault, \CJKsfdefault, \CJKttdefault, \CJKfamilydefault} % The CJK families used by the standard font commands. % The family hooks below re-select the CJK family whenever % \tn{rmfamily} and its siblings run, and \tn{normalfont} selects % \cs{CJKfamilydefault}, which follows \cs{CJKrmdefault} until the % document gives it a value of its own. % \begin{variable}[int]{\l_@@_family_default_init_tl} % The value \cs{CJKfamilydefault} starts with, kept so that the hook at % |\begin{document}| can tell an untouched default from a chosen one. % \begin{macrocode} \clist_map_inline:nn { { rm } { zhsong } , { sf } { zhhei } , { tt } { zhfs } } { \exp_args:Nc \tl_new:N { CJK \use_i:nn #1 default } \exp_args:Nc \tl_set:Nx { CJK \use_i:nn #1 default } { \use_ii:nn #1 } } \tl_new:N \CJKfamilydefault \tl_set:Nn \CJKfamilydefault { zhsong } \tl_new:N \l_@@_family_default_init_tl \tl_set_eq:NN \l_@@_family_default_init_tl \CJKfamilydefault % \end{macrocode} % \end{variable} % \end{variable} % % \begin{macro}{\defaultCJKfontfeatures} % \changes{v0.0.1}{2026/11/11}{Provide \cs{defaultCJKfontfeatures} and % \cs{addCJKfontfeatures}.} % Set the features prepended to every CJK family, replacing the initial % \texttt{Script = CJK}. Families registered earlier keep the value they % were given; call \cs{setCJKmainfont} again to apply a new default. % \begin{macrocode} \NewDocumentCommand \defaultCJKfontfeatures { m } { \tl_set:Nn \l_@@_default_feature_tl {#1} } % \end{macrocode} % \end{macro} % % \begin{macro}{\addCJKfontfeatures} % Add features to the current family for the rest of the group. % The star form is accepted for compatibility; % \pkg{LuaCJK} has no concept of blocks, so both forms act on the same font. % \begin{macrocode} \NewDocumentCommand \addCJKfontfeatures { s o m } { \exp_args:NV \@@_add_family_features:n \l_@@_family_tl {#3} } % \end{macrocode} % \begin{macro}[int]{\@@_add_family_features:n} % Append features to the stored ones of a family and register it again. % \begin{macrocode} \cs_new_protected:Npn \@@_add_family_features:n #1#2 { \prop_get:NnNTF \g_@@_font_feature_prop {#1} \l_@@_feature_tl { \tl_set:Ne \l_@@_feature_tl { \exp_not:V \l_@@_feature_tl \tl_if_blank:nF {#2} { , \exp_not:n {#2} } } \prop_get:NnNF \g_@@_font_name_prop {#1} \l_@@_registered_name_tl { \tl_clear:N \l_@@_registered_name_tl } \prop_get:NnNF \g_@@_shift_decl_prop {#1} \l_@@_shift_dim_tl { \tl_clear:N \l_@@_shift_dim_tl } \group_begin: \fontspec_gset_family:NVV \l_@@_fontspec_tl \l_@@_feature_tl \l_@@_registered_name_tl \tl_if_empty:NF \l_@@_shift_dim_tl { \prop_gput:NVV \g_@@_font_shift_prop \l_@@_fontspec_tl \l_@@_shift_dim_tl } \group_end: \prop_put:NnV \g_@@_family_prop {#1} \l_@@_fontspec_tl \prop_put:NnV \g_@@_font_feature_prop {#1} \l_@@_feature_tl \selectfont } { \msg_warning:nnn { LuaCJK } { family-unknown } {#1} } } % \end{macrocode} % \end{macro} % \end{macro} % % \begin{macro} % {\setCJKmainfont, \setCJKsansfont, \setCJKmonofont, \setCJKfamilyfont} % The public font commands, all built on % \cs{@@_set_family_with_options:nnnn}. % \begin{macrocode} \NewDocumentCommand \setCJKmainfont { o m o } { \exp_args:NV \@@_set_family_with_options:nnnn \CJKrmdefault {#1} {#2} {#3} } \NewDocumentCommand \setCJKsansfont { o m o } { \exp_args:NV \@@_set_family_with_options:nnnn \CJKsfdefault {#1} {#2} {#3} } \NewDocumentCommand \setCJKmonofont { o m o } { \exp_args:NV \@@_set_family_with_options:nnnn \CJKttdefault {#1} {#2} {#3} } \NewDocumentCommand \setCJKfamilyfont { m o m o } { \@@_set_family_with_options:nnnn {#1} {#2} {#3} {#4} } \NewDocumentCommand \setCJKfallbackfamilyfont { m o m o } { \tl_set:Ne \l_@@_fallback_key_tl {#1} \tl_if_novalue:nTF {#2} { \tl_if_novalue:nTF {#4} { \@@_add_fallback:nn { } {#3} } { \@@_add_fallback:nn {#4} {#3} } } { \@@_add_fallback:nn {#2} {#3} } } % \end{macrocode} % \begin{macro}[int]{\@@_set_family_with_options:nnnn} % Accept the options before or after the font name. % \begin{macrocode} \cs_new_protected:Npn \@@_set_family_with_options:nnnn #1#2#3#4 { \tl_if_novalue:nTF {#2} { \tl_if_novalue:nTF {#4} { \@@_set_family_with_options_aux:nnn {#1} { } {#3} } { \@@_set_family_with_options_aux:nnn {#1} {#4} {#3} } } { \@@_set_family_with_options_aux:nnn {#1} {#2} {#3} } } \cs_generate_variant:Nn \@@_set_family:nnn { nVn } \cs_new_protected:Npn \@@_set_family_with_options_aux:nnn #1#2#3 { \@@_extract_font_keys:nn {#1} {#2} \@@_set_family:nVn {#1} \l_@@_features_tl {#3} } % \end{macrocode} % \end{macro} % \begin{macro}[int]{\@@_add_fallback:nn} % \changes{v0.0.1}{2026/11/11}{Provide \cs{setCJKfallbackfamilyfont} and % \keys{AutoFallBack} for characters that the selected CJK font does not % have.} % \changes{v0.0.2}{2026/11/15}{The fallback list is resolved again at every % font selection, so the fallback follows the series, shape, and size of % the text.} % Register one fallback font for a CJK family, % keeping the order in which the fonts were declared. % \begin{macrocode} \cs_new_protected:Npn \@@_add_fallback:nn #1#2 { \@@_parse_font_options:n {#1} \tl_if_empty:NT \l_@@_shift_tl { \tl_set:Nn \l_@@_features_tl {#1} } \group_begin: \fontspec_gset_family:NVn \l_@@_fallback_family_tl \l_@@_features_tl {#2} \tl_if_empty:NF \l_@@_shift_tl { \prop_gput:NVV \g_@@_font_shift_prop \l_@@_fallback_family_tl \l_@@_shift_tl } \prop_get:NVNF \g_@@_fallback_prop \l_@@_fallback_key_tl \l_@@_tmpa_tl { \tl_clear:N \l_@@_tmpa_tl } \clist_put_right:NV \l_@@_tmpa_tl \l_@@_fallback_family_tl \prop_gput:NVV \g_@@_fallback_prop \l_@@_fallback_key_tl \l_@@_tmpa_tl \group_end: \@@_refresh_fallbacks: } % \end{macrocode} % \end{macro} % \end{macro} % % \begin{macro}{\CJKfamily} % Switch to a registered family and gobble the following spaces. % \begin{macrocode} \NewDocumentCommand \CJKfamily { m } { \@@_switch_family:n {#1} \tex_ignorespaces:D } % \end{macrocode} % \end{macro} % % \begin{macro}{\newCJKfontfamily} % Define a control sequence that switches to a new family, % and register the family under the same name. % \begin{macrocode} \NewDocumentCommand \newCJKfontfamily { o m o m o } { \tl_set:Ne \l_@@_tmpa_tl { \IfNoValueTF {#1} { \cs_to_str:N #2 } {#1} } \cs_set_protected:Npx #2 { \exp_not:N \@@_switch_family:n { \exp_not:V \l_@@_tmpa_tl } } \exp_args:NV \@@_set_family_with_options:nnnn \l_@@_tmpa_tl {#3} {#4} {#5} } % \end{macrocode} % \end{macro} % % \begin{macro}{\setCJKmathfont} % \changes{v0.0.2}{2026/11/15}{A CJK character in a formula takes the face of % the math version: the bold face of the family under \cs{boldmath} when the % family has one, and the upright shape instead of the shape of the text.} % Register a family used for CJK characters in math mode and refresh % the attributes at once. % \begin{macrocode} \NewDocumentCommand \setCJKmathfont { o m o } { \@@_set_family_with_options:nnnn { LuaCJK / math } {#1} {#2} {#3} \tl_set:Nn \l_@@_math_family_tl { LuaCJK / math } \@@_update_font: } % \end{macrocode} % \end{macro} % % \begin{macro}{\CJKfontspec} % \changes{v0.0.2}{2026/11/15}{\cs{CJKfontspec} and \cs{newCJKfontfamily} % take their options after the font name as well.} % Register a one-off family and switch to it immediately. % \begin{variable}[int]{\g_@@_fontspec_int} % Gives every call its own family name. % \begin{macrocode} \int_new:N \g_@@_fontspec_int % \end{macrocode} % \end{variable} % \begin{macrocode} \NewDocumentCommand \CJKfontspec { o m o } { \int_gincr:N \g_@@_fontspec_int \tl_set:Ne \l_@@_tmpa_tl { LuaCJK / fontspec / \int_use:N \g_@@_fontspec_int } \exp_args:NV \@@_set_family_with_options:nnnn \l_@@_tmpa_tl {#1} {#2} {#3} \exp_args:NV \@@_switch_family:n \l_@@_tmpa_tl \tex_ignorespaces:D } % \end{macrocode} % \end{macro} % % \begin{macro}{\songti, \heiti, \fangsong, \kaishu, \lishu, \youyuan} % \changes{v0.0.1}{2026/11/11}{Provide \cs{songti}, \cs{heiti}, \cs{fangsong}, % \cs{kaishu}, \cs{lishu}, and \cs{youyuan} as shorthands for the families % \texttt{zhsong}, \texttt{zhhei}, \texttt{zhfs}, \texttt{zhkai}, % \texttt{zhli}, and \texttt{zhyou}.} % Shortcuts for the usual families, % plus the family hooks of the standard font commands. % \begin{macrocode} \cs_new_protected:Npn \songti { \CJKfamily { zhsong } } \cs_new_protected:Npn \heiti { \CJKfamily { zhhei } } \cs_new_protected:Npn \fangsong { \CJKfamily { zhfs } } \cs_new_protected:Npn \kaishu { \CJKfamily { zhkai } } \cs_new_protected:Npn \lishu { \CJKfamily { zhli } } \cs_new_protected:Npn \youyuan { \CJKfamily { zhyou } } \clist_map_inline:nn { rm, sf, tt } { \hook_gput_code:nnn { cmd / #1family / after } { LuaCJK } { \exp_args:Nv \@@_switch_family:n { CJK#1default } } } \hook_gput_code:nnn { cmd / normalfont / after } { LuaCJK } { \@@_reset_family: } % \end{macrocode} % \end{macro} % % \subsection{Names and configuration} % % \begin{macro}{\normalspacedchars} % \changes{v0.0.1}{2026/11/11}{Provide \cs{normalspacedchars} so that slashes, % backslashes, and hyphens take no glue on either side.} % The characters in the list get no glue on either side, which is what % one wants for slashes, backslashes, and hyphens inside Latin text. % The list is kept on the Lua side as a set of code points, % so the setting is global: a group does not restore the previous list. % \begin{variable}[int]{\g_@@_normal_spaced_chars_tl} % The list, initially \texttt{/}, \texttt{\c_backslash_str}, % and \texttt{-}. % \begin{macrocode} \tl_new:N \g_@@_normal_spaced_chars_tl \tl_gset:Nn \g_@@_normal_spaced_chars_tl { / , \c_backslash_str , - } % \end{macrocode} % \end{variable} % \begin{macrocode} \cs_new_protected:Npn \@@_set_normal_spaced_chars: { \lua_now:e { LuaCJK.set_normal_spaced_chars ( "\lua_escape:e { \g_@@_normal_spaced_chars_tl } " ) } } \NewDocumentCommand \normalspacedchars { m } { \tl_gset:Nn \g_@@_normal_spaced_chars_tl {#1} \@@_set_normal_spaced_chars: } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_ignore_format_chars:} % \changes{v0.0.1}{2026/11/11}{Ignore the zero-width format characters % (U+200B, U+200C, U+200D, U+2060, U+180E) and the variation selectors at % the input, so that they neither take glue nor separate two CJK characters.} % These characters have no advance of their own, but the font loader replaces % one that the font does not have by a space glyph and a negative kern. % The spacing pass then sees a Latin atom and surrounds it with % \keys{CJKecglue} on both sides, so a zero-width space would widen the line % by two \keys{CJKecglue} and remove the break opportunity it was meant to % create. % Giving the characters category 9 removes them before the font loader runs. % A character produced by \tn{char} bypasses the category codes and is left % alone: \tn{char} asks for the glyph itself, not for the character's % typographic role. % \begin{macrocode} \clist_const:Nn \c_@@_ignored_chars_clist { "180E , "200B , "200C , "200D , "2060 , "FEFF } \cs_new_protected:Npn \@@_ignore_format_chars: { \clist_map_inline:Nn \c_@@_ignored_chars_clist { \char_set_catcode_ignore:n {##1} } \int_step_inline:nnn { "FE00 } { "FE0F } { \char_set_catcode_ignore:n {##1} } \int_step_inline:nnn { "E0100 } { "E01EF } { \char_set_catcode_ignore:n {##1} } } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_load_cj:} % Read the \texttt{CJ} set from \file{LineBreak.txt} once. % The key \keys{CJLineBreak} calls this when it selects \texttt{strict}; % a second call is cheap because the Lua side remembers that it has read % the file. % \begin{macrocode} \cs_new_protected:Npn \@@_load_cj: { \lua_now:e { LuaCJK.load_cj() } } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_load_local_config:} % \changes{v0.0.1}{2026/11/11}{Provide the \keys{LocalConfig} option, which % reads \file{luacjk.cfg} or \file{luacjk-.cfg} at the end of the % package.} % Load the file chosen by the \keys{LocalConfig} option. % The key is read while the package is being loaded, but the file is only input % at the end, so that it can use everything \pkg{LuaCJK} defines. % \begin{macrocode} \cs_new_protected:Npn \@@_load_local_config: { \tl_if_empty:NF \l_@@_local_config_tl { \exp_args:NV \@@_load_config_file:n \l_@@_local_config_tl } } \cs_new_protected:Npn \@@_load_config_file:n #1 { \file_if_exist:nTF {#1} { \file_input:n {#1} } { \msg_warning:nnn { LuaCJK } { config-missing } {#1} } } \msg_new:nnn { LuaCJK } { config-missing } { The ~ configuration ~ file ~ `#1' ~ was ~ not ~ found. } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_load_fandol:} % \changes{v0.0.1}{2026/11/11}{Provide the \keys{LoadFandol} option, which % supplies Fandol fonts when the document declares none.} % When the \keys{LoadFandol} option is on and the document % did not declare any CJK font, fall back to the four Fandol faces. % This runs at |\begin{document}| so that a family declared anywhere % in the preamble still wins. % \begin{macrocode} \cs_new_protected:Npn \@@_load_fandol: { \setCJKmainfont{FandolSong-Regular.otf} [ BoldFont = FandolSong-Bold.otf ] \setCJKsansfont{FandolHei-Regular.otf} [ BoldFont = FandolHei-Bold.otf ] \setCJKmonofont{FandolFang-Regular.otf} \setCJKfamilyfont{zhsong}{FandolSong-Regular.otf} [ BoldFont = FandolSong-Bold.otf ] \setCJKfamilyfont{zhhei}{FandolHei-Regular.otf} [ BoldFont = FandolHei-Bold.otf ] \setCJKfamilyfont{zhkai}{FandolKai-Regular.otf} \setCJKfamilyfont{zhfs}{FandolFang-Regular.otf} } % \end{macrocode} % \end{macro} % % \subsection{The Lua module} % % The Lua module reads the attribute numbers and installs its callbacks. % The current font is resolved once the document starts. % \begin{macrocode} \lua_load_module:n { LuaCJK } \lua_now:e { LuaCJK.attr_font = \int_use:N \c_@@_font_attr_int ; LuaCJK.attr_cjk_space = \int_use:N \c_@@_cjk_space_attr_int ; LuaCJK.attr_xecglue = \int_use:N \c_@@_xecglue_attr_int ; LuaCJK.attr_inserted = \int_use:N \c_@@_inserted_attr_int ; LuaCJK.attr_math_font = \int_use:N \c_@@_math_font_attr_int ; LuaCJK.attr_punct_font = \int_use:N \c_@@_punct_font_attr_int ; LuaCJK.attr_nobreak = \int_use:N \c_@@_nobreak_attr_int ; LuaCJK.attr_settings = \int_use:N \c_@@_settings_attr_int ; LuaCJK.attr_disabled = \int_use:N \c_@@_disabled_attr_int ; LuaCJK.name_ccglue = "\cs_to_str:N \l_@@_ccglue_skip" ; LuaCJK.name_ecglue = "\cs_to_str:N \l_@@_ecglue_skip" ; } \bool_gset_true:N \g_@@_lua_loaded_bool \@@_settings_push: \@@_set_normal_spaced_chars: \@@_ignore_format_chars: \hook_gput_code:nnn { begindocument } { LuaCJK } { \tl_if_eq:NNT \CJKfamilydefault \l_@@_family_default_init_tl { \tl_gset:Nn \CJKfamilydefault { \CJKrmdefault } } \bool_if:NT \g_@@_load_fandol_bool { \prop_if_empty:NT \g_@@_family_prop { \@@_load_fandol: } } \@@_update_font: \g_@@_font_attr = \g_@@_fontid_int } \@@_load_local_config: \hook_gput_code:nnn { package / listings / after } { LuaCJK } { \RequirePackage { LuaCJK-listings } } \cs_if_exist:cT { ver@listings.sty } { \RequirePackage { LuaCJK-listings } } % \end{macrocode} % % \begin{macrocode} % % \end{macrocode} % % The package ships two Lua modules. \file{LuaCJK.lua} only loads the core % module; all node processing lives in \file{LuaCJK-core.lua}. % \begin{variable}[int]{LuaCJK} % The table shared by the two modules. % The core module adds its functions and constants to it. % \begin{macrocode} %<*lua> LuaCJK = LuaCJK or {} require("LuaCJK-core") % % \end{macrocode} % \end{variable} % % The core module starts by creating the shared table and caching the node % identifiers. % The callbacks run on every list, so these lookups happen once. % \begin{macrocode} %<*lua-core> LuaCJK = LuaCJK or {} % \end{macrocode} % % \begin{variable}[int] % { % M, GLYPH, GLUE, % HLIST, VLIST, KERN, % LIGATURE, PENALTY, MATH, % DISC, WHATSIT, MARK, % INS, ADJUST, LOCAL_PAR, % SPACESKIP, ITALICCORR, new_node, % free_node, has_attribute, set_attribute % } % Short names for the module table, the node types, % and the node functions. % \begin{macrocode} local M = LuaCJK local GLYPH = node.id("glyph") local GLUE = node.id("glue") local HLIST = node.id("hlist") local VLIST = node.id("vlist") local KERN = node.id("kern") local LIGATURE = node.id("ligature") local PENALTY = node.id("penalty") local MATH = node.id("math") local DISC = node.id("disc") local WHATSIT = node.id("whatsit") local MARK = node.id("mark") local INS = node.id("ins") local ADJUST = node.id("adjust") local LOCAL_PAR = node.id("local_par") local SPACESKIP = node.subtype("spaceskip") or 13 local ITALICCORR = node.subtype("italiccorrection") or 3 local new_node = node.new local free_node = node.free local has_attribute = node.has_attribute local set_attribute = node.set_attribute local unset_attribute = node.unset_attribute % \end{macrocode} % \end{variable} % % \begin{variable}[int] % { % LuaCJK.attr_font, LuaCJK.attr_cjk_space, LuaCJK.attr_xecglue, % LuaCJK.attr_inserted, LuaCJK.attr_math_font % } % The attribute numbers. % The \TeX\ side fills them in after loading the module. % \begin{macrocode} for _, field in ipairs { "font", "cjk_space", "xecglue", "inserted", "math_font", "punct_font", "nobreak", "settings", } do local key = "attr_" .. field M[key] = M[key] or nil end % \end{macrocode} % \end{variable} % % \subsection{Settings and data} % % Most options describe how text is set, not how the document is configured, % so they have to travel with the characters. % A snapshot of the current options is registered once and its number is % stored in an attribute on the glyphs created afterwards; a group that ends % before the paragraph does therefore still decides how its own text is set. % % \begin{variable}[int] % {LuaCJK.settings, LuaCJK.settings_by_signature, LuaCJK.attr_settings} % The snapshots, an index from a signature to a snapshot so that % repeated font selections reuse one entry, and the attribute number. % \begin{macrocode} local DEFAULT_SETTINGS = { ccglue = { width = 0, stretch = 0, shrink = 0 }, ecglue = { width = 0, stretch = 0, shrink = 0 }, punct_style = "quanjiao", kinsoku = true, cj_strict = false, latin_punct = false, cjk_math = false, punct_width = 0, bound_width = 0, check_single = false, widow_penalty = 10000, allow_break = false, auto_fallback = false, blocks_off = {}, } M.settings = { DEFAULT_SETTINGS } M.settings_by_signature = {} % \end{macrocode} % \end{variable} % % \begin{macro}[int]{LuaCJK.new_settings} % Read the current glue registers, build a snapshot, and select it. % The glue has to be read here rather than passed in, because that is the only % place where the registers still hold the values of the moment. % The stretch and shrink orders travel with the value, so a register such as % |0pt plus 1filll| can still fill a line after the node has been built. % \begin{macrocode} local function glue_spec (name) local skip = name and tex.skip[name] if skip then return { width = skip.width, stretch = skip.stretch, shrink = skip.shrink, stretch_order = skip.stretch_order, shrink_order = skip.shrink_order } end return { width = 0, stretch = 0, shrink = 0 } end function M.new_settings (style, kinsoku, cj_strict, latin_punct, cjk_math, punct_width, bound_width, check_single, widow_penalty, allow_break, auto_fallback, off_list) local off = {} for name in string.gmatch(off_list or "", "[^,]+") do local trimmed = string.match(name, "^%s*(.-)%s*$") if trimmed ~= "" then off[trimmed] = true end end local entry = { ccglue = glue_spec(M.name_ccglue), ecglue = glue_spec(M.name_ecglue), punct_style = style, kinsoku = kinsoku, cj_strict = cj_strict, latin_punct = latin_punct, cjk_math = cjk_math, punct_width = punct_width, bound_width = bound_width, check_single = check_single, widow_penalty = widow_penalty, allow_break = allow_break, auto_fallback = auto_fallback, blocks_off = off, } local signature = table.concat({ entry.ccglue.width, entry.ccglue.stretch, entry.ccglue.shrink, entry.ccglue.stretch_order, entry.ccglue.shrink_order, entry.ecglue.width, entry.ecglue.stretch, entry.ecglue.shrink, entry.ecglue.stretch_order, entry.ecglue.shrink_order, style, tostring(kinsoku), tostring(cj_strict), tostring(latin_punct), tostring(cjk_math), punct_width, bound_width, tostring(check_single), widow_penalty, tostring(allow_break), tostring(auto_fallback), off_list, }, "|") local id = M.settings_by_signature[signature] if not id then id = #M.settings + 1 M.settings[id] = entry M.settings_by_signature[signature] = id end if M.attr_settings then tex.setattribute(M.attr_settings, id) end return id end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{settings_of} % The snapshot of a node, or the defaults when it carries none. % \begin{macrocode} local function settings_of (n) if M.attr_settings and n then local id = has_attribute(n, M.attr_settings) if id and id > 0 and M.settings[id] then return M.settings[id] end end return M.settings[1] end M.settings_of = settings_of % \end{macrocode} % \end{macro} % % \begin{variable}[int]{LuaCJK.cj, LuaCJK.cj_count, LuaCJK.cj_loaded} % \changes{v0.0.1}{2026/11/11}{Provide \keys{CJLineBreak}, which forbids a % line break before the characters whose Unicode \texttt{Line\_Break} % property is \texttt{CJ}.} % The characters whose \texttt{Line\_Break} property is \texttt{CJ} % (the Japanese small kana and the like), how many there are, and whether % the file has been read. % The set is read from \file{LineBreak.txt} of the \pkg{unicode-data} bundle % only when \keys{CJLineBreak} asks for \texttt{strict}, so a document that % keeps the default never pays for the file. % \begin{macrocode} M.cj = {} M.cj_count = 0 M.cj_loaded = false local function read_cj (data) local set, count = {}, 0 for first, last, class in data:gmatch("(%x+)%.%.(%x+)%s*;%s*(%a+)") do if class == "CJ" then for code = tonumber(first, 16), tonumber(last, 16) do set[code] = true end count = count + 1 end end for code, class in data:gmatch("(%x+)%s*;%s*(%a+)") do if class == "CJ" then set[tonumber(code, 16)] = true count = count + 1 end end return set, count end function M.load_cj () if M.cj_loaded then return M.cj_count end M.cj_loaded = true local path = kpse and kpse.find_file and kpse.find_file("LineBreak.txt") local file = path and io.open(path) if not file then texio.write_nl("term and log", "Package LuaCJK Warning: LineBreak.txt not found; " .. "CJLineBreak = strict has no effect.") return 0 end local data = file:read("*a") file:close() M.cj, M.cj_count = read_cj(data) return M.cj_count end % \end{macrocode} % \end{variable} % % The quotation marks, the middle dot, and the ellipses are shared with Latin % scripts. With \keys{LatinPunct} on, they use the Latin font and are not % compressed; the em dash is not in the table, because it stays CJK. % % \begin{macro}[int]{LuaCJK.is_cjk} % Test whether a character is treated as CJK. % The table is the set of code points this package sets with the CJK class. % It is a policy rather than a Unicode property: the punctuation that CJK % text shares with Latin (U+2014, the quotation marks, U+2026) is listed % here even though its script is Common, and the combining Bopomofo tone % marks U+02EA and U+02EB are left out on purpose, because they attach to the % preceding letter and a glue between the two would break the attachment. % The script \file{scripts/check-charclass.lua} compares the table with % \file{Scripts.txt} and reports a code point whose script is Han, Hiragana, % Katakana, Bopomofo, or Hangul but which the table does not list. % \begin{macrocode} M.class_override = {} M.punct_override = {} local CJK_RANGES = { { 0x1100, 0x11FF }, -- Hangul Jamo { 0x2014, 0x2014 }, -- em dash, shared with Latin { 0x2018, 0x2019 }, -- single quotation marks { 0x201C, 0x201D }, -- double quotation marks { 0x2026, 0x2026 }, -- horizontal ellipsis, shared with Latin { 0x2E80, 0x9FFF }, -- radicals, punctuation, kana, ideographs { 0xA960, 0xA97C }, -- Hangul Jamo Extended-A { 0xAC00, 0xD7C6 }, -- Hangul syllables and Jamo Extended-B { 0xD7CB, 0xD7FB }, -- Hangul Jamo Extended-B { 0xF900, 0xFAFF }, -- compatibility ideographs { 0xFE30, 0xFE4F }, -- compatibility forms { 0xFF00, 0xFFEF }, -- fullwidth forms { 0x16FE2, 0x16FE3 }, -- Old Chinese hook and iteration marks { 0x16FF0, 0x16FF6 }, -- Han, Vietnamese alternate reading marks { 0x1AFF0, 0x1AFF3 }, -- Kana Extended-B { 0x1AFF5, 0x1AFFB }, { 0x1AFFD, 0x1AFFE }, { 0x1B000, 0x1B128 }, -- Kana Supplement and Kana Extended-A { 0x1B132, 0x1B132 }, { 0x1B150, 0x1B152 }, { 0x1B155, 0x1B155 }, { 0x1B164, 0x1B168 }, { 0x1F200, 0x1F200 }, -- square hiragana hoka { 0x20000, 0x3FFFF }, -- ideographic planes } local function in_ranges (c, ranges) for i = 1, #ranges do if c >= ranges[i][1] and c <= ranges[i][2] then return true end end return false end local LATIN_PUNCT = { [0x00B7] = true, [0x2018] = true, [0x2019] = true, [0x201C] = true, [0x201D] = true, [0x2025] = true, [0x2026] = true, [0x2027] = true, } local function is_cjk (c, s) local override = M.class_override[c] if override ~= nil then return override end if s and s.latin_punct and LATIN_PUNCT[c] then return false end return in_ranges(c, CJK_RANGES) end M.is_cjk = is_cjk % \end{macrocode} % \end{macro} % % \begin{variable}[int]{PUNCT_CLASS} % The punctuation \pkg{LuaCJK} knows about, grouped as opening, closing, % middle, dash, and ellipsis. % The group decides compression, line-breaking prohibitions, and hanging. % \begin{macrocode} local PUNCT_CLASS = { [0x2018] = "open", [0x201C] = "open", [0x3008] = "open", [0x300A] = "open", [0x300C] = "open", [0x300E] = "open", [0x3010] = "open", [0x3014] = "open", [0x3016] = "open", [0x3018] = "open", [0x301A] = "open", [0xFF08] = "open", [0xFF3B] = "open", [0xFF5B] = "open", [0x2019] = "close", [0x201D] = "close", [0x3001] = "close", [0x3002] = "close", [0x3009] = "close", [0x300B] = "close", [0x300D] = "close", [0x300F] = "close", [0x3011] = "close", [0x3015] = "close", [0x3017] = "close", [0x3019] = "close", [0x301B] = "close", [0xFF01] = "close", [0xFF09] = "close", [0xFF0C] = "close", [0xFF0E] = "close", [0xFF1A] = "close", [0xFF1B] = "close", [0xFF1F] = "close", [0xFF3D] = "close", [0xFF5D] = "close", [0x00B7] = "middle", [0x2027] = "middle", [0x30FB] = "middle", [0x2013] = "dash", [0x2014] = "dash", [0x2015] = "dash", [0xFF0D] = "dash", [0x2026] = "ellipsis", [0x22EF] = "ellipsis", } % \end{macrocode} % \end{variable} % % \begin{macro}[int]{punct_class} % Look up the group of a character. % \begin{macrocode} local function punct_class (c, s) local override = M.punct_override[c] if override ~= nil then return override or nil end if s and s.latin_punct and LATIN_PUNCT[c] then return nil end if M.charsets.middle[c] then return "middle" end return PUNCT_CLASS[c] end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{LuaCJK.declare_ranges} % Fill a table with \meta{value} over a list of code-point ranges, % each given as \texttt{\{first, last\}}. % \begin{macrocode} local function fill_ranges (target, value, ranges) for _, range in ipairs(ranges) do local first = range[1] local last = range[2] or first for code = first, last do target[code] = value end end end function M.declare_class (ranges, cjk) fill_ranges(M.class_override, cjk, ranges) end function M.declare_punct (ranges, group) fill_ranges(M.punct_override, group, ranges) end function M.reset_classes () M.class_override = {} M.punct_override = {} M.normal_spaced = {} end % \end{macrocode} % \end{macro} % % \begin{variable}[int]{M.charsets} % The four character sets that the punctuation options can replace. % \texttt{kaiming} holds the marks that keep a full % em width in the \texttt{kaiming} style; \texttt{middle} % and \texttt{long} are filled from the punctuation groups below, % and \texttt{nobreak} starts as a copy of \texttt{long}. % \begin{macrocode} M.charsets = { kaiming = { [0xFF0E] = true, -- . [0x3002] = true, -- 。 [0xFF1F] = true, -- ? [0xFF01] = true, -- ! }, middle = {}, long = {}, nobreak = {}, } for code, group in pairs(PUNCT_CLASS) do if group == "middle" then M.charsets.middle[code] = true PUNCT_CLASS[code] = nil elseif group == "dash" or group == "ellipsis" then M.charsets.long[code] = true end end for code in pairs(M.charsets.long) do M.charsets.nobreak[code] = true end % \end{macrocode} % \end{variable} % % \begin{macro}[int]{LuaCJK.set_charset} % Replace, extend, or reduce one of the sets above. % The ranges arrive from \TeX\ as a list of \texttt{\{first, last\}} pairs. % \begin{macrocode} function M.set_charset (kind, mode, ranges) local target = M.charsets[kind] if not target then return end if mode == "set" then target = {} M.charsets[kind] = target end for _, range in ipairs(ranges) do local first = range[1] local last = range[2] or first for code = first, last do if mode == "remove" then target[code] = nil else target[code] = true end end end end % \end{macrocode} % \end{macro} % % \begin{variable}[int]{LuaCJK.punct_styles} % The built-in styles. Each entry holds the compression switch, % the compression ratio, the opening and closing exemption, % whether the line-end hanging applies, and the optional fixed width. % \begin{macrocode} M.punct_class = function (c, s) return punct_class(c, s or M.settings[1]) end M.punct_styles = { quanjiao = { compress_adjacent = true, ratio = 0.5, keep_open_close = true, hang = true }, plain = { compress_adjacent = false, ratio = 0.0, keep_open_close = true }, banjiao = { compress_adjacent = false, ratio = 0.0, keep_open_close = true, fixed_ratio = 0.5, hang = true }, kaiming = { compress_adjacent = false, ratio = 0.0, keep_open_close = true, fixed_ratio = 0.5, fixed_full = true, hang = true }, CCT = { compress_adjacent = false, ratio = 0.0, keep_open_close = true, fixed_ratio = 0.7, hang = true }, hangmobanjiao = { compress_adjacent = false, ratio = 0.0, keep_open_close = true, hang = true }, } % \end{macrocode} % \end{variable} % % \begin{macro}[int]{LuaCJK.define_punct_style} % \changes{v0.0.1}{2026/11/11}{Provide \keys{KaiMingPunct}, % \keys{MiddlePunct}, \keys{LongPunct}, and \keys{NoBreakLongPunct}, each % with \texttt{+} and \texttt{-} forms, to edit the punctuation character % sets.} % Register a style declared by \cs{LuaCJKDeclarePunctStyle}. % \begin{macrocode} function M.define_punct_style (name, compress, ratio, keep) M.punct_styles[name] = { compress_adjacent = compress, ratio = ratio, keep_open_close = keep, } end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{punct_style} % Read the style name from the \TeX\ token list and return the matching % table, falling back to \texttt{plain}. % \begin{macrocode} M.punct_width = {} M.punct_kern = {} local function first_code (s) for _, code in utf8.codes(s) do return code end end function M.set_punct_width (chars, sp) for _, code in utf8.codes(chars) do M.punct_width[code] = sp end end function M.add_normal_spaced (ranges) for _, range in ipairs(ranges) do local first = range[1] local last = range[2] or first for code = first, last do M.normal_spaced[code] = true end end end function M.set_punct_kern (before, after, sp) local a, b = first_code(before), first_code(after) if a and b then M.punct_kern[a] = M.punct_kern[a] or {} M.punct_kern[a][b] = sp end end local function user_width (char) return M.punct_width[char] end local function user_kern (before, after) local row = M.punct_kern[before] return row and row[after] end function M.edit_punct_style (name, compress, ratio, keep) local style = M.punct_styles[name] if not style then return end if compress ~= nil then style.compress_adjacent = compress end if ratio ~= nil then style.ratio = ratio end if keep ~= nil then style.keep_open_close = keep end end local function style_of (settings) local name = settings and settings.punct_style return M.punct_styles[name] or M.punct_styles.plain end % \end{macrocode} % \end{macro} % % \begin{variable}[int]{LuaCJK.attr_disabled} % The attribute number of the suspension switch. % \begin{macrocode} M.attr_disabled = M.attr_disabled or nil % \end{macrocode} % \end{variable} % % \begin{macro}[int]{disabled} % Test the suspension attribute on a node. % \begin{macrocode} local function disabled (n) if not (M.attr_disabled and n) then return false end local v = has_attribute(n, M.attr_disabled) return v ~= nil and v ~= 0 end % \end{macrocode} % \end{macro} % % \subsection{Node rewriting} % % A CJK glyph carries the identifier of the family that applies % to it in an attribute. The pass below replaces it by a copy in that font, % and takes a sub-block font or a fallback font instead when one applies. % % \begin{variable}[int]{M.blocks, M.block_order, M.block_fonts} % The declared sub-blocks in declaration order, % and the font identifier of each block for each family. % The order decides which block wins when two ranges overlap. % \begin{macrocode} M.blocks = {} M.block_order = {} M.block_fonts = {} function M.declare_block (name, ranges) if not M.blocks[name] then table.insert(M.block_order, name) end M.blocks[name] = { ranges = ranges } end function M.set_block_font (main, name, fid) local row = M.block_fonts[main] if not row then row = {} M.block_fonts[main] = row end if fid and fid > 0 then row[name] = fid else row[name] = nil end end % \end{macrocode} % \end{variable} % % \begin{macro}[int]{block_font} % The font of the first block of \meta{main} that contains \meta{char} % and is not canceled in \meta{settings}. % \begin{macrocode} local function block_font (main, char, settings) local row = M.block_fonts[main] if not row then return nil end local off = settings and settings.blocks_off or {} for _, name in ipairs(M.block_order) do local block = M.blocks[name] if block and not off[name] and row[name] then for _, range in ipairs(block.ranges) do if char >= range[1] and char <= (range[2] or range[1]) then return row[name] end end end end return nil end % \end{macrocode} % \end{macro} % % \begin{variable}[int]{M.fallback} % The fallback fonts of each family, as identifiers, % in the order in which they were declared. % \begin{macrocode} M.fallback = {} function M.set_fallback (main, list) M.fallback[main] = list end % \end{macrocode} % \end{variable} % % \begin{variable}[int]{M.yoffset} % The vertical offset of each font, in scaled points, positive upwards. % The \keys{Baseline} key of a font declaration fills it, % and \texttt{copy_glyph} applies it, so the offset belongs to the font % a glyph is taken from and not to the text that asks for it. % \begin{macrocode} M.yoffset = {} function M.set_yoffset (fid, sp) M.yoffset[fid] = sp end % \end{macrocode} % \end{variable} % % \begin{macro}[int]{has_char} % Whether \meta{fid} has a glyph for \meta{char}. The answer is cached % per font and code point, because the pass asks it once per glyph. % \begin{variable}[int]{has_char_cache} % The cache, keyed by the font identifier and the code point packed into % one number. % \begin{macrocode} local has_char_cache = {} local function has_char (fid, char) local key = fid * 0x200000 + char local cached = has_char_cache[key] if cached ~= nil then return cached end local f = font.getfont(fid) local ok = f and f.characters and f.characters[char] ~= nil has_char_cache[key] = ok return ok end % \end{macrocode} % \end{variable} % \end{macro} % % \begin{macro}[int]{fallback_font} % The first fallback font of \meta{fid} that has \meta{char}, or nothing % when the feature is off or no fallback covers the character. % \begin{macrocode} local function fallback_font (fid, char, settings) if not (settings and settings.auto_fallback) then return nil end local list = M.fallback[fid] if not list then return nil end for _, candidate in ipairs(list) do if has_char(candidate, char) then return candidate end end end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{copy_glyph} % \changes{v0.0.1}{2026/11/11}{A character the font lacks leaves no % placeholder: the \texttt{.notdef} or private-use glyph a font substitutes % for it is removed.} % Copy a glyph node, changing only its font. % \begin{macrocode} local function copy_glyph (n, fid) local g = new_node("glyph") g.font = fid for _, field in ipairs { "char", "attr", "lang", "left", "right", "uchyph", "xoffset", "yoffset", "expansion_factor", "data", "subtype", } do g[field] = n[field] end local shift = M.yoffset[fid] if shift then g.yoffset = (g.yoffset or 0) + shift end return g end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{rewrite_fonts} % \changes{v0.0.1}{2026/11/11}{Provide \keys{LatinPunct}, which keeps the % shared quotation marks, the middle dot, and the ellipses in the Latin font % instead of the CJK font.} % Replace each CJK glyph by a copy in the font its attribute names, % after giving way to a sub-block font or to a fallback font. % A glyph that the old font did not have may sit next to the placeholder % \pkg{luaotfload} emitted for it; the placeholder is freed at the same % time, because it would otherwise add an advance of its own. % \begin{macrocode} local function rewrite_fonts (head) local prev = nil local cur = head while cur do local nxt = cur.next if cur.id == GLYPH and is_cjk(cur.char, settings_of(cur)) and M.attr_font then local target = has_attribute(cur, M.attr_font) if target and target > 0 then local block = block_font(target, cur.char, settings_of(cur)) if block then target = block end end if target and M.attr_punct_font and punct_class(cur.char) then local punct = has_attribute(cur, M.attr_punct_font) if punct and punct > 0 then target = punct end end if target and target > 0 and not has_char(target, cur.char) then target = fallback_font(target, cur.char, settings_of(cur)) or target end if target and target > 0 and target ~= cur.font then if nxt and nxt.id == GLYPH and nxt.font == cur.font and (nxt.char == 0 or (nxt.char >= 0xF0000 and nxt.char <= 0x10FFFD)) then local after = nxt.next free_node(nxt) nxt = after end local g = copy_glyph(cur, target) g.next = nxt if prev then prev.next = g else head = g end free_node(cur) cur = g end end prev = cur cur = nxt end return head end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{class_of_char} % Map a character to \texttt{CJK} or \texttt{LATIN}. % \begin{macrocode} local function class_of_char (c, s) if is_cjk(c, s) then return "CJK" end return "LATIN" end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{class_of_node} % Return the class at both edges of a node. A box carries the class of the % node at its edge, so filling glue or a kern between the last character and % the edge leaves the box with no class there; a box of no size is % transparent, and formulas count as Latin. % \begin{macro}[int]{transparent_node} % Whether a node carries neither text nor advance, so that the text reaches % the edge past it. % \begin{macrocode} local function transparent_node (n) return n.id == WHATSIT or n.id == MARK or n.id == INS or n.id == ADJUST or n.id == PENALTY end % \end{macrocode} % \end{macro} % \begin{macro}[int]{box_visible} % Whether a box carries material of its own. A box of no size is passed over, % so the boundary rules read the text on both sides of it as one run. % \begin{macrocode} local function box_visible (n) return n.width ~= 0 or n.height ~= 0 or n.depth ~= 0 end % \end{macrocode} % \end{macro} % \begin{macro}[int]{first_visible_class} % The class of the text at the start of a list: a glyph, a formula, or a box % inspected at its own edge. Glue, a kern, a rule, and a discretionary are not % text, so a list that starts with one has no class at that point, and a box % whose first edge has no class ends the search instead of yielding to a % character after it. % \begin{macrocode} local function first_visible_class (list) local n = list while n do if n.id == GLYPH then return class_of_char(n.char, settings_of(n)) elseif n.id == HLIST or n.id == VLIST then return first_visible_class(n.head) elseif n.id == MATH then return "LATIN" elseif not transparent_node(n) then return nil end n = n.next end return nil end % \end{macrocode} % \end{macro} % \begin{macro}[int]{last_visible_class} % The class of the text at the end of a list, read the same way: a node that % is not text ends the text, so the class is nothing when something follows % the last character. % \begin{macrocode} local function last_visible_class (list) local n = list local last = nil while n do if n.id == GLYPH then last = class_of_char(n.char, settings_of(n)) elseif n.id == HLIST or n.id == VLIST then last = last_visible_class(n.head) elseif n.id == MATH then last = "LATIN" elseif not transparent_node(n) then last = nil end n = n.next end return last end % \end{macrocode} % \end{macro} % \begin{macro}[int]{first_punct} % The punctuation group of the first visible character of a list, the % character itself, and whether a character was found at all, so that a box % with no text can be skipped. A formula counts as material with no mark. % \begin{macrocode} local function first_punct (list) local n = list while n do if n.id == GLYPH then return punct_class(n.char, settings_of(n)), n, true elseif n.id == HLIST or n.id == VLIST then local p, g, found = first_punct(n.head) if found then return p, g, true end elseif n.id == MATH then return nil, nil, true end n = n.next end return nil, nil, false end % \end{macrocode} % \end{macro} % \begin{macro}[int]{last_punct} % The same for the last visible character of a list. % \begin{macrocode} local function last_punct (list) local n = list local punct = nil local glyph = nil local found = false while n do if n.id == GLYPH then punct = punct_class(n.char, settings_of(n)) glyph = n found = true elseif n.id == HLIST or n.id == VLIST then local p, g, deep = last_punct(n.head) if deep then punct = p glyph = g found = true end elseif n.id == MATH then punct = nil glyph = nil found = true end n = n.next end return punct, glyph, found end % \end{macrocode} % \end{macro} % \begin{macrocode} local function class_of_node (n) if n.id == GLYPH then local c = class_of_char(n.char, settings_of(n)) return c, c elseif n.id == HLIST or n.id == VLIST then return first_visible_class(n.head), last_visible_class(n.head) elseif n.id == MATH then return "LATIN", "LATIN" end return nil, nil end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{is_inserted} % Test whether a node was inserted by \pkg{LuaCJK}. % \begin{macrocode} local function is_inserted (n) return M.attr_inserted and has_attribute(n, M.attr_inserted) ~= nil end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{inherit_attributes} % \changes{v0.0.1}{2026/11/11}{Glue, kerns, and penalties inserted by the % package carry the attributes of the neighbouring node, so a decoration % such as \cs{CJKunderdot} covers CJK/CJK and CJK/Latin boundaries without a % gap.} % Give a new node the attribute list of the node it is spliced next to. % \pkg{LuaCJK} creates glue, kerns, and penalties after \TeX\ has attached % the surrounding attributes to the text, so a package that % marks spans with attributes, such as \pkg{lua-ul}, would otherwise read % the new node as the end of its span and start a new one. Assigning % to the \meta{attr} field is reference counted, and later per-attribute % changes are copy-on-write, so the source node is never modified. % \begin{macrocode} local function inherit_attributes (new, source) if source and source.attr then new.attr = source.attr end end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{make_glue} % Create a glue node from a skip register, inherit the attributes % of \meta{source}, and mark the node as inserted. % The node keeps the stretch and shrink orders of the register. % \begin{macrocode} local function make_glue (spec, source) if not spec then return nil end local s = spec local g = new_node("glue") inherit_attributes(g, source) g.width = s.width g.stretch = s.stretch g.shrink = s.shrink g.stretch_order = s.stretch_order or 0 g.shrink_order = s.shrink_order or 0 if M.attr_inserted then set_attribute(g, M.attr_inserted, 1) end return g end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{set_glue} % Give an existing glue node the dimensions of a skip register, % including its stretch and shrink orders. % \begin{macrocode} local function set_glue (g, spec) if not spec then return false end local s = spec g.width = s.width g.stretch = s.stretch g.shrink = s.shrink g.stretch_order = s.stretch_order or 0 g.shrink_order = s.shrink_order or 0 if M.attr_inserted then set_attribute(g, M.attr_inserted, 1) end return true end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{make_kern} % Create a kern node, inherit the attributes of \meta{source}, % and mark the node as inserted. % \begin{macrocode} local function make_kern (width, source) local k = new_node("kern") inherit_attributes(k, source) k.kern = width if M.attr_inserted then set_attribute(k, M.attr_inserted, 1) end return k end % \end{macrocode} % \end{macro} % % \subsection{The passes} % % \begin{macro}[int]{spacing} % \changes{v0.0.1}{2026/11/11}{Provide CJK/CJK and CJK/Latin spacing with % \keys{CJKglue}, \keys{CJKecglue}, \keys{CJKspace}, and \keys{xCJKecglue}; % a value keeps its stretch and shrink orders. A source space between two % CJK characters becomes the inter-character glue, so the boundary keeps its % break opportunity and its stretch, and no glue is inserted between two % long punctuation marks.} % Walk a list and insert the glue between CJK and Latin material. % The walk tracks the last visible node and the class at its end, % so that a formula acts as one Latin atom at its left edge; % inside the formula the boundary rules are not applied. % A box is an atom as well, and the class at its edge is the class of the % node at that edge: filling glue or a kern means the box has no text there, % so no glue is added beside it. A box of no size is passed over, so an % empty box inside CJK text does not break the run. % \begin{macro}[int]{handle_boundary} % \changes{v0.0.1}{2026/11/11}{A mark keeps its break opportunity beside % suspended text: the boundary glue at a mark has no width.} % Decide what to do at the boundary between two visible nodes. % Only source-generated glue is a candidate for replacement; % other glue and penalties are left alone, a penalty being an explicit % barrier and a discretionary opaque, so neither is bridged. % A boundary that touches a suspended run is bridged by nothing, except that a % mark keeps its break opportunity: \keys{Verb} suspends the spacing, and the % glue at a mark has no width, so restoring it there moves no ink. An opening % mark is excluded, because a break after it is prohibited and the kinsoku pass % cannot see the prohibition across a suspended node. % \begin{macro}[int]{insert_boundary_glue} % Insert the CJK--CJK or CJK--Latin glue, % or the compression kern for two adjacent punctuation marks. % A run of dashes or ellipses is one unit: the marks are joined without glue, % so that justification cannot pull a two-em dash apart, and the absence of a % break opportunity replaces the explicit prohibition. % A punctuation mark takes no CJK--Latin advance on either side: the advance % of the mark already carries its spacing, and the glue would add a third of % an em that the typography does not ask for. % The boundary keeps a glue of zero width, so that it stays a break % opportunity and the kinsoku penalty can forbid the break. % \begin{macrocode} M.normal_spaced = {} function M.set_normal_spaced_chars (chars) local set = {} for _, code in utf8.codes(chars) do set[code] = true end M.normal_spaced = set end local function normal_spaced (n) return n and n.id == GLYPH and M.normal_spaced[n.char] == true end local function long_punct (n) return n and n.id == GLYPH and M.charsets.long[n.char] == true end local PUNCT_BOUNDARY_GLUE = { width = 0, stretch = 0, shrink = 0 } local function insert_boundary_glue (head, prev, prev_visible, prev_end, cur, cur_start, settings) if not prev_end or not cur_start then return head end if long_punct(prev) and long_punct(cur) then return head end local style = style_of(settings) local prev_punct = nil if prev_visible then if prev_visible.id == GLYPH then prev_punct = punct_class(prev_visible.char, settings) elseif prev_visible.id == HLIST or prev_visible.id == VLIST then prev_punct = last_punct(prev_visible.head) end end local cur_punct = nil if cur then if cur.id == GLYPH then cur_punct = punct_class(cur.char, settings) elseif cur.id == HLIST or cur.id == VLIST then cur_punct = first_punct(cur.head) end end local prev_mark = prev_punct and prev.id == GLYPH local cur_mark = cur_punct and cur.id == GLYPH local spec = nil if prev_end == "CJK" and cur_start == "CJK" then spec = settings.ccglue elseif prev_end ~= cur_start and not normal_spaced(prev) and not normal_spaced(cur) then if prev_punct or cur_punct then spec = PUNCT_BOUNDARY_GLUE else spec = settings.ecglue end end if prev_mark and cur_mark then local explicit = user_kern(prev.char, cur.char) if explicit then local k = make_kern(explicit, prev) k.next = cur if prev then prev.next = k else head = k end return head end end if prev_mark and cur_mark and style.compress_adjacent then local exempt = (style.keep_open_close and prev_punct == "open" and cur_punct == "close") or (M.charsets.long[prev.char] and M.charsets.long[cur.char]) if not exempt and prev.width and prev.width > 0 then local k = make_kern(-math.floor(style.ratio * prev.width + 0.5), prev) local g = spec and make_glue(spec, prev) or nil k.next = g or cur if g then g.next = cur end if prev then prev.next = k else head = k end return head end end if not spec then return head end local g = make_glue(spec, prev or cur) if not g then return head end g.next = cur if prev then prev.next = g else head = g end return head end % \end{macrocode} % \end{macro} % \begin{macro}[int]{restore_source_spaces} % \changes{v0.0.2}{2026/11/15}{The space in front of \cs{textbf} and its % siblings is still the inter-character glue: \LaTeX\ moves a space across % the italic correction of a text font command, and the move leaves the glue % with the subtype of a skip the document wrote.} % Give a source space that a text font command moved across the italic % correction its subtype back. % \cs{textbf} and the other text font commands save the skip in front of them, % remove it, apply the italic correction, and put it back with \tn{hskip}, so % the glue keeps the dimensions of the space but not its subtype. % The move is the only way a normal glue comes to follow an italic-correction % kern, and the subtype is the one thing the spacing pass reads, so putting the % subtype back is enough; a typeface whose interword space is rigid therefore % needs no special case, at the price of retiming a skip that the document % wrote in that very place. % \begin{macrocode} local function restore_source_spaces (head) local prev = nil local cur = head while cur do if cur.id == GLUE and cur.subtype ~= SPACESKIP and prev and prev.id == KERN and prev.subtype == ITALICCORR then cur.subtype = SPACESKIP end prev = cur cur = cur.next end return head end % \end{macrocode} % \end{macro} % \begin{macro}[int]{handle_source_space} % Remove or retime a space typed in the source. % Only a space that came from the source is a candidate; an explicit % \tn{hskip} or \tn{hfill} has a different subtype and is left alone, and the % space that a text font command moved across the italic correction has had % its subtype put back above. % A space between two CJK characters is not kept when \keys{CJKspace} is off, % but dropping it must not drop the boundary with it: the space becomes the % inter-character glue of \keys{CJKglue}. Freeing it instead would leave the % two characters adjacent with no glue, and the pass that would add the glue % runs after line breaking, so the break opportunity would be lost. % \begin{macrocode} local function handle_source_space (head, glue, prev_end, cur, cur_start, settings) if glue.subtype ~= SPACESKIP then return head end local prev_cjk = prev_end == "CJK" local cur_cjk = cur_start == "CJK" if prev_cjk and cur_cjk then local cjk_space = M.attr_cjk_space and (has_attribute(glue, M.attr_cjk_space) or 0) ~= 0 if not cjk_space then set_glue(glue, settings.ccglue) if M.attr_inserted then set_attribute(glue, M.attr_inserted, 1) end end elseif prev_cjk or cur_cjk then local xecglue = M.attr_xecglue and (has_attribute(glue, M.attr_xecglue) or 0) ~= 0 if xecglue then set_glue(glue, settings.ecglue) end end return head end % \end{macrocode} % \end{macro} % \begin{macrocode} local function handle_boundary (head, prev, prev_visible, prev_end, cur, start_class, settings) if not prev_visible or not start_class then return head end if disabled(prev_visible) or disabled(cur) then local occupied = prev and (prev.id == GLUE or prev.id == KERN or prev.id == PENALTY or prev.id == DISC) if disabled(cur) and not disabled(prev_visible) and not occupied then local mark = prev_visible.id == GLYPH and punct_class(prev_visible.char, settings) if mark and mark ~= "open" then local g = make_glue(PUNCT_BOUNDARY_GLUE, prev_visible) if g then g.next = cur if prev then prev.next = g else head = g end end end end return head end if prev and prev.id == GLUE then if not is_inserted(prev) then return handle_source_space( head, prev, prev_end, cur, start_class, settings) end elseif prev and prev.id == PENALTY then elseif prev and prev.id == DISC then else return insert_boundary_glue( head, prev, prev_visible, prev_end, cur, start_class, settings) end return head end % \end{macrocode} % \end{macro} % \begin{macrocode} local function spacing (head) head = restore_source_spaces(head) local prev = nil local prev_visible = nil local prev_end = nil local math_level = 0 local cur = head while cur do local nxt = cur.next if cur.id == MATH then if cur.subtype == 0 then if math_level == 0 then head = handle_boundary( head, prev, prev_visible, prev_end, cur, "LATIN", settings_of(prev_visible or cur)) end math_level = math_level + 1 prev_visible = nil prev_end = nil else if math_level > 0 then math_level = math_level - 1 end if math_level == 0 then prev_visible = cur prev_end = "LATIN" end end elseif math_level > 0 then else local start_class, end_class = class_of_node(cur) local visible if cur.id == HLIST or cur.id == VLIST then visible = box_visible(cur) else visible = start_class ~= nil end if visible then head = handle_boundary( head, prev, prev_visible, prev_end, cur, start_class, settings_of(prev_visible)) prev_visible = cur prev_end = end_class elseif cur.id == PENALTY or cur.id == DISC then prev_visible = nil prev_end = nil end end prev = cur cur = nxt end return head end % \end{macrocode} % \end{macro} % % \TeX\ may break at glue, at kern, and at a discretionary. The first two are % places where \pkg{LuaCJK} inserts its own nodes, so after building the list a % second pass puts a penalty 10000 in front of every break opportunity that % must not be used. % A discretionary states the value of its own break, so there the pass sets % the node's penalty field rather than inserting a penalty. % The hyphens of a word are not in the list yet when the pass runs: \TeX\ % inserts them during line breaking, so a \texttt{hyphenate} callback forbids % those as well. % % \begin{macro}[int]{apply_kinsoku} % \changes{v0.0.1}{2026/11/11}{Provide \keys{PunctKinsoku} line-breaking % prohibitions: closing punctuation cannot start a line and opening % punctuation cannot end a line.} % \changes{v0.0.2}{2026/11/15}{\keys{AllowBreakBetweenPuncts} no longer lets a % closing mark start a line or an opening mark end one, and a mark inside a % box takes part in the prohibitions.} % Add the prohibitions where the settings of the text ask for them. % A node marked by \cs{LuaCJKnobreak} or by the verbatim addon % is prohibited regardless, because the mark itself is the prohibition. % A box is read at the edge that faces the break, the way the spacing pass % reads it, so that a mark at the edge of the box is neither a line start nor % a line end. % \begin{macrocode} % \end{macrocode} % \begin{macro}[int]{no_break_mark} % Whether a node carries the attribute of \cs{LuaCJKnobreak}. % \begin{macrocode} local function no_break_mark (n) if not (M.attr_nobreak and n) then return false end local value = has_attribute(n, M.attr_nobreak) return value ~= nil and value ~= 0 end % \end{macrocode} % \end{macro} % \begin{macro}[int]{next_glyph} % The glyph a discretionary is followed by: the letters of a word are % separated from it by a kern or a ligature at most, and a glue, a penalty, % another discretionary or a formula ends the word. % \begin{macrocode} local function next_glyph (n) while n and (n.id == KERN or n.id == LIGATURE) do n = n.next end if n and n.id == GLYPH then return n end return nil end % \end{macrocode} % \end{macro} % \begin{macro}[int]{forbid_marked_disc} % Forbid the break at a discretionary that involves a marked node. % The break lies between the letters on either side of the discretionary, so % it is prohibited when either of them is marked, the way the break at a glue % is. % \TeX\ gives a discretionary the attributes of the character in front of it, % which is why the node itself is read as well as its neighbours. % \begin{macrocode} local function forbid_marked_disc (disc, left) if no_break_mark(disc) or no_break_mark(left) or no_break_mark(next_glyph(disc.next)) then disc.penalty = 10000 end end % \end{macrocode} % \end{macro} % \begin{macro}[int]{adjacent_marks} % Whether two marks are neighbours that stay together: a dash or an ellipsis % is left out, because a run of them is one unit of its own, governed by the % \texttt{nobreak} character set. % \begin{macrocode} local function adjacent_marks (prev_mark, next_mark) return prev_mark and next_mark and prev_mark ~= "dash" and prev_mark ~= "ellipsis" and next_mark ~= "dash" and next_mark ~= "ellipsis" end % \end{macrocode} % \end{macro} % \begin{macro}[int]{forbids_break} % The rules: closing and middle punctuation must not start a line, % opening punctuation must not end one, % a run of dashes or ellipses stays together, % and two adjacent marks are not separated. % \keys{AllowBreakBetweenPuncts} lifts the last rule alone: a mark that may % not begin or end a line is a rule of the script, so the option cannot % permit one. % \begin{macrocode} local function forbids_break (prev_mark, prev_char, next_mark, next_char, settings) if next_mark == "close" or next_mark == "middle" then return true end if prev_mark == "open" then return true end if M.charsets.nobreak[prev_char] and M.charsets.nobreak[next_char] then return true end if adjacent_marks(prev_mark, next_mark) and not (settings and settings.allow_break) then return true end return false end % \end{macrocode} % \end{macro} % \begin{macro}[int]{edge_mark} % The punctuation group and the character at one edge of a visible node: % a glyph is its own edge, and a box is read at the edge that faces the % break, so a mark inside a box takes part in the rules. A formula carries % no mark. % \begin{macrocode} local function edge_mark (n, at_end) if n.id == GLYPH then return punct_class(n.char, settings_of(n)), n.char, n elseif n.id == HLIST or n.id == VLIST then local mark, g = nil, nil if at_end then mark, g = last_punct(n.head) else mark, g = first_punct(n.head) end if g then return mark, g.char, g end return nil, nil, nil end return nil, nil, nil end % \end{macrocode} % \end{macro} % \begin{macro}[int]{next_visible} % The next node that carries text: a glyph, a formula, or a box. A box of % no size is passed over, and an explicit break barrier ends the search. % \begin{macrocode} local function next_visible (n) while n do if n.id == GLYPH or n.id == MATH then return n elseif n.id == HLIST or n.id == VLIST then if box_visible(n) then return n end elseif n.id == PENALTY or n.id == DISC then return nil end n = n.next end return nil end % \end{macrocode} % \end{macro} % \begin{macro}[int]{is_break_point} % Glue and kerns are break opportunities; % only the ones \pkg{LuaCJK} inserted and the source spaces are considered. % \begin{macrocode} local function is_break_point (n) if n.id == GLUE then return is_inserted(n) or n.subtype == SPACESKIP elseif n.id == KERN then return is_inserted(n) end return false end % \end{macrocode} % \end{macro} % \begin{macrocode} local function apply_kinsoku (head) local prev = nil local last = nil local cur = head while cur do if cur.id == GLYPH or ((cur.id == HLIST or cur.id == VLIST) and box_visible(cur)) then last = cur elseif cur.id == DISC then forbid_marked_disc(cur, last) elseif is_break_point(cur) and last then local nxt = next_visible(cur.next) local settings = settings_of(last) local prev_mark, prev_char = edge_mark(last, true) local next_mark, next_char = nil, nil if nxt then next_mark, next_char = edge_mark(nxt, false) end local cj = settings.cj_strict and next_char and M.cj[next_char] if nxt and (no_break_mark(last) or no_break_mark(nxt) or cj or (settings.kinsoku and not disabled(last) and not disabled(nxt) and forbids_break(prev_mark, prev_char, next_mark, next_char, settings))) then if not (prev and prev.id == PENALTY and prev.penalty >= 10000) then local p = new_node("penalty") inherit_attributes(p, last) p.penalty = 10000 if M.attr_inserted then set_attribute(p, M.attr_inserted, 1) end p.next = cur if prev then prev.next = p else head = p end cur = p end end end prev = cur cur = cur.next end return head end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{apply_fixed_widths} % Styles such as \texttt{banjiao} give every punctuation mark a fixed % advance. The glyph advance cannot be changed, so a compensating kern % goes right after the glyph. A penalty in front of that kern keeps \TeX\ % from treating it as an extra break opportunity. % The kern alone is only right for a mark whose ink sits at the left of its % advance, which is what a closing mark does. An opening mark keeps its ink % at the right, so the ink is moved left by the same amount; a middle mark is % centered, so it moves by half. A dash or an ellipsis is not shortened by the % style: its advance is the length of the stroke, and halving it would let the % two halves of a two-em dash overlap. % \begin{macrocode} local function ink_shift (class, delta) if class == "open" then return delta elseif class == "middle" then return math.floor(delta / 2 + 0.5) end return 0 end local function fixed_width (cur, settings, style) local target = user_width(cur.char) if not target and settings.punct_width > 0 then target = settings.punct_width end if target then return target end local ratio = style.fixed_ratio if not ratio or M.charsets.long[cur.char] then return nil end local full = style.fixed_full and M.charsets.kaiming if full and full[cur.char] then return nil end local f = font.getfont(cur.font) local size = f and f.size or 0 if size > 0 then return math.floor(ratio * size + 0.5) end return nil end local function apply_fixed_widths (head) local cur = head while cur do local settings = settings_of(cur) local style = style_of(settings) if cur.id == GLYPH and punct_class(cur.char, settings) and not disabled(cur) then local target = fixed_width(cur, settings, style) if target then local delta = target - cur.width if delta ~= 0 then local shift = ink_shift(punct_class(cur.char, settings), delta) local p = new_node("penalty") inherit_attributes(p, cur) p.penalty = 10000 local k = make_kern(delta, cur) p.next = k k.next = cur.next cur.next = p if shift ~= 0 then cur.xoffset = cur.xoffset + shift end end end end cur = cur.next end return head end % \end{macrocode} % \end{macro} % % A CJK character in math mode reaches the math list as a \texttt{math_char} % noad. Its family points at the math font, which usually has no CJK glyph, % so \pkg{LuaCJK} replaces the nucleus by a \texttt{sub_box} holding the % character in the CJK font. % % \begin{variable}[int]{MATH_CHAR, MATH_TEXT_CHAR} % The node types of the two kinds of math character noad. % \begin{macrocode} local MATH_CHAR = node.id("math_char") local MATH_TEXT_CHAR = node.id("math_text_char") % \end{macrocode} % \end{variable} % % \begin{macro}[int]{any_cjk_math_enabled} % Whether any noad of the list asks for CJK in math mode. The switch % is part of the settings of the text, so the answer is per list. % \begin{macrocode} local function any_cjk_math_enabled (head) local cur = head while cur do if settings_of(cur).cjk_math then return true end cur = cur.next end return false end % \end{macrocode} % \end{macro} % % \begin{variable}[int]{SUB_MLIST} % The node type of a nested math list. % \begin{macrocode} local SUB_MLIST = node.id("sub_mlist") % \end{macrocode} % \end{variable} % % \begin{macro}[int]{rewrite_math} % \changes{v0.0.1}{2026/11/11}{Provide \keys{CJKmath} and \cs{setCJKmathfont} % for CJK characters in math mode, including nested math lists such as % fractions. The family is registered as a math family, so a character in a % script or a fraction keeps the size of that math style; with % \keys{CJKmath} off, a CJK character in math mode warns once.} % Rewrite the CJK noads of a math list, % or warn once when the feature is off. % \begin{macro}[int]{rewrite_math_list} % Walk a math list, recursing into sub lists (fractions, braces, % scripts) so that nested CJK characters are rewritten too. % The declaration is split from the definition because of that recursion. % \begin{macrocode} local rewrite_math_list % \end{macrocode} % \begin{macro}[int]{rewrite_noad} % Point the nucleus of one noad at the math family of its CJK family, and % recurse into the sub lists of its numerator, denominator, superscript, and % subscript. % The family number comes from the attribute \TeX\ set for the current group, % which is why \cs{setCJKmathfont} keeps its meaning inside a group: the % family of the node changes, not a global font. % \begin{macrocode} local function point_at_family (char, family) char.fam = family if M.attr_font then unset_attribute(char, M.attr_font) end end local function rewrite_field (s, noad, on, family) if not s then return end if s.id == MATH_CHAR or s.id == MATH_TEXT_CHAR then if is_cjk(s.char, settings_of(noad)) then point_at_family(s, family) end elseif s.id == SUB_MLIST then rewrite_math_list(s.head, on, family) end end local function rewrite_noad (noad, enabled, family) if disabled(noad) then return end local on = enabled or settings_of(noad).cjk_math if not on or not family then return end rewrite_field(noad.nucleus, noad, on, family) for _, field in ipairs({ "num", "denom", "sup", "sub" }) do rewrite_field(noad[field], noad, on, family) end end % \end{macrocode} % \end{macro} % \begin{macrocode} rewrite_math_list = function (head, enabled, family) local cur = head while cur do rewrite_noad(cur, enabled, family) cur = cur.next end return head end % \end{macrocode} % \end{macro} % % \begin{variable}[int]{LuaCJK.warned_cjk_math} % Whether the one-time warning has been shown. % \begin{macrocode} M.warned_cjk_math = M.warned_cjk_math or false % \end{macrocode} % \end{variable} % % \begin{macro}[int]{warn_disabled_cjk_math} % Warn once when a CJK character appears in math mode while the feature % is off. % \begin{macro}[int]{find_cjk_math} % Search a math list, including its sub lists, for a CJK character. % \begin{macrocode} local function find_cjk_math (head) local cur = head while cur do local nuc = cur.nucleus if nuc then if (nuc.id == MATH_CHAR or nuc.id == MATH_TEXT_CHAR) and is_cjk(nuc.char, settings_of(cur)) then return true elseif nuc.id == SUB_MLIST and find_cjk_math(nuc.head) then return true end end for _, field in ipairs({ "num", "denom", "sup", "sub" }) do local sub = cur[field] if sub and sub.id == SUB_MLIST and find_cjk_math(sub.head) then return true end end cur = cur.next end return false end % \end{macrocode} % \end{macro} % \begin{macrocode} local function warn_disabled_cjk_math (head) if M.warned_cjk_math then return end if find_cjk_math(head) then M.warned_cjk_math = true texio.write_nl("term and log", "Package LuaCJK Warning: CJK character in math mode, " .. "but CJKmath is not enabled.") texio.write_nl("term and log", "(LuaCJK) Use \\LuaCJKsetup{CJKmath = true} " .. "to typeset it with the CJK font.") end end % \end{macrocode} % \end{macro} % \begin{macrocode} local function math_family_of (head) local cur = head while cur do if M.attr_math_font then local family = has_attribute(cur, M.attr_math_font) if family and family > 0 then return family end local nuc = cur.nucleus if nuc then family = has_attribute(nuc, M.attr_math_font) if family and family > 0 then return family end end end cur = cur.next end return nil end local function rewrite_math (head) if not any_cjk_math_enabled(head) then warn_disabled_cjk_math(head) return head end return rewrite_math_list(head, true, math_family_of(head)) end % \end{macrocode} % \end{macro} % % A line that ends at a closing or middle mark, or that starts with an opening % mark, measures the mark's advance short of its ink: the blank half stands in % the margin. A line that never counted that blank refuses the mark it could % have held, and is set loose instead. The hang therefore has to be known while % the line is broken, and by then only the node list is left, so it is % expressed as a zero-size rule of negative width beside the break: a closing % mark that ends a line is followed by such a rule and the break glue carries % the same width, so the two cancel while the line is set and the rule alone % shortens the line when the break is taken; a glue and a rule in the same % order let an opening mark begin a line. % A kern would not do, because a kern that follows a character is a break % opportunity of its own and would take a kinsoku penalty, while \TeX\ never % discards a rule at a line break. % % Character protrusion is no help either, because it does not move the ink at % the end of a line: \tn{protrudechars} together with \tn{rpcode} makes % \tn{rightmarginkern} report the amount while the glyph keeps its position, % measured at 10pt with a right code of 1000 in a short line, an overfull line, % and a line whose width is the natural width. Only \tn{lpcode} moves the line, % which is what the visible effect of \pkg{microtype}'s protrusion on a Latin % line consists of. % % \begin{macro}[int]{bound_width} % \changes{v0.0.1}{2026/11/11}{Provide \keys{PunctBoundWidth}, which leaves % only the given advance of a mark inside the line and hangs the rest into % the margin, at the end of a line, at its start, and when the mark ends a % box such as \tn{mbox}.} % The advance that a mark keeps inside the line when it ends one, % or nothing when the option is off. % Without it the hang is the blank the font carries, or half of the advance % when the file has no outline to read. % \begin{macrocode} local function bound_width (settings) local value = settings and settings.bound_width or 0 if value > 0 then return value end return nil end % \end{macrocode} % \end{macro} % % The hang of a mark is the blank its glyph carries, so the ink of a % line-final or line-initial mark stands on the measure instead of a half em % inside it. \pkg{xeCJK} reads that blank from the font with the \XeTeX\ % primitive \tn{XeTeXglyphbounds}; LuaTeX has no such query, so the outline is % read from the font file, once per file and once per glyph. % % Only the tables a measurement needs are parsed, and the bytes of a glyph are % read when it is first asked for, so a sixteen-megabyte CJK face does not sit % in memory a second time. The box is the one the control points span, which is % what \XeTeX\ reports as well. % % \begin{variable}[int]{font_files} % One entry per font file, holding its directory, its character map, and the % boxes already measured. % \begin{macrocode} local font_files = {} % \end{macrocode} % \end{variable} % \begin{macro}[int]{u16, i16, u32} % The big-endian integers of the file format. % \begin{macrocode} local function u16 (s, o) return s:byte(o + 1) * 256 + s:byte(o + 2) end local function i16 (s, o) local v = u16(s, o) if v >= 32768 then v = v - 65536 end return v end local function u32 (s, o) return u16(s, o) * 65536 + u16(s, o + 2) end % \end{macrocode} % \end{macro} % \begin{macro}[int]{font_file} % The parsed directory of one font file, or nothing when it cannot be read. % The handle stays open, because one file serves every mark of the document. % \begin{macrocode} local function font_file (filename) local f = font_files[filename] if f ~= nil then return f or nil end local fh = io.open(filename, "rb") if not fh then font_files[filename] = false return nil end local function read (off, len) fh:seek("set", off) return fh:read(len) or "" end local head = read(0, 12) if #head < 12 then font_files[filename] = false return nil end local count = u16(head, 4) local dir = read(12, count * 16) local tables = {} for i = 0, count - 1 do local rec = i * 16 local tag = dir:sub(rec + 1, rec + 4) tables[tag] = { offset = u32(dir, rec + 8), length = u32(dir, rec + 12) } end local t = { fh = fh, read = read, tables = tables, boxes = {} } if tables.head then t.upem = u16(read(tables.head.offset + 18, 2), 0) end if not (t.upem and t.upem > 0) then fh:close() font_files[filename] = false return nil end font_files[filename] = t return t end % \end{macrocode} % \end{macro} % \begin{macro}[int]{font_cmap} % The character map, preferring a Unicode subtable: a format 12 table for % the third platform, then a format 4 one, then whichever remains. % \begin{macrocode} local function font_cmap (f) local off = f.tables.cmap and f.tables.cmap.offset if not off then return end local head = f.read(off, 4) local n = u16(head, 2) local recs = f.read(off + 4, n * 8) local choice for i = 0, n - 1 do local p = i * 8 local pid, eid = u16(recs, p), u16(recs, p + 2) local sub = off + u32(recs, p + 4) local fmt = u16(f.read(sub, 2), 0) local rank if fmt == 12 and pid == 3 and eid == 10 then rank = 3 elseif fmt == 12 then rank = 2 elseif fmt == 4 and pid == 3 and eid == 1 then rank = 2 elseif fmt == 4 then rank = 1 end if rank and (not choice or rank > choice.rank) then choice = { rank = rank, sub = sub, format = fmt } end end if not choice then return end if choice.format == 4 then local head4 = f.read(choice.sub, 14) local segX2 = u16(head4, 6) local seg = segX2 // 2 f.cmap = { format = 4, data = f.read(choice.sub, u16(head4, 2)), segments = seg, end_of = 14, start_of = 14 + segX2 + 2, delta_of = 14 + segX2 + 2 + segX2, range_of = 14 + segX2 + 2 + segX2 + segX2, } else local head12 = f.read(choice.sub, 16) f.cmap = { format = 12, data = f.read(choice.sub, u32(head12, 4)) } end end % \end{macrocode} % \end{macro} % \begin{macro}[int]{font_glyph} % The glyph index of a character, or zero. % \begin{macrocode} local function font_glyph (f, code) if f.cmap then local c = f.cmap if c.format == 4 then local end_of, start_of, delta_of, range_of = c.end_of, c.start_of, c.delta_of, c.range_of for i = 0, c.segments - 1 do if code <= u16(c.data, end_of + i * 2) then local start = u16(c.data, start_of + i * 2) if code < start then return 0 end local delta = i16(c.data, delta_of + i * 2) local ro = u16(c.data, range_of + i * 2) if ro == 0 then return (code + delta) % 65536 end local addr = range_of + i * 2 + ro + (code - start) * 2 local g = u16(c.data, addr) if g == 0 then return 0 end return (g + delta) % 65536 end end return 0 elseif c.format == 12 then local p = 16 while p + 12 <= #c.data do local s, e, g = u32(c.data, p), u32(c.data, p + 4), u32(c.data, p + 8) if code >= s and code <= e then return g + (code - s) end p = p + 12 end return 0 end return 0 end return 0 end % \end{macrocode} % \end{macro} % \begin{macro}[int]{font_advance} % The advance of a glyph from \texttt{hmtx}; a glyph past the last entry takes % the last advance, as the format says. % \begin{macrocode} local function font_advance (f, gid) if f.advances then return f.advances[gid] or f.last_advance or 0 end local hhea = f.tables.hhea and f.tables.hhea.offset local hmtx = f.tables.hmtx and f.tables.hmtx.offset if not (hhea and hmtx) then return f.upem end local nhm = u16(f.read(hhea + 34, 2), 0) if nhm == 0 then return f.upem end local data = f.read(hmtx, nhm * 4) local advances, last = {}, 0 for i = 0, nhm - 1 do last = u16(data, i * 4) advances[i] = last end f.advances, f.last_advance = advances, last return advances[math.min(gid, nhm - 1)] or last end % \end{macrocode} % \end{macro} % \begin{macro}[int]{cff_index} % A \texttt{CFF} index: where its data starts and the offset of each item, % which is one-based from that start. % \begin{macrocode} local function cff_index (f, p) local head = f.read(p, 3) local count = u16(head, 0) if count == 0 then return { count = 0, next = p + 2, data = p + 2 } end local off_size = head:byte(3) local offs = f.read(p + 3, (count + 1) * off_size) local function off (i) local v = 0 local base = (i - 1) * off_size for k = 1, off_size do v = v * 256 + offs:byte(base + k) end return v end local data = p + 3 + (count + 1) * off_size return { count = count, data = data, off = off, next = data + off(count + 1) - 1 } end % \end{macrocode} % \end{macro} % \begin{macro}[int]{cff_string} % One item of a \texttt{CFF} index, which is a charstring or a dictionary. % \begin{macrocode} local function cff_string (f, idx, i) local a, b = idx.off(i), idx.off(i + 1) return f.read(idx.data + a - 1, b - a) end % \end{macrocode} % \end{macro} % \begin{macro}[int]{cff_dict} % A \texttt{CFF} dictionary as its operators and operands. Only the two % operators that point at the charstrings and the private dictionary are read, % so a real number is skipped rather than converted. % \begin{macrocode} local function cff_dict (s) local out, stack = {}, {} local i, n = 1, #s while i <= n do local b = s:byte(i) if b <= 21 then i = i + 1 local op = b if b == 12 then op = 1200 + s:byte(i) i = i + 1 end out[op] = stack stack = {} elseif b == 28 then local v = s:byte(i + 1) * 256 + s:byte(i + 2) if v >= 32768 then v = v - 65536 end stack[#stack + 1] = v i = i + 3 elseif b == 29 then local v = s:byte(i + 1) * 16777216 + s:byte(i + 2) * 65536 + s:byte(i + 3) * 256 + s:byte(i + 4) if v >= 2147483648 then v = v - 4294967296 end stack[#stack + 1] = v i = i + 5 elseif b == 30 then i = i + 1 while i <= n do local c = s:byte(i) i = i + 1 if c % 16 == 15 then break end end elseif b >= 32 and b <= 246 then stack[#stack + 1] = b - 139 i = i + 1 elseif b >= 247 and b <= 250 then stack[#stack + 1] = (b - 247) * 256 + s:byte(i + 1) + 108 i = i + 2 elseif b >= 251 and b <= 254 then stack[#stack + 1] = -(b - 251) * 256 - s:byte(i + 1) - 108 i = i + 2 else i = i + 1 end end return out end % \end{macrocode} % \end{macro} % \begin{macro}[int]{cff_load} % The charstrings of a \texttt{CFF} font and the two subroutine indexes a % charstring may call into. % \begin{macrocode} local function cff_load (f) local base = f.tables["CFF "] and f.tables["CFF "].offset if not base then return end local hdr = f.read(base + 2, 1) local p = base + hdr:byte(1) local names = cff_index(f, p) local tops = cff_index(f, names.next) local strings = cff_index(f, tops.next) local gsubrs = cff_index(f, strings.next) local top = cff_dict(cff_string(f, tops, 1)) local cs = top[17] and top[17][1] if not cs then return end f.charstrings = cff_index(f, base + cs) f.gsubrs = gsubrs local priv = top[18] if priv then local size, offset = priv[1], priv[2] local pdict = cff_dict(f.read(base + offset, size)) if pdict[19] then f.subrs = cff_index(f, base + offset + pdict[19][1]) end end f.cff = true end % \end{macrocode} % \end{macro} % \begin{macro}[int]{cff_box} % The horizontal extent of a Type 2 charstring. Every point of the outline is % visited, the control points of a curve included, which is the box the font's % own readers report; hints are counted only so that their masks can be % skipped. % \begin{macrocode} local function cff_box (f, cs) local x, xmin, xmax = 0, math.huge, -math.huge local stack, stems = {}, 0 local function point (dx, dy) x = x + (dx or 0) if x < xmin then xmin = x end if x > xmax then xmax = x end end local function number (b, i) if b >= 32 and b <= 246 then return b - 139, i + 1 end if b >= 247 and b <= 250 then return (b - 247) * 256 + cs:byte(i + 1) + 108, i + 2 end if b >= 251 and b <= 254 then return -(b - 251) * 256 - cs:byte(i + 1) - 108, i + 2 end if b == 28 then local v = cs:byte(i + 1) * 256 + cs:byte(i + 2) if v >= 32768 then v = v - 65536 end return v, i + 3 end if b == 255 then return (cs:byte(i + 1) * 256 + cs:byte(i + 2)) / 65536, i + 3 end return nil, i end local run run = function (code, depth) local i, n = 1, #code while i <= n do local b = code:byte(i) local v v, i = number(b, i) if v ~= nil then stack[#stack + 1] = v else i = i + 1 if b == 21 then point(stack[1], stack[2]) stack = {} elseif b == 22 then point(stack[1], 0) stack = {} elseif b == 4 then point(0, stack[1]) stack = {} elseif b == 5 then for k = 1, #stack - 1, 2 do point(stack[k], stack[k + 1]) end stack = {} elseif b == 6 or b == 7 then local horiz = (b == 6) for k = 1, #stack do if horiz then point(stack[k], 0) else point(0, stack[k]) end horiz = not horiz end stack = {} elseif b == 8 then for k = 1, #stack - 5, 6 do point(stack[k], stack[k + 1]) point(stack[k + 2], stack[k + 3]) point(stack[k + 4], stack[k + 5]) end stack = {} elseif b == 24 then local k = 1 while k <= #stack - 2 do point(stack[k], stack[k + 1]) k = k + 2 end point(stack[k], stack[k + 1]) stack = {} elseif b == 25 then local k = 1 while k <= #stack - 6 do point(stack[k], stack[k + 1]) point(stack[k + 2], stack[k + 3]) point(stack[k + 4], stack[k + 5]) k = k + 6 end point(stack[k], stack[k + 1]) stack = {} elseif b == 26 then local k, dx = 1, 0 if #stack % 2 == 1 then dx, k = stack[1], 2 end while k <= #stack - 3 do point(dx, stack[k]) point(stack[k + 1], stack[k + 2]) point(stack[k + 3], 0) dx = 0 k = k + 4 end stack = {} elseif b == 27 then local k, dy = 1, 0 if #stack % 2 == 1 then dy, k = stack[1], 2 end while k <= #stack - 3 do point(stack[k], dy) point(stack[k + 1], stack[k + 2]) point(0, stack[k + 3]) dy = 0 k = k + 4 end stack = {} elseif b == 30 or b == 31 then local horiz = (b == 31) local k = 1 while k <= #stack - 3 do local last = (#stack - k == 3) if horiz then point(stack[k], last and stack[k + 3] or 0) point(stack[k + 1], stack[k + 2]) point(0, 0) else point(last and stack[k + 3] or 0, stack[k]) point(stack[k + 1], stack[k + 2]) point(0, 0) end horiz = not horiz k = k + 3 end stack = {} elseif b == 1 or b == 3 or b == 18 or b == 23 then stems = stems + #stack // 2 stack = {} elseif b == 19 or b == 20 then stems = stems + #stack // 2 stack = {} i = i + (stems + 7) // 8 elseif b == 10 or b == 29 then local sub = table.remove(stack) local arr = (b == 10) and f.subrs or f.gsubrs if arr and arr.count > 0 and sub then local bias = (arr.count < 1240) and 107 or (arr.count < 33900 and 1131 or 32768) local idx = sub + bias + 1 if idx >= 1 and idx <= arr.count and depth < 8 then run(cff_string(f, arr, idx), depth + 1) end end elseif b == 11 or b == 14 then return else stack = {} end end end end run(cs, 0) if xmin == math.huge then return nil end return xmin, xmax end % \end{macrocode} % \end{macro} % \begin{macro}[int]{font_box} % The box of one character in font units: a \texttt{CFF} outline from its % charstring, a \texttt{glyf} outline from the ten-byte header that starts it, % and nothing for a character the font does not have. % \begin{macrocode} local function font_box (f, code) local known = f.boxes[code] if known ~= nil then return known or nil end if f.cff == nil then cff_load(f) end if f.cmap == nil then font_cmap(f) end local gid = font_glyph(f, code) if not gid or gid == 0 then f.boxes[code] = false return nil end local advance = font_advance(f, gid) local xmin, xmax if f.charstrings then if gid > f.charstrings.count then return nil end xmin, xmax = cff_box(f, cff_string(f, f.charstrings, gid + 1)) else local glyf = f.tables.glyf and f.tables.glyf.offset local loca = f.tables.loca and f.tables.loca.offset local head = f.tables.head and f.tables.head.offset if not (glyf and loca and head) then return nil end local long = u16(f.read(head + 50, 2), 0) == 1 local function loc (i) if long then return u32(f.read(loca + i * 4, 4), 0) end return u16(f.read(loca + i * 2, 2), 0) * 2 end local a, b = loc(gid), loc(gid + 1) if b <= a then return nil end local hdr = f.read(glyf + a, 10) xmin, xmax = i16(hdr, 2), i16(hdr, 6) end if not xmin then return nil end local box = { xmin = xmin, xmax = xmax, advance = advance } f.boxes[code] = box return box end % \end{macrocode} % \end{macro} % \begin{macro}[int]{mark_blank} % \changes{v0.0.2}{2026/11/15}{The hang of a mark is the blank its font % carries, read from the outline, so the ink of a line-final or line-initial % mark stands on the measure instead of half an em inside it; a file that % cannot be read falls back to half the advance.} % The blank of a mark on the side that hangs, in scaled points, or nothing % when the file cannot be read or is one member of a collection. The advance is % the one the style gives the mark, so a compressed mark is measured inside its % own advance, where \cs{apply_fixed_widths} puts its ink. % \begin{macrocode} local function mark_blank (g, mark, target) local font = font.getfont(g.font) if not (font and font.filename and font.size and (font.index or 0) == 0) then return nil end local f = font_file(font.filename) if not f then return nil end local box = font_box(f, g.char) if not box then return nil end local scale = font.size / f.upem local delta = target - box.advance * scale local blank if mark == "open" then blank = box.xmin * scale + delta else blank = target - box.xmax * scale if mark == "middle" then blank = blank - delta / 2 end end if blank < 0 then blank = 0 elseif blank > target then blank = target end return math.floor(blank + 0.5) end % \end{macrocode} % \end{macro} % \begin{macro}[int]{hang_amount} % How much of a mark's advance stands outside the line: the part that % \keys{PunctBoundWidth} leaves out, the blank the font carries for the mark, % or half of the advance the style gives it when the file has no outline to % read. The advance is the one \cs{apply_fixed_widths} gives it, so a % compressed style hangs less. % \begin{macrocode} local function hang_amount (g, mark, settings) local target = fixed_width(g, settings, style_of(settings)) or g.width local bound = bound_width(settings) if bound then return math.max(0, target - bound) end local blank = mark_blank(g, mark, target) if blank then return blank end return math.floor(0.5 * target + 0.5) end % \end{macrocode} % \end{macro} % \begin{macro}[int]{hang_bounds} % \changes{v0.0.2}{2026/11/15}{The hanging of a mark takes part in line % breaking, so a line that ends at a closing mark or begins with an opening % mark holds the mark the margin allows; the ink of that mark stands outside % the line, and \keys{PunctBoundWidth} decides how much of the advance stays % inside.} % Bracket every break point that a mark ends or starts, so the line measures % the hang and the text inside a line does not move. A break that a preceding % penalty already forbids is left alone, so a fixed width does not carry a rule % that can never be used. % \begin{macrocode} local function hang_bounds (head) local prev = nil local last = nil local cur = head while cur do if cur.id == GLYPH or ((cur.id == HLIST or cur.id == VLIST) and box_visible(cur)) then last = cur elseif is_break_point(cur) and last and cur.id == GLUE and not (prev and prev.id == PENALTY and prev.penalty >= 10000) then local nxt = next_visible(cur.next) local tail, _, tail_glyph = edge_mark(last, true) local lead, _, lead_glyph = nil, nil, nil if nxt then lead, _, lead_glyph = edge_mark(nxt, false) end local w_end = 0 local w_start = 0 if tail_glyph and not disabled(tail_glyph) and (tail == "close" or tail == "middle") and style_of(settings_of(tail_glyph)).hang then w_end = hang_amount(tail_glyph, tail, settings_of(tail_glyph)) end if lead_glyph and not disabled(lead_glyph) and lead == "open" and style_of(settings_of(lead_glyph)).hang then w_start = hang_amount(lead_glyph, "open", settings_of(lead_glyph)) end if w_end > 0 or w_start > 0 then local delta = w_end + w_start cur.width = cur.width + delta if w_end > 0 then local r = new_node("rule") r.width = -w_end r.height = 0 r.depth = 0 inherit_attributes(r, tail_glyph) r.next = cur if prev then prev.next = r else head = r end prev = r end if w_start > 0 then local r = new_node("rule") r.width = -w_start r.height = 0 r.depth = 0 inherit_attributes(r, lead_glyph) r.next = cur.next cur.next = r end end end prev = cur cur = cur.next end return head end % \end{macrocode} % \end{macro} % % \TeX\ can break at the glue that \pkg{LuaCJK} inserts between CJK characters. % At the end of a paragraph the last two such break points get a penalty, % so that at least three CJK characters travel to the last line together % instead of a single one. % % \begin{macro}[int]{apply_check_single} % \changes{v0.0.1}{2026/11/11}{Provide \keys{CheckSingle} and % \keys{WidowPenalty}, which stop a single CJK character from ending up % alone on the last line of a paragraph.} % Insert the widow penalties. Only a paragraph whose last visible node % is a CJK glyph is examined, and only when its settings ask for it. % \begin{macrocode} local function apply_check_single (head) local trail = {} local node = head while node do trail[#trail + 1] = node node = node.next end local last for i = #trail, 1, -1 do local item = trail[i] if item.id == GLYPH then if is_cjk(item.char, settings_of(item)) then last = i end break elseif item.id == HLIST or item.id == VLIST then break end end if not last then return head end local settings = settings_of(trail[last]) if not settings.check_single then return head end local penalty = settings.widow_penalty local inserted = 0 local i = last while i > 1 and inserted < 2 do local j = i - 1 while j >= 1 and (trail[j].id == GLUE or trail[j].id == PENALTY) do j = j - 1 end if j < 1 then break end local glyph = trail[j] if glyph.id ~= GLYPH or not is_cjk(glyph.char, settings_of(glyph)) then break end local target = trail[j + 1] if target and target.id == GLUE then local p = new_node("penalty") inherit_attributes(p, glyph) p.penalty = penalty p.next = target glyph.next = p end inserted = inserted + 1 i = j end return head end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{LuaCJK.process} % Run the passes over a list, in the order in which they depend on each % other: the font first, because the classes depend on it; then the % spacing, the fixed widths, the prohibitions, and the widow penalties. % The hang of a mark is inserted only when the list is a paragraph about to be % broken, because a box packed by \tn{hpack} has no break to hang at. % \begin{macrocode} local function process (head, linebreak) head = rewrite_fonts(head) head = spacing(head) head = apply_fixed_widths(head) if linebreak then head = hang_bounds(head) end head = apply_kinsoku(head) head = apply_check_single(head) return head end M.process = process % \end{macrocode} % \end{macro} % % \begin{macro}[int]{LuaCJK.pre_linebreak} % The line-breaking callback. % \begin{macrocode} function M.pre_linebreak (head, groupcode) return process(head, true) end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{LuaCJK.hpack} % The box-packing callback. % \begin{macrocode} function M.hpack (head, groupcode, size, packtype, direction, attributelist) return process(head) end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{LuaCJK.pre_mlist_to_hlist} % The math callback. % \begin{macrocode} function M.pre_mlist_to_hlist (head, display, penalties) return rewrite_math(head) end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{LuaCJK.hyphenate} % The hyphenation callback. % \TeX\ hyphenates a word while it breaks the lines, after the % pre-linebreaking pass has already built the list, so the discretionaries of % a word appear after the prohibitions have been placed and nothing forbids % them yet. % The callback hyphenates exactly as \TeX\ does and then reads the % discretionaries it has just created, one word at a time. % \begin{macrocode} function M.hyphenate (head, tail) lang.hyphenate(head, tail) local left = nil local cur = head while cur do if cur.id == GLYPH then left = cur elseif cur.id == DISC then forbid_marked_disc(cur, left) end if cur == tail then break end cur = cur.next end end % \end{macrocode} % \end{macro} % % The callbacks are registered in this order: spacing, font correction, % math, and hyphenation, which is the only one that runs inside the line % breaker rather than around it. % \begin{macrocode} luatexbase.add_to_callback("pre_linebreak_filter", M.pre_linebreak, "LuaCJK.spacing") luatexbase.add_to_callback("hpack_filter", M.hpack, "LuaCJK.spacing") luatexbase.add_to_callback("pre_mlist_to_hlist_filter", M.pre_mlist_to_hlist, "LuaCJK.math") luatexbase.add_to_callback("hyphenate", M.hyphenate, "LuaCJK.hyphenate") % % \end{macrocode} % % \subsection{Decorations} % % \pkg{lua-ul} draws each decoration as a pair of nodes: a leaders glue % of width $w$ and a glue of width $-w$ that gives the space back, % so the text does not move. Changing those numbers is enough to inset % the decoration and to leave out the punctuation, which is why \pkg{LuaCJK} % adjusts the box instead of drawing the decoration itself. % \TeX\ numbers the glue subtypes, and 100 is \tn{leaders}, % the one both \pkg{lua-ul} and this package use. % % \begin{variable}[int] % {LuaCJKfntef.attr, LuaCJKfntef.settings} % The attribute that carries the number of a settings entry, and the entry % itself: the inset at both ends and whether punctuation is skipped. % A separate entry per call keeps two decorations in one paragraph apart. % \begin{macrocode} %<*lua-fntef> LuaCJKfntef = LuaCJKfntef or {} local M = LuaCJKfntef local GLYPH = node.id("glyph") local GLUE = node.id("glue") local LEADERS = 100 local KERN = node.id("kern") local HLIST = node.id("hlist") local RULE = node.id("rule") M.attr = M.attr or nil M.settings = M.settings or {} M.next_id = M.next_id or 1 % \end{macrocode} % \end{variable} % % \begin{macro}[int]{LuaCJKfntef.em_descent} % The descent of the em box of the current font, in scaled points. % A CJK em box is one em square, so its descent is both what a highlight has % to add below the baseline to cover a character, and what separates the ink % from the bottom of that square. % % The value comes from \texttt{ascent}, \texttt{descent} and % \texttt{units_per_em} of the font file, cached per file name: opening a CJK % font costs about half a second, and one file serves every size. % \begin{macrocode} local em_metrics = {} local function font_em (filename) local m = em_metrics[filename] if m ~= nil then return m or nil end local ok, t = pcall(fontloader.open, filename) if ok and t and t.units_per_em and t.units_per_em > 0 and t.descent then m = { descent = t.descent, upem = t.units_per_em } fontloader.close(t) else m = false end em_metrics[filename] = m return m or nil end function M.em_descent (id) local f = tonumber(id) f = f and f > 0 and font.getfont(f) or nil local size = f and f.size or tex.sp("1em") local m = f and f.filename and font_em(f.filename) local sp if m then sp = math.floor(m.descent / m.upem * size + 0.5) else sp = math.floor(0.12 * size + 0.5) end return string.format("%d", sp) end % \end{macrocode} % \end{macro} % % A decoration records its settings by selecting an entry: % the number of the entry goes into the attribute on the decorated text, and % attribute zero means that the text carries no decoration of this package. % % \begin{macro}[int]{LuaCJKfntef.new_settings} % Register one entry and select it. % \begin{macrocode} function M.new_settings (subtract, skip) local id = M.next_id M.next_id = id + 1 M.settings[id] = { subtract = subtract, skip = skip } if M.attr then tex.setattribute(M.attr, id) end end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{kind} % The punctuation group of a character, as the core module classifies it. % \begin{macrocode} local function kind (c) local class = LuaCJK.punct_class if class then return class(c) end return nil end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{segments} % The intervals of a span of width \meta{w} that the decoration may cover: % the first and the last \meta{inset} are taken off, and with % \meta{skip-punct} every punctuation mark inside is cut out as well. % A cut is clamped to the span, % so a mark that straddles the end keeps the part that fits. % \begin{macrocode} local function segments (w, start, inset, skip_punct) local cuts = {} if skip_punct then local x = 0 local g = start while g and x < w do local width = g.width or 0 if g.id == GLYPH and kind(g.char) then cuts[#cuts + 1] = { x, math.min(x + width, w) } end x = x + width g = g.next end end local result = {} local low = math.min(inset, w) local high = math.max(w - inset, low) for _, cut in ipairs(cuts) do if cut[1] > low then result[#result + 1] = { low, math.min(cut[1], high) } end low = math.max(low, cut[2]) end if low < high then result[#result + 1] = { low, high } end return result end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{settings_at} % The settings of the decoration whose leaders pair ends just % before \meta{node}: the number is read from that node, % because the pair itself is inserted before the marked text. % \begin{macrocode} local function settings_at (node_) if not node_ or not M.attr then return nil end local id = node.has_attribute(node_, M.attr) if not id or id == 0 then return nil end return M.settings[id] end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{single_rule} % The rule that a decoration box reduces to, and how far the box moves it % down. A box holding nothing but kerns, glues and one rule qualifies. % \tn{leaders} repeats the box, and only whole copies fit, so the last % fraction of the span stays empty: with a one-point object that is a % visible gap at the right end of a highlight. A box that reduces to one % rule is therefore drawn as a single rule of the full width instead. % \begin{macrocode} local function single_rule (list, down) local found local n = list while n do if n.id == RULE then if found then return nil end found = n elseif n.id == HLIST then if found then return nil end local r, d = single_rule(n.head, down + (n.shift or 0)) if not r then return nil end found, down = r, d elseif n.id == KERN then -- horizontal material, no vertical effect elseif n.id == GLUE and not n.leader then -- a plain glue between the pieces else return nil end n = n.next end if found then return found, down end return nil end local function box_rule (box) if not box or box.id ~= HLIST then return nil end local rule, down = single_rule(box.head, box.shift or 0) -- Only a solid block qualifies: the rule must fill the box, or the -- pattern would lose whatever sits beside it. if rule and rule.width == box.width then return rule, down end return nil end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{replace_pair} % Replace the leaders glue of one decoration. % The pieces are kerns and shorter leaders whose total width stays % \meta{w}, so the text does not move; the negative glue that follows % the leaders is kept and becomes the tail of the replacement. % \begin{macrocode} local function replace_pair (head, prev, leaders, settings) local w = leaders.width local neg = leaders.next local subtract = settings and settings.subtract or 0 local skip = settings and settings.skip or false local parts = segments(w, neg.next, subtract, skip) local rule, down = box_rule(leaders.leader) local first, last local cursor = 0 local function append (node_) if last then last.next = node_ else first = node_ end last = node_ end for _, part in ipairs(parts) do if part[1] > cursor then local k = node.new("kern") k.kern = part[1] - cursor append(k) end if rule then local r = node.copy(rule) r.width = part[2] - part[1] r.height = rule.height - down r.depth = rule.depth + down r.next = nil append(r) else local copy = node.copy(leaders) copy.width = part[2] - part[1] copy.stretch = 0 copy.shrink = 0 copy.next = nil append(copy) end cursor = part[2] end if cursor < w then local k = node.new("kern") k.kern = w - cursor append(k) end if first then if prev then prev.next = first else head = first end last.next = neg end return head end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{adjust} % Walk a horizontal list and rewrite every leaders pair whose following node % carries a settings entry. % \begin{macrocode} local function adjust (head) if not M.attr then return head end local prev = nil local n = head while n do local nxt = n.next if n.id == GLUE and n.subtype == LEADERS and n.next and n.next.id == GLUE and n.next.width == -n.width then local neg = n.next local settings = settings_at(neg.next) if settings or box_rule(n.leader) then head = replace_pair(head, prev, n, settings) n = neg nxt = neg.next end end prev = n n = nxt end return head end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{LuaCJKfntef.hpack} % The box-packing callback. The pass also runs before a box is appended % to a vertical list, because a box built earlier is not packed again. % \begin{macrocode} function M.hpack (head) return adjust(head) end luatexbase.add_to_callback("hpack_filter", M.hpack, "LuaCJKfntef.adjust") luatexbase.add_to_callback("pre_append_to_vlist_filter", function (box) if box.id == node.id("hlist") then box.head = adjust(box.head) end return box end, "LuaCJKfntef.adjust") % % \end{macrocode} % \end{macro} % % The \pkg*{LuaCJKfntef} package is a thin layer over \pkg{lua-ul}, % which provides the underlining engine. It is generated from this file % and may be loaded on its own; it loads \pkg{LuaCJK} first. % \begin{macrocode} %<*fntef> \sys_if_engine_luatex:F { \msg_new:nnn { LuaCJKfntef } { engine } { LuaCJKfntef ~ requires ~ LuaTeX. } \msg_critical:nn { LuaCJKfntef } { engine } \tex_endinput:D } \RequirePackage { LuaCJK, lua-ul, luacolor, l3draw } \lua_load_module:n { LuaCJKfntef } \newattribute \g_@@_fntef_settings_attr \int_const:Nn \c_@@_fntef_settings_attr_int { \int_use:N \allocationnumber } \g_@@_fntef_settings_attr = 0 \lua_now:e { LuaCJKfntef.attr = \int_use:N \c_@@_fntef_settings_attr_int } % \end{macrocode} % % \begin{macro}{\CJKunderline} % \changes{v0.0.1}{2026/11/11}{Provide \pkg{LuaCJKfntef}, a \pkg{lua-ul} based % companion package providing \cs{CJKunderline}, \cs{CJKunderdot}, and % \cs{CJKsout}. \cs{CJKunderline} takes \keys{thickness}, \keys{depth}, % \keys{format} for the color of the rule, and any other key of % \pkg{lua-ul}; the geometry defaults to \texttt{depth = 0.2em} and % \texttt{thickness = 0.4pt}, and \keys{top} and \keys{bottom} override the % computed values.} % Underline the argument. The optional argument takes % \keys{thickness, depth, textformat}, and any other key is passed on to % \pkg{lua-ul}, which understands % \texttt{top}, \texttt{bottom}, and \texttt{height}. % \begin{variable}[int] % { % \l_@@_fntef_depth_dim, \l_@@_fntef_thickness_dim, % \l_@@_fntef_textformat_tl, \l_@@_fntef_pass_tl, % \l_@@_fntef_top_dim, \l_@@_fntef_bottom_dim, % \l_@@_fntef_color_key_tl, \l_@@_fntef_silent_bool % } % The requested geometry, the format of the text, % the color that an underline type is cached by, % and the keys that \pkg{lua-ul} receives. % \begin{macrocode} \dim_new:N \l_@@_fntef_depth_dim \dim_new:N \l_@@_fntef_thickness_dim \tl_new:N \l_@@_fntef_textformat_tl \tl_new:N \l_@@_fntef_format_tl \dim_new:N \l_@@_fntef_subtract_dim \tl_new:N \l_@@_fntef_skip_tl \tl_new:N \l_@@_fntef_skip_js_tl \bool_new:N \l_@@_fntef_silent_bool \bool_new:N \l_@@_fntef_hidden_bool \dim_new:N \l_@@_fntef_rule_height_dim \dim_new:N \l_@@_fntef_rule_depth_dim \prop_new:N \g_@@_fntef_format_prop \int_new:N \g_@@_fntef_format_int \tl_new:N \l_@@_fntef_cache_key_tl \tl_new:N \l_@@_fntef_cache_name_tl \tl_new:N \l_@@_fntef_color_key_tl \tl_new:N \l_@@_fntef_pass_tl \dim_new:N \l_@@_fntef_top_dim \dim_new:N \l_@@_fntef_bottom_dim \dim_new:N \l_@@_fntef_gap_dim \dim_new:N \l_@@_fntef_height_dim \tl_new:N \l_@@_fntef_symbol_tl \dim_set:Nn \l_@@_fntef_depth_dim { .2em } \dim_set:Nn \l_@@_fntef_thickness_dim { .4pt } \dim_set:Nn \l_@@_fntef_gap_dim { 1.1pt } \dim_set:Nn \l_@@_fntef_height_dim { .3em } % \end{macrocode} % \end{variable} % \end{macro} % % Every decoration takes the same options, so one key set serves them all: % the keys a command does not use are accepted and ignored, and a bare item % that is not a key is a color name, which is \keys{format}. % % \begin{macro}[int]{\@@_fntef_option_pair:nn, \@@_fntef_option_bare:n, % \@@_fntef_options:n} % Parse the option list of a decoration and reset the defaults first. % A pair goes to the key set as it is; a bare item is a key when it exists % and a color name otherwise, so |[subtract]| keeps its meaning and |[red]| % is |[format = red]|. % \begin{macrocode} \cs_new_protected:Npn \@@_fntef_option_pair:nn #1#2 { \keys_set:nn { LuaCJKfntef / decoration } { #1 = {#2} } } \cs_new_protected:Npn \@@_fntef_option_bare:n #1 { \keys_if_exist:nnTF { LuaCJKfntef / decoration } {#1} { \keys_set:nn { LuaCJKfntef / decoration } {#1} } { \keys_set:nn { LuaCJKfntef / decoration } { format = #1 } } } \cs_new_protected:Npn \@@_fntef_options:n #1 { \@@_fntef_reset_decoration: \tl_if_novalue:nF {#1} { \keyval_parse:NNn \@@_fntef_option_bare:n \@@_fntef_option_pair:nn {#1} } } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_fntef_body:n} % The text of a decoration, with the format of the call applied and hidden % when \keys{hidden} is on. Every command goes through this, so % \keys{textformat} and \keys{hidden} mean the same thing everywhere. % \begin{macrocode} \cs_new_protected:Npn \@@_fntef_body:n #1 { \l_@@_fntef_textformat_tl \@@_fntef_hide:n {#1} } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_fntef_hide:n} % The text of a decoration when \keys{hidden} is on. % \begin{macrocode} \cs_new_protected:Npn \@@_fntef_hide:n #1 { \bool_if:NTF \l_@@_fntef_hidden_bool { \phantom {#1} } { #1 } } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_fntef_push_settings:} % Hand the decoration settings to Lua, or clear the attribute when they are % all at their defaults. % \begin{macrocode} \cs_new_protected:Npn \@@_fntef_push_settings: { \bool_lazy_and:nnTF { \dim_compare_p:nNn { \l_@@_fntef_subtract_dim } = { 0pt } } { \str_if_eq_p:Vn \l_@@_fntef_skip_tl { 0 } } { \lua_now:e { tex.setattribute ( \int_use:N \c_@@_fntef_settings_attr_int, 0 ) } } { \str_if_eq:VnTF \l_@@_fntef_skip_tl { 1 } { \tl_set:Nn \l_@@_fntef_skip_js_tl { true } } { \tl_set:Nn \l_@@_fntef_skip_js_tl { false } } \lua_now:e { LuaCJKfntef.new_settings ( \dim_to_decimal_in_sp:n { \l_@@_fntef_subtract_dim } , \l_@@_fntef_skip_js_tl ) } } } \cs_new_protected:Npn \@@_fntef_prepare_underline:n #1 { \@@_fntef_options:n {#1} \dim_set:Nn \l_@@_fntef_top_dim { -\l_@@_fntef_depth_dim } \dim_set:Nn \l_@@_fntef_bottom_dim { -\l_@@_fntef_depth_dim - \l_@@_fntef_thickness_dim } \tl_put_left:Nx \l_@@_fntef_pass_tl { top = \dim_use:N \l_@@_fntef_top_dim , bottom = \dim_use:N \l_@@_fntef_bottom_dim , } \@@_fntef_push_settings: \dim_set:Nn \l_@@_fntef_rule_height_dim { \l_@@_fntef_top_dim } \dim_set:Nn \l_@@_fntef_rule_depth_dim { -\l_@@_fntef_bottom_dim } } % \end{macrocode} % \begin{macro}[int]{\@@_fntef_formatted_underline:} % A colored rule needs its own underline type: \pkg{luacolor} colors % by attribute, while \tn{color} would emit whatsits in front % of \tn{leaders}. The type is cached by color and geometry, % so a document builds one type per distinct combination. % \begin{macrocode} \cs_new_protected:Npn \@@_fntef_formatted_underline: { \tl_set:Ne \l_@@_fntef_cache_key_tl { \l_@@_fntef_format_tl | \dim_use:N \l_@@_fntef_rule_height_dim | \dim_use:N \l_@@_fntef_rule_depth_dim } \luaul_set_color:n { \l_@@_fntef_format_tl } \prop_get:NVNF \g_@@_fntef_format_prop \l_@@_fntef_cache_key_tl \l_@@_fntef_cache_name_tl { \int_gincr:N \g_@@_fntef_format_int \tl_set:Ne \l_@@_fntef_cache_name_tl { LuaCJKfntef / ul / \int_use:N \g_@@_fntef_format_int } \exp_args:Nc \newunderlinetype { \l_@@_fntef_cache_name_tl } [ \dim_to_decimal_in_sp:n { 1ex } @ \luaul_context_color: ] { \luaul_apply_color: \tex_leaders:D \tex_vrule:D height \l_@@_fntef_rule_height_dim depth \l_@@_fntef_rule_depth_dim } \prop_gput:NVV \g_@@_fntef_format_prop \l_@@_fntef_cache_key_tl \l_@@_fntef_cache_name_tl } \use:c { \l_@@_fntef_cache_name_tl } } % \end{macrocode} % \end{macro} % \begin{macrocode} \cs_new_protected:Npn \@@_fntef_underLine:nn #1#2 { \underLine [ #1 ] { #2 } } \NewDocumentCommand \CJKunderline { s t- o +m } { \group_begin: \@@_fntef_prepare_underline:n {#3} \tl_if_empty:NTF \l_@@_fntef_format_tl { \exp_args:NV \@@_fntef_underLine:nn \l_@@_fntef_pass_tl { \@@_fntef_body:n {#4} } } { \@@_fntef_formatted_underline: { \@@_fntef_body:n {#4} } } \group_end: } % \end{macrocode} % \end{macro} % % \begin{macro}{\CJKsout} % Strike through the argument. % \begin{macrocode} \cs_new_protected:Npn \@@_fntef_use_sout: { \tl_set:Ne \l_@@_fntef_cache_key_tl { sout | \dim_use:N \l_@@_fntef_height_dim | \dim_use:N \l_@@_fntef_thickness_dim | \l_@@_fntef_format_tl } \tl_if_empty:NF \l_@@_fntef_format_tl { \luaul_set_color:n { \l_@@_fntef_format_tl } } \prop_get:NVNF \g_@@_fntef_format_prop \l_@@_fntef_cache_key_tl \l_@@_fntef_cache_name_tl { \int_gincr:N \g_@@_fntef_format_int \tl_set:Ne \l_@@_fntef_cache_name_tl { LuaCJKfntef / st / \int_use:N \g_@@_fntef_format_int } \tl_if_empty:NTF \l_@@_fntef_format_tl { \exp_args:Nc \newunderlinetype { \l_@@_fntef_cache_name_tl } { \tex_leaders:D \tex_vrule:D height \dim_eval:n { \l_@@_fntef_height_dim + \l_@@_fntef_thickness_dim / 2 } depth \dim_eval:n { -\l_@@_fntef_height_dim + \l_@@_fntef_thickness_dim / 2 } } } { \exp_args:Nc \newunderlinetype { \l_@@_fntef_cache_name_tl } [ \dim_to_decimal_in_sp:n { 1ex } @ \luaul_context_color: ] { \luaul_apply_color: \tex_leaders:D \tex_vrule:D height \dim_eval:n { \l_@@_fntef_height_dim + \l_@@_fntef_thickness_dim / 2 } depth \dim_eval:n { -\l_@@_fntef_height_dim + \l_@@_fntef_thickness_dim / 2 } } } \prop_gput:NVV \g_@@_fntef_format_prop \l_@@_fntef_cache_key_tl \l_@@_fntef_cache_name_tl } \use:c { \l_@@_fntef_cache_name_tl } } \NewDocumentCommand \CJKsout { s t- o +m } { \group_begin: \@@_fntef_decoration_setup:n {#3} \@@_fntef_use_sout: \@@_fntef_body:n {#4} \group_end: } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_fntef_use_double:} % Build the two underline types of the double rule, one for each line, % keyed by the geometry so that repeated calls reuse them. % \begin{macrocode} \cs_new_protected:Npn \@@_fntef_use_double: { \tl_set:Ne \l_@@_fntef_cache_key_tl { double-top | \dim_use:N \l_@@_fntef_depth_dim | \dim_use:N \l_@@_fntef_thickness_dim } \tl_set:Ne \l_@@_fntef_any_code_tl { \exp_not:N \tex_leaders:D \exp_not:N \tex_vrule:D height \dim_eval:n { -\l_@@_fntef_depth_dim } depth \dim_eval:n { \l_@@_fntef_depth_dim + \l_@@_fntef_thickness_dim } } \exp_args:NV \@@_fntef_use_leader_type:nn \l_@@_fntef_cache_key_tl \l_@@_fntef_any_code_tl \tl_set:Ne \l_@@_fntef_cache_key_tl { double-bottom | \dim_use:N \l_@@_fntef_depth_dim | \dim_use:N \l_@@_fntef_thickness_dim | \dim_use:N \l_@@_fntef_gap_dim } \tl_set:Ne \l_@@_fntef_any_code_tl { \exp_not:N \tex_leaders:D \exp_not:N \tex_vrule:D height \dim_eval:n { -\l_@@_fntef_depth_dim - \l_@@_fntef_thickness_dim - \l_@@_fntef_gap_dim } depth \dim_eval:n { \l_@@_fntef_depth_dim + 2 \l_@@_fntef_thickness_dim + \l_@@_fntef_gap_dim } } \exp_args:NV \@@_fntef_use_leader_type:nn \l_@@_fntef_cache_key_tl \l_@@_fntef_any_code_tl } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_fntef_decoration_setup:n} % \changes{v0.0.1}{2026/11/11}{Every decoration takes the same key set: % \keys{color}, \keys{gap}, \keys{height}, \keys{symbol}, \keys{sep}, % \keys{textformat}, and \keys{hidden}, which keeps the dimensions of the % text and draws no glyph. A bare color name is accepted, so |[red]| and % |[subtract]| both work, and a key that a command does not use is ignored. % Every decoration accepts \keys{selectable}, \texttt{false} by default: a % decoration symbol is then wrapped in an \texttt{ActualText} sequence that % readers drop from the text they copy or select. Every decoration also % accepts \keys{subtract} (inset at both ends) and \keys{skip} (leave out % the punctuation). \cs{CJKsout} strikes at \texttt{height = 0.3em}, through % the middle of a CJK glyph; \texttt{0.5ex} gives the Latin position.} % The settings that the decoration pass reads. % They are pushed on every call, so a command without options starts % from the defaults instead of inheriting the previous call. % \begin{macrocode} \keys_define:nn { LuaCJKfntef / decoration } { subtract .code:n = { \str_if_eq:nnTF {#1} { true } { \dim_set:Nn \l_@@_fntef_subtract_dim { .5em } } { \str_if_eq:nnTF {#1} { false } { \dim_zero:N \l_@@_fntef_subtract_dim } { \dim_set:Nn \l_@@_fntef_subtract_dim {#1} } } }, subtract .default:n = { true }, skip .code:n = { \str_if_eq:nnTF {#1} { true } { \tl_set:Nn \l_@@_fntef_skip_tl { 1 } } { \tl_set:Nn \l_@@_fntef_skip_tl { 0 } } }, skip .default:n = { true }, hidden .bool_set:N = \l_@@_fntef_hidden_bool, hidden .default:n = { true }, selectable.bool_set_inverse:N = \l_@@_fntef_silent_bool, selectable.default:n = { true }, depth .dim_set:N = \l_@@_fntef_depth_dim, depth .value_required:n = true, thickness .dim_set:N = \l_@@_fntef_thickness_dim, thickness .value_required:n = true, gap .dim_set:N = \l_@@_fntef_gap_dim, gap .value_required:n = true, height .dim_set:N = \l_@@_fntef_height_dim, height .value_required:n = true, format .tl_set:N = \l_@@_fntef_format_tl, color .tl_set:N = \l_@@_fntef_format_tl, color .value_required:n = true, symbol .tl_set:N = \l_@@_fntef_symbol_tl, sep .dim_set:N = \l_@@_fntef_symbol_sep_dim, sep .value_required:n = true, textformat.tl_set:N = \l_@@_fntef_textformat_tl, unknown .code:n = { \tl_put_right:Nx \l_@@_fntef_pass_tl { \l_keys_key_str \tl_if_empty:NF \l_keys_value_tl { = \exp_not:V \l_keys_value_tl } , } }, } \cs_new_protected:Npn \@@_fntef_reset_decoration: { \dim_zero:N \l_@@_fntef_subtract_dim \tl_set:Nn \l_@@_fntef_skip_tl { 0 } \bool_set_true:N \l_@@_fntef_silent_bool \bool_set_false:N \l_@@_fntef_hidden_bool \dim_set:Nn \l_@@_fntef_depth_dim { .2em } \dim_set:Nn \l_@@_fntef_thickness_dim { .4pt } \dim_set:Nn \l_@@_fntef_gap_dim { 1.1pt } \dim_set:Nn \l_@@_fntef_height_dim { .3em } \clist_map_inline:nn { format, symbol, textformat, pass } { \tl_clear:c { l_@@_fntef_##1_tl } } \dim_set:Nn \l_@@_fntef_symbol_sep_dim { .1em } } \cs_new_protected:Npn \@@_fntef_decoration_setup:n #1 { \@@_fntef_options:n {#1} \@@_fntef_push_settings: } % \end{macrocode} % \end{macro} % % \begin{macro}{\CJKunderdblline} % \changes{v0.0.1}{2026/11/11}{Provide \cs{CJKunderdblline}, % \cs{CJKunderwave}, and \cs{CJKxout}; the wave and the slash stay in phase % along a line, their patterns scale with the font size, a decoration does % not open up its line, and it fills its span exactly.} % \changes{v0.0.2}{2026/11/15}{The double rule takes \keys{format} like the % other decorations, and a rule or a symbol that carries no \keys{format} % follows the color of the surrounding text.} % Two parallel rules. The optional argument takes the whole key set of the % other decorations; \texttt{*} and \texttt{-} are accepted for compatibility. % \begin{macrocode} \NewDocumentCommand \CJKunderdblline { s t- o +m } { \group_begin: \@@_fntef_decoration_setup:n {#3} \@@_fntef_use_double: \@@_fntef_body:n {#4} \group_end: } % \end{macrocode} % \end{macro} % % \begin{macro}{\CJKunderwave} % A wave drawn with \pkg{l3draw}, one period per half em, repeated % by \tn{leaders}. The drawing is kept as code rather than as a box, % so the |em| units are resolved when the decoration is used % and the pattern follows the current font size. % % The period is forced to exactly half an em by \tn{hbox}| to .5em|. % \pkg{l3draw} sizes its box by the bounding box of the stroked path, % which is a full stroke width wider than the path; repeating that box would % leave a gap of one stroke width between two periods. % The two \tn{hss} absorb the excess symmetrically, so the ink fills % the period exactly and no overfull box is reported. The stroke % is set to |.04em|, which is \texttt{0.4pt} at 10pt, so the pattern keeps % its look from the previous release while it scales with the font size. % \begin{variable}[int]{\l_@@_fntef_pattern_box} % A scratch box for the drawn patterns. % Each pattern is built here and then has its height and depth set to zero: % the ink is a PDF literal, so only the reported dimensions change, % and a decoration never opens up the line it sits on. % \begin{macrocode} \box_new:N \l_@@_fntef_pattern_box \dim_new:N \l_@@_fntef_symbol_kern_dim \dim_new:N \l_@@_fntef_symbol_raise_dim \dim_new:N \l_@@_fntef_em_descent_dim % \end{macrocode} % \end{variable} % \begin{macrocode} \newunderlinetype \@@_fntef_wave { \tex_leaders:D \hbox_to_wd:nn { .5em } { \tex_hss:D \hbox_set:Nn \l_@@_fntef_pattern_box { \box_move_up:nn { -.42em } { \hbox:n { \draw_begin: \draw_set_linewidth:n { .04em } \draw_path_moveto:n { 0em , 0em } \draw_path_curveto:nnn { .0625em , .16em } { .1875em , .16em } { .25em , 0em } \draw_path_curveto:nnn { .3125em , -.16em } { .4375em , -.16em } { .5em , 0em } \draw_path_use_clear:n { stroke } \draw_end: } } } \box_set_ht:Nn \l_@@_fntef_pattern_box { \c_zero_dim } \box_set_dp:Nn \l_@@_fntef_pattern_box { \c_zero_dim } \box_use_drop:N \l_@@_fntef_pattern_box \tex_hss:D } } \cs_new_protected:Npn \@@_fntef_silent_begin: { \bool_if:NT \l_@@_fntef_silent_bool { \tex_pdfextension:D literal direct { /Span <>> BDC } } } \cs_new_protected:Npn \@@_fntef_silent_end: { \bool_if:NT \l_@@_fntef_silent_bool { \tex_pdfextension:D literal direct { EMC } } } \cs_new_protected:Npn \@@_fntef_use_symbol:n #1 { \dim_set:Nn \l_@@_fntef_any_depth_dim {#1} \dim_set:Nn \l_@@_fntef_em_descent_dim { \lua_now:e { tex.print(LuaCJKfntef.em_descent ( \int_use:N \tex_attribute:D \c_@@_font_attr_int )) } sp } \tl_set:Ne \l_@@_fntef_any_code_tl { \exp_not:N \tex_leaders:D \exp_not:N \hbox:n { \exp_not:N \hbox_set:Nn \exp_not:N \l_@@_fntef_pattern_box { \exp_not:V \l_@@_fntef_symbol_tl } \exp_not:N \dim_set:Nn \exp_not:N \l_@@_fntef_symbol_kern_dim { \exp_not:N \dim_eval:n { ( 1em - \exp_not:N \box_wd:N \l_@@_fntef_pattern_box ) / 2 } } \exp_not:N \dim_set:Nn \exp_not:N \l_@@_fntef_symbol_raise_dim { \exp_not:N \dim_eval:n { - \dim_use:N \l_@@_fntef_em_descent_dim - \exp_not:N \l_@@_fntef_any_depth_dim - \exp_not:N \box_ht:N \l_@@_fntef_pattern_box } } \exp_not:N \box_set_ht:Nn \exp_not:N \l_@@_fntef_pattern_box { \exp_not:N \c_zero_dim } \exp_not:N \box_set_dp:Nn \exp_not:N \l_@@_fntef_pattern_box { \exp_not:N \c_zero_dim } \exp_not:N \skip_horizontal:n { \exp_not:N \l_@@_fntef_symbol_kern_dim } \exp_not:N \@@_fntef_silent_begin: \exp_not:N \box_move_up:nn { \exp_not:N \l_@@_fntef_symbol_raise_dim } { \exp_not:N \box_use_drop:N \l_@@_fntef_pattern_box } \exp_not:N \@@_fntef_silent_end: \exp_not:N \skip_horizontal:n { \exp_not:N \l_@@_fntef_symbol_kern_dim } } } \tl_set:Ne \l_@@_fntef_cache_key_tl { symbol | \dim_use:N \l_@@_fntef_em_descent_dim | \dim_use:N \l_@@_fntef_any_depth_dim | \bool_if:NTF \l_@@_fntef_silent_bool { silent } { text } | \tl_to_str:V \l_@@_fntef_symbol_tl } \exp_args:NV \@@_fntef_use_leader_type:nn \l_@@_fntef_cache_key_tl \l_@@_fntef_any_code_tl } \NewDocumentCommand \CJKunderwave { s t- o +m } { \group_begin: \@@_fntef_decoration_setup:n {#3} \tl_if_empty:NTF \l_@@_fntef_symbol_tl { \@@_fntef_wave } { \@@_fntef_use_symbol:n { .42em } } \@@_fntef_body:n {#4} \group_end: } % \end{macrocode} % \end{macro} % % \begin{macro}{\CJKxout} % A repeated slash, as in \pkg{ulem}'s \tn{xout}. % The drawing is kept as code so that it scales with the font size, % and the stroke width is |.04em| like the wave. % The slash runs from |-.15em| to |.8em|, so it crosses the whole body % of a CJK glyph instead of stopping in its upper half. % % \pkg{l3draw} moves a drawing so that the bottom of its bounding box sits % on the baseline of the box, which shifts a path that goes below the % baseline upwards. The \tn{raise} of |-.17em| takes the slash back down % by the part that lies below the baseline (|.15em|) plus half the stroke, % so the ink lands where the path says. % \begin{macrocode} \newunderlinetype \@@_fntef_slash { \tex_leaders:D \hbox:n { \hbox_set:Nn \l_@@_fntef_pattern_box { \box_move_up:nn { -.17em } { \hbox:n { \draw_begin: \draw_set_linewidth:n { .04em } \draw_path_moveto:n { 0em , -.15em } \draw_path_lineto:n { .42em , .8em } \draw_path_use_clear:n { stroke } \draw_end: } } } \box_set_ht:Nn \l_@@_fntef_pattern_box { \c_zero_dim } \box_set_dp:Nn \l_@@_fntef_pattern_box { \c_zero_dim } \box_use_drop:N \l_@@_fntef_pattern_box } } \NewDocumentCommand \CJKxout { s t- o +m } { \group_begin: \@@_fntef_decoration_setup:n {#3} \@@_fntef_slash \@@_fntef_body:n {#4} \group_end: } % \end{macrocode} % \end{macro} % % \begin{macro}{\LuaCJKemdescent} % \changes{v0.0.1}{2026/11/11}{Provide \cs{LuaCJKemdescent}, the depth of the % em box of the current font, so that an object of height \texttt{1em} % placed at that depth covers the one-em square of a CJK character in any % font.} % The depth of the em box of the current font, as a dimension. A CJK em box % is one em square, so an object of height \texttt{1em} placed at this depth % covers a character the way a PDF reader selects it. % \begin{macrocode} \cs_new:Npn \LuaCJKemdescent { \lua_now:e { tex.print(LuaCJKfntef.em_descent ( \int_use:N \tex_attribute:D \c_@@_font_attr_int )) } sp } % \end{macrocode} % \end{macro} % % \begin{macro}{\CJKunderanyline} % \changes{v0.0.1}{2026/11/11}{Provide \cs{CJKunderanyline} and % \cs{CJKunderanysymbol} for arbitrary rules and repeated symbols; % \cs{CJKunderanysymbol} puts one symbol per em, centered in the cell, its % depth measured from the baseline to the top of the symbol.} % Underline with an arbitrary object: the object is put in a box, % lowered by \meta{depth}, and repeated by \tn{leaders}. % The underline type is cached by the object, the depth, and the color, % so repeating a command with the same arguments costs nothing. % \begin{variable}[int]{\l_@@_fntef_any_code_tl, \l_@@_fntef_any_depth_dim} % The leader code being built and its depth. % \begin{macrocode} \tl_new:N \l_@@_fntef_any_code_tl \dim_new:N \l_@@_fntef_any_depth_dim % \end{macrocode} % \end{variable} % \begin{macrocode} \NewDocumentCommand \CJKunderanyline { m m +m } { \group_begin: \@@_fntef_decoration_setup:n { } \dim_set:Nn \l_@@_fntef_any_depth_dim { -#1 } \tl_set:Ne \l_@@_fntef_any_code_tl { \exp_not:N \tex_leaders:D \exp_not:N \hbox:n { \exp_not:N \box_move_up:nn { \dim_use:N \l_@@_fntef_any_depth_dim } { \exp_not:N \hbox:n { \exp_not:n {#2} } } } } \tl_set:Ne \l_@@_fntef_cache_key_tl { anyline | #1 | \tl_to_str:n {#2} } \exp_args:NV \@@_fntef_use_leader_type:nn \l_@@_fntef_cache_key_tl \l_@@_fntef_any_code_tl \@@_fntef_body:n {#3} \group_end: } % \end{macrocode} % \end{macro} % % \begin{macro}[int]{\@@_fntef_use_leader_type:nn} % Build the underline type of a decoration that is made of \tn{leaders}, % which repeats one rule or one symbol along the decorated text, and use it. % A type is built once per leader code and color and then reused, so a % document pays for each combination of geometry and color only once. % % The type keeps the color that was in force when it was built: % \pkg{luacolor} colors a rule by attribute, and \pkg{lua-ul} puts the % attributes of the built box on every leader it inserts. % A type cached by the geometry alone would therefore keep the color of the % first call, so the color is part of the key: \keys{format} when it is % given, and otherwise the value of \pkg{luacolor}'s attribute, which is the % color of the surrounding text. A requested color is also applied inside % the box, so that it holds wherever the leader is placed. % % The name of the type already carries a counter, so the context that % \pkg{lua-ul} puts in the name of the type it defines is a constant: one % that expands again when the type is used would rebuild the type with % whatever color happened to be current then. % \begin{macrocode} \cs_new_protected:Npn \@@_fntef_use_leader_type:nn #1#2 { \tl_if_empty:NTF \l_@@_fntef_format_tl { \tl_set:Ne \l_@@_fntef_color_key_tl { ambient | \tex_the:D \use:c { LuaCol@Attribute } } } { \luaul_set_color:n { \l_@@_fntef_format_tl } \tl_set:Nn \l_@@_fntef_color_key_tl { format | \l_@@_fntef_format_tl } } \tl_set:Ne \l_@@_fntef_cache_key_tl { \exp_not:n {#1} | \l_@@_fntef_color_key_tl } \prop_get:NVNF \g_@@_fntef_format_prop \l_@@_fntef_cache_key_tl \l_@@_fntef_cache_name_tl { \int_gincr:N \g_@@_fntef_format_int \tl_set:Ne \l_@@_fntef_cache_name_tl { LuaCJKfntef / ul / \int_use:N \g_@@_fntef_format_int } \tl_if_empty:NTF \l_@@_fntef_format_tl { \exp_args:Nc \newunderlinetype { \l_@@_fntef_cache_name_tl } [ LuaCJKfntef ] {#2} } { \exp_args:Nc \newunderlinetype { \l_@@_fntef_cache_name_tl } [ LuaCJKfntef ] { \luaul_apply_color: #2 } } \prop_gput:NVV \g_@@_fntef_format_prop \l_@@_fntef_cache_key_tl \l_@@_fntef_cache_name_tl } \use:c { \l_@@_fntef_cache_name_tl } } % \end{macrocode} % \end{macro} % % \begin{macro}{\CJKunderanysymbol} % One symbol per em, which is what one wants for a repeated mark: % the cell is as wide as a CJK character, so a row of symbols lines up with % the characters above, and the symbol is centered in its cell. % The depth is handed to \cs{LuaCJKfntef_use_symbol:n}, % which puts the top of the symbol that far below the baseline. % \begin{macrocode} \NewDocumentCommand \CJKunderanysymbol { O{} m m +m } { \group_begin: \@@_fntef_decoration_setup:n {#1} \tl_set:Nn \l_@@_fntef_symbol_tl {#3} \@@_fntef_use_symbol:n {#2} \@@_fntef_body:n {#4} \group_end: } % \end{macrocode} % \end{macro} % % \begin{macro}{\CJKunderdot} % Emphasise with dots below the glyphs. The optional argument takes the key % set of \cs{CJKunderline}, like the other decorations. % \begin{macrocode} \NewDocumentCommand \CJKunderdot { s t- o +m } { \group_begin: \@@_fntef_decoration_setup:n {#3} \tl_if_empty:NTF \l_@@_fntef_symbol_tl { \@@_fntef_underdot:n { \@@_fntef_body:n {#4} } } { \@@_fntef_use_symbol:n { .3em } \@@_fntef_body:n {#4} } \group_end: } % \end{macrocode} % \begin{macro}[int]{\@@_fntef_underdot:n} % Apply the dot pattern inside a group. One repeating unit is a dot of % \texttt{.06em} with \texttt{.12em} on each side, lowered by \texttt{.4em}. % The unit is written with |em| units so that it scales with the font size. % \begin{macro}[int]{\@@_fntef_dots} % The underline type that repeats one dot of the pattern. % \begin{macrocode} \newunderlinetype \@@_fntef_dots { \tex_leaders:D \hbox:n { \hbox_set:Nn \l_@@_fntef_pattern_box { \skip_horizontal:n { .12em } \box_move_up:nn { -.4em } { \hbox:n { \tex_vrule:D width .06em height .06em depth \c_zero_dim } } \skip_horizontal:n { .12em } } \box_set_ht:Nn \l_@@_fntef_pattern_box { \c_zero_dim } \box_set_dp:Nn \l_@@_fntef_pattern_box { \c_zero_dim } \box_use_drop:N \l_@@_fntef_pattern_box } } % \end{macrocode} % \end{macro} % \begin{macrocode} \cs_new_protected:Npn \@@_fntef_underdot:n #1 { \group_begin: \@@_fntef_dots #1 \group_end: } % \end{macrocode} % \end{macro} % \end{macro} % % \begin{environment}{CJKfilltwosides} % \changes{v0.0.1}{2026/11/11}{Provide the \env{CJKfilltwosides} environment, % which justifies every line to a given width and places the box on the % baseline as requested. It is tagged: a text block in vertical mode, part % of the surrounding paragraph in horizontal mode.} % Set the body in a vertical box whose lines are justified to the given % width, with the lines separated by |\\|. % The inter-character glue is given an infinite stretch inside the box, % so a line of CJK characters reaches the width even when there is no % Latin word space to stretch. % The optional argument places the box on the baseline: \texttt{t} for % top, \texttt{c} for centered (the default), and \texttt{b} for bottom. % A width of zero or less keeps the surrounding line width. % \begin{variable}[int] % { % \l_@@_fntef_fill_box, \l_@@_fntef_fill_width_dim, % \l_@@_fntef_fill_half_dim, \l_@@_fntef_fill_position_tl, % \l_@@_fntef_fill_tag_tl, \l_@@_fntef_fill_struct_bool % } % The box being filled, its requested width, the half height used % for centring, and the requested baseline position. % \begin{macrocode} \box_new:N \l_@@_fntef_fill_box \dim_new:N \l_@@_fntef_fill_width_dim \dim_new:N \l_@@_fntef_fill_half_dim \tl_new:N \l_@@_fntef_fill_position_tl \tl_new:N \l_@@_fntef_fill_tag_tl \bool_new:N \l_@@_fntef_fill_struct_bool % \end{macrocode} % \end{variable} % \begin{macrocode} \cs_new_protected:Npn \@@_fntef_fill_begin:nn #1#2 { \group_begin: \bool_set_false:N \l_@@_fntef_fill_struct_bool \cs_if_exist:NT \tag_struct_begin:n { \mode_if_vertical:TF { \tag_struct_begin:n { tag = text-block } \tl_set:Nn \l_@@_fntef_fill_tag_tl { text-block } \bool_set_true:N \l_@@_fntef_fill_struct_bool } { \tl_clear:N \l_@@_fntef_fill_tag_tl } \cs_if_exist:NT \tagpdfparaOff { \tagpdfparaOff } } \tl_set:Nn \l_@@_fntef_fill_position_tl {#1} \dim_set:Nn \l_@@_fntef_fill_width_dim {#2} \LuaCJKsetup { CJKglue = 0pt plus 1filll } \vbox_set:Nw \l_@@_fntef_fill_box \dim_compare:nNnT { \l_@@_fntef_fill_width_dim } > { 0pt } { \dim_set:Nn \tex_hsize:D { \l_@@_fntef_fill_width_dim } } \cs_if_exist:NT \tag_mc_begin:n { \tl_if_empty:NTF \l_@@_fntef_fill_tag_tl { \tag_mc_begin:n { } } { \tag_mc_begin:n { tag = \l_@@_fntef_fill_tag_tl } } } \tex_noindent:D \tex_ignorespaces:D } \cs_new_protected:Npn \@@_fntef_fill_end: { \tex_par:D \cs_if_exist:NT \tag_mc_end: { \tag_mc_end: } \vbox_set_end: \str_case:Vn \l_@@_fntef_fill_position_tl { { t } { \dim_set:Nn \l_@@_fntef_fill_half_dim { \box_ht_plus_dp:N \l_@@_fntef_fill_box } \box_set_ht:Nn \l_@@_fntef_fill_box { 0pt } \box_set_dp:Nn \l_@@_fntef_fill_box { \l_@@_fntef_fill_half_dim } } { c } { \dim_set:Nn \l_@@_fntef_fill_half_dim { \box_ht_plus_dp:N \l_@@_fntef_fill_box / 2 } \box_set_ht:Nn \l_@@_fntef_fill_box { \l_@@_fntef_fill_half_dim } \box_set_dp:Nn \l_@@_fntef_fill_box { \l_@@_fntef_fill_half_dim } } } \box_use_drop:N \l_@@_fntef_fill_box \cs_if_exist:NT \tag_struct_end: { \cs_if_exist:NT \tagpdfparaOn { \tagpdfparaOn } \bool_if:NT \l_@@_fntef_fill_struct_bool { \tag_struct_end: } } \group_end: } \NewDocumentEnvironment { CJKfilltwosides } { O{c} m } { \@@_fntef_fill_begin:nn {#1} {#2} } { \@@_fntef_fill_end: } % \end{macrocode} % \end{environment} % % \begin{environment}{CJKfilltwosides*} % The starred form behaves like the plain one; % a width of zero or less keeps the surrounding line width in both. % \begin{macrocode} \NewDocumentEnvironment { CJKfilltwosides* } { O{c} m } { \@@_fntef_fill_begin:nn {#1} {#2} } { \@@_fntef_fill_end: } % % \end{macrocode} % \end{environment} % % \subsection{Listings and ctex} % % The block listing environments are patched by making the CJK characters % active while the character table of a listing is built. The active % character is defined through the \tn{lccode} of two helper characters: the % token in the body keeps category code 12, so it can be stored in the token % list of \pkg{listings} and typeset later. The definitions are made once; % each listing only sets the category codes again in its own group. % \begin{macrocode} %<*listings> \RequirePackage { LuaCJK, listings } \makeatletter \newif\ifLuaCJK@listings@cjkrun \newif\ifLuaCJK@listings@defined \lst@AddToHook{InitVars}{\LuaCJK@listings@cjkrunfalse} \def\LuaCJK@listings@process#1{% \lst@whitespacefalse \LuaCJK@listings@setcjkflag \advance\lst@length\@ne \lst@Append#1} \def\LuaCJK@listings@setcjkflag{% \lst@ifletter \lst@Output \else \ifLuaCJK@listings@cjkrun\else\lst@OutputOther\fi \lst@lettertrue \fi \LuaCJK@listings@cjkruntrue} \def\LuaCJK@listings@define#1#2{% \@tempcnta=#1\relax \loop\ifnum\@tempcnta<#2\relax \advance\@tempcnta\@ne \lccode`\~=\@tempcnta \lccode`\/=\@tempcnta \lowercase{\gdef~{\LuaCJK@listings@process/}}% \repeat} \def\LuaCJK@listings@catcodes#1#2{% \@tempcnta=#1\relax \loop\ifnum\@tempcnta<#2\relax \advance\@tempcnta\@ne \catcode\@tempcnta=13\relax \repeat} \def\LuaCJK@listings@ranges#1{% #1{"2E80}{"9FFF}% #1{"F900}{"FAFF}% #1{"FF00}{"FFEF}% #1{"AC00}{"D7AF}} \def\LuaCJK@listings@activate{% \ifLuaCJK@listings@defined\else \LuaCJK@listings@ranges\LuaCJK@listings@define \global\LuaCJK@listings@definedtrue \fi \LuaCJK@listings@ranges\LuaCJK@listings@catcodes} % \end{macrocode} % % \tn{lstinline} reads its argument with changed category codes, % so the patch is limited to the block listing environments. % \begin{macrocode} \lst@AddToHook{SelectCharTable}{% \ifx\lst@inlinechars\@empty \LuaCJK@listings@activate \fi} \lst@AddToHook{OutputBox}{% \ifx\lst@inlinechars\@empty \LuaCJKdisable \fi} \makeatother % % \end{macrocode} % % \pkg{ctex} inputs an engine file named |ctex-engine-|\meta{name}|.def|, % where \meta{name} is the detected engine unless the |engine| option names % another. \pkg{LuaCJK}'s file is therefore called |ctex-engine-luacjk.def| and % is selected with |engine = luacjk|. The middle part must differ from % |luatex|: % \pkg{ctex} installs |ctex-engine-luatex.def|, a second file of that name % would collide on CTAN, and the two files would describe different backends % under one name. % % The file defines the handful of \cs{ctex_}\meta{name} commands that the % \pkg{ctex} kernel calls, and maps the parts of the \pkg{luatexja} % interface that \pkg{ctex} and some documents still use. % \begin{macrocode} %<*ctex> \typeout{LuaCJK~ctex~engine~loaded} \sys_if_engine_luatex:F { \msg_new:nnn { LuaCJK } { ctex-engine } { The ~ ctex ~ engine ~ file ~ of ~ LuaCJK ~ requires ~ LuaTeX. } \msg_critical:nn { LuaCJK } { ctex-engine } \tex_endinput:D } % \end{macrocode} % % \pkg{CJKfntef} loads the legacy \pkg{CJK} package, which redefines the font % family interface \pkg{LuaCJK} owns, so this file refuses it in both orders: % the run stops when \pkg{CJKfntef} is already loaded, and the % \texttt{package/CJKfntef/before} hook stops it when it is requested later. % The message points at \pkg{LuaCJKfntef}, which does the same work through % \pkg{lua-ul}. \pkg{ctex}'s own \LuaTeX\ backend refuses the package for the % same reason, and that code does not run when the engine file is ours. % \begin{macrocode} \msg_new:nnnn { LuaCJK } { cjkfntef } { Package ~ `CJKfntef' ~ is ~ not ~ compatible ~ with ~ the ~ LuaCJK ~ backend ~ of ~ ctex. } { Use ~ the ~ `LuaCJKfntef' ~ package ~ instead. } \cs_if_exist:cT { ver@CJKfntef.sty } { \msg_critical:nn { LuaCJK } { cjkfntef } } \hook_gput_code:nnn { package / CJKfntef / before } { LuaCJK } { \msg_critical:nn { LuaCJK } { cjkfntef } } % \end{macrocode} % % The file loads the package it serves. % \begin{macrocode} \RequirePackage { LuaCJK } % \end{macrocode} % % The fontset of \pkg{ctex} defines these shortcuts itself and checks % for an existing definition, so the ones of \pkg{LuaCJK} have to go first. % \begin{macrocode} \clist_map_inline:nn { \songti, \heiti, \fangsong, \kaishu, \lishu, \youyuan, \yahei, \pingfang } { \cs_undefine:N #1 } % % \end{macrocode} % % The interface that the \pkg{ctex} kernel and its classes rely on. % \begin{macrocode} %<*ctex> \cs_new_protected:Npn \ctex_add_to_selectfont:n #1 { \hook_gput_code:nnn { selectfont } { LuaCJK } {#1} } \cs_new_eq:NN \ctex_ltj_zero_globaldefs: \prg_do_nothing: \cs_new_protected:Npn \CTEX@alchar #1 {#1} \cs_new_eq:NN \CTEX@beginallalchar \prg_do_nothing: \cs_new_eq:NN \CTEX@endallalchar \prg_do_nothing: \clist_map_inline:nn { \ccwd, \cht, \cdp, \cwd } { \dim_new:N #1 } \cs_new_protected:Npn \ctex_update_ccwd: { \dim_set:Nn \ccwd { \f@size \p@ } } \cs_new_protected:Npn \ctex_update_em_unit: { \dim_set:Nn \ccwd { \f@size \p@ } } \cs_new_protected:Npn \ctex_update_kanjisize: { \dim_set:Nn \cwd { \ccwd } \dim_set:Nn \cht { \f@size \p@ } \dim_set:Nn \cdp { \c_zero_dim } } % % \end{macrocode} % % \pkg{ctex} keeps its own copy of the CJK glue so that it can tell whether % the document changed it. The two copies are kept in step here. % \begin{macrocode} %<*ctex> \skip_new:N \l__ctex_ccglue_skip \skip_set_eq:NN \l__ctex_ccglue_skip \l_@@_ccglue_skip \cs_new_protected:Npn \ctex_ltj_set_kanjiskip:N #1 { \skip_set_eq:NN \l_@@_ccglue_skip #1 \bool_set_true:N \l_@@_ccglue_touched_bool \@@_settings_push: } \cs_new_protected:Npn \ctex_ltj_set_xkanjiskip:N #1 { \skip_set_eq:NN \l_@@_ecglue_skip #1 \bool_set_true:N \l_@@_ecglue_touched_bool \@@_settings_push: } \cs_new_protected:Npn \ctex_update_ccglue: { \skip_set_eq:NN \l_@@_ccglue_skip \l__ctex_ccglue_skip \bool_set_true:N \l_@@_ccglue_touched_bool \@@_settings_push: } \prg_new_conditional:Npnn \ctex_if_ccglue_touched: { TF } { \skip_if_eq:nnTF { \l__ctex_ccglue_skip } { \l_@@_ccglue_skip } { \prg_return_false: } { \prg_return_true: } } \ctex_at_end:n { \cs_set_protected:Npn \__ctex_update_stretch_auxii: { \ctex_if_ccglue_touched:TF { \ctex_update_ccwd: } { \__ctex_update_stretch_auxiii: } } } % % \end{macrocode} % % The gap between CJK and Latin text has its own default under \pkg{ctex}, % |.25em|, and follows the size in the same way. % \begin{macrocode} %<*ctex> \skip_set:Nn \l_@@_ecglue_skip { .25em plus 1pt minus 1pt } \skip_new:N \l__ctex_xkanjiskip_skip \skip_set_eq:NN \l__ctex_xkanjiskip_skip \l_@@_ecglue_skip \tl_new:N \l__ctex_xkanjiskip_tl \tl_set:Nn \l__ctex_xkanjiskip_tl { .25em plus 1pt minus 1pt } \cs_new_protected:Npn \ctex_update_xkanjiskip: { \skip_if_eq:nnT { \l_@@_ecglue_skip } { \l__ctex_xkanjiskip_skip } { \skip_set:Nn \l__ctex_xkanjiskip_skip { \l__ctex_xkanjiskip_tl } \skip_set_eq:NN \l_@@_ecglue_skip \l__ctex_xkanjiskip_skip \bool_set_true:N \l_@@_ecglue_touched_bool \@@_settings_push: } } \clist_map_inline:nn { \ctex_update_xkanjiskip: , \ctex_update_kanjisize: } { \ctex_add_to_selectfont:n {#1} } % % \end{macrocode} % % The \pkg{ctex} option |punct| selects a punctuation style, and |space| % has no counterpart here. % \begin{macrocode} %<*ctex> \cs_new_protected:Npn \ctex_set_jfm:n #1 { \tl_set:Nn \l__ctex_jfm_tl {#1} \LuaCJKsetup { PunctStyle = {#1} } } \cs_generate_variant:Nn \ctex_set_jfm:n { o } \tl_new:N \l__ctex_jfm_tl \ctex_define:n { space .code:n = { \msg_warning:nn { ctex } { invalid-option } } } \ctex_define:n { punct .code:n = { \tl_set:Ne \l__ctex_punct_tl {#1} \ctex_set_jfm:o { \l__ctex_punct_tl } }, punct .default:n = { quanjiao }, } \ctex_define:n { experiment .meta:nn = { ctex / experiment } {#1} } \keys_define:nn { ctex / experiment } { CJKecglue .code:n = { \tl_set:Nn \l__ctex_xkanjiskip_tl {#1} \skip_set:Nn \l__ctex_xkanjiskip_skip {#1} \ctex_ltj_set_xkanjiskip:N \l__ctex_xkanjiskip_skip }, CJKecglue .value_required:n = true } \ctex_set_jfm:o { \l__ctex_punct_tl } % % \end{macrocode} % % A very small part of the \pkg{luatexja} parameter interface is kept, for % documents that set the skips directly. Only |kanjiskip| and |xkanjiskip| % are understood; any other key is silently ignored, because this % is not a reimplementation of the \pkg{luatexja} parameter system. % \begin{macrocode} %<*ctex> \keys_define:nn { LuaCJK / ctex / ltj } { kanjiskip .code:n = { \@@_set_ccglue:n {#1} }, xkanjiskip .code:n = { \@@_set_ecglue:n {#1} }, unknown .code:n = { }, } \NewDocumentCommand \ltjsetparameter { m } { \keys_set:nn { LuaCJK / ctex / ltj } {#1} } \cs_new:Npn \ltjgetparameter #1 { \str_case:nnF {#1} { { kanjiskip } { \skip_use:N \l_@@_ccglue_skip } { xkanjiskip } { \skip_use:N \l_@@_ecglue_skip } } { 0pt } } % % \end{macrocode} % % \end{implementation} % % \Finale