4

Is there a way to check what the character following a macro is? For instance, suppose I have a command \foo{...} that formats some text, but I want to adjust the spacing if the next character is a comma or a semicolon, as in foo{...},. I've looked at the following, but can't seem to get it to work for my case: Conditional based on character following the macro.

The specific issue that I have is that I'm using a command for underlines omitting the descenders, adapted from underline omitting the descenders (the adaptation just makes the depth and thickness of the underline scale to the current font size). In most cases, this works great. But since I like using TeX Gyre Pagella as my font, the comma character runs into the underline:

comma runs into underline

An easy fix is to add a manual \kern 0.075em after printing the underlined text, either in the command definition or manually if the next character is a comma, which produces this better-looking result:

manually adjusted fix

If I add this \kern to the command, it adds it every time, which doesn't always look right (if the next character is a period or a space, it adds unnecessary space). And I'd like to avoid having to do this manually whenever the next character is a comma or a semicolon if possible---I'd like to be able to check what the character after the macro is and determine whether to add a \kern that way. Is there a way to do this?

MWE:

\documentclass{article}

\usepackage{fontspec} % For setting the typeface
    \setmainfont{TeX Gyre Pagella} % Set the typeface
\usepackage{xcolor}
\usepackage{soul}
\usepackage{xparse}

\makeatletter

\newlength{\myfontsize}

\ExplSyntaxOn
    \cs_new:Npn \white_text:n #1
    {
        \fp_set:Nn \l_tmpa_fp {#1 * .01}
        \llap{\textcolor{white}{\the\SOUL@syllable}\hspace{\fp_to_decimal:N \l_tmpa_fp em}}
        \llap{\textcolor{white}{\the\SOUL@syllable}\hspace{-\fp_to_decimal:N \l_tmpa_fp em}}
    }
    \NewDocumentCommand{\whiten}{ m }
        {
            \int_step_function:nnnN {1}{1}{#1} \white_text:n
        }
\ExplSyntaxOff

\NewDocumentCommand{ \varul }{ D<>{10} +m }{%
    \begingroup
        \setlength{\myfontsize}{\f@size pt}%
        \setul{0.094\myfontsize}{0.047\myfontsize}%
        \def\SOUL@uleverysyllable{%
            \setbox0=\hbox{\the\SOUL@syllable}%
            \ifdim\dp0>\z@%
                \SOUL@ulunderline{\phantom{\the\SOUL@syllable}}%
                \whiten{#1}%
                \llap{%
                    \the\SOUL@syllable%
                    \SOUL@setkern\SOUL@charkern%
                }%
            \else%
                \SOUL@ulunderline{%
                    \the\SOUL@syllable%
                    \SOUL@setkern\SOUL@charkern%
                }%
            \fi}%
        \ul{#2}%
    \endgroup
}
\makeatother 

\begin{document}
    \noindent testing \varul{hello}, world
\end{document} 
Jigsaw
  • 935

4 Answers4

5

Foreword:

  1. You know that underlining is terrible. And getting a comma farther from the preceding word, well...

  2. The solution works if the comma appears as is after \varul{...} in the LaTeX markup; however, a comma that would come from the expansion of a macro call located after \varul{...} wouldn't be detected this way.

With these things in mind, you can use \peek_meaning:NTF to detect the comma without eating and discarding space tokens along the way. The problem with \@ifnextchar for this application is that you don't want spaces to be ignored after calls of your macro, but \@ifnextchar compares the next non-blank token in the input stream to its first argument (using \ifx), and eats any space token encountered in-between (\peek_meaning_ignore_spaces:NTF would do the same, by the way).

I modified part of the code between \ExplSyntaxOn and \ExplSyntaxOff to better follow expl3 conventions (function naming scheme, use of \hbox_overlap_left:n instead of \llap, \jigsaw_white_text:n declared with \cs_new_protected:Npn since it is not expandable...). More of this could be done in the definition of \jigsaw_varul:nn, though.

\documentclass{article}
\usepackage{fontspec} % For setting the typeface
\setmainfont{TeX Gyre Pagella} % Set the typeface
\usepackage{xcolor}
\usepackage{soul}
\usepackage{xparse}

\makeatletter
\ExplSyntaxOn
\newlength{\myfontsize}

\cs_new_protected:Npn \jigsaw_white_text:n #1
  {
    \fp_set:Nn \l_tmpa_fp { #1 * .01 }
    \hbox_overlap_left:n
      {
        \textcolor { white } { \the\SOUL@syllable }
        \hspace { \fp_to_decimal:N \l_tmpa_fp em }
      }
    \hbox_overlap_left:n
      {
        \textcolor { white } { \the\SOUL@syllable }
        \hspace { -\fp_to_decimal:N \l_tmpa_fp em }
      }
  }

\NewDocumentCommand \whiten { m }
  {
    \int_step_function:nnnN {1} {1} {#1} \jigsaw_white_text:n
  }

\cs_new_protected:Npn \jigsaw_varul:nn #1#2
  {
    \group_begin:
      \setlength{\myfontsize}{\f@size pt}
      \setul{0.094\myfontsize}{0.047\myfontsize}
      \cs_set:Npn \SOUL@uleverysyllable
        {
          \setbox0=\hbox{\the\SOUL@syllable}
          \ifdim \dp0>\z@ \scan_stop:
            \SOUL@ulunderline{\phantom{\the\SOUL@syllable}}
            \whiten{#1}
            \llap{
              \the\SOUL@syllable
              \SOUL@setkern\SOUL@charkern
            }
          \else
            \SOUL@ulunderline{
              \the\SOUL@syllable
              \SOUL@setkern\SOUL@charkern
            }
          \fi
        }
      \ul{#2}
    \group_end:
    \peek_meaning:NT , { \kern 0.075em \scan_stop: }
  }

% I changed '+m' into 'm' because I don't think it would be wise to underline
% several *whole* paragraphs.
\NewDocumentCommand \varul { D<>{10} m }
  {
    \jigsaw_varul:nn {#1} {#2}
  }
\ExplSyntaxOff
\makeatother

\begin{document}
  \noindent
  Testing \varul{hello}, world.\\
  Testing \varul{hello} world.
\end{document}

enter image description here

For comparison, without the added \kern, this would give:

enter image description here

frougon
  • 24,283
  • 1
  • 32
  • 55
5

At TeX primitive level, you can use \futurelet primitive command. I don't use LaTeX construction in my example below because it seems to be more complicated than we need. We want to show how \futurelet works only. The line \vtop{...} does the underline with ignoring depth of the box. This primitive TeX construction works in LaTeX too:).

The important is \futurelet\next\utextA at the end of the macro. It leaves the following character (or space) unchanged but it does \let\next= this character and runs \utextA. You can test the next character by \ifx in this macro.

\fontfam[pagella] % pagella font family if you are using OPmac or OpTeX

\def\utext#1{\leavevmode
   \vtop{\hbox{#1}\kern-\prevdepth \kern.2ex\hrule height.6pt}%
   \futurelet\next\utextA
}
\def\utextA{\ifx\next,\kern.075em \fi}

\utext{hello}, \utext{hello}.

\bye
wipet
  • 74,238
  • This approach seems to be much simpler than using expl3. Isn't the present question an example of the type of macro for which\futurelet was intended? Nice article on \futurelet is in this TUGBoat article – John Mar 31 '20 at 23:43
  • @John It is okay but not simpler than using \peek_meaning:NTF. Most of the expl3 code from the OP's question does other things, things which are unrelated to “inspecting the next character.” This answer can afford to be short because it simply removed these other things (that doesn't mean it is uninteresting!). – frougon Apr 01 '20 at 07:24
  • In case this wasn't already clear, I didn't write this comment to downplay wipet's work. :-) Rather, I'm sad seeing expl3 being quite unfairly blamed because people judge on overall code length (including spaces used for indentation and readability) without even understanding said code. And this happens very often on this site! One last thing: the \futurelet primitive is the cornerstone around which \@ifnextchar and \peek_meaning:NTF, as well as its sister functions, are built. – frougon Apr 01 '20 at 07:57
3

You can test the next character quite easily. I also made the code more expl3-like. But it's much simpler if you avoid underlining altogether.

\documentclass{article}

\usepackage{fontspec} % For setting the typeface
\usepackage{xcolor}
\usepackage{soul}
\usepackage{xparse}

\setmainfont{TeX Gyre Pagella} % Set the typeface

\makeatletter

\newlength{\myfontsize}

\ExplSyntaxOn

\cs_new_protected:Npn \jigsaw_white_text:n #1
 {
  \fp_set:Nn \l_tmpa_fp {#1 * .01}
  \llap{\textcolor{white}{\the\SOUL@syllable}\hspace{\fp_to_decimal:N \l_tmpa_fp em}}
  \llap{\textcolor{white}{\the\SOUL@syllable}\hspace{-\fp_to_decimal:N \l_tmpa_fp em}}
 }
\cs_new_protected:Npn \jigsaw_whiten:n #1
 {
  \int_step_function:nN {#1} \jigsaw_white_text:n
 }

\dim_new:N \jigsaw_fontsize_dim

\NewDocumentCommand{ \varul }{ D<>{10} +m }
 {
  \group_begin:
  \dim_set:Nn \jigsaw_fontsize_dim {\f@size pt}
  \setul{0.094\jigsaw_fontsize_dim}{0.047\jigsaw_fontsize_dim}
  \cs_set_protected:Npn \SOUL@uleverysyllable
   {
    \hbox_set:Nn \l_tmpa_box { \the\SOUL@syllable }
    \dim_compare:nTF { \box_dp:N \l_tmpa_box > 0pt }
     {% depth > 0
      \SOUL@ulunderline{\phantom{\the\SOUL@syllable}}%
      \jigsaw_whiten:n { #1 }
      \llap
       {
        \the\SOUL@syllable%
        \SOUL@setkern\SOUL@charkern%
       }
     }
     {% depth = 0
      \SOUL@ulunderline
       {
        \the\SOUL@syllable
        \SOUL@setkern\SOUL@charkern%
       }
     }
   }
  \ul{#2}%
  \group_end:
  \jigsaw_check_next:
 }

\cs_new_protected:Npn \jigsaw_check_next:
 {
  \peek_charcode:NTF , { \kern 0.075em } 
   {
    \peek_charcode:NT ; { \kern 0.075em }
   }
 }

\ExplSyntaxOff
\makeatother 

\begin{document}

testing hello world

testing \varul{hello} world

testing hello, world

testing \varul{hello}, world

testing hello; world

testing \varul{hello}; world

testing hello. world

testing \varul{hello}. world

\end{document} 

enter image description here

egreg
  • 1,121,712
  • In what group the blank space is it contained? Is it a punctuation char? Thanks!! – manooooh Apr 01 '20 at 07:27
  • @manooooh I’m not sure what you’re referring to, sorry. – egreg Apr 01 '20 at 07:31
  • By group I mean category code, sorry. In what cat code is the blank space? And the % comment symbol? – manooooh Apr 01 '20 at 07:35
  • 1
    @manooooh The space has category code 10, the comment char has catcode 14. – egreg Apr 01 '20 at 07:36
  • 1
    @manooooh Open your TeXbook on page 37 and read. :-) (also: math mode has a \mathpunct class for punctuation, but there isn't anything called this way in horizontal mode; category 12 [other] is where punctuation usually lives, but it can be 13 too, e.g. as often happens with babel). – frougon Apr 01 '20 at 08:23
2

This isn't so much a fix as a workaround. Here, just add the macro \noul between the \varul{...} and the subsequent punctuation. What is does is underline the punctuation in white, effectively erasing the overlap.

\documentclass{article}

\usepackage{fontspec} % For setting the typeface
    \setmainfont{TeX Gyre Pagella} % Set the typeface
\usepackage{xcolor}
\usepackage{soul}
\usepackage{xparse}

\makeatletter

\newlength{\myfontsize}

\ExplSyntaxOn
    \cs_new:Npn \white_text:n #1
    {
        \fp_set:Nn \l_tmpa_fp {#1 * .01}
        \llap{\textcolor{white}{\the\SOUL@syllable}\hspace{\fp_to_decimal:N \l_tmpa_fp em}}
        \llap{\textcolor{white}{\the\SOUL@syllable}\hspace{-\fp_to_decimal:N \l_tmpa_fp em}}
    }
    \NewDocumentCommand{\whiten}{ m }
        {
            \int_step_function:nnnN {1}{1}{#1} \white_text:n
        }
\ExplSyntaxOff

\NewDocumentCommand{ \varul }{ D<>{10} +m }{%
    \begingroup
        \setlength{\myfontsize}{\f@size pt}%
        \setul{0.094\myfontsize}{0.047\myfontsize}%
        \def\SOUL@uleverysyllable{%
            \setbox0=\hbox{\the\SOUL@syllable}%
            \ifdim\dp0>\z@%
                \SOUL@ulunderline{\phantom{\the\SOUL@syllable}}%
                \whiten{#1}%
                \llap{%
                    \the\SOUL@syllable%
                    \SOUL@setkern\SOUL@charkern%
                }%
            \else%
                \SOUL@ulunderline{%
                    \the\SOUL@syllable%
                    \SOUL@setkern\SOUL@charkern%
                }%
            \fi}%
        \ul{#2}%
    \endgroup
}
\makeatother 
\newcommand\noul[1]{\textcolor{white}{\rlap{\varul{#1}}}#1}
\begin{document}
    \noindent testing \varul{hello}, world

    \noindent testing \varul{hello}\noul, world
\end{document} 

enter image description here