我想用伪代码执行以下操作:
\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
是一个代表性的宏标记,其实际名称由用户选择。
\tcpeek\zz
-s\futurelet
将未来输入流的下一个标记*放入\zz
,未来输入流保持不被干扰。\tcpop\zz
- 从未来的输入流中吸收一个参数**,并将该参数作为的替换文本\zz
。\tcpopliteral\zz
- 类似于\tcpop
,这会从输入流中吸收一个参数。但是,在这种情况下,前导空格和任何括号分组都会保留在 中\zz
,因此\zz
包含来自输入流的文字标记。\tcpopto<tok>\zz
<tok>
-以 的方式 从未来输入流中删除标记,直到 出现为止\def\zz#1<tok><input stream>
。所有已删除的标记***(包括终止标记<tok>
)都将被\def
编入\zz
。\tcpush\zz
- 将替换文本\zz
作为输入流的下一个元素。\tcpushgroup\zz
- 的作用类似于\tcpush
,除了的替换文本\zz
被包裹在一个明确的括号组中。\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
组保护机制)。
**当\tcpop
ing 时,\ifspacepopped
将反映输入流中前导 cat-10 标记的出现(显式或隐式);但是,空白(显式)前导空格(又称空格)将随着参数被吸收而丢失,这是 TeX 的正常方式。相反,隐式空格将作为参数被吸收,这也是 TeX 的方式。宏\tcpop
不会穿透组末 } 或tokcycle
“转义字符” |
,而是返回空结果。
***与 TeX 一样,\tcpopto
如果群组吸收不平衡,就会中断。