The main problems are:
\tl_if_blank:nTF
expects a braced “normal argument” (n
type) directly containing the tokens to test, such as \tl_if_blank:nTF { abc~def } { true } { false }
or, if we are in a macro definition and argument #1
is a token list, \tl_if_blank:nTF {#1} { true } { false }
. But what you want to test here is the contents (value) of a token list variable. For this, you need \tl_if_blank:VTF
(the V
causes the value of the first argument to be passed to the base form \tl_if_blank:nTF
). Example:
\tl_if_blank:VTF \l__my_var_tl { true } { false }
When your \myQFormat
has been fully executed, its \group_end:
has restored the two token list variables \l__Qoptions_label_tl
and \l__Qoptions_sublabel_tl
to the values they had before the group started, i.e., empty here. So, when \question
uses these variables as part of the question format, they are both empty.
For the second point, the following code calls \keys_set:nn
inside the argument of \qformat
. This allows you to use the names \l__Qoptions_label_tl
and \l__Qoptions_sublabel_tl
in the format definition. I'll show another possible approach below.
As egreg noted, the \tl_if_blank:...
test for the label is not really needed, since the initial value of \l__Qoptions_label_tl
is set with label .initial:n = { Question }
(the braces may be omitted since there is no comma in Question
). The only case where this test would be useful is if you want to obtain “Question” as the label when using the option label=
or even label={ }
(indeed, “blank” means ”empty or spaces only” in this context, therefore if you pass an argument containing only space tokens, \tl_if_blank:nTF
will execute the “true” branch).
\documentclass[addpoints]{exam}
\usepackage{xparse}
\usepackage[xparse]{tcolorbox}
\renewcommand{\questionshook}{%
\setlength{\leftmargin}{0pt}%
\setlength{\labelwidth}{-\labelsep}%
}
\NewTColorBox{MarksTCBox} { O{} }{
left skip= 0pt,
right skip=0pt,
left=2pt,
right=2pt,
capture=hbox,
halign=center,
valign=center,
boxrule=0pt,
arc=0pt,
top=2pt,
bottom=2pt,
boxsep=0pt,
nobeforeafter,
box align = base,
baseline=4pt,
#1,
}
\ExplSyntaxOn
\keys_define:nn { Qoptions }
{
label .tl_set:N = \l__Qoptions_label_tl,
label .initial:n = { Question },
sublabel .tl_set:N = \l__Qoptions_sublabel_tl,
}
\cs_new_protected:Npn \Qoptions_question_header:n #1
{
\qformat
{
% \question appears to create a group, so the options are duly cleared
% when the question title has been typeset.
\keys_set:nn { Qoptions } {#1}
\textbf
{
\underline
{
\large
% Possible but not really needed (see above):
% \tl_if_blank:VTF \l__Qoptions_label_tl { Question }
% { \l__Qoptions_label_tl }
\tl_use:N \l__Qoptions_label_tl
\nobreakspace \thequestion \
\tl_if_blank:VF \l__Qoptions_sublabel_tl
{ [ \l__Qoptions_sublabel_tl ] \ }
\begin{MarksTCBox}
\scan_stop: [\totalpoints\ Marks]
\end{MarksTCBox}
}
}
\hfill % Otherwise, you'll have an Underfull \hbox for each question.
}
}
\NewDocumentCommand { \myQFormat } { O{} }
{
\Qoptions_question_header:n {#1}
}
\ExplSyntaxOff
\begin{document}
\begin{questions}
\myQFormat
\question[15]\hspace*{0pt}\vspace*{\baselineskip}
The output should be ``Question 1 [15 Marks]''
\myQFormat[label = Part]
\question[10]\hspace*{0pt}\vspace*{\baselineskip}
The output should be ``Part 2 [10 Marks]''
\myQFormat[label=Part, sublabel=Subtitle]
\question[5]\hspace*{0pt}\vspace*{\baselineskip}
The output should be ``Part 3 [Subtitle] [5 Marks]''
\myQFormat[sublabel=Subtitle]
\question[10]\hspace*{0pt}\vspace*{\baselineskip}
The output should be ``Question 4 [Subtitle] [10 Marks]''
\end{questions}
\end{document}

Other approach
Another way to avoid the problem due to the values of \l__Qoptions_label_tl
and \l__Qoptions_sublabel_tl
being restored too early (before \question
uses them) is as follows. You can pass the values of \l__Qoptions_label_tl
and \l__Qoptions_sublabel_tl
to \Qoptions_question_header:n
using a V
argument type. Since you have two “normal arguments” to pass, the function now has to be named \Qoptions_question_header:nn
and accept two arguments #1
and #2
(the label and sublabel). You can then use \cs_generate_variant:Nn \Qoptions_question_header:nn { VV }
to create the needed function variant (namely, \Qoptions_question_header:VV
), and call \Qoptions_question_header:VV \l__Qoptions_label_tl \l__Qoptions_sublabel_tl
in \myQFormat
to pass the values of \l__Qoptions_label_tl
and \l__Qoptions_sublabel_tl
to \Qoptions_question_header:nn
.
\documentclass[addpoints]{exam}
\usepackage{xparse}
\usepackage[xparse]{tcolorbox}
\renewcommand{\questionshook}{%
\setlength{\leftmargin}{0pt}%
\setlength{\labelwidth}{-\labelsep}%
}
\NewTColorBox{MarksTCBox} { O{} }{
left skip= 0pt,
right skip=0pt,
left=2pt,
right=2pt,
capture=hbox,
halign=center,
valign=center,
boxrule=0pt,
arc=0pt,
top=2pt,
bottom=2pt,
boxsep=0pt,
nobeforeafter,
box align = base,
baseline=4pt,
#1,
}
\ExplSyntaxOn
\keys_define:nn { Qoptions }
{
label .tl_set:N = \l__Qoptions_label_tl,
label .initial:n = { Question },
sublabel .tl_set:N = \l__Qoptions_sublabel_tl,
}
\cs_new_protected:Npn \Qoptions_question_header:nn #1#2
{
\qformat
{
\textbf
{
\underline
{
\large
% Possible but not really needed (see above):
% \tl_if_blank:nTF {#1} { Question } {#1}
#1 \nobreakspace \thequestion \
\tl_if_blank:nF {#2} { [ #2 ] \ }
\begin{MarksTCBox}
\scan_stop: [\totalpoints\ Marks]
\end{MarksTCBox}
}
}
\hfill % Otherwise, you'll have an Underfull \hbox for each question.
}
}
\cs_generate_variant:Nn \Qoptions_question_header:nn { VV }
\NewDocumentCommand { \myQFormat } { O{} }
{
\group_begin:
\keys_set:nn { Qoptions } {#1}
\Qoptions_question_header:VV \l__Qoptions_label_tl \l__Qoptions_sublabel_tl
\group_end:
}
\ExplSyntaxOff
\begin{document}
\begin{questions}
\myQFormat
\question[15]\hspace*{0pt}\vspace*{\baselineskip}
The output should be ``Question 1 [15 Marks]''
\myQFormat[label = Part]
\question[10]\hspace*{0pt}\vspace*{\baselineskip}
The output should be ``Part 2 [10 Marks]''
\myQFormat[label=Part, sublabel=Subtitle]
\question[5]\hspace*{0pt}\vspace*{\baselineskip}
The output should be ``Part 3 [Subtitle] [5 Marks]''
\myQFormat[sublabel=Subtitle]
\question[10]\hspace*{0pt}\vspace*{\baselineskip}
The output should be ``Question 4 [Subtitle] [10 Marks]''
\end{questions}
\end{document}
Same output as above.