% \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.1 2026-11-11 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{Noto Serif CJK SC}[ Language = Chinese Simplified, ItalicFont = LXGWWenKaiGBLite-Regular.ttf ] \setCJKsansfont{Noto Sans CJK SC}[Language = Chinese Simplified] \setCJKmonofont{LXGWZhuqueFangsong-Regular.ttf} [Language = Chinese Simplified] \setCJKfamilyfont{zhsong}{Noto Serif CJK SC} [Language = Chinese Simplified] \setCJKfamilyfont{zhhei}{Noto Sans CJK SC} [Language = Chinese Simplified] \setCJKfamilyfont{zhkai}{LXGWWenKaiGBLite-Regular.ttf} [Language = Chinese Simplified] \setCJKfamilyfont{zhfs}{LXGWZhuqueFangsong-Regular.ttf} [Language = Chinese Simplified] } \LuaCJKdocsetup \AddToHook{env/codemo/after}{\LuaCJKdocsetup} \usepackage{LuaCJKfntef} \LuaCJKsetup{CJKmath = true, LatinPunct = true} \setCJKmathfont{Noto Serif CJK SC}[Language = Chinese Simplified] \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 instead of at every % use.} % % \changes{v0.0.1}{2026/11/11}{Add \pkg{LuaCJK-listings}, which keeps CJK % characters in place inside a \pkg{listings} block, counts each of them as % two columns, and turns the spacing adjustments off; 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|. The name differs from the file that \pkg{ctex} installs, so both % packages can be installed side by side.} % % \changes{v0.0.1}{2026/11/11}{Refuse \pkg{CJKfntef} in the \pkg{ctex} engine % file, as \pkg{ctex}'s own \LuaTeX\ backend does; the message points at % \pkg{LuaCJKfntef}.} % % \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} % % LuaCJK 需要 \LuaTeX\ 引擎。它与 \pkg{luatexja} 冲突: % 无论先后加载哪一个,都会报错。 % % \begin{function}{\LuaCJKversion} % 打印 LuaCJK 的版本。 % \end{function} % % \begin{function}{\LuaCJKsetup} % \begin{syntax} % \tn{LuaCJKsetup} \marg{key-value list} % \end{syntax} % 配置 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}(初始值)什么都不加载。 % 文件在宏包末尾读取,所以它可以使用 LuaCJK 定义的每一个命令。 % 文件不存在时只给出警告。 % \end{keyval} % % 要在 \pkg{ctex} 中使用 LuaCJK,把它的 \keys{engine} 选项设为 % \val{luacjk}: % \begin{quote} % |\documentclass[engine = luacjk]{ctexart}| % \end{quote} % \pkg{ctex} 于是载入 LuaCJK 的 |ctex-engine-luacjk.def| 而不是它自己的 % \LuaTeX\ 后端,这个文件同时载入宏包,LuaCJK 不必再单独载入。 % % 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 的方块更高; % 这不是 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}中文\\ \setCJKsansfont{FandolKai-Regular.otf} [Scale = 1.2]\sffamily 中文 \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}中文宋体\quad \CJKfamily{zhhei}中文黑体\\ \CJKfamily{zhkai}中文楷体\quad \CJKfamily{zhfs}中文仿宋 \end{codemo} % \iffalse % % \fi % % \begin{function}{\newCJKfontfamily} % \begin{syntax} % \tn{newCJKfontfamily}\oarg{family}\meta{command}\oarg{options}\marg{font} % \end{syntax} % 定义一个切换到新声明字族的命令。 % 与 \cs{newfontfamily} 一样,该命令只作用于当前分组。 % \end{function} % % \begin{function}{\CJKfontspec} % \begin{syntax} % \tn{CJKfontspec} \oarg{options} \marg{font} % \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 中文\quad \songti 中文 \end{codemo} \endgroup % \iffalse % % \fi % % \begin{variable} % {\CJKrmdefault, \CJKsfdefault, \CJKttdefault, % \CJKfamilydefault} 由 \tn{rmfamily}、\tn{sffamily} 与 \tn{ttfamily} % 选择的 CJK 字族。 % \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{加载 Fandol} % % \begin{keyval} % \item[\keys{LoadFandol}]\val{\TTF} % 当文档没有声明任何 CJK 字体时,在 |\begin{document}| 处加载 % 四款 Fandol 字体:宋体作为主字体, % 黑体作为无衬线字体,仿宋作为等宽字体, % 并且把宋、黑、楷、仿宋全部注册为具名字族。 % \end{keyval} % % \subsection{伪粗体与伪斜体} % % 一种 CJK 字族常常没有粗体或斜体。下面这些键让 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{中文} and 中文 \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}中文\quad% \CJKfamily{zhsong}中文\quad% \addCJKfontfeatures{Scale = 1.2}中文 \end{codemo} \endgroup % \iffalse % % \fi % % \subsection{数学模式中的汉字} % % \begin{keyval} % \item[\keys{CJKmath}]\val{\TFF}: 是否允许在数学模式里直接输入汉字。 % 打开时,这样的字符用它自己的一个数学字族的 CJK 字体排出, % 而不是到数学字体里去找, % 所以上标或分数里的字符会取那个数学样式的尺寸。 % 可以用 |\LuaCJKsetup{CJKmath = true}| 打开它,也可以用宏包选项 % |CJKmath = true|。 % 不打开它时,数学模式里的汉字会到数学字体里去找,通常就此消失, % 所以这种情况下 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\ 就能取它的文本、脚本或脚脚本尺寸。 % 字族号取自 240--255 这一段,是 \LuaTeX\ 允许的 256 个字族里的最后十六个, % 正常文档不会用到那里;一个文档如果在数学里用到十六个以上的 CJK 字族, % 就会报错停止。 % 脚本尺寸是当前正文尺寸的百分之七十五, % 也就是标准尺寸表里的比例。 % 数学参数不取自 CJK 字体,因为它没有 % \texttt{MathConstants} 表:这些参数仍然是文档数学字体的, % 这就是为什么两个汉字组成的分数用的是周围数学的横线与间距。 % % \iffalse %<*example> % \fi \begin{codemo} $ x + 中文 $ 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} % \begin{syntax} % \tn{begin}\{CJKfilltwosides\}\oarg{position}\marg{width} % \ \meta{lines} \\ % \tn{end}\{CJKfilltwosides\} % \end{syntax} % 把各行排进一个竖直盒子里, % 盒中的行两端对齐到 \meta{width},各行之间用 |\\| 分开。 % 它的 \meta{position} 参数决定盒子在基线上的位置: % \texttt{t} 表示顶部,\texttt{c} 表示居中(默认),\texttt{b} 表示底部。 % 宽度为零或为负时沿用周围的行宽。 % 带星号的形式行为相同;它不与内容的自然宽度作比较。 % \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{hangmobanjiao} 在行两端固定的半字位移。 % \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}: 两个相邻的标点是否可以 % 被断行分开。标点仍然会被压缩。 % \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{4em}{字字字字字字\par}\par \LuaCJKsetup{CheckSingle = true} \parbox{4em}{字字字字字字\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{5em}{字字字字字字}\quad \parbox{5em}{字字字字\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 中中\\ \ttfamily\LuaCJKVerbAddon 中中 \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} 都保留调整, % 它们在 LuaCJK 里行为相同。 % 暂停只去掉间距:紧挨着被暂停材料的一个标点, % 仍然保留它后面的断行机会。 % \end{keyval} % % \pkg{listings} 一次处理一个字符,并且在读到字符的那一刻就把 % 字符表之外的字符排出来。CJK 字符在它那张表之外, % 于是会跑到周围拉丁文字的前面。配套的 \pkg{LuaCJK-listings} 修正了这一点: % 它把 CJK 字符加入 \pkg{listings} 的字符表,所以它们留在原处, % 把每个字符算作两列,并在列表内部关掉间距调整。 % 使用 \pkg{listings} 时 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{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{中文}\quad \CJKunderwave[symbol = \textbullet]{中文}\\ \CJKunderdot {中文}\quad \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 {中文 test 中文}\\ \CJKunderdblline{中文 test 中文}\\ \CJKunderwave {中文 test 中文}\\ \CJKunderdot {中文 test 中文}\\ \CJKsout {中文 test 中文}\\ \CJKxout {中文 test 中文} \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}、 % 变体序列)不生效:间距、标点进距与禁则都来自 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},理由同上:字体的两字宽破折号连字 % 在 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} % % 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 LuaCJK. % \end{function} % % \begin{function}{\LuaCJKsetup} % \begin{syntax} % \tn{LuaCJKsetup} \marg{key-value list} % \end{syntax} % Configure 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 LuaCJK defines. A missing file only warns. % \end{keyval} % % To use 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 |ctex-engine-luacjk.def| instead of its own % \LuaTeX\ backend, and that file loads the package, so LuaCJK is not loaded % separately. % % 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. 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}中文\\ \setCJKsansfont{FandolKai-Regular.otf} [Scale = 1.2]\sffamily 中文 \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}中文宋体\quad \CJKfamily{zhhei}中文黑体\\ \CJKfamily{zhkai}中文楷体\quad \CJKfamily{zhfs}中文仿宋 \end{codemo} % \iffalse % % \fi % % \begin{function}{\newCJKfontfamily} % \begin{syntax} % \tn{newCJKfontfamily}\oarg{family}\meta{command}\oarg{options}\marg{font} % \end{syntax} % Define a command that switches to a newly declared family. % The command is local to the current group, like \cs{newfontfamily}. % \end{function} % % \begin{function}{\CJKfontspec} % \begin{syntax} % \tn{CJKfontspec} \oarg{options} \marg{font} % \end{syntax} % Select a CJK font for the current group. % \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 中文\quad \songti 中文 \end{codemo} \endgroup % \iffalse % % \fi % % \begin{variable} % {\CJKrmdefault, \CJKsfdefault, \CJKttdefault, % \CJKfamilydefault} The CJK families selected by \tn{rmfamily}, % \tn{sffamily}, and \tn{ttfamily}. % \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. % \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{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 % 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{中文} and 中文 \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}中文\quad% \CJKfamily{zhsong}中文\quad% \addCJKfontfeatures{Scale = 1.2}中文 \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|. % Without it a CJK character in math mode is looked up in the math font % and usually disappears, so 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 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} $ x + 中文 $ 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} % \begin{syntax} % \tn{begin}\{CJKfilltwosides\}\oarg{position}\marg{width} % \ \meta{lines} \\ % \tn{end}\{CJKfilltwosides\} % \end{syntax} % 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. % \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 hangs into the margin, or into the indentation at the % start of a paragraph, and the line keeps its width. % This replaces the fixed half-em shift of \texttt{hangmobanjiao} at either % end of a line. \texttt{false} turns the option off. % \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. % 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, and a run of long punctuation is kept together. % 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{4em}{字字字字字字\par}\par \LuaCJKsetup{CheckSingle = true} \parbox{4em}{字字字字字字\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{5em}{字字字字字字}\quad \parbox{5em}{字字字字\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 中中\\ \ttfamily\LuaCJKVerbAddon 中中 \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 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. % 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} % % \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{中文}\quad \CJKunderwave[symbol = \textbullet]{中文}\\ \CJKunderdot {中文}\quad \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 {中文 test 中文}\\ \CJKunderdblline{中文 test 中文}\\ \CJKunderwave {中文 test 中文}\\ \CJKunderdot {中文 test 中文}\\ \CJKsout {中文 test 中文}\\ \CJKxout {中文 test 中文} \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 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 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} % % 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}{Refuse to load with \pkg{luatexja}, which % adjusts the same nodes; \cs{LuaCJKversion} 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 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 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 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 as % well, and a dash or an ellipsis keeps its advance, so a run of long % punctuation is not collapsed.} % 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_blocks:nn} % Pull the block options out of a family's option list % and register one \pkg{fontspec} family per block font. % The remaining options go into \cs{l_@@_features_tl}. % \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 % } % The block font of each family and block, the blocks found in the % option list being processed, the filtered options, 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 \cs_new_protected:Npn \@@_block_option:nn #1#2 { \tl_set:Nn \l_@@_option_key_tl {#1} \tl_trim_spaces:N \l_@@_option_key_tl \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} } { \tl_put_right:Nn \l_@@_features_tl { , ~ #1 = {#2} } } } \cs_new_protected:Npn \@@_block_option:n #1 { \tl_put_right:Nn \l_@@_features_tl { , ~ #1 } } \cs_new_protected:Npn \@@_extract_blocks:nn #1#2 { \clist_if_empty:NTF \g_@@_block_names_clist { \tl_set:Nn \l_@@_features_tl {#2} } { \tl_clear:N \l_@@_features_tl \prop_gclear:N \g_@@_pending_block_prop \keyval_parse:NNn \@@_block_option:n \@@_block_option:nn {#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} keeps its real % faces.} % 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 instead of raising an error.} % 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_@@_main_fontid_int % } % 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 \cs_generate_variant:Nn \prop_get:NnNF { NVNF } \cs_generate_variant:Nn \prop_gput:Nnn { NVV } \tl_new:N \l_@@_fallback_ids_tl \int_new:N \l_@@_main_fontid_int % \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 \@@_recall_all_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 the paragraph does still decides how its own text is set. This % covers \keys{CJKglue}, \keys{CJKecglue}, \keys{PunctStyle}, % \keys{PunctKinsoku}, \keys{LatinPunct}, \keys{CJKmath}, \keys{PunctWidth}, % \keys{PunctBoundWidth}, \keys{CheckSingle}, \keys{WidowPenalty}, % \keys{AllowBreakBetweenPuncts}, \keys{AutoFallBack}, and the cancelled % sub-blocks.} % 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.} % 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; the prohibition survives \keys{PunctKinsoku} % being off, as does that of \cs{LuaCJKVerbAddon}, because the switch only % controls the punctuation rules.} % Keep the argument on one line. The nodes of the argument are marked, % and the line-breaking pass puts a penalty in front of every break % opportunity that involves a marked node. % \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:} % \changes{v0.0.1}{2026/11/11}{Add the \keys{Verb} option (\texttt{true}, % \texttt{env}, \texttt{env+}, \texttt{false}) to control what happens % inside \cs{verb} and the verbatim-like environments.} % 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 automatically in % \cs{verb}, \env{verbatim}, \pkg{fancyvrb}, and \pkg{listings}; % \cs{LuaCJKdisable} records its state per node.} % 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 } % \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 } \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 \group_end: \@@_recall_fallback:n {#1} \prop_if_in:NVT \g_@@_family_prop \l_@@_family_tl { \selectfont } } % \end{macrocode} % % \begin{macro}[int]{\@@_recall_fallback:n} % Resolve the fallback families of one CJK family to font identifiers % at the current size, and hand the list to Lua together with the % identifier of the family itself. Font identifiers depend on the size, % so this runs again whenever a family is registered. % The variables used here are declared with the other options above. % \begin{variable}[int]{\g_@@_lua_loaded_bool} % \begin{macrocode} \bool_new:N \g_@@_lua_loaded_bool \cs_new_protected:Npn \@@_recall_all_fallbacks: { \prop_map_inline:Nn \g_@@_family_prop { \@@_recall_fallback:n {##1} } } % \end{macrocode} % \end{variable} % \begin{macrocode} \cs_new_protected:Npn \@@_recall_fallback:n #1 { \prop_if_in:NnT \g_@@_fallback_prop {#1} { \prop_get:NnNF \g_@@_fallback_prop {#1} \l_@@_tmpa_tl { \tl_clear:N \l_@@_tmpa_tl } \group_begin: \tl_clear:N \l_@@_fallback_ids_tl \prop_get:NnNF \g_@@_family_prop {#1} \l_@@_family_name_tl { \tl_clear:N \l_@@_family_name_tl } \exp_args:NV \fontfamily \l_@@_family_name_tl \selectfont \int_set:Nn \l_@@_main_fontid_int { \tex_fontid:D \tex_font:D } \clist_map_inline:Nn \l_@@_tmpa_tl { \fontfamily {##1} \selectfont \tl_put_right:Nx \l_@@_fallback_ids_tl { \int_eval:n { \tex_fontid:D \tex_font:D } , } } \bool_if:NT \g_@@_lua_loaded_bool { \tl_if_empty:NF \l_@@_fallback_ids_tl { \lua_now:e { LuaCJK.set_fallback ( \int_use:N \l_@@_main_fontid_int , { \l_@@_fallback_ids_tl } ) } } } \group_end: } } % \end{macrocode} % \end{macro} % \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]{\@@_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 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 } \group_end: \g_@@_font_attr = \g_@@_fontid_int } { \g_@@_font_attr = \c_zero_int \tl_clear:N \l_@@_family_name_tl } \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 } \@@_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. % \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} \fontsize { \use:c { f@size } pt } { \baselineskip } \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]{\@@_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. % \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 } % \end{macrocode} % \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; % 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 } \group_begin: \fontspec_gset_family:NVV \l_@@_fontspec_tl \l_@@_feature_tl \l_@@_registered_name_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_blocks: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.} % 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 { \group_begin: \fontspec_gset_family:Nnn \l_@@_fallback_family_tl {#1} {#2} \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: \exp_args:NV \@@_recall_fallback:n \l_@@_fallback_key_tl } % \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 } { \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} { \NoValue } } % \end{macrocode} % \end{macro} % % \begin{macro}{\setCJKmathfont} % 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} % 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 } { \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} { \NoValue } \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 } } } % \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}{Add 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 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}{Add 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 } { \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, PENALTY, MATH, % DISC, WHATSIT, MARK, INS, % ADJUST, LOCAL_PAR, SPACESKIP, 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 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 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 reads the % \texttt{Line\_Break} property \texttt{CJ} from \file{LineBreak.txt} and % forbids a line break before those characters.} % 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 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, % and optional fixed width or hanging parameters. % \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 }, 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 }, kaiming = { compress_adjacent = false, ratio = 0.0, keep_open_close = true, fixed_ratio = 0.5, fixed_full = true }, CCT = { compress_adjacent = false, ratio = 0.0, keep_open_close = true, fixed_ratio = 0.7 }, hangmobanjiao = { compress_adjacent = false, ratio = 0.0, keep_open_close = true, hanging = 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{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}{Drop the placeholder glyph that a font without % the character leaves behind (one \texttt{.notdef} glyph in some fonts, a % private-use glyph in others).} % 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 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, or % \meta{nil} when that character is not a mark. The second result says % 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)), true elseif n.id == HLIST or n.id == VLIST then local p, found = first_punct(n.head) if found then return p, true end elseif n.id == MATH then return nil, true end n = n.next end return 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 found = false while n do if n.id == GLYPH then punct = punct_class(n.char, settings_of(n)) found = true elseif n.id == HLIST or n.id == VLIST then local p, deep = last_punct(n.head) if deep then punct = p found = true end elseif n.id == MATH then punct = nil found = true end n = n.next end return punct, 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 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}{Inserted glue, kerns, and penalties inherit the % attributes of the neighbouring node, so an attribute-based decoration such % as \cs{CJKunderdot} stays in one piece across CJK/CJK and CJK/Latin % boundaries.} % Give a new node the attribute list of the node it is spliced next to. % 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}. % The default glues are recomputed at every font selection, so they follow % \cs{small}, \cs{Large}, and \cs{zihao}, and a value keeps its stretch and % shrink orders. No glue is inserted between two long punctuation marks, so % a run of dashes or ellipses stays one unit.} % 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 punctuation mark keeps its break opportunity % when the text that follows is suspended by \keys{Verb}: suspension removes % the spacing, and 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]{handle_source_space} % \changes{v0.0.1}{2026/11/11}{A source space between two CJK characters % becomes the inter-character glue instead of disappearing, so the break % opportunity and the stretch of the boundary survive line breaking.} % 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. % 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) 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 and at kern. Both are places where 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. % % \begin{macro}[int]{apply_kinsoku} % \changes{v0.0.1}{2026/11/11}{Provide \keys{PunctKinsoku} line-breaking % prohibitions: closing punctuation cannot start a line, opening punctuation % cannot end a line, and long punctuation runs are kept together.} % 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. % \begin{macrocode} % \end{macrocode} % \begin{macro}[int]{class_char} % The character of a glyph, or nothing for any other node. % \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 local function class_char (n) if n and n.id == GLYPH then return n.char end return nil end % \end{macrocode} % \end{macro} % \begin{macro}[int]{next_visible} % The next visible glyph, stopping at an explicit break barrier. % \begin{macrocode} local function next_visible (n) while n do if n.id == GLYPH then return n elseif n.id == HLIST or n.id == VLIST then if box_visible(n) then return nil 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]{forbids_break} % The rules: closing and middle punctuation must not start a line, % opening punctuation must not end one, % and a run of dashes or ellipses stays together. % \begin{macrocode} local function forbids_break (prev_char, next_char, settings) local prev_punct = prev_char and punct_class(prev_char, settings) local next_punct = next_char and punct_class(next_char, settings) if settings and settings.allow_break and prev_punct and next_punct then return false end if next_punct == "close" or next_punct == "middle" then return true end if prev_punct == "open" then return true end if M.charsets.nobreak[prev_char] and M.charsets.nobreak[next_char] then return true end return false end % \end{macrocode} % \end{macro} % \begin{macro}[int]{is_break_point} % Glue and kerns are break opportunities; % only the ones 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_visible = nil local cur = head while cur do if cur.id == GLYPH then last_visible = cur elseif is_break_point(cur) and last_visible then local nxt = next_visible(cur.next) local settings = settings_of(last_visible) local cj = settings.cj_strict and class_char(nxt) and M.cj[class_char(nxt)] if nxt and (no_break_mark(last_visible) or no_break_mark(nxt) or cj or (settings.kinsoku and not disabled(last_visible) and not disabled(nxt) and forbids_break( class_char(last_visible), class_char(nxt), settings))) then if not (prev and prev.id == PENALTY and prev.penalty >= 10000) then local p = new_node("penalty") inherit_attributes(p, last_visible) 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 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} % % The glyph advance stays inside the line, so shifting the ink sideways lets % a line-final closing punctuation hang into the margin without changing % the line width. Line-initial opening punctuation is shifted the other way. % % The shift is an offset on the glyph rather than character protrusion, % because protrusion does not move the ink at the end of a line: % \tn{protrudechars} together with \tn{rpcode} makes \tn{rightmarginkern} % report the amount, but the glyph in the output keeps its position. % Measured at 10pt with a right code of 1000: the marginal kern is 10pt and % the quad of the period is unchanged, in a short line, an overfull line, and % a line whose width is the natural width. The left code does move the line: % the same measurement with \tn{lpcode} shifts the first glyph by one em, % which is what the visible effect of \pkg{microtype}'s protrusion on a Latin % line consists of. % % \begin{macro}[int]{find_first_last_glyph} % The first and the last glyph node of a list, descending into horizontal % boxes. Only the two outer glyphs are wanted, so the search returns as soon % as a glyph is found on the left and keeps the last one it meets. % \begin{macrocode} local function find_first_last_glyph (list) local first, last local function walk (l) local n = l while n do if n.id == GLYPH then if not first then first = n end last = n elseif n.id == HLIST then walk(n.head) end n = n.next end end walk(list) return first, last end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{shift_glyph} % Move the ink of a glyph by a horizontal offset. % The advance is not touched, so nothing in the line changes width. % \begin{macrocode} local function shift_glyph (g, delta) if delta == 0 then return end g.xoffset = g.xoffset + delta end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{bound_width} % \changes{v0.0.1}{2026/11/11}{Provide \keys{PunctBoundWidth}, which leaves % only the given advance inside the line and hangs the rest of the mark into % the margin: at the end of a line, at its start, and when the mark ends a % box such as \tn{mbox}. Only the lines of the paragraph itself are shifted.} % The advance that a mark keeps inside the line when it ends one, % or nothing when the option is off. % Without it the shift is half an em, as the hanging style asks. % \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} % % \begin{macro}[int]{hang_line} % Shift the punctuation glyphs at the two ends of one line. % A closing or middle mark at the end, and an opening mark at the start, % take their own settings; with a bound width the end shift is the part % that would stick out otherwise. % \begin{macrocode} local function hang_line (line) if line.id ~= HLIST then return end local first, last = find_first_last_glyph(line.head) if last and disabled(last) then last = nil end if first and disabled(first) then first = nil end local last_settings = settings_of(last or first) local first_settings = settings_of(first or last) local last_style = style_of(last_settings) local first_style = style_of(first_settings) local bound = bound_width(last_settings) local start_bound = bound_width(first_settings) if not (last_style.hanging or first_style.hanging or bound or start_bound) then return end if last and punct_class(last.char, last_settings) then local cls = punct_class(last.char, last_settings) if cls == "close" or cls == "middle" then if bound then shift_glyph(last, last.width - bound) elseif last_style.hanging then local f = font.getfont(last.font) local size = f and f.size or 0 shift_glyph(last, math.floor(0.5 * size + 0.5)) end end end if first and punct_class(first.char, first_settings) then local cls = punct_class(first.char, first_settings) if cls == "open" then if start_bound then shift_glyph(first, start_bound - first.width) elseif first_style.hanging then local f = font.getfont(first.font) local size = f and f.size or 0 shift_glyph(first, -math.floor(0.5 * size + 0.5)) end end end end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{process_lines} % Walk the lines of the paragraph and apply \cs{hang_line} to each. % The list that reaches the callback is the vertical list of line boxes, so % a horizontal box found here is a line. Descending into a line's own boxes % would shift a mark that is not at the edge of the line at all; the edge % glyphs are looked up inside the line instead. % \begin{macrocode} local function process_lines (list) local n = list while n do if n.id == HLIST then hang_line(n) end n = n.next end end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{hang_punctuation} % Apply the hanging shift to every line of the paragraph. % \begin{macrocode} local function hang_punctuation (head) process_lines(head) return head end % \end{macrocode} % \end{macro} % % \TeX\ can break at the glue that 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. % \begin{macrocode} local function process (head) head = rewrite_fonts(head) head = spacing(head) head = apply_fixed_widths(head) 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) 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.post_linebreak} % The hanging punctuation callback. % \begin{macrocode} function M.post_linebreak (head, groupcode) return hang_punctuation(head) end % \end{macrocode} % \end{macro} % % The callbacks are registered in this order: spacing, font correction, % math, and hanging. % \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("post_linebreak_filter", M.post_linebreak, "LuaCJK.hang") % % \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 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 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}{Add \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{textformat}, \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. 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); the decoration boxes of % \pkg{lua-ul} are adjusted in place, with a matching kern and negative glue % keeping the text where it was.} % 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_silent_bool % } % The requested geometry, the format of the text, % 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_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. % \cs{CJKsout} strikes at \texttt{height = 0.3em}, through the middle of a % CJK glyph, and \texttt{height = 0.5ex} restores 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}{Add \cs{CJKunderdblline}, \cs{CJKunderwave}, % and \cs{CJKxout}; the wave and the slash are drawn with \pkg{l3draw} and % repeated with leaders, so they stay in phase along a line. Their patterns % are built at use time and scale with the font size, the box that holds % them has zero height and depth, so a decoration never opens up its line, % and a rule-based decoration fills its span exactly.} % Two parallel rules. The optional argument takes \keys{subtract} and % \keys{skip}; \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}{Add \cs{CJKunderanyline} and % \cs{CJKunderanysymbol} for arbitrary rules and repeated symbols; the % underline type is cached by the arguments, and \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 and the depth, % so repeating a command with the same arguments costs nothing. % \begin{variable}[int]{\l_@@_fntef_any_code_tl} % The leader code being built. % \begin{macrocode} \tl_new:N \l_@@_fntef_any_code_tl \dim_new:N \l_@@_fntef_any_depth_dim % \end{macrocode} % \end{variable} % \begin{macrocode} \cs_new_protected:Npn \@@_fntef_use_leader_type:nn #1#2 { \prop_get:NnNF \g_@@_fntef_format_prop {#1} \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 } [ \int_use:N \g_@@_fntef_format_int ] {#2} \prop_gput:NnV \g_@@_fntef_format_prop {#1} \l_@@_fntef_cache_name_tl } \use:c { \l_@@_fntef_cache_name_tl } } \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}{\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}{Add the \env{CJKfilltwosides} environment, % which justifies every line to a given width (the CJK glue is given % infinite stretch inside the box) 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. 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 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 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