% \iffalse meta-comment
%
% File: LuaCJK.dtx
% -----------------------------------------------------------------------
%   Copyright (C) 2026 by Mingyu Xia <myhsia@outlook.com>               *
% -----------------------------------------------------------------------
%   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
%</internal>
%
%<*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.
%</readme>
%
%<*package|fntef|listings|ctex>
\NeedsTeXFormat{LaTeX2e}[2026/06/01]
\RequirePackage { expl3 }
\GetIdInfo $Id: LuaCJK.dtx v0.0.1 2026-11-11 myhsia<myhsia@outlook.com>$
%<package>  {A LuaTeX CJK typesetting package}
%<package>\ProvidesExplPackage {\ExplFileName}
%<fntef>  {Font effects for LuaCJK (CTEX)}
%<fntef>\ProvidesExplPackage {LuaCJKfntef}
%<listings>  {CJK support for listings}
%<listings>\ProvidesPackage {LuaCJK-listings}
%<ctex>  {LuaTeX adapter for ctex (LuaCJK)}
%<ctex>\ProvidesExplFile {ctex-engine-luacjk.def}
%<package>  {\ExplFileDate} {\ExplFileVersion} {\ExplFileDescription}
%<fntef>  {\ExplFileDate} {\ExplFileVersion} {\ExplFileDescription}
%<ctex>  {\ExplFileDate} {\ExplFileVersion} {\ExplFileDescription}
%<listings>  [\ExplFileDate\space\ExplFileVersion\space\ExplFileDescription]
%<package>\edef \DocFileDate        {\ExplFileDate}
%<package>\edef \DocFileVersion     {\ExplFileVersion}
%<package>\edef \DocFileDescription {\ExplFileDescription}
%<package>\tl_greplace_all:Nnn \DocFileDate { / } { - }
%</package|fntef|listings|ctex>
%
%<*internal>
\fi
%</internal>
%
%<*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}
%</driver>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \fi
%
% \iffalse
%<*example>
% \fi
\begingroup
\begin{codemo}
\LuaCJKsetup{CJKspace = true}中 文 中 文
\end{codemo}
\endgroup
% \iffalse
%</example>
% \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
%</example>
% \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
%</example>
% \fi
%
% \iffalse
%<*example>
% \fi
\begin{codemo}
\begin{CJKfilltwosides*}{0pt}
  瞻彼阕者，\\虚室生白
\end{CJKfilltwosides*}
\end{codemo}
% \iffalse
%</example>
% \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
%</example>
% \fi
%
% \iffalse
%<*example>
% \fi
\begingroup
\begin{codemo}
\LuaCJKsetup{LatinPunct = false}%
a’a\quad 字！字\\
\LuaCJKsetup{LatinPunct, PunctWidth = .8em}
a’a\quad 字！字
\end{codemo}
\endgroup
% \iffalse
%</example>
% \fi
%
% \iffalse
%<*example>
% \fi
\begingroup
\begin{codemo}
字字。\\
\LuaCJKsetup{PunctBoundWidth = .5em}字字。\\
（字字
\end{codemo}
\endgroup
% \iffalse
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \fi
%
% \iffalse
%<*example>
% \fi
\begingroup
\begin{codemo}
今天是 \CJKunderline{\hspace*{3em}} 年\\
第 \CJKunderline{\hspace*{2em}} 题
\end{codemo}
\endgroup
% \iffalse
%</example>
% \fi
%
% \iffalse
%<*example>
% \fi
\begingroup
\begin{codemo}
\CJKunderline{中文}\CJKunderline{中文}\\
\CJKunderline[subtract]{中文}\\
\CJKunderline[skip]{中，文}
\end{codemo}
\endgroup
% \iffalse
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \fi
%
% \iffalse
%<*example>
% \fi
\begin{codemo}
\CJKunderline   {中文 test 中文}\\
\CJKunderdblline{中文 test 中文}\\
\CJKunderwave   {中文 test 中文}\\
\CJKunderdot    {中文 test 中文}\\
\CJKsout        {中文 test 中文}\\
\CJKxout        {中文 test 中文}
\end{codemo}
% \iffalse
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \fi
%
% \iffalse
%<*example>
% \fi
\begingroup
\begin{codemo}
\LuaCJKsetup{CJKspace = true}中 文 中 文
\end{codemo}
\endgroup
% \iffalse
%</example>
% \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
%</example>
% \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
%</example>
% \fi
%
% \iffalse
%<*example>
% \fi
\begin{codemo}
\begin{CJKfilltwosides*}{0pt}
  瞻彼阕者，\\虚室生白
\end{CJKfilltwosides*}
\end{codemo}
% \iffalse
%</example>
% \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
%</example>
% \fi
%
% \iffalse
%<*example>
% \fi
\begingroup
\begin{codemo}
\LuaCJKsetup{LatinPunct = false}%
a’a\quad 字！字\\
\LuaCJKsetup{LatinPunct, PunctWidth = .8em}
a’a\quad 字！字
\end{codemo}
\endgroup
% \iffalse
%</example>
% \fi
%
% \iffalse
%<*example>
% \fi
\begingroup
\begin{codemo}
字字。\\
\LuaCJKsetup{PunctBoundWidth = .5em}字字。\\
（字字
\end{codemo}
\endgroup
% \iffalse
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \fi
%
% \iffalse
%<*example>
% \fi
\begingroup
\begin{codemo}
今天是 \CJKunderline{\hspace*{3em}} 年\\
第 \CJKunderline{\hspace*{2em}} 题
\end{codemo}
\endgroup
% \iffalse
%</example>
% \fi
%
% \iffalse
%<*example>
% \fi
\begingroup
\begin{codemo}
\CJKunderline{中文}\CJKunderline{中文}\\
\CJKunderline[subtract]{中文}\\
\CJKunderline[skip]{中，文}
\end{codemo}
\endgroup
% \iffalse
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \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
%</example>
% \fi
%
% \iffalse
%<*example>
% \fi
\begin{codemo}
\CJKunderline   {中文 test 中文}\\
\CJKunderdblline{中文 test 中文}\\
\CJKunderwave   {中文 test 中文}\\
\CJKunderdot    {中文 test 中文}\\
\CJKsout        {中文 test 中文}\\
\CJKxout        {中文 test 中文}
\end{codemo}
% \iffalse
%</example>
% \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-<name>.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}
%</package>
%    \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")
%</lua>
%    \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")
%</lua-core>
%    \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")
%</lua-fntef>
%    \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 <</ActualText <FEFF200B>>> 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:               }
%</fntef>
%    \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
%</listings>
%    \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 }
%</ctex>
%    \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 }
  }
%</ctex>
%    \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: }
      }
  }
%</ctex>
%    \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} }
%</ctex>
%    \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 }
%</ctex>
%    \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 }
  }
%</ctex>
%    \end{macrocode}
%
% \end{implementation}
%
% \Finale
