T-F-S / tcolorbox

A LaTeX package to create highly customizable colored boxes.
http://www.ctan.org/pkg/tcolorbox
LaTeX Project Public License v1.3c
213 stars 15 forks source link

Enforce some checks on formatters passed to `index key formatter` and the like #265

Open muzimuzhi opened 5 months ago

muzimuzhi commented 5 months ago

In v6.2.0, passing an empty value to a formatter key like \tcbset{index key formatter=} would end with a partial \if..., silently.

\documentclass{article}
\usepackage{tcolorbox}
\tcbuselibrary{documentation}

\show\currentiflevel % 0
\tcbset{index key formatter=}
\show\currentiflevel % 1

\begin{document}
\end{document}

This is because the key uses \let <internal-macro> #1 (not \def), where #1 is the value passed in, after some sanitization. Then if an empty value is given, #1 is empty and \let eats a token which belongs to the pgfkeys internals, here happens to be an \fi.

In v6.2.0 there are three such keys:

Example below shows one possible way to guard against invalid formatters. \let is now only executed when the #1 contains exactly a single control sequence, otherwise an error is raised.

The helper conditional function \__tcobox_if_single_cs:n(TF) is the same as \__tcobox_if_valid_box_name:n(TF) proposed in #264, but with a more general name.

\documentclass{article}
\usepackage{tcolorbox}
\tcbuselibrary{documentation, theorems}

\makeatletter
\ExplSyntaxOn
\tcbset{
  % defined in "documentation" library
  index~key~formatter/.code=
    { \__tcobox_store_formatter_in:nN {#1} \kvtcb@doc@format@key  },
  index~keys~formatter/.code=
    { \__tcobox_store_formatter_in:nN {#1} \kvtcb@doc@format@keys  },
  % defined in "theorems" library
  description~formatter/.code=
    { \__tcobox_store_formatter_in:nN {#1} \__tcobox_theo_format_description:n }
}

% assume it will be added to both "documentation" and "theorems" libraries
\cs_if_exist:NF \__tcobox_store_formatter_in:nN
  {
    \cs_new_protected:Npn \__tcobox_store_formatter_in:nN #1#2
      {
        % There's no reliable way to check whether a command actually takes
        % a single mandatory argument, so we only test for a single control
        % sequence here.
        \__tcobox_if_single_cs:nTF {#1}
          { \cs_set_eq:NN #2 #1 }
          {
            \tcb@error
              {
                Invalid~formatter~ "\tl_to_str:n {#1}" ~passed~to \MessageBreak
                "\pgfkeyscurrentkey". \MessageBreak
                A~formatter~should~be~a~single~command~taking~one \MessageBreak
                mandatory~argument
              }
          }
      }
  }

% also useful in \newtcobox etc. (see #264), so can be put in tcolorbox.sty
\prg_new_conditional:Npnn \__tcobox_if_single_cs:n #1 { p, TF }
  {
    \bool_lazy_and:nnTF
      { \tl_if_single_p:n {#1} } % false if #1 is empty
      { \token_if_cs_p:N #1 }
      { \prg_return_true: }
      { \prg_return_false: }
  }
\ExplSyntaxOff
\makeatother

\newcommand{\myformatter}[1]{<#1>: }

% valid formatters
\tcbset{index key formatter=\textbf}
\tcbset{index key formatter=\myformatter}

\tcbset{description formatter=\textit}
\tcbset{description formatter=\myformatter}

% invalid formatters
\tcbset{index key formatter=}
\tcbset{index key formatter=a}
\tcbset{index key formatter=abc}

\tcbset{description formatter=}
\tcbset{description formatter=a}
\tcbset{description formatter=abc}

\begin{document}
content
\end{document}
T-F-S commented 5 months ago

Although I think that the dangers here are not so very high, I like the code and I will add it to the next version. Thank you!