I am working on an extension to the exam class, and would like to add optional arguments to the \question command. Currently, the command takes one optional argument, the number of points the question should have.
The implementation of \question comes down to a \@doitem command (line 3584 of exam.cls):
\def\@doitem{\@ifnextchar[{\@readpoints}% {\item@points@pageinfo}% } That is, if there is an optional argument given, it is passed to \@readpoints, and if not, \item@points@pageinfo is expanded instead. The \@readpoints command reads the points and then expands \item@points@pageinfo. The latter command generates the question number and point designation.
Instead of a “free text” optional argument for the points, I would like to pass it to a points keyword argument. I would also prefer to set a question label with a label keyword argument, instead of an explicit \label command. The first argument needs to be processed before \@readpoints, and the second after \item@points@pageinfo. [Another use case is given in this question, where OP wants to add question metadata like the learning objective assessed.]
Here's what I have so far:
\tl_new:N \l_exam_points_tl \RenewDocumentCommand{\@doitem}{={points} +O{}}{ \tl_clear:N \l_exam_points_tl \keys_set_groups:nnn {exam/item} {points} {#1} \tl_if_empty:NTF \l_exam_points_tl { \item@points@pageinfo }{ \@readpoints[\l_exam_points_tl] } \keys_set_exclude_groups:nnn {exam/item} {points} {#1} } \keys_define:nn {exam/item} { points .tl_set:N = \l_exam_points_tl, points .groups:n = {points}, hello .code:n = {[Hello,~#1!]},% just for testing label .code:n = {\label{#1}}, unknown .meta:n = {points=\l_keys_key_str}, unknown .groups:n = {points}, } The argument specification to the new \@doitem command allows for an optional argument either as free text or as a series of keyvals (Section 2.9 of usrguide.pdf). The keys are scanned for any which set the points (Section 27.7 of interface3.pdf). If so, the value is saved in \l_exam_points_tl for passing to \@readpoints. The rest are deferred until after \item@points@pageinfo.
This works pretty well! If the optional argument text is points=5 or just 5, the points are set correctly. If the optional argument is label=foo, a label is set after the question counter is incremented. And label=foo,points=5 does both.
What doesn't work yet is an optional argument like label=foo,5. I was hoping the unknown key handler could handle that, by rerouting it to points. I tried to put the unknown key handler in the group that is processed before \@readpoints. But what I wrote has no effect.
I also tried unknown .code:n = {\tl_set:Nx \l_exam_points_tl {\l_keys_key_str}}, and I can tell by \cs_show:N that it does read and set \l_exam_points_tl to the correct value of the unknown key string. But it doesn't do it until after \item@points@pageinfo, which is too late. Without the unknown key handler, I get an error about an unknown key (naturally!)
So is there a way for unknown keys to be handled within a group, or otherwise have an unknown key processed in advance? “No” is a reasonable answer. If I am adding keyword arguments to a question that already has a point value as a free text argument, I can simultaneously add the points keyword. But I thought I'd ask to see if I understand the situation well.
Here is a complete document that illustrates my question.
\documentclass{exam} \title{Know your US States and Capitals!} \makeatletter \ExplSyntaxOn \DeclareCommandCopy\examorig@doitem\@doitem \tl_new:N \l_exam_points_tl \RenewDocumentCommand{\@doitem}{={points} +O{}}{ \tl_clear:N \l_exam_points_tl \keys_set_groups:nnn {exam/item} {points} {#1} \tl_if_empty:NTF \l_exam_points_tl { \item@points@pageinfo }{ \@readpoints[\l_exam_points_tl] } \keys_set_exclude_groups:nnn {exam/item} {points} {#1} \cs_show:N \l_exam_points_tl } \keys_define:nn {exam/item} { points .tl_set:N = \l_exam_points_tl, points .groups:n = {points}, hello .code:n = {[Hello,~#1!]}, label .code:n = {\label{#1}}, unknown .meta:n = {points=\l_keys_key_str}, % unknown .code:n = {\tl_set:Nx \l_exam_points_tl {\l_keys_key_str}}, unknown .groups:n = {points}, } \ExplSyntaxOff \makeatother \begin{document} \begin{questions} \question[label=CA,points=3] What is the capital of California? \begin{choices} \choice Los Angeles \choice Sacramento \choice San Francisco \choice San Diego \end{choices} \question[hello=world,label=TX] Which city serves as the capital of Texas? \begin{choices} \choice Dallas \choice Houston \choice Austin \choice San Antonio \end{choices} % freetext argument `2` is converted to `points=2` and processed \question[2] What is the capital of Florida? \begin{choices} \choice Miami \choice Orlando \choice Jacksonville \choice Tallahassee \end{choices} % non-keyword argument '3' is ignored, or rather it is read at the wrong time \question[label=NY,3] Which city is the capital of New York? \begin{choices} \choice Buffalo \choice Albany \choice New York City \choice Syracuse \end{choices} \question[hello=Donald,points=7] What is the capital of Nevada? \begin{choices} \choice Reno \choice Henderson \choice Carson City \choice Las Vegas \end{choices} % non-keyword argument '6' is ignored, or rather it is read at the wrong time \question[hello=Duck,6] Which city serves as the capital of Colorado? \begin{choices} \choice Denver \choice Boulder \choice Colorado Springs \choice Aurora \end{choices} \end{questions} \end{document}
label=foo,5as an argument,5would be a key, not a value. So, you would need to take care that you only pass keys to\@doitemif they consist of digits only.\keys_set_groups:nnnis that (citing thel3keysmanual) "[u]nknown keys are not assigned to any group and are thus never set". I assume that the assignment to a group does not work for theunknownkey. Maybe use different branches rather than groups.