LaTeX3:以优雅的方式向前引用具有“未来”值的计数器

LaTeX3:以优雅的方式向前引用具有“未来”值的计数器

我想用伪代码执行以下操作:

\begin{pointtracker}{Advanced Geology}
  \begin{itemize}
    \item Count the fingers on your left hand. Are there more than 9? \addpoints{1}
    \item Draw a rectangle with five edges. \addtopoints{3}
  \end{itemize}
\end{pointtracker}

生产


高级地质学:4分

  • 数一数你左手的手指,有超过 9 根吗?(1 分)
  • 画一个有五条边的矩形 (3 分)

xcntperchap软件包(遗憾的是在当前的 Ubuntu 上已损坏)通过将计数器写入外部文件(使用\iow_write)并从那里检索它们来实现这一点。它使用 LaTeX 结构级别作为逻辑层,在其中保存并适当重置计数。每当一个点发生变化时,它都需要运行两次 LaTeX。

我绝对可以将其适应用例,但由于我仍然必须实施一种变通方法才能xcntperchap在运行 Ubuntu 20.10 的同事的计算机上工作,所以我还不如自己实现一些不那么通用的东西(我想),努力自学一点 LaTeX3。(到目前为止,进展顺利。)

那么,是否真的有必要将内容写入外部文件以便稍后在第二次运行中“前向引用”?

expl3例如,我是否可以使用或其他现代工具来扩展函数的内容,从而执行所有\addpoints函数(LaTeX3 意义上的“函数”),并在实际将扩展放入代码之前得到总点数。从而避免运行两次?

答案1

你可以吸收环境中的内容,并进行两次加工,一次放入将被丢弃的盒子中,赋予\addtopoints不同的含义,即加分。

接下来,您将获得总数并可以打印所有内容。

\documentclass{article}
%\usepackage{xparse} % not needed for LaTeX 2020-10-01 or later

\ExplSyntaxOn

\NewDocumentEnvironment{pointtracker}{m+b}
 {
  % typeset the body in a box that's then discarded
  \marcus_points_count:n { #2 }
  \subsubsection*{#1: ~ \int_eval:n { \g__marcus_points_int } ~ points }
  #2
 }{}
\NewDocumentCommand{\addtopoints}{m}
 {
  \__marcus_points_print:n { #1 }
 }

\box_new:N \l__marcus_points_box
\int_new:N \g__marcus_points_int

\cs_new_protected:Nn \__marcus_points_print:n { (#1\nobreakspace points) }
\cs_new_protected:Nn \__marcus_points_add:n { \int_gadd:Nn \g__marcus_points_int { #1 } }

\cs_new_protected:Nn \marcus_points_count:n
 {
  \int_gzero:N \g__marcus_points_int
  \vbox_set:Nn \l__marcus_points_box
   {
    % the box that will be discarded, here \addtopoints just adds
    \cs_set_eq:NN \addtopoints \__marcus_points_add:n
    #1
   }
 }

\ExplSyntaxOff

\begin{document}

\begin{pointtracker}{Advanced Geology}
  \begin{itemize}
    \item Count the fingers on your left hand. Are there more than 9? \addtopoints{1}
    \item Draw a rectangle with five edges. \addtopoints{3}
  \end{itemize}
\end{pointtracker}

\end{document}

在此处输入图片描述

或者,您可以利用\label-\ref机制

\documentclass{article}
%\usepackage{xparse} % not needed for LaTeX 2020-10-01 or later

\ExplSyntaxOn

\NewDocumentEnvironment{pointtracker}{m}
 {
  \int_gincr:N \g__marcus_points_env_int
  \int_gzero:N \g__marcus_points_int
  \subsubsection*{ #1: ~ \ref{ points@\int_eval:n { \g__marcus_points_env_int } } ~ points }
 }
 {
  \tl_set:cx { @currentlabel } { \int_eval:n { \g__marcus_points_int } }
  \label { points@\int_eval:n { \g__marcus_points_env_int } }
 }

\NewDocumentCommand{\addtopoints}{m}
 {
  (#1\nobreakspace points)
  \int_gadd:Nn \g__marcus_points_int { #1 }
 }

\int_new:N \g__marcus_points_env_int
\int_new:N \g__marcus_points_int

\ExplSyntaxOff

\begin{document}

\begin{pointtracker}{Advanced Geology}
  \begin{itemize}
    \item Count the fingers on your left hand. Are there more than 9? \addtopoints{1}
    \item Draw a rectangle with five edges. \addtopoints{3}
  \end{itemize}
\end{pointtracker}

\end{document}

每个pointtracker环境都会步进一个内部计数器,用于定义唯一标签。总点数存储在其中\@currentlabel,这将是\ref下次运行 LaTeX 时使用的。当然,如果您在现有环境之间添加环境,这可能需要多次运行才能稳定下来。

答案2

编辑后,也可以通过嵌套环境来计算全局点pointtracker。这个问题启发了该tokcycle包未来的改进。请参阅补充

tokcycle包提供了一种优雅的方式来实现这一点。它传递输入的所有标记并接受如何处理它们的指令。默认情况下,它将它们回显到\cytoks标记列表中,然后在环境的末尾进行排版。

它将标记分为 4 个类别(字符、组、宏和空格),并允许每个类别使用不同的指令。由于这里我们需要在遇到时执行某些操作\addtopoints,因此我们使用来\Macrodirective完成它。

在环境中,遇到令牌时,pointracker令牌会被回显到令牌列表中。但是,如果遇到令牌,它\cytoks\addtopoints此外调用 magic 宏\z(感谢 David Carlisle)。从编写包的过程中,我知道输入流中的下一个标记将是\@tokcycle <continuation of input stream>,这告诉包继续通过标记循环处理输入流(我们知道输入流中的下一个内容将是 的参数\addtopoints,即{<number>})。

\z所做的就是重新排列输入流,使\@tokcycle{2}变成\addtocounter{pointcount}{2}\@tokcycle{2},从而实现我们期望的目标,即在标记被排版之前,在标记循环期间计算点数。当它最终开始排版 时\the\cytoks,它们将是输入流的准确标记,但会预先知道\thepointcount

我现在已向环境添加了一个选项,用于指定应用于数字的标签(默认points)。这将允许在嵌套环境中使用total points作为外部环境中的可选参数。还可以将大小修饰符应用于强制参数,也许会使外部环境大于默认的\Large

我再次强调,这里只发生了一次 LaTeX 编译。但是,每个pointtracker环境都会在排版之前循环遍历其环境的标记以查找点分配,这可以视为一种双重传递的形式。

\documentclass{article}
\usepackage{tokcycle,environ}
\newcounter{pointcount}
\newcommand\addtopoints[1]{(\textit{#1 point}%
  \ifnum#1=1\relax\else\textit{s}\fi)}
\def\z#1#2{\addtocounter{pointcount}{#2}#1{#2}}
\NewEnviron{pointtracker}[2][points]{%
  \par\bigskip\resettokcycle
  \setcounter{pointcount}{0}%
  \Macrodirective{\addcytoks{##1}\tctestifx{\addtopoints##1}{\z}{}}%
  \def\tmp{\tokencyclexpress{\Large\bfseries #2: \thepointcount{} #1}}%
  \expandafter\tmp\BODY\endtokencyclexpress
}
\begin{document}
\begin{pointtracker}[total points]{\LARGE Science Test}
\begin{pointtracker}{Advanced Geology}
  \begin{itemize}
    \item Count the fingers on your left hand. Are there more than 9?
          \addtopoints{1}
    \item Draw a rectangle with five edges. \addtopoints{3}
  \end{itemize}
\end{pointtracker}
\bigskip
In this next phase of the test, feel free to use your calculator.
\begin{pointtracker}{Meteorology}
  \begin{itemize}
    \item How many borgs does it take to brew a Danish beer?
          \addtopoints{2}
    \item What is the meaning of life? \addtopoints{4}
  \end{itemize}
\end{pointtracker}
\end{pointtracker}
\end{document}

在此处输入图片描述


补充

上面的答案让我开始思考在 使用的指令中构建某种前瞻功能的实用性tokcycle。例如,在这个问题中,如果没有前瞻功能,我将被迫在\addtopoints宏指令中定义并设置一个标志,然后在组指令中留意该标志,以便读取后面的参数,其中包含我希望计数的数字。然后重置标志。这种方法有效,但可能很麻烦。

我上面使用的技巧\z是基于我对 的了解tokcycle,而普通用户显然不具备这些知识。如果能够定义一个编程层来完成这类任务,岂不是更好、更有用吗?也许可以使用如下语法:

\def\z{\tcpop\Q\addtocounter{pointcount}{\Q}\tcpushgroup{\Q}}

本质上,从输入流弹出一个参数,将数字添加到我的计数中,然后将该参数推回到输入流(带括号)。这里,\Q只是一个用户可以选择的本地宏名称。

随着 V1.4 的推出tokcycle[2021-05-27],上述语法成为现实。最重要的是,new\z可以在指令的任何地方调用,而不仅仅是“在末尾”,就像我上面的硬连线方法所要求的那样。

以下是 v1.4 中新功能的摘要:

虽然在令牌循环中对令牌的正常处理会提供有关令牌的隐式/显式/活动性质的非常详细的信息,但下面描述的前瞻功能在辨别方面却远没有那么详尽。它们旨在用于当人们已经知道输入流中即将出现哪种令牌时。

在下面的描述中,\zz是一个代表性的宏标记,其实际名称由用户选择。

  1. \tcpeek\zz-s \futurelet将未来输入流的下一个标记*放入\zz,未来输入流保持不被干扰。

  2. \tcpop\zz- 从未来的输入流中吸收一个参数**,并将该参数作为的替换文本\zz

  3. \tcpopliteral\zz- 类似于\tcpop,这会从输入流中吸收一个参数。但是,在这种情况下,前导空格和任何括号分组都会保留在 中\zz,因此\zz包含来自输​​入流的文字标记。

  4. \tcpopto<tok>\zz<tok>-以 的方式 从未来输入流中删除标记,直到 出现为止\def\zz#1<tok><input stream>。所有已删除的标记***(包括终止标记<tok>)都将被\def编入\zz

  5. \tcpush\zz- 将替换文本\zz作为输入流的下一个元素。

  6. \tcpushgroup\zz- 的作用类似于\tcpush,除了的替换文本\zz被包裹在一个明确的括号组中。

  7. \tcpopwhitespace\zz- 若要查看输入流运行头处的空白之外的内容,而不吸收后面的内容,可以使用此宏吸收空白(表示一个显式空格标记的显式连续空格)。此时,可以使用\tcpeek. 探测后面的内容\zz,如果吸收了空白,则将包含空格,否则将为空。此宏不会吸收隐式空格。

此外,\tcappto#1from#2允许将 的替换文本#2附加到 的替换文本中#1。此宏还与 和 两种弹出形式相结合\tcpopappto\tcpopliteralappto其中from#2被视为输入流,弹出的标记附加到所提供宏的替换文本中。

上述宏将在 Character、Macro 和 Space 指令(而不是 Group 指令)中使用。它们可以帮助在遇到特定宏时预览参数,确定空格是否是输入流中的下一个标记,在一个指令的上下文中执行跨越多个输入标记的操作。我相信用户会想到更多用途。

一般情况下,使用偷看的标记来做出决策,但不要将偷看的标记输出到\cytoks,因为每次调用该指令时,指令中使用的标记都会重新分配。当\cytoks最终排版时,只保留最终的分配。

例如\tcpopto,吸收,现在,未来选修的参数放入\B,包括括号: \tcpeek\Q\ifx[\Q\tcpopto]\B\fi

注意:如果需要将弹出的前瞻标记保存到\cytoks输出流,则需要进行一次扩展,因为您需要弹出宏的替换文本。因此,\addcytoks[1]{\zz}。如果被\tcpop'ed 的前瞻标记是组的一部分(即,如果紧接在前的标记\tcpeek显示 \ifx与 等价\bgroup),则有两种方法可以保留 内的分组\cytoks\groupedcytoks{\addcytoks[1]{\zz}}\addcytoks[1]{\expandafter{\zz}}。另一种方法是将它们\tcpush\tcpushgroup回输入流,以便由适当的指令重新处理。更好的是,使用\tcpopliteral将它们从输入流中提取出来,同时保持它们的分组(和前导空格)完好无损。

\tcdepth*当输入流以其他方式显示下一个标记时,与当前分组末尾相关的显式 cat-2 括号\tcpeek\zz将显示\ifx\zz\empty为真(这是一种tokcycle组保护机制)。

**当\tcpoping 时,\ifspacepopped将反映输入流中前导 cat-10 标记的出现(显式或隐式);但是,空白(显式)前导空格(又称空格)将随着参数被吸收而丢失,这是 TeX 的正常方式。相反,隐式空格将作为参数被吸收,这也是 TeX 的方式。宏\tcpop不会穿透组末 } 或tokcycle“转义字符” |,而是返回空结果。

***与 TeX 一样,\tcpopto如果群组吸收不平衡,就会中断。

相关内容