Take the 2-minute tour ×
TeX - LaTeX Stack Exchange is a question and answer site for users of TeX, LaTeX, ConTeXt, and related typesetting systems. It's 100% free, no registration required.

In the spirit of my previous questions Defining \xthinspace: Thin space only if not followed by certain characters and Ellipses & Correct Space Factor, I’m trying to define a generic \xspace-like macro system.

The syntax I’m leaning towards is as follows:

\makexfollow{xfollow}{(always)}{(followed)}{(not followed)}
\addtoxfollowexceptions{x}
\newcommand*{\foo}{foo\xfollow}

\foo \foo x

yielding

foo(always)(not followed)foo(always)(followed)x

By editing & wrapping egreg’s code, I got most of the way there; see my code below. I even figured out how to use \cs:w … \cs_end: and the :c specifier to parameterize most of the internal csnames.

The trouble is with the commands \addtoxfollowexceptions & \removefromxfollowexceptions; I cannot figure out how to declare them to replace the xfollow part of the name with something generic.

How can I achieve this?

\documentclass{article}

\usepackage{xparse}

\ExplSyntaxOn

\NewDocumentCommand{\makexfollow}{m m m m}
  {
    % Token list for “exceptions”
    \tl_new:c { g_#1_exceptions_tl }

    % Add to list
    % Should be `\cs:w addto#1exceptions \cs_end:` or something similar
    \NewDocumentCommand{\addtoxfollowexceptions}{m}
      {
        \tl_gput_right:cn { g_#1_exceptions_tl } { ##1 }
      }

    % Remove from list
    % Should be `\cs:w removefrom#1exceptions \cs_end:` or something similar
    \NewDocumentCommand{\removefromxfollowexceptions}{m}
      {
        \tl_gremove_all:cn { g_#1_exceptions_tl } { ##1 }
      }

    \cs_new_protected:cpn { #1 }
      {
        % Unconditional part
        #2

        % Is next token one of the “exceptions”?
        \bool_set_false:c { l_#1_followed_bool }

        % Peek ahead, ignoring spaces, then hand-off to `\xfollow_check:`
        % with `\l_peek_token` set.
        %
        % This is a clever hack: We’re actually checking whether the next
        % non-space token is a space token; if not, call `\xfollow_check:`.
        % Of course, by definition this will always test false, achieving
        % the result described above.
        \peek_catcode_ignore_spaces:NF \c_space_token
          { \cs:w #1_check: \cs_end: }
      }

      % Compare `\l_peek_token` against `\g_xfollow_exceptions_tl`
      \cs_new_protected:cpn { #1_check: }
        {
          % Loop over exceptions list
          \tl_map_inline:cn { g_#1_exceptions_tl }
            {
              \token_if_eq_charcode:NNT ####1 \l_peek_token
                {
                  % Tokens match; set flag and break from loop
                  \bool_set_true:c { l_#1_followed_bool }
                  \prg_map_break:
                }
            }
          % Conditional part
          \bool_if:cTF { l_#1_followed_bool }
            {#3}
            {#4}
        }

  }

\ExplSyntaxOff

\makexfollow{xfollow}{(always)}{(followed)}{(not followed)}
\addtoxfollowexceptions{x}
\newcommand*{\foo}{foo\xfollow}

\begin{document}

\foo \foo x

\end{document}
share|improve this question
add comment

3 Answers

up vote 5 down vote accepted

At present, xparse only includes documented functions to generate commands as control sequences (\foo), not by control sequence name (foo). This is deliberate, as the aim is to keep the syntax relatively clear: there has been some discussion about this on the LaTeX-L list. That said, we have not explored in detail the need to create 'user defined' names in the way you are attempting. Thus at the moment you need to use

\exp_args:Nc \NewDocumentCommand { addto #1 exceptions  }

to achieve what you want. Generating by name is a pretty specialised thing to do (out of the entire set of document commands), so we may yet stick with this method only.

However, I think in your case this would actually be the wrong approach. From the description of what you wish to achieve, it's not clear that you are generating document commands. It seems to me that what you want is commands which belong in the preamble, not in the document. Again, this is an area which is not fully decided at the present time, but the position so far has been that these design level functions should in general take mandatory arguments and thus be generated directly using \cs_new:Npn or similar. As such, I would favour using mixed-case names and using \cs_new_protected:cpn here

\cs_new_protected:cpn { AddTo \tl_expandable_uppercase:n #1 Exceptions } ...

(I'm using \tl_expandable_uppercase:n to change the case of the first letter of#1`: we still need a good name for this function, but in terms of functionality it is very handy so will I think stay.)

As I say, both of these are areas which need some further discussion. That's something best done on the LaTeX-L list.


A side note: I would favour \use:c { ... } over \cs:w ... \cs_end:. In general, we suggest using the 'higher-level' interfaces where available, and anything that is :w is normally used only when necessary.

share|improve this answer
    
Thanks for the style tips. –  J. C. Salomon Jul 17 '12 at 11:11
    
@Joseph I'm not sure that this example is really a case where \exp_args:Nc \NewDocumentCommand is justified. Also, I really don't like the use of \cs_new_protected:cpn with a non-code-level argument. One reason I prefer names of document-level commands not to be built as csnames is that it makes it harder to search for where a given command was defined. –  Bruno Le Floch Jul 17 '12 at 15:57
    
@BrunoLeFloch There is some discussion to be had about design-level definitions, and how they are handled. It's not clear if they should go via xparse or not, nor whether using csnames is allowed. A classic example would be \newlistof for floats: this creates a document-level command, but by name, so how do we deal with it? –  Joseph Wright Jul 17 '12 at 16:28
    
@JosephWright In a way similar to my answer below, I would pass the type of float as an argument to \listof, so \listof{tables} rather than \listoftables. I'm not sure exactly what fits in the "design-level". –  Bruno Le Floch Jul 17 '12 at 17:24
    
@BrunoLeFloch It's not always easy, I'd agree. –  Joseph Wright Jul 17 '12 at 17:29
add comment

I strongly advise against defining addto #1 exceptions in the way you are describing, or AddTo #1 Exceptions in the way Joseph advocates. It is better to pass #1 as a parameter to a single \AddToPeekExceptions command. Namely, I would prefer the syntax

\documentclass{article}

\NewPeekCommand {\xfollow} {(always)} {(followed)} {(not followed)}
\AddToPeekExceptions {\xfollow} {x}
\newcommand*{\foo}{foo\xfollow}

\begin{document}

\foo \foo x

\end{document}

where \NewPeekCommand only defines one control sequence, meant to be used in the document (or in defining other commands).

I tried to write the implementation below as cleanly as possible.

  1. The first argument of \NewPeekCommand, \AddToPeekExceptions, and \RemoveFromPeekExceptions is given as a control sequence rather than characters because it will be used in this way in the document. This prevents mishaps such as \NewPeekCommand{123} then trying to use \123.

  2. As a result, I am paranoid on the input: the first argument of those user functions is checked to really be a single control sequence, otherwise an error is raised.

  3. For the three user-level command, I provide a code-level command which does not test its arguments, for use by package writers wanting to interface with this package. The names of those commands start with \NPC_ and they are documented.

  4. The \NPC_new_peek_command:Nnnn command only defines the exception token list and the command itself. It doesn't define a bunch of helpers like you did; instead the command name (and the (always), (followed) and (not followed) tokens) is given as a parameter to a single auxiliary, \__NPC_check:NnTF, responsible for looking at the next token.

  5. I kept your trick of using \peek_catcode_ignore_spaces:NTF \c_space_token.

  6. The \__NPC_check:NnTF and \__NPC_check_ii:NnTF auxiliaries and the internal variable \l__NPC_bool have a name containing __NPC, indicating that they are not for use outside this package.

  7. Feel free to do whatever you like with this code (in particular changing names of commands). It shouldn't be too hard to make a package out of it.

    % \begin{documentation}
    %
    % This package provides \cs{NewPeekCommand}, \cs{AddToPeekExceptions},
    % and \cs{RemoveFromPeekExceptions}.
    %
    % ^^A Todo: add example of use.
    %
    % \section{Design-level commands}
    %
    % \begin{function}{\NewPeekCommand}
    %   \begin{syntax}
    %     \cs{NewPeekCommand} \marg{function} \marg{always} \marg{followed} \marg{not followed}
    %   \end{syntax}
    %   Defines the \meta{function} as a new 'peek' command, which looks at
    %   the following token.  If this token is part of the exception list
    %   (see \cs{AddToPeekExceptions}), use the \meta{always} and
    %   \meta{followed} tokens; otherwise use the \meta{always} and
    %   \meta{not followed} tokens.
    % \end{function}
    %
    % \begin{function}{\AddToPeekExceptions}
    %   \begin{syntax}
    %     \cs{AddToPeekExceptions} \marg{function} \marg{exceptions}
    %   \end{syntax}
    %   Adds every token in the \meta{exceptions} to the exception list for
    %   the \meta{function}.
    % \end{function}
    %
    % \begin{function}{\RemoveFromPeekExceptions}
    %   \begin{syntax}
    %     \cs{RemoveFromPeekExceptions} \marg{function} \marg{exceptions}
    %   \end{syntax}
    %   Removes every token in the \meta{exceptions} from the exception list
    %   for the \meta{function}.
    % \end{function}
    %
    % \section{Low-level analogs}
    %
    % \begin{function}{\NPC_new_peek_command:Nnnn}
    %   This command is used to implement \cs{NewPeekCommand}, without
    %   checking that the arguments take the correct form.
    % \end{function}
    %
    % \begin{function}{\NPC_add_to_peek_exceptions:Nn}
    %   This command is used to implement \cs{AddToPeekExceptions}, without
    %   checking that the arguments take the correct form.
    % \end{function}
    %
    % \begin{function}{\NPC_remove_from_peek_exceptions:Nn}
    %   This command is used to implement \cs{RemoveFromPeekExceptions},
    %   without checking that the arguments take the correct form.
    % \end{function}
    %
    % \end{documentation}
    %
    % \begin{implementation}
    %
    % First load \pkg{xparse} and turn on the \texttt{expl} syntax.  There
    % should probably be a \cs{ProvideExplPackage} in there.  See
    % \emph{e.g.}, \pkg{siunitx} for good practice.
    %    \begin{macrocode}
    \RequirePackage{xparse}
    \ExplSyntaxOn
    %    \end{macrocode}
    %
    % \section{User commands}
    %
    % \begin{macro}{\NewPeekCommand}
    %   We check that |#1| is a single control sequence.  First check that
    %   |#1| is a token list with exactly one token.  Then check that this
    %   token is a control sequence.  That test will fail if |#1| is a
    %   single space (but who cares).  If the input is correct (T branch),
    %   call \cs{NPC_new_peek_command:Nnnn}, otherwise raise an error and do
    %   nothing.
    %    \begin{macrocode}
    \NewDocumentCommand { \NewPeekCommand } {m m m m}
      {
        \bool_if:nTF { \tl_if_single_token_p:n {#1} && \token_if_cs_p:N {#1} }
          { \NPC_new_peek_command:Nnnn #1 {#2} {#3} {#4} }
          { \msg_error:nnnn { NPC } { cs-expected } { \NewPeekCommand } {#1} }
      }
    %    \end{macrocode}
    % \end{macro}
    %
    % \begin{macro}{\AddToPeekExceptions}
    %   We check that |#1| is a single control sequence.  If it isn't, raise
    %   an error.  Otherwise, there is still a need to check that the
    %   exception list exists, in other words that the command |#1| was
    %   declared with \cs{NewPeekCommand}.  If the input was correct, call
    %   \cs{NPC_add_to_peek_exceptions:Nn}.
    %    \begin{macrocode}
    \NewDocumentCommand { \AddToPeekExceptions } { m m }
      {
        \bool_if:nTF { \tl_if_single_token_p:n {#1} && \token_if_cs_p:N {#1} }
          {
            \tl_if_exist:cF { g_ \cs_to_str:N #1 _exceptions_tl }
              {
                \msg_error:nnnn { NPC } { undeclared-peek-command }
                  { \AddToPeekExceptions } {#1}
                \tl_new:c { g_ \cs_to_str:N #1 _exceptions_tl }
              }
            \NPC_add_to_peek_exceptions:Nn #1 {#2}
          }
          {
            \msg_error:nnnn { NPC } { cs-expected }
              { \AddToPeekExceptions } {#1}
          }
      }
    %    \end{macrocode}
    % \end{macro}
    %
    % \begin{macro}{\RemoveFromPeekExceptions}
    %   The error checking is the same as for \cs{AddToPeekExceptions}.
    %   Then call \cs{NPC_remove_from_peek_exceptions:Nn}.
    %    \begin{macrocode}
    \NewDocumentCommand { \RemoveFromPeekExceptions } { m m }
      {
        \bool_if:nTF { \tl_if_single_token_p:n {#1} && \token_if_cs_p:N {#1} }
          {
            \tl_if_exist:cF { g_ \cs_to_str:N #1 _exceptions_tl }
              {
                \msg_error:nnnn { NPC } { undeclared-peek-command }
                  { \RemoveFromPeekExceptions } {#1}
                \tl_new:c { g_ \cs_to_str:N #1 _exceptions_tl }
              }
            \NPC_remove_from_peek_exceptions:Nn #1 {#2}
          }
          {
            \msg_error:nnnn { NPC } { cs-expected }
              { \RemoveFromPeekExceptions } {#1}
          }
      }
    %    \end{macrocode}
    % \end{macro}
    %
    % \section{Messages}
    %
    % If the first argument of one of the user functions is not a single
    % control sequence, complain.
    %    \begin{macrocode}
    \msg_new:nnnn { NPC } { cs-expected }
      { The~first~argument~of~#1 must~be~a~control~sequence. }
      {
        The~command~#1 received~'#2'~as~its~first~argument,~instead~
        of~a~single~control~sequence~such~as~\token_to_str:N \xspace.
      }
    %    \end{macrocode}
    %
    % If the first argument of \cs{AddToPeekExceptions} or
    % \cs{RemoveFromPeekExceptions} is an unknown control sequence,
    % complain.
    %    \begin{macrocode}
    \msg_new:nnn { NPC } { undeclared-peek-command }
      {
        The~first~argument~of~#1 must~be~declared~with~
        \token_to_str:N \NewPeekCommand.
      }
    %    \end{macrocode}
    %
    % \section{Low-level}
    %
    % \begin{macro}{\NPC_add_to_peek_exceptions:Nn}
    %   Adds all the tokens in |#2| to the list of exceptions for the peek
    %   command |#1|.
    %    \begin{macrocode}
    \cs_new_protected:Npn \NPC_add_to_peek_exceptions:Nn #1#2
      { \tl_gput_right:cn { g_ \cs_to_str:N #1 _exceptions_tl } { #2 } }
    %    \end{macrocode}
    % \end{macro}
    %
    % \begin{macro}{\NPC_remove_from_peek_exceptions:Nn}
    %   Removes each the tokens in |#2| from the list of exceptions for the
    %   peek command |#1|.  Allowing more than one token in |#2| requires
    %   putting a loop there.
    %    \begin{macrocode}
    \cs_new_protected:Npn \NPC_remove_from_peek_exceptions:Nn #1#2
      {
        \tl_map_inline:nn {#2}
          { \tl_gremove_all:cn { g_ \cs_to_str:N #1 _exceptions_tl } { ##1 } }
      }
    %    \end{macrocode}
    % \end{macro}
    %
    % \begin{macro}{\NPC_new_peek_command:Nnnn}
    %   Declare a token list to hold the exceptions for the command |#1|,
    %   then declare |#1| which simply calls the internal
    %   \cs{__NPC_check:NnTF} with the appropriate arguments.
    %    \begin{macrocode}
    \cs_new_protected:Npn \NPC_new_peek_command:Nnnn #1#2#3#4
      {
        \tl_new:c { g_ \cs_to_str:N #1 _exceptions_tl }
        \cs_new_protected_nopar:Npn #1
          { \__NPC_check:NnTF #1 {#2} {#3} {#4} }
      }
    %    \end{macrocode}
    % \end{macro}
    %
    % \begin{macro}[aux]{\__NPC_check:NnTF}
    %   Peek ahead, ignoring spaces.  This is a clever hack: we’re actually
    %   checking whether the next non-space token is a space token; if not,
    %   call \cs{__NPC_check_ii:NnTF}.  Of course, by definition this will
    %   always test false.
    %    \begin{macrocode}
    \cs_new_protected:Npn \__NPC_check:NnTF #1#2#3#4
      {
        \peek_catcode_ignore_spaces:NF \c_space_token
          { \__NPC_check_ii:NnTF #1 {#2} {#3} {#4} }
      }
    %    \end{macrocode}
    % \end{macro}
    %
    % \begin{macro}[aux]{\__NPC_check_ii:NnTF}
    %   Now \cs{l_peek_token} is set to the following token, and we wish to
    %   compare it with the exceptions for |#1|.  Loop over the exception
    %   list, comparing the character code of each exception with
    %   \cs{l_peek_token}, and if there is a match, turn on the boolean and
    %   break the map.  After the loop, leave either the \meta{always} and
    %   \meta{followed} tokens, or the \meta{always} and \meta{not followed}
    %   tokens in the input stream.
    %    \begin{macrocode}
    \cs_new_protected:Npn \__NPC_check_ii:NnTF #1#2#3#4
      {
        \bool_set_false:N \l__NPC_bool
        \tl_map_inline:cn { g_\cs_to_str:N #1 _exceptions_tl }
          {
            \token_if_eq_charcode:NNT ##1 \l_peek_token
              {
                \bool_set_true:N \l__NPC_bool
                \tl_map_break:
              }
          }
        \bool_if:NTF \l__NPC_bool { #2 #3 } { #2 #4 }
      }
    %    \end{macrocode}
    % \end{macro}
    %
    % \begin{variable}{\l__NPC_bool}
    %   This boolean is used to keep track of whether the \cs{l_peek_token}
    %   appears in the exception list or not.
    %    \begin{macrocode}
    \bool_new:N \l__NPC_bool
    %    \end{macrocode}
    % \end{variable}
    %
    % \end{implementation}
    %
    
    
    
    
    
    \ExplSyntaxOff
    
    \documentclass{article}
    
    \NewPeekCommand {\xfollow} {(always)} {(followed)} {(not followed)}
    \AddToPeekExceptions {\xfollow} {x}
    \newcommand*{\foo}{foo\xfollow}
    
    \begin{document}
    
    \foo \foo x
    
    \end{document}
    
share|improve this answer
    
Reasonable approach: this type of interface work is tricky. –  Joseph Wright Jul 17 '12 at 17:29
3  
@JosephWright’s answer is the one I accepted, since it answers the question I actually asked. This one answers the questions I didn’t think to ask, and is the basis for a package I’ve just uploaded to CTAN. Until it’s there, see <github.com/jcsalomon/xpeek>;. –  J. C. Salomon Jul 18 '12 at 2:27
add comment

Just use \cs_new_protected:cpn

\documentclass{article}

\usepackage{xparse}

\ExplSyntaxOn

\NewDocumentCommand{\makexfollow}{m m m m}
  {
    % Token list for exceptions
    \tl_new:c { g_#1_exceptions_tl }

    % Add to list
    \cs_new_protected:cpn { addto #1 exceptions } ##1
      {
        \tl_gput_right:cn { g_#1_exceptions_tl } { ##1 }
      }

    % Remove from list
    \cs_new_protected:cpn { removefrom #1 exceptions} ##1
      {
        \tl_gremove_all:cn { g_#1_exceptions_tl } { ##1 }
      }

    \cs_new_protected:cpn { #1 }
      {
        % Unconditional part
        #2

        % Is next token one of the exceptions
        \bool_set_false:c { l_#1_followed_bool }

        % Peek ahead, ignoring spaces, then hand-off to `\xfollow_check:`
        % with `\l_peek_token` set.
        %
        % This is a clever hack: We're actually checking whether the next
        % non-space token is a space token; if not, call `\xfollow_check:`.
        % Of course, by definition this will always test false, achieving
        % the result described above.
        \peek_catcode_ignore_spaces:NF \c_space_token
          { \cs:w #1_check: \cs_end: }
      }

      % Compare `\l_peek_token` against `\g_xfollow_exceptions_tl`
      \cs_new_protected:cpn { #1_check: }
        {
          % Loop over exceptions list
          \tl_map_inline:cn { g_#1_exceptions_tl }
            {
              \token_if_eq_charcode:NNT ####1 \l_peek_token
                {
                  % Tokens match; set flag and break from loop
                  \bool_set_true:c { l_#1_followed_bool }
                  \prg_map_break:
                }
            }
          % Conditional part
          \bool_if:cTF { l_#1_followed_bool }
            {#3}
            {#4}
        }

  }

\ExplSyntaxOff

\makexfollow{xfollow}{(always)}{(followed)}{(not followed)}
\addtoxfollowexceptions{x}

\show\xfollow
\show\removefromxfollowexceptions

\newcommand*{\foo}{foo\xfollow}

\begin{document}

\foo \foo x

\end{document}
share|improve this answer
    
Thanks! I tried something very similar last night, but kept getting odd error messages about redefining csnames. This version works, though. –  J. C. Salomon Jul 17 '12 at 10:56
    
BTW, given that the original code was yours, may I have your permission to package this under the LPPL? –  J. C. Salomon Jul 17 '12 at 10:58
    
@J.C.Salomon Of course! I'll have my name in a footnote! (Quote from "A new leaf", with Walter Matthau) –  egreg Jul 17 '12 at 18:16
    
done; see <github.com/jcsalomon/xpeek>;. See also Relicensing code from answers on meta for the reason I asked. –  J. C. Salomon Jul 18 '12 at 2:31
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.