1

I would like to understand what makes the following not work, and how to fix it in order to get the desired output as given under each call of the command \myQFormat.

\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:Npn \Question_header:n #1
{
    \qformat{
        \textbf{
            \underline{
                \large \tl_if_blank:nTF {\l__Qoptions_label_tl} { Question } { \l__Qoptions_label_tl }~
                (\thequestion)\ \IfValueT{\l__Qoptions_sublabel_tl}{[\l__Qoptions_sublabel_tl]\ }
                \begin{MarksTCBox}
                    \scan_stop: [\totalpoints\ Marks]
                \end{MarksTCBox}
            }
        }
    }
}

\NewDocumentCommand { \myQFormat } { O{} }
{
    \group_begin:
    \keys_set:nn { Qoptions } { #1 }
    \Question_header:n { #1 }
    \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}

2 Answers 2

1

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}

enter image description here

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.

9
  • Many thanks for the clarification. I have some inquiries if you don't mind: 1- does setting an initial value of label make sense or redundant when I have this line \tl_if_blank:VTF \l__Qoptions_label_tl { Question } { \l__Qoptions_label_tl } ? 2- what does \cs_new_protected:Npn mean? I mean its description in words like this is a function that accepts one input braced argument and uses it internally without braces. I tried to take a look at expl3 manual but it is not that friendly.
    – Diaa
    May 10, 2020 at 12:27
  • 1
    1) Yes, just try replacing label .initial:n = { Question } with label .initial:n = { XXX } and you'll see the difference. This defines \l__Qoptions_label_tl at the earliest place (when \keys_define:nn { Qoptions } {...} is executed). It is used when the label option is not passed to \myQFormat. 2) \cs_new_protected:Npn is similar to \protected\long\def, but in expl3 style, and errors out if the function is already defined. It is one of the ways to define a code-level function (as opposed to document-level ones defined with xparse's \NewDocumentCommand and friends).
    – frougon
    May 10, 2020 at 12:45
  • 1
    The p in \cs_new_protected:Npn means we must write the parameter text after the function name (e.g., #1#2 in the second approach), just as with \def. It's simple work and makes reading the definition by TeX slightly faster than with \cs_new_protected:Nn. The protected makes it so that if the function your are defining (such as \Qoptions_question_header:n or \Qoptions_question_header:nn here) is used in an expansion-only context (e.g., \edef\x{\Qoptions_question_header:n{...}} or \write\somestream{\Qoptions_question_header:n{...}},
    – frougon
    May 10, 2020 at 12:52
  • 1
    or inside x and e-type arguments in expl3), it won't expand. This is useful because \Qoptions_question_header:n and \Qoptions_question_header:nn contain several “unexpandable macros” that are only useful to expand in a typesetting context (\textbf is one of them). In general, you should define macros as \protected unless you know they will behave properly in an expansion-only context—this is what xparse does with \NewDocumentCommand and friends, as opposed to \NewExpandableDocumentCommand and friends.
    – frougon
    May 10, 2020 at 12:52
  • Your comments are as informative as your original answer. I need to clear my mind in order to fully understand what you have written. I highly appreciate your willingness to explain every issue I raised. I hope you don't mind if I come back in the future with more inquiries :).
    – Diaa
    May 10, 2020 at 12:55
1

When you call \tl_if_blank:nTF { \l__Qoptions_label_tl } you always get false, because the argument is not empty.

With your new approach you just do \tl_use:N \l__Qoptions_label_tl, because the variable will always contain the desired string: either the initial value or what has been specified with the label key.

Using \IfValueT is good only if you pass a specific argument. With the new approach you test whether the value stored in the sublabel variable is empty (which means it hasn't been set). You can test whether if the sublabel key has received a value with \tl_if_blank:V(TF) (the parentheses mean that you can use T, F or TF and supply the appropriate number of arguments).

\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
                \tl_use:N \l__Qoptions_label_tl
                \c_space_tl
                \thequestion
                \c_space_tl
                \tl_if_blank:VF \l__Qoptions_sublabel_tl
                  { [ \l__Qoptions_sublabel_tl ] \c_space_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}

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .