`\mathchoice` 和 LaTeX3 的变量展开问题
问题背景
我在用 tikz 给 exam-zh 绘制中国化(主要是初高中)的部分数学符号,比如 \subset, \cap 等。在 @syvshc 的提醒下,我发现需要考虑到符号在不同模式(比如正文、角标)中的大小问题,然后在 tex.se 上查到一个回答 Define a math symbol relative to font size。里面提到了使用 \mathchoice 来处理。
MWE
\documentclass{article}
\usepackage{tikz}
\begin{document}
\makeatletter
\ExplSyntaxOn
% 直线长度
\dim_new:N \l__examzh_symbols_cap_line_length_dim
\dim_set:Nn \l__examzh_symbols_cap_line_length_dim { 0.56em }
% 半径大小
\dim_new:N \l__examzh_symbols_cap_radius_dim
\dim_set:Nn \l__examzh_symbols_cap_radius_dim { 0.28em }
\cs_new:Npn \__examzh_symbols_cap:
{
\begin{tikzpicture}[line~cap=round, line~width = 0.6pt,baseline = {([yshift = 1.2pt]current~bounding~box.south)}]
\draw (\l__examzh_symbols_cap_radius_dim,0) arc (0 \c_colon_str 180 \c_colon_str \l__examzh_symbols_cap_radius_dim);
\draw
(\l__examzh_symbols_cap_radius_dim,0) --++ (0,-\l__examzh_symbols_cap_line_length_dim)
(-\l__examzh_symbols_cap_radius_dim,0) --++ (0,-\l__examzh_symbols_cap_line_length_dim);
\end{tikzpicture}
}
\cs_set_eq:NN \__examzh_symbols_old_cap: \cap
\RenewDocumentCommand { \cap } { s }
{
\IfBooleanTF {#1}
{ \__examzh_symbols_old_cap: }
{
\mathrel
{
\__examzh_symbols_symbol_four_size:n
{ \__examzh_symbols_cap: }
}
}
}
\cs_new:Npn \__examzh_symbols_symbol_four_size:n #1
{
\mathchoice
{
\hbox:n
{
\fontsize{\tf@size}{\tf@size}\selectfont #1
}
}
{
\hbox:n
{
\fontsize{\tf@size}{\tf@size}\selectfont #1
}
}
{
\hbox:n
{
\fontsize{\sf@size}{\sf@size}\selectfont #1
}
}
{
\hbox:n
{
\fontsize{\ssf@size}{\ssf@size}\selectfont #1
}
}
}
\ExplSyntaxOff
\makeatother
$A\cap B_{A\cap B}$
{\large$A\cap B$\par}
{\Large$A\cap B$\par}
{\LARGE$A\cap B$\par}
{\small$A\cap B$\par}
{\footnotesize$A\cap B$\par}
$A\cap* B_{A\cap* B}$
{\large$A\cap* B$\par}
{\Large$A\cap* B$\par}
{\LARGE$A\cap* B$\par}
{\small$A\cap* B$\par}
{\footnotesize$A\cap* B$\par}
\end{document}
结果发现在不同模式下重定义的 \cap 大小是一样的:

分析
因为之前在 LaTeX 工作室 b站视频,项子越讲 LaTeX3 的 LaTeX3 教程三:宏展开 时候讲到了一个 lazy evaluation 的想法,我猜想这里也可能是这样的,于是我将其中的参数变量去掉,
\documentclass{article}
\usepackage{tikz}
\begin{document}
\makeatletter
\ExplSyntaxOn
\cs_new:Npn \__examzh_symbols_cap:
{
\begin{tikzpicture}[line~cap=round, line~width = 0.6pt,baseline = {([yshift = 1.2pt]current~bounding~box.south)}]
\draw (0.28em,0) arc (0 \c_colon_str 180 \c_colon_str 0.28em);
\draw
(0.28em,0) --++ (0,-0.56em)
(-0.28em,0) --++ (0,-0.56em);
\end{tikzpicture}
}
\cs_set_eq:NN \__examzh_symbols_old_cap: \cap
\RenewDocumentCommand { \cap } { s }
{
\IfBooleanTF {#1}
{ \__examzh_symbols_old_cap: }
{
\mathrel
{
\__examzh_symbols_symbol_four_size:n
{ \__examzh_symbols_cap: }
}
}
}
\cs_new:Npn \__examzh_symbols_symbol_four_size:n #1
{
\mathchoice
{
\hbox:n
{
\fontsize{\tf@size}{\tf@size}\selectfont #1
}
}
{
\hbox:n
{
\fontsize{\tf@size}{\tf@size}\selectfont #1
}
}
{
\hbox:n
{
\fontsize{\sf@size}{\sf@size}\selectfont #1
}
}
{
\hbox:n
{
\fontsize{\ssf@size}{\ssf@size}\selectfont #1
}
}
}
\ExplSyntaxOff
\makeatother
$A\cap B_{A\cap B}$
{\large$A\cap B$\par}
{\Large$A\cap B$\par}
{\LARGE$A\cap B$\par}
{\small$A\cap B$\par}
{\footnotesize$A\cap B$\par}
\end{document}
发现不同模式的大小有所区别,是理想的效果:

所以我大概猜想问题出于展开的顺序
- 使用
\cap时,先展开为\mathchoice...然后根据环境判断 - 选择其中一个之后,继续展开为
... \__examzh_symbols_cap: - 然后
\__examzh_symbols_cap:展开为tikzpicture环境
接下来我就不知道怎么分析了,但感觉貌似变量的 em 的相对大小是在 dim_set 这个函数作用「时刻」相对,所以也就是在正文的大小。
请教一下如何解决这个问题?
和 \mathchoice 无关。tex 的 dimension 只能存储绝对长度,所有相对单位(em, ex, mu)都会使用当前字号转换为绝对单位。内部储存时用的 sp,用 \the<dimen> 时总是使用 pt。在下面的例子里,三处 \showthe 往 log 和 stdout 输出的内容相同,都是 10.0002pt
\newlength{\mylen} \mylen=1em
\showthe\mylen
\tiny\showthe\mylen
\Huge\showthe\mylen
于是 preamble 里的 \dim_set:Nn \l__examzh_symbols_cap_line_length_dim { 0.56em } 就把 l__examzh_symbols_cap_line_length_dim 唯一确定了。办法就是,在具体命令内部才给这些 dimension 赋(使用相对单位的)值。
和
\mathchoice无关。tex 的 dimension 只能存储绝对长度,所有相对单位(em, ex, mu)都会使用当前字号转换为绝对单位。内部储存时用的 sp,用\the<dimen>时总是使用 pt。在下面的例子里,三处\showthe往 log 和 stdout 输出的内容相同,都是 10.0002pt\newlength{\mylen} \mylen=1em \showthe\mylen \tiny\showthe\mylen \Huge\showthe\mylen于是 preamble 里的
\dim_set:Nn \l__examzh_symbols_cap_line_length_dim { 0.56em }就把l__examzh_symbols_cap_line_length_dim唯一确定了。办法就是,在具体命令内部才给这些 dimension 赋(使用相对单位的)值。
我把设置放在了命令里确实可以了,但我发现 \scriptstyle 和 \scriptscriptstyle 都是 \textstyle 的大小:
\documentclass{article}
\usepackage{tikz}
\begin{document}
\makeatletter
\ExplSyntaxOn
\keys_define:nn { exam-zh / symbols }
{
cap-line-length .dim_set:N = \l__examzh_symbols_cap_line_length_dim,
cup-line-length .dim_set:N = \l__examzh_symbols_cup_line_length_dim,
cap-radius .dim_set:N = \l__examzh_symbols_cap_radius_dim,
cup-radius .dim_set:N = \l__examzh_symbols_cup_radius_dim,
}
\cs_new:Npn \__examzh_symbols_cap:
{
\begin{tikzpicture}[line~cap=round, line~width = 0.6pt,baseline = {([yshift = 1.2pt]current~bounding~box.south)}]
\draw (\l__examzh_symbols_cap_radius_dim,0) arc (0 \c_colon_str 180 \c_colon_str \l__examzh_symbols_cap_radius_dim);
\draw
(\l__examzh_symbols_cap_radius_dim,0) --++ (0,-\l__examzh_symbols_cap_line_length_dim)
(-\l__examzh_symbols_cap_radius_dim,0) --++ (0,-\l__examzh_symbols_cap_line_length_dim);
\end{tikzpicture}
}
\cs_set_eq:NN \__examzh_symbols_old_cap: \cap
\RenewDocumentCommand { \cap } { s }
{
\group_begin:
\keys_set:nn { exam-zh / symbols }
{
cap-line-length = 0.56em,
cap-radius = 0.28em
}
\IfBooleanTF {#1}
{ \use:c { __examzh_symbols_old_cap : } }
{
\mathrel
{
\__examzh_symbols_symbol_four_size:n
{ \__examzh_symbols_cap: }
}
}
\group_end:
}
\cs_new:Npn \__examzh_symbols_symbol_four_size:n #1
{
\mathchoice
{
\hbox:n
{
\fontsize{\tf@size}{\tf@size}\selectfont #1
}
}
{
\hbox:n
{
\fontsize{\tf@size}{\tf@size}\selectfont #1
}
}
{
\hbox:n
{
\fontsize{\sf@size}{\sf@size}\selectfont #1
}
}
{
\hbox:n
{
\fontsize{\ssf@size}{\ssf@size}\selectfont #1
}
}
}
\ExplSyntaxOff
\makeatother
$A\cap B_{A\cap B}$
$A\cap B_{A{\scriptstyle \cap} B}$
$A{\scriptscriptstyle \cap} B$
$A\cap B_{A{\scriptscriptstyle \cap} B}$
{\large$A\cap B$\par}
{\Large$A\cap B$\par}
{\LARGE$A\cap B$\par}
{\small$A\cap B$\par}
{\footnotesize$A\cap B$\par}
\end{document}
我把设置放在了命令里确实可以了,但我发现
\scriptstyle和\scriptscriptstyle都是\textstyle的大小:
\keys_set:nn 还是用得太早了,导致 \mathchoice 里的 \fontsize{\tf@size}{\tf@size}\selectfont 等完全没起作用。可以用 latex2e kernel 提供的 \mathpalette 或者 amsmath 里的 \text,部分代替你的 \__examzh_symbols_symbol_four_size:n
我不了解你的完整需求,只从提供的例子看,有一点「为了 latex3 而 late3」的感觉。latex3 只是基于 primitives 的一套宏,它不提供任何新的、用 primitives、用 latex2e 表达不了的特性。
注意,你要实现的,其实是参数化地设计字符,类似 https://www.zhihu.com/question/352249034/answer/870883390 。要想出来的字符在各种尺寸都好看,会需要越来越多的参数,在 tex 里的实现会越来越复杂、运行越来越耗时。其实也可以设计个新字体,只提供需要替换的符号。
下面的例子展示了,「好看」的 \cap (包括它的前后空白)不能只靠「画一个固定的图案、让它随字号放缩」实现。得有随字号变化的参数。
\documentclass{article}
\usepackage{amsmath}
\usepackage{tikz}
\makeatletter
\ExplSyntaxOn
\keys_define:nn { exam-zh / symbols }
{
cap-line-width .tl_set:N = \c__examzh_symbols_cap_line_width_tl,
cap-height .tl_set:N = \c__examzh_symbols_cap_height_tl,
cap-radius .tl_set:N = \c__examzh_symbols_cap_radius_tl,
}
\keys_set:nn { exam-zh / symbols }
{
cap-line-width = 0.06em,
cap-height = 0.56em,
cap-radius = 0.28em,
}
\ExplSyntaxOff
% \__examzh_symbols_cap:nnn {<line-width>} {<height>} {<radius>}
\csname cs_new:cn\endcsname {__examzh_symbols_cap:nnn}
{%
\begin{tikzpicture}[
line cap=round,
line width=#1,
baseline={([yshift=#1*2]current bounding box.south)}
]
\draw (#3,0) -- +(0,#2) arc (0:180:#3) -- +(0,-#2);
\end{tikzpicture}%
}
\ExplSyntaxOn
\cs_generate_variant:Nn \__examzh_symbols_cap:nnn {VVV}
\cs_set_eq:NN \__examzh_symbols_old_cap: \cap
\RenewDocumentCommand { \cap } { s }
{
\IfBooleanTF {#1}
{ \__examzh_symbols_old_cap: }
{
\__examzh_symbols_cap_wrap:n
{
\__examzh_symbols_cap:VVV
\c__examzh_symbols_cap_line_width_tl
\c__examzh_symbols_cap_height_tl
\c__examzh_symbols_cap_radius_tl
}
}
}
\cs_new:Nn \__examzh_symbols_cap_wrap:n
{
\mathrel { \mathpalette {} { \text {#1} } }
}
\ExplSyntaxOff
\makeatother
\begin{document}
\def\test#1{
{#1\par
\noindent\texttt{\detokenize{#1}}\par
\texttt{\string\cap*}: $A \cap* B_{A \cap* B_{A \cap* B}}$\par
\texttt{\string\cap\ }: $A \cap B_{A \cap B_{A \cap B}}$\par
}
}
\test\scriptsize
\test\normalsize
\test\Large
\end{document}

非常感谢!我马上去测试一下
注意,你要实现的,其实是参数化地设计字符,类似 https://www.zhihu.com/question/352249034/answer/870883390 。要想出来的字符在各种尺寸都好看,会需要越来越多的参数,在 tex 里的实现会越来越复杂、运行越来越耗时。其实也可以设计个新字体,只提供需要替换的符号。
好的,后续完善了之后会学习这方面的知识~
\documentclass{article}
\usepackage{amsmath}
\usepackage{tikz}
\makeatletter
% \__examzh_symbols_cap_symbol:nnn {<line-width>} {<height>} {<radius>}
\csname cs_new_protected:cn\endcsname {__examzh_symbols_cap_symbol:nnn}
{%
\begin{tikzpicture}[
line cap=round,
line width=#1,
baseline={([yshift=#1*2]current bounding box.south)}
]
\draw (#3,0) -- +(0,#2) arc (0:180:#3) -- +(0,-#2);
\end{tikzpicture}%
}
\ExplSyntaxOn
\NewCommandCopy \__examzh_symbols_old_cap: \cap
\RenewDocumentCommand { \cap } { s }
{
\IfBooleanTF {#1}
{ \__examzh_symbols_old_cap: }
{ \__examzh_symbols_cap: }
}
\cs_new_protected:Nn \__examzh_symbols_cap:
{
\mathrel
{
\mathchoice
{ \__examzh_symbols_cap_wrapper:nnnn {.06em} {.56em} {.28em} {0mu} }
{ \__examzh_symbols_cap_wrapper:nnnn {.06em} {.56em} {.28em} {0mu} }
{ \__examzh_symbols_cap_wrapper:nnnn {.05em} {.50em} {.25em} {2mu} }
{ \__examzh_symbols_cap_wrapper:nnnn {.04em} {.46em} {.23em} {2mu} }
}
}
% \__examzh_symbols_cap_wrapper:nnnn
% {<line-width>} {<height>} {<radius>} {<sep>}
\cs_new_protected:Nn \__examzh_symbols_cap_wrapper:nnnn
{
% mind the gap:
% https://github.com/latex3/latex3/issues/575#issuecomment-571342176
\mkern #4 % is mu really necessary here??
\text { \__examzh_symbols_cap_buffer:nnn {#1} {#2} {#3} }
\mkern #4
}
\cs_new:Nn \__examzh_symbols_cap_buffer:nnn
{
\exp_args:Nc \__examzh_symbols_cap_buffer:Nnnn
{ __examzh_cap_ \f@size _#1_#2_#3 } {#1} {#2} {#3}
}
\cs_new_protected:Nn \__examzh_symbols_cap_buffer:Nnnn
{
\cs_if_exist:NTF #1
{ \box_use:N #1 }
{
\box_new:N #1
\hbox_gset:Nn #1
{ $ \__examzh_symbols_cap_symbol:nnn {#2} {#3} {#4} $ }
\box_use:N #1
}
}
\ExplSyntaxOff
\makeatother
\begin{document}
\def\test#1{
{#1\par
\noindent\texttt{\detokenize{#1}}\par
\texttt{\string\cap*}:
$\displaystyle A \cap* B \textstyle A \cap* B_{A \cap* B_{A \cap* B}}$\par
\texttt{\string\cap\ }:
$\displaystyle A \cap B \textstyle A \cap B_{A \cap B_{A \cap B}}$\par
}
}
\foreach \i in {
\tiny, \scriptsize, \footnotesize, \small,
\normalsize,
\large, \Large, \LARGE, \huge, \Huge
} {\expandafter\test\i}
\end{document}

觉得……还是应该去造字体。
\csname cs_new_protected:cn\endcsname {__examzh_symbols_cap_symbol:nnn}
请问一下这里为什么要这样用,而不是
\ExplSyntaxOn
\cs_new_protected:cn \__examzh_symbols_cap_symbol:nnn #1#2#3
{...}
\ExplSyntaxOff
呢
觉得……还是应该去造字体。
这个超出了我目前的能力了(所以也是为什么一直还没处理这个符号的原因(我的锅)),暑假会去学习相关知识的。
感谢您的回复 @muzimuzhi
\csname cs_new_protected:cn\endcsname {__examzh_symbols_cap_symbol:nnn}请问一下这里为什么要这样用,而不是
\ExplSyntaxOn \cs_new_protected:cn \__examzh_symbols_cap_symbol:nnn #1#2#3 {...} \ExplSyntaxOff呢
应该是因为下面就可以不用把空格和冒号特殊处理了吧?
是。\__examzh_symbols_cap:nnn 的定义是一个 tikzpicture,让空格和冒号恢复默认 catcodes,写起来和读起来都更方便。
写成 \protected\@namedef{__examzh_symbols_cap:nnn} #1#2#3 {...} 也是可以的。
你举的 \cs_new_protected:cn \__examzh_symbols_cap_symbol:nnn #1#2#3 {...} 不大对。如果后面的 \__examzh_symbols_cap_symbol:nnn #1#2#3 {...} 不变,前面应该用 \cs_new_protected:Npn。
是。
\__examzh_symbols_cap:nnn的定义是一个 tikzpicture,让空格和冒号恢复默认 catcodes,更可读。写成
\protected\@namedef{__examzh_symbols_cap:nnn} #1#2#3 {...}也是可以的。你举的
\cs_new_protected:cn \__examzh_symbols_cap_symbol:nnn #1#2#3 {...}不大对。如果后面的\__examzh_symbols_cap_symbol:nnn #1#2#3 {...}不变,前面应该用\cs_new_protected:Npn。
感谢。是我疏忽了这个 p。平时都用 Npn , cpn,一下没看出问题。
提个建议,不要用类似于“中国化”的表达方式。