目录:

需求:希望可以在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支持

参考:

各个包实现其功能的原理:

  • $\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缀上定理的序号即可。不过……enumitemcleveref不兼容!也就是说,用enumitem生成的列表,#3会完全置空。不知道enumitemcleveref哪个会去解决兼容性问题……(如果谁想去提issue欢迎去提,我懒了……)
  • #3\cref@resetby生成,但是\cref@resetby是硬编码的,只会检测是否被指定计数器重置……(真坑)。作者说,他看不出任何必要将其改为按照列表查找。好吧,我们这边就改成按列表查找。
  • 我们需要告诉cleverefthmenum会被theorem重置。这个由\@addtoreset完成。但是似乎\@addtoreset命令无效。
  • 所以我也选择硬编码……而且我还不知道计数器的重置关系(因为似乎\@addtoreset命令无效),所以我顺带硬编码了计数器的重置关系。优雅一点,用列表解决……
  • \cref@resetby@list格式为若干<ChildEnvironment>,<ParentEnvironment>;,最后的分号不能省略。遇到空<ChildEnvironment>即认为列表结束。 列表读取方法如下:
    1. 检查当前<ChildEnvironment>,若空丢弃列表剩余部分。
    2. 检查当前<ChildEnvironment>,若为#1,定义#2并丢弃列表剩余部分。
    3. 检查下一个<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}