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}
Diaa
  • 9,599

2 Answers2

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.

frougon
  • 24,283
  • 1
  • 32
  • 55
  • 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 '20 at 12:27
  • 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 '20 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 '20 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 '20 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 '20 at 12:55
  • In order to really understand the protected thing, you probably need to search the site for “fully expandable” and “protected macros”. It's probably too difficult to make it clear in two or three comments (well, depending on what you already know). But if it's too complicated, just always use protected until you stumble on a problem which makes you realize that you don't want it. An example where you don't want it is if you define a macro that is used in an x-type argument (see below). – frougon May 10 '20 at 13:12
  • For instance: `\cs_new:Npn \Qoptions_foo:n #1 { \str_case:nnF {#1} { {A} {output for A} {B} {output for B} } {error} }

    \tl_set:Nx \l_tmpa_tl { Bla \Qoptions_foo:n {A} bla... }. Here,\tl_set:Nxuses\Qoptions_foo:ninside\edefbehind the scenes for thex-type argument, and in order for\Qoptions_foo:nto expand in this context (inside\edef), it must be a non-\protected` macro.

    – frougon May 10 '20 at 13:12
  • I modified the answer a bit regarding the \tl_if_blank:... test for the label (little simplification noticed by egreg). – frougon May 10 '20 at 14:13