Skip to content

xeCJK:命令边界处的间距应由实际输出字符类别决定(长期追踪) #992

Description

@Liam0205

Note

本 issue 取代 #491,长期跟踪 xeCJK 在命令边界处的中西文及行内公式间距。#491 中的历史讨论、定点修复和图片仍保留作参考。

问题

命令夹在中文、西文、数字或行内公式之间时,边界间距应由命令实际排出的内容决定,并与去掉外层命令后的直接输入一致。

  • 首个或末个可见内容属于西文或数字时,对应边界应与直接输入这些内容一致;
  • 首个或末个可见内容属于中文时,对应边界应与直接输入中文一致;
  • 命令中包含行内公式时,应与直接输入同一公式一致,不能把公式改用西文字母作为比较基准;
  • 中西文混合输出应分别按照首、尾内容判断;
  • 没有可见输出的命令不应改变前后内容之间的边界。

比较时需要覆盖命令左右有无源码空格的四种组合,即 00、10、01 和 11。判定不能只看总宽度:一侧多出的间距可能与另一侧缺少的间距互相抵消;默认词间空格也可能恰好与 CJKecglue 等宽。因此测试同时使用左右混合方向、可区分间距和节点/断行回归。

当前状态

逐命令结果记录在命令边界状态表中。每个编号分别列出 xCJKecglue=false 和 xCJKecglue=true;候选内容与直接输入使用相同的选项值。

状态表中的 MWE 均指向具体、可直接编译的 .tex 文件,不以目录、README 或“同上”代替。公式矩阵的四个入口为:

xCJKecglue=<glue> 会同时把 CJKecglue 设为该 <glue>,并启用 xCJKecglue。因此不为这一简写复制整张矩阵,只用独立回归测试检查它与显式设置 CJKecglue=<glue>, xCJKecglue=true 的结果相同。CJKspace 是另一项独立设置,不与 xCJKecglue 做全组合。

状态表只反映已经合并到 master 并重新验证的实现。未合并修复的测试结果应留在相应 PR 中,不能提前把红叉改成绿勾。

已知限制

TeX 的 glue 节点不记录它来自源码空格还是显式 \hskip。两者参数完全相同时,xeCJK 无法可靠区分;需要保留显式 glue 时,可以在它前面加入 \kern0pt,阻止边界恢复逻辑把它当作源码空格处理。

当前表格证明已经登记的命令与直接输入一致,不表示 xeCJK 可以推断任意用户宏的输出。新的命令仍需先确定其可见内容和比较基准,再选择相应的 capture/register 策略。

维护方式

发现新的命令或边界组合时:

  1. 以相同可见内容的直接输入作为比较基准;
  2. 分别设置 xCJKecglue=false 和 xCJKecglue=true;
  3. 在每种设置下检查 00、10、01、11 四种源码空格组合;
  4. 同时使用默认间距和可区分宽度的自定义间距;
  5. 左右边界分别检查;涉及可伸缩间距时再检查节点和实际断行;
  6. 修复合并并从 master 复验后,更新状态表、具体 MWE、截图和回归测试。

当所有已知且可以实现的组合都有回归测试,无法实现的组合也都有机制说明、支持边界和可用的规避方法时,可以关闭本 issue。

Activity

  1. pinned this issue on Jul 18, 2026
  2. self-assigned this
    on Jul 18, 2026
  3. Liam0205 commented on Jul 18, 2026

    @Liam0205
    ContributorAuthor

    命令边界状态表1

    • 每格依次表示源码空格 00、10、01、11;第一位是命令左侧,第二位是右侧,1 表示有空格。
    • 每个编号分为 xCJKecglue=false、true 两行;候选和直接输入使用相同的选项值。
    • ✅ 表示该选项值下,默认间距和可区分间距(CJKecglue=5pt、CJKglue=1pt)都与直接输入相同;❌ 表示至少一种设置不同;— 表示尚未测试。
    • 当前绿色单元均已纳入回归测试;command-boundary01 执行 1668 个普通命令断言,command-boundary-math01 执行 5504 次公式比较,均全部通过。
    • 「中·西·中」表示命令输出西文或数字、两侧是中文;「西·中·西」表示命令输出中文、两侧是西文;「中·中·中」表示命令和两侧都输出中文。
    • 四个公式列都以直接 $x$ 为比较基准;两个混合方向分别检查公式左、右边界,避免一侧多出的间距与另一侧缺少的间距互相抵消。
    编号 命令 xCJKecglue 中·西·中 西·中·西 中·中·中 中·公式·中 中·公式·西 西·公式·西 西·公式·中 MWE 排版对照(5pt/1pt) 说明
    1 普通展开宏(\newcommand) false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    普通展开宏的间距对照
    普通 show-case 源文件
    macro 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    1 普通展开宏(\newcommand) true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    普通展开宏的间距对照
    普通 show-case 源文件
    macro 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    2 显式分组 {...} false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    显式分组的间距对照
    普通 show-case 源文件
    group 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    命令输出中文时,右侧源码空格也能正确处理。回归测试:command-boundary01、command-boundary02。
    2 显式分组 {...} true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    显式分组的间距对照
    普通 show-case 源文件
    group 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    命令输出中文时,右侧源码空格也能正确处理。回归测试:command-boundary01、command-boundary02。
    3 \textbf false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    textbf 的间距对照
    普通 show-case 源文件
    textbf 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    3 \textbf true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    textbf 的间距对照
    普通 show-case 源文件
    textbf 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    4 \textit false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    textit 的间距对照
    普通 show-case 源文件
    textit 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    4 \textit true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    textit 的间距对照
    普通 show-case 源文件
    textit 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    5 \texttt false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    texttt 的间距对照
    普通 show-case 源文件
    texttt 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    5 \texttt true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    texttt 的间距对照
    普通 show-case 源文件
    texttt 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    6 \textcolor false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    textcolor 的间距对照
    普通 show-case 源文件
    textcolor 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    颜色切换不改变可见边界。回归测试:command-boundary01、fntef-color01、color-ecglue01。
    6 \textcolor true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    textcolor 的间距对照
    普通 show-case 源文件
    textcolor 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    颜色切换不改变可见边界。回归测试:command-boundary01、fntef-color01、color-ecglue01。
    7 \mbox false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    mbox 的间距对照
    普通 show-case 源文件
    mbox 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    命令结束时读取盒子实际排出的首尾类别。回归测试:command-boundary01、command-boundary02。 命令内公式四种周边文字均与直接 $x$ 一致;原编号 28 已并入本行。
    7 \mbox true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    xCJKecglue 为 true 时 mbox 的间距对照
    普通 show-case 源文件
    mbox 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    PR #1005 重放 Default 类 marker 时同步外层列表的 spacefactor;嵌套盒子和中西混合内容的四种源码空格组合均与直接输入一致。回归测试:command-boundary01、command-boundary02。 命令内公式四种周边文字均与直接 $x$ 一致;原编号 28 已并入本行。
    8 \makebox false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    makebox 的间距对照
    普通 show-case 源文件
    makebox 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    处理方法同 \mbox;另测带可选参数的 \makebox,以便发现 LaTeX 内核接口变化。
    8 \makebox true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    makebox 的间距对照
    普通 show-case 源文件
    makebox 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    处理方法同 \mbox;另测带可选参数的 \makebox,以便发现 LaTeX 内核接口变化。
    9 \fbox false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    fbox 的间距对照
    普通 show-case 源文件
    fbox 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    处理方法同 \mbox;回归测试另含带可选参数的 \framebox。
    9 \fbox true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    xCJKecglue 为 true 时 fbox 的间距对照
    普通 show-case 源文件
    fbox 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    PR #1005 对 box 注册策略的同一修复也覆盖嵌套 \fbox;四种源码空格组合均与直接输入一致。
    10 \colorbox false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    colorbox 的间距对照
    普通 show-case 源文件
    colorbox 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    先收集 \colorbox 直接排出的多个节点,再按实际内容恢复边界。colorbox-measure01 另检查 #995 中的离线测量。
    10 \colorbox true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    colorbox 的间距对照
    普通 show-case 源文件
    colorbox 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    先收集 \colorbox 直接排出的多个节点,再按实际内容恢复边界。colorbox-measure01 另检查 #995 中的离线测量。
    11 \CJKunderline false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    CJKunderline 的间距对照
    普通 show-case 源文件
    cjkunderline 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    恢复的左侧间距排在下划线装饰范围之外。回归测试覆盖 ulem 与 xeCJKfntef 的双向嵌套。
    11 \CJKunderline true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    CJKunderline 的间距对照
    普通 show-case 源文件
    cjkunderline 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    恢复的左侧间距排在下划线装饰范围之外。回归测试覆盖 ulem 与 xeCJKfntef 的双向嵌套。
    12 \CJKunderdot false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    CJKunderdot 的间距对照
    普通 show-case 源文件
    cjkunderdot 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    处理方法同 \CJKunderline;回归测试含 \uline 与 \CJKunderdot 的双向嵌套。
    12 \CJKunderdot true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    CJKunderdot 的间距对照
    普通 show-case 源文件
    cjkunderdot 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    处理方法同 \CJKunderline;回归测试含 \uline 与 \CJKunderdot 的双向嵌套。
    13 \CJKsout false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    CJKsout 的间距对照
    普通 show-case 源文件
    cjksout 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    处理方法同 \CJKunderline;回归测试含与 \CJKunderline 的嵌套。
    13 \CJKsout true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    CJKsout 的间距对照
    普通 show-case 源文件
    cjksout 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    处理方法同 \CJKunderline;回归测试含与 \CJKunderline 的嵌套。
    14 \ref(未加载 hyperref) false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ — — — — 普通:默认 · 5pt/1pt 未加载 hyperref 时 ref 的间距对照
    普通 show-case 源文件
    在 \@setref 和 \real@setref 执行期间观察实际输出。
    14 \ref(未加载 hyperref) true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ — — — — 普通:默认 · 5pt/1pt 未加载 hyperref 时 ref 的间距对照
    普通 show-case 源文件
    在 \@setref 和 \real@setref 执行期间观察实际输出。
    15 内容后随 \null(LaTeX 内核 \ref 使用的结构) false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ — — — — 普通:默认 · 5pt/1pt 内容后随 null 时的间距对照
    普通 show-case 源文件
    只在零尺寸尾盒确实遮住边界标记时恢复状态。若显式 \hskip 与词间空格无法区分,可在前面加入 \kern0pt。
    15 内容后随 \null(LaTeX 内核 \ref 使用的结构) true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ — — — — 普通:默认 · 5pt/1pt xCJKecglue 为 true 时 null 边界的间距对照
    普通 show-case 源文件
    PR #1005 只移动由实际 marker 证明的有限节点序列。有源码空格时,7pt glue 按直接输入换成 CJKecglue;没有源码空格时,显式 7pt glue 原样保留。回归测试:command-boundary01、command-boundary02。
    16 \hyperref[...]{...} false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    hyperref 的间距对照
    普通 show-case 源文件
    hyperref 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    在整个 PDF 注释范围内观察实际首尾内容。回归测试:command-boundary01、hyperref-ecglue01。
    16 \hyperref[...]{...} true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    hyperref 的间距对照
    普通 show-case 源文件
    hyperref 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    在整个 PDF 注释范围内观察实际首尾内容。回归测试:command-boundary01、hyperref-ecglue01。
    17 \href false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    href 的间距对照
    普通 show-case 源文件
    href 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    处理方法同 \hyperref。
    17 \href true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 普通:默认 · 5pt/1pt
    公式:默认 · 5pt/1pt
    href 的间距对照
    普通 show-case 源文件
    href 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    处理方法同 \hyperref。
    18 \ref(已加载 hyperref) false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ — — — — 普通:默认 · 5pt/1pt 加载 hyperref 后 ref 的间距对照
    普通 show-case 源文件
    由 \hyperref 的处理覆盖;回归测试:ref-ecglue02。
    18 \ref(已加载 hyperref) true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ — — — — 普通:默认 · 5pt/1pt 加载 hyperref 后 ref 的间距对照
    普通 show-case 源文件
    由 \hyperref 的处理覆盖;回归测试:ref-ecglue02。
    19 \pageref(数字标签,已加载 hyperref) false ✅✅✅✅ — — — — — — 普通:默认 · 5pt/1pt pageref 和 eqref 的间距对照
    普通 show-case 源文件
    回归测试:command-boundary01。
    19 \pageref(数字标签,已加载 hyperref) true ✅✅✅✅ — — — — — — 普通:默认 · 5pt/1pt pageref 和 eqref 的间距对照
    普通 show-case 源文件
    回归测试:command-boundary01。
    20 \eqref(西文标签,已加载 hyperref) false ✅✅✅✅ — — — — — — 普通:默认 · 5pt/1pt pageref 和 eqref 的间距对照
    普通 show-case 源文件
    根据两端固定的圆括号按西文类别恢复边界。回归测试:command-boundary01。
    20 \eqref(西文标签,已加载 hyperref) true ✅✅✅✅ — — — — — — 普通:默认 · 5pt/1pt pageref 和 eqref 的间距对照
    普通 show-case 源文件
    根据两端固定的圆括号按西文类别恢复边界。回归测试:command-boundary01。
    21 \url / \nolinkurl(西文内容) false ✅✅✅✅ — — — — — — 普通:默认 · 5pt/1pt url 和 nolinkurl 的间距对照
    普通 show-case 源文件
    在整个 URL 排版期间观察实际输出。回归测试:url-ecglue01、command-boundary01。
    21 \url / \nolinkurl(西文内容) true ✅✅✅✅ — — — — — — 普通:默认 · 5pt/1pt url 和 nolinkurl 的间距对照
    普通 show-case 源文件
    在整个 URL 排版期间观察实际输出。回归测试:url-ecglue01、command-boundary01。
    22 \HD@target(hypdoc 的不可见锚点) false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ — — — — 普通:默认 · 5pt/1pt hypdoc 不可见锚点的间距对照
    普通 show-case 源文件
    在锚点前保存边界状态,锚点结束后原样恢复。回归测试:hypdoc-ecglue01。
    22 \HD@target(hypdoc 的不可见锚点) true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ — — — — 普通:默认 · 5pt/1pt hypdoc 不可见锚点的间距对照
    普通 show-case 源文件
    在锚点前保存边界状态,锚点结束后原样恢复。回归测试:hypdoc-ecglue01。
    23 \meta(hypdoc) false ✅✅✅✅ — ✅✅✅✅ — — — — 普通:默认 · 5pt/1pt meta 和 cs 的间距对照
    普通 show-case 源文件
    保留参数盒子内部的排版,同时根据外层尖括号按西文类别恢复边界。
    23 \meta(hypdoc) true ✅✅✅✅ — ✅✅✅✅ — — — — 普通:默认 · 5pt/1pt meta 和 cs 的间距对照
    普通 show-case 源文件
    保留参数盒子内部的排版,同时根据外层尖括号按西文类别恢复边界。
    24 \cs(hypdoc) false ✅✅✅✅ — — — — — — 普通:默认 · 5pt/1pt meta 和 cs 的间距对照
    普通 show-case 源文件
    将开头反斜线按西文类别处理,末尾类别取自实际输出。回归测试:command-boundary01。
    24 \cs(hypdoc) true ✅✅✅✅ — — — — — — 普通:默认 · 5pt/1pt meta 和 cs 的间距对照
    普通 show-case 源文件
    将开头反斜线按西文类别处理,末尾类别取自实际输出。回归测试:command-boundary01。
    25 \verb false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ — — — — 普通:默认 · 5pt/1pt verb 的间距对照
    普通 show-case 源文件
    观察 \verb 从开始扫描到结束的完整输出;\verb* 和 \MakeShortVerb 定义的简写形式使用相同入口。
    25 \verb true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ — — — — 普通:默认 · 5pt/1pt verb 的间距对照
    普通 show-case 源文件
    观察 \verb 从开始扫描到结束的完整输出;\verb* 和 \MakeShortVerb 定义的简写形式使用相同入口。
    26 \blx@pagetracker(biblatex 页码跟踪) false ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ — — — — 普通:默认 · 5pt/1pt biblatex 页码跟踪的间距对照
    普通 show-case 源文件
    把页码跟踪产生的写出节点作为不可见内容处理。测试替身只覆盖跟踪节点位于条目文字之前的情况。
    26 \blx@pagetracker(biblatex 页码跟踪) true ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ — — — — 普通:默认 · 5pt/1pt biblatex 页码跟踪的间距对照
    普通 show-case 源文件
    把页码跟踪产生的写出节点作为不可见内容处理。测试替身只覆盖跟踪节点位于条目文字之前的情况。
    27 跨 \hbox 的显式 \hskip(前一个盒子以中文结尾,#996) false — — ✅——— — — — — #996 MWE 跨盒显式 hskip 的间距对照
    普通 show-case 源文件
    待检查状态不会越过盒子误处理下一处显式间距。这里只测试了 00;无法与词间空格区分的显式 \hskip 仍可用 \kern0pt 保护。
    27 跨 \hbox 的显式 \hskip(前一个盒子以中文结尾,#996) true — — ✅——— — — — — #996 MWE 跨盒显式 hskip 的间距对照
    普通 show-case 源文件
    DELTA=0.0pt;稳定回归由 boundary-crossbox01 固化。
    28 \mbox{\vrule ...}(标尺盒子,#998) false ✅✅✅✅ — — — — — — 普通:默认 · 5pt/1pt mbox 内标尺的间距对照
    普通 show-case 源文件
    把盒子末尾直接排出的标尺按西文类别处理。直接输入 \vrule 不触发字符类别转换;如需保留该行为,可用原语 \hbox{\vrule ...}。
    28 \mbox{\vrule ...}(标尺盒子,#998) true ✅✅✅✅ — — — — — — 普通:默认 · 5pt/1pt mbox 内标尺的间距对照
    普通 show-case 源文件
    把盒子末尾直接排出的标尺按西文类别处理。直接输入 \vrule 不触发字符类别转换;如需保留该行为,可用原语 \hbox{\vrule ...}。
    29 \unit / \qty / \num(siunitx,#1000) false ✅✅✅✅ — — — — — — #1000 MWE siunitx 命令的间距对照
    普通 show-case 源文件
    将这些命令的首尾按西文类别处理,并兼容 siunitx v2 的 \si、\SI。\ang 的比较基准尚未确定,不在本行范围内。
    29 \unit / \qty / \num(siunitx,#1000) true ✅✅✅✅ — — — — — — #1000 MWE siunitx 命令的间距对照
    普通 show-case 源文件
    默认间距和可区分间距各有 36/36 项通过;稳定回归由 siunitx-ecglue01 固化。
    30 \(x\) false — — — ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 公式:默认 · 5pt/1pt paren 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    与直接 $x$ 使用相同的公式边界语义。
    30 \(x\) true — — — ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 公式:默认 · 5pt/1pt paren 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    与直接 $x$ 使用相同的公式边界语义。
    31 \ensuremath{x} false — — — ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 公式:默认 · 5pt/1pt ensuremath 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    与直接 $x$ 和 \(x\) 使用相同的公式边界语义。
    31 \ensuremath{x} true — — — ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ ✅✅✅✅ 公式:默认 · 5pt/1pt ensuremath 命令内行内公式的间距对照
    公式 false 源文件 · 公式 true 源文件
    与直接 $x$ 和 \(x\) 使用相同的公式边界语义。
    测试方法

    每项测试都把命令包装的内容与去掉包装后的相同可见内容比较。二者分别放入受限水平盒子测量,并扣除各自单独排版时的固有宽度差,剩余差值只来自左右边界。

    每格同时检查默认间距和可区分间距。默认词间空格可能与 CJKecglue 等宽,而默认 CJKglue 的自然宽度可能为零,因此只测默认设置会漏掉节点类型错误。节点结构另由 xeCJK/testfiles/ 中的回归测试检查。

    普通命令和公式矩阵都使用具体的可编译入口:例如 core false/default、core true/default、math false/default 和 math true/default。每行 MWE 单元格列出该场景所需的默认/5pt/1pt 驱动,不再用目录、README 或“同上”代替。

    更新表格
    • 新增或修复一个场景后,同时运行默认间距和可区分间距测试。
    • 更新对应行的标记、说明和排版图片。
    • 修复合并后,从合并提交重新验证,再修改本评论;未合并结果只写在相应 PR 中。
    • 无法实现的组合保留 ❌,并说明原因和可用的规避方法。
    • matrix-verdicts.tsv 保留为初始审计快照,只有明确替换整张表的基线时才更新。

    Footnotes

    1. 本表只记录已经合并到 master 并重新验证的结果。2026-07-18 的初始审计基于 master 4628cb44(xeCJK v3.10.3、ctex v2.6.3)。PR fix(xecjk): unify command-boundary spacing recovery #999 合并为 cb6b2f73 后,第 1—26 行从该提交重新测试,280 个可表达组合全部通过。PR fix(xecjk): close the #996/#998/#1000 boundary-capture gaps #1001 合并为 10500b33 后,当时编号 27—30 的各行从该提交重新测试;当时的矩阵只覆盖 xCJKecglue=false。2026-07-21 又从 master 10500b33 检查 xCJKecglue=true:排除已转交 xeCJK:需要明确行内公式与中文相邻时的间距规则 #1002 的行内公式后,默认间距通过 318/320 项,可区分间距通过 312/320 项。PR fix(xecjk): 恢复 xCJKecglue 命令右边界状态 #1005 合并为 8007e4df 后,从该提交重新运行 16 个驱动;普通命令在 xCJKecglue=false/true 与默认/可区分间距四种配置下均为 320/320 通过,第 7、9、15 行据此更新。初始逐项差值保存在 matrix-verdicts.tsv。 PR fix(xeCJK): 修正命令内行内公式的边界间距 #1009 合并后的 master 为 1b77613b;从该提交重新解包 xeCJK 后,四套公式矩阵均为 272/272 通过,17 张公式 show-case 也全部一致。原编号 28 的 \mbox{$x$} 已并入编号 7,后续编号随之递补。 ↩

  4. Liam0205 commented on Jul 18, 2026

    @Liam0205
    ContributorAuthor

    Note

    本评论记录 2026-07-18 基于 master 4628cb44 所做的阶段性分析。随后合并的 PR #999 采用了更完整的捕获和注册机制(capture/register),取代了下文分析阶段设想的实现。当前结果以命令边界状态表为准。

    阶段性分析:从逐命令修补转向统一处理

    对状态表前 26 行进行节点检查后,当时发现的 132 个不一致组合大致可以归入四类。这说明问题虽然涉及许多命令,但底层原因并没有同样繁多;继续为每个命令分别保存和恢复状态,既容易重复代码,也很难保证左右边界的处理一致。

    1. 源码空格遮住边界标记

    部分场景中,边界标记仍在当前列表内,但它后面已经出现了源码空格形成的 glue(可伸缩间距)节点。从命令边界进入西文的旧恢复路径(Boundary→Default)只检查 \lastkern,因此找不到被 glue 节点遮住的标记;当时从命令边界进入中文的路径(Boundary→CJK)则已经包含相应检查。这是两个方向处理不对称造成的共同缺口。

    典型节点形状为:

    中文 → 标记用 kern 节点 → 源码空格产生的 glue 节点 → 西文
    

    2. 盒子或数学节点遮住边界标记

    \mbox、\makebox、\fbox、\colorbox 等命令会把内容放进盒子。普通的 \lastkern 或 \lastskip 无法检查盒子内部,也无法越过盒子找到先前的边界标记。因此,仅在命令入口保存状态还不够,命令结束时还要根据实际输出重建左右边界。

    3. 特殊操作节点遮住边界标记

    TeX 使用 whatsit 节点记录写出文件、切换语言等不直接排出文字的特殊操作。biblatex 页码跟踪和部分 \verb 路径会留下这类节点。它们与盒子问题相似:实际文字没有改变,但末尾节点已经不再是边界标记。过去的定点补丁通常只防止插入多余间距,没有完整恢复本应存在的间距。

    这里不能采用“遇到任意 whatsit 都继续向前查找”的通用规则,因为无法证明所有 whatsit 都能安全忽略。最终实现需要在已知命令的完整作用范围内观察输出。

    4. 命令本身改变了排版语义

    \eqref、\meta、\cs 等命令可能增加括号、数学节点、反斜线或字体变化。此时命令输出与参数文本并不相同,不能只根据参数内容判断边界类别,而应观察实际排出的首尾内容。数学公式的比较基准还有额外问题,目前由 #1002 继续讨论。

    #999 采用的最终方案

    上述分析确认了“应建立统一处理层”的方向。分析阶段曾设想从列表末尾逐个取出节点,找到边界标记后再按原顺序恢复这些节点;最终实现没有采用这种方法。PR #999 按命令产生的节点形式建立了五种注册策略:

    • box:处理末尾留下单个盒子的命令;
    • wrapped-box:收集会直接写出多个节点的盒子命令;
    • stream:观察命令执行期间直接写入当前列表的内容;
    • transparent:用于不应改变可见边界的命令;
    • post-transparent:处理命令结束后才能观察到的零尺寸盒子。

    运行期间,XeTeX 的字符类别转换(interchar)会报告实际出现的首、尾类别;命令结束后,框架据此恢复左右边界。这样,命令名称只决定采用哪种注册策略,间距判断仍由实际输出决定。PR #1001 又补齐了跨盒显式 glue、盒内数学内容或标尺(rule),以及 siunitx 命令等后续缺口。

    这套实现仍有一个无法消除的限制:参数完全相同的源码空格和显式 \hskip 会产生结构与参数都相同的 glue 节点,TeX 无法可靠判断其来源。需要保留这类显式间距时,可以在前面加入 \kern0pt。

    初始矩阵、节点探针和逐项差值仍保存在 gh-assets/issues/992/,作为这次阶段性分析的复现材料。

  5. Liam0205 commented on Jul 18, 2026

    @Liam0205
    ContributorAuthor

    历史上关于这类问题有过几次讨论。结论大致是:

    技术上实现一个 case 不难。但是有很多的 edge case,所以很难保证完备性。因此,暂时不去实现;同时在 xeCJK 层面,中文与西文之间建议手工加上空格。

    因为这样的讨论和结论,在相当长时间内,xeCJK 对这类问题都是「回避」的。即是说,知道它们存在,但总是保持现状留待以后。大约 2026-04,接入 Claude Code 之后,我开始试着逐个解决最常见的 cases。这些工作确实解决了一系列问题,但也确实引入了一系列新的问题——所谓按下葫芦起了瓢。这即是 xeCJK 在 3.10.0 -- 3.10.2 上类似问题的反复踩坑经历。

    但踩坑不应该掉到坑里,而应该踩出价值。上一评论的工作试图对此类问题进一步分类,以期能找到相对完备的解法。或者说,至少对于常见的问题,我们能有统一的解决模板。发现新的问题之后,只需要将对应的命令注册进去即可自动解决。

    接下来将试着做一做这个方案的原型。

  6. Liam0205 commented on Jul 21, 2026

    @Liam0205
    ContributorAuthor

    #992 的状态表还没有系统检查行内公式。目前只记录了几个内部使用数学模式的命令,例如 \eqref、\mbox{$x$} 和 siunitx 命令;下面两类常见写法仍未覆盖:

    • 直接在中文旁边使用 $x$、\(x\) 或 \ensuremath{x};
    • 在已有命令中放入公式,例如 \textbf{$x$}、\textcolor{red}{$x$} 或 \fbox{$x$}。

    这些场景的实测结果和待解决问题记录在 #1002。现在还不能直接把结果填入状态表,因为需要先确定比较基准:去掉外层命令后,应当与直接输入的 $x$ 相同,还是应当按西文字母 x 的边界处理?目前这两种写法本身就会产生不同间距。

    #1002 明确预期行为后,再补充相应的状态表项目和回归测试。

  7. added theissue type on Jul 23, 2026
  8. changed the issue type fromtoon Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions