增加枚举子定理环境,并正确处理引用
目录:
需求:希望可以在theorem
环境中生成列表,并直接引用列表项的标签时,自动附上对应theorem
的序号。
例如,Theorem 1 (ii) blah
应当引用为1 (ii)
。最好还可以支持cleveref
的自动排序。
获得父环境
参考:如何知道当前环境的父环境 - StackExchange
- 首先问题是如何获得当前环境。
\begin{<environment>}
会将<environment>
赋值给\@currenvir
,随后执行\<environment>
。故而我们只需保存\@currenvir
即可。 - 现在我们来将其保存到
\@parentenvir
。合适的时间节点当然是执行内环境的\begin
时,此时已经进入\begingroup
,因此所有定义仅在局部生效;用\end
离开当前环境后,\@parentenvir
变回原来含义(即,仍然是相对于现在环境的父环境)。 \begin
脆弱,因此其被翻译为\expandafter\protect\csname begin \endcsname
,即\begin
(意会一下,这里最后的空格也是命令的一部分),我们实际要修改的是\begin
。- 所以我们……调包吧。
xpatch
包允许我们直接修改被保护的命令,详见patchcmd和xpatch用法详解 - StackExchange。因此我们只需将\begin
中的\begingroup
替换为\begingroup\let\@parentenvir\@currenvir
即可。$\LaTeX$命令:\xpatchcmd{\begin}{\begingroup}{\begingroup\let\@parentenvir\@currenvir}{}{}
生成子环境标签
因为我不想对每个定理类型都定义一个枚举环境,因此我的整体思路是,在进入thmenum
时获取父环境并重定义\label
。\newlist{thmenum}{enumerate}{2}
允许我们新定义thmenum
环境;随后我们使用如下代码初始化环境并定义标签和引用格式。
\setlist[thmenum,1]{
label=(\alph*),
before=\setsublabel[lemma]
}
\setlist[thmenum,2]{
label=(\roman*),
ref=(\alph{thmenumi}.\roman*)
}
before=
允许我们在进入thmenum
环境时执行一段代码,因此我们需要在这里重定义\label
。不过我没能在这里直接写代码,因此我将要添加的代码抽出为\setsublabel
。以下假定父环境是<parent>
,并详细分析setsublabel
的定义。
简单版本
使用\item
时会调用\refstepcounter
,后者则会重定义\@currentlabel
。\@currentlabel
将被用为执行\ref
时输出的标签。因此我们只需要重新定义\@currentlabel
即可。为了方便用户修改,我将修改逻辑抽象为\deal@currentlabel
函数,\kern +.2222em
用来添加4mu
的不可断空格。lemma
在这里是定理编号的计数器,可按需修改。
考虑到我们并不是所有时候都需要引用完整的编号,我额外提供了sub@<label>
版本的标签,用来只引用不带定理编号的标签。
此代码无需修改即兼容hyperref
。
完整代码(这个与成品不同,因为我脱掉了最外层的\providecommand{\setsublabel}[1][theorem]
。\setsublabel
的唯一参数接受定理的计数器名):
\providecommand{\deal@currentlabel}[2]{\csname the#1\endcsname\kern +.2222em #2}
\let\old@label\label
\renewcommand{\label}[1]{%
\old@label{sub@#1}%
\begingroup
\let\old@currentlabel\@currentlabel
\edef\@currentlabel{\deal@currentlabel{lemma}\old@currentlabel}%
\old@label{#1}%
\endgroup
}
添加对cleveref
支持
参考:
- 定义方式:如何正确实现cleveref的\cref@currentlabel - StackExchange
- 排序方式:cleveref不能对嵌套列表正确排序 - StackExchange
- 公式tag:cleveref不排序自定义tag - StackExchange
各个包实现其功能的原理:
- $\LaTeX$原生的
\label{<label>}
会在.aux
文件中新增一行\newlabel{<label>}{<data>}
。 hyperref
包修改了\label
写入.aux
的<data>
,在其中插入了很多自己的信息。cleveref
采用的方式与其不同:对于\label{<label>}
,cleveref
会额外写入一行\newlabel{<label>@cref}{<data>}
,在其中放入cleveref
需要的东西。每次引用计数时,其需要的东西会置于\cref@currentlabel
中。
简单说,\cref@currentlabel
形如[#1][#2][#3]#4
。其中
#4
对应\@currentlabel
。#1
储存的是定理类型。#2
和#3
用来排序。#3
标识父层计数器的值(原文:会导致当前计数器重置的计数器的值);逗号分隔(如1,3,4
可能表示Section 1.3.4
)。排序时顺序比较(注意,是“数串”的字典序而非“字符串”的字典序)。#2
为单个数字,表示当前计数器的值。对于相同#3
应当仅有一个#2
,否则会导致合并(即定理xx至xx
)时出错。
更改引用显示
首先处理第4个参数(引用显示):直接用\@currentlabel
覆盖。
\def\deal@cref@currentlabel[#1][#2][#3]#4\relax{[#1][#2][#3]\@currentlabel}%
\let\old@currentlabel\cref@currentlabel
\edef\cref@currentlabel{\expandafter\deal@cref@currentlabel\old@currentlabel\relax}%
更改环境类型
处理第1个参数(环境类型)。首先获取父标签,然后直接用label
传进去就可以啦。
\let\old@label\label@optarg
\let\thmenum@parent\@parentenvir
\old@label[\thmenum@parent]{sub@<label>}
更改排序策略
由于第2个参数无需改动,我们只需关注第3个参数。 开始解决问题:
- 本来我们的问题很简单:给
#3
缀上定理的序号即可。不过……enumitem
和cleveref
不兼容!也就是说,用enumitem
生成的列表,#3
会完全置空。不知道enumitem
和cleveref
哪个会去解决兼容性问题……(如果谁想去提issue欢迎去提,我懒了……) #3
由\cref@resetby
生成,但是\cref@resetby
是硬编码的,只会检测是否被指定计数器重置……(真坑)。作者说,他看不出任何必要将其改为按照列表查找。好吧,我们这边就改成按列表查找。- 我们需要告诉
cleveref
,thmenum
会被theorem
重置。这个由\@addtoreset
完成。但是似乎\@addtoreset
命令无效。 - 所以我也选择硬编码……而且我还不知道计数器的重置关系(因为似乎
\@addtoreset
命令无效),所以我顺带硬编码了计数器的重置关系。优雅一点,用列表解决…… \cref@resetby@list
格式为若干<ChildEnvironment>,<ParentEnvironment>;
,最后的分号不能省略。遇到空<ChildEnvironment>
即认为列表结束。 列表读取方法如下:- 检查当前
<ChildEnvironment>
,若空丢弃列表剩余部分。 - 检查当前
<ChildEnvironment>
,若为#1
,定义#2
并丢弃列表剩余部分。 - 检查下一个
<ChildEnvironment>,<ParentEnvironment>;
对。
- 检查当前
对应$\LaTeX$代码:
\def\cref@resetby@list{thmenumii,thmenumi;thmenumi,lemma;}
\let\cref@old@resetby\cref@resetby
\def\cref@resetby#1#2{%
\let#2\relax%
\def\@eatall##1\relax{}%
\def\@tmpcode##1,##2;{%
\let\@next\@eatall
\ifx,##1,\else
\ifnum\pdfstrcmp{#1}{##1}=\z@
\message{Enter @tmpcode, input=##1,##2; command=#1^^J}%
\def#2{##2}%
\else
\let\@next\@tmpcode
\fi
\fi
\@next
}%
\expandafter\@tmpcode\cref@resetby@list,;\relax
\ifx#2\relax%
\cref@old@resetby{#1}{#2}%
\fi
}
成品
以下是可供修改的接口:
命令 | 参数 | 意义 |
---|---|---|
\deal@currentlabel#1#2 |
#1: 父计数器名; #2: 当前\@currentlabel 值 |
修改\@currentlabel |
\deal@cref@currentlabel[#1][#2][#3]#4 |
当前\cref@currentlabel 值 |
修改\cref@currentlabel |
\cref@resetby@list |
字符串,使用\def 重定义 |
子环境-父环境配对 |
重定义了\label
,用法和原先一致。
提供命令:\setsublabel
,接受一个可选参数(默认值theorem
),为父计数器名,用法为每次进入环境thmenum
时调用。例:
\newlist{thmenum}{enumerate}{2}
\setlist[thmenum,1]{
label=(\alph*),
before=\setsublabel[lemma]
}
\setlist[thmenum,2]{
label=(\roman*),
ref=(\alph{thmenumi}.\roman*)
}
代码
\documentclass{article}
\usepackage{amsmath,amsthm,xpatch}
\newtheorem{lemma}{Lemma}[section]
\usepackage{enumitem}
\usepackage{hyperref}
\usepackage{cleveref}
\makeatletter
\providecommand{\deal@currentlabel}[2]{\csname the#1\endcsname\kern +.2222em #2}
\@ifpackageloaded{cleveref}{
\crefname{thmenumi}{item}{items}
\crefalias{thmenumii}{thmenumi}
\xpatchcmd{\begin}{\begingroup}{\begingroup\let\@parentenvir\@currenvir}{}{}
\def\cref@resetby@list{thmenumii,thmenumi;thmenumi,lemma;}
\let\cref@old@resetby\cref@resetby
\def\cref@resetby#1#2{%
\let#2\relax%
\def\@eatall##1\relax{}%
\def\@tmpcode##1,##2;{%
\let\@next\@eatall
\ifx,##1,\else
\ifnum\pdfstrcmp{#1}{##1}=\z@
\message{Enter @tmpcode, input=##1,##2; command=#1^^J}%
\def#2{##2}%
\else
\let\@next\@tmpcode
\fi
\fi
\@next
}%
\expandafter\@tmpcode\cref@resetby@list,;\relax
\ifx#2\relax%
\cref@old@resetby{#1}{#2}%
\fi
}
\def\deal@cref@currentlabel[#1][#2][#3]#4\relax{[#1][#2][#3]\@currentlabel}%
\providecommand{\setsublabel}[1][theorem]{%
\let\old@label\label@optarg
\let\thmenum@parent\@parentenvir
\renewcommand{\label}[2][\thmenum@parent]{%
\old@label[#1]{sub@##2}%
\begingroup
\let\old@currentlabel\@currentlabel
\edef\@currentlabel{\deal@currentlabel{#1}\old@currentlabel}%
\let\old@currentlabel\cref@currentlabel
\edef\cref@currentlabel{\expandafter\deal@cref@currentlabel\old@currentlabel\relax}%
\old@label[#1]{##2}%
\endgroup
}%
}
}{
\providecommand{\setsublabel}[1][theorem]{%
\let\old@label\label
\renewcommand{\label}[1]{%
\old@label{sub@##1}%
\begingroup
\let\old@currentlabel\@currentlabel
\edef\@currentlabel{\deal@currentlabel{#1}\old@currentlabel}%
\old@label{##1}%
\endgroup
}%
}
}
\makeatother
\newlist{thmenum}{enumerate}{2}
\setlist[thmenum,1]{
label=(\alph*),
before=\setsublabel[lemma]
}
\setlist[thmenum,2]{
label=(\roman*),
ref=(\alph{thmenumi}.\roman*)
}
\begin{document}
\begin{lemma}
\label{0}
\begin{thmenum}[start=4]
\item q\label{1}q
\begin{thmenum}[start=3]
\item q\label{2}q
\end{thmenum}
\end{thmenum}
\end{lemma}
\begin{lemma}
\label{4}
\end{lemma}
\ref{0},\ref{1},\ref{2},\ref{4} \\
\ref{sub@1},\ref{sub@2} \\
\cref{0,1,2,4} \\
\cref{sub@1,sub@2}
\end{document}