10

This question is a variant of my previous question requesting a macro that takes the rest of the line as argument. I would like to write a macro that would take the next word as argument, where word boundary is defined as a space or punctuation (say anything which is not a letter, a digit, or the colon character).

The difficulty lies with the fact that a normal \def can be set to expect a specific character, not a set of characters.

This is not a theoretical challenge---the concrete application is a macro that will do better references, so instead of writing Lemma~\cite{Lemma:Kantor}, one could write \cf Lemma Kantor

An approximation is

\def\cf #1 #2 {#1~\ref{#1:#2}}

but of course it would fail in cases it is used just before punctuation.

Yossi Gil
  • 15,951

2 Answers2

11

You need to read every character on its own using a loop. Actually you need to look ahead using \futurelet because a space would otherwise be removed by the argument grapper.

The following code works and treats line breaks as spaces. One colon is allowed as separator between the two words instead of a space. If you want to allow multiple colons you have to add them into the if list in \cf@@.

\documentclass{article}

\makeatletter
\def\cf{%
  \begingroup
  \def\name{}%
  \cf@
}

\def\cf@{%
  \futurelet\ntoken\cf@@
}

\def\cf@@{%
  \ifcase 0%
    \ifx\ntoken\@sptoken 0\else
    \ifcat a\ntoken 1\else
    \ifcat 0\ntoken 2\fi% test of token is catcode "other"
    \fi\fi
  \relax
    \expandafter\cf@end
  \or
    \expandafter\cf@add
  \else
    \expandafter\cf@checknum
  \fi
}

% Checks if token is a number (ASCII 48-57)
\def\cf@checknum#1{%
  \ifcase 0%
    \ifnum`#1>47
    \ifnum`#1<58 1\fi\fi
  \relax
    \def\next{\cf@end#1}%
  \else
    \def\next{\cf@add{#1}}%
  \fi
  \next
}

\def\cf@add#1{%
  \edef\name{\name#1}%
  \cf@
}

\def\cf@end{%
  \let\type\name
  \edef\name{\name:}%
  \def\cf@end{%
    \edef\@tempa{%
        \endgroup
        \type\noexpand~%
        \noexpand\ref{\name}%
    }%
    \@tempa
  }%
  % Remove colon
  % support active colons (e.g. `[french]{babel}`)
  \scantokens{\expandafter\let\csname cf@colon\endcsname=:}%
  \@ifnextchar\cf@colon%
    {\expandafter\cf@\@gobble}%
    {\cf@}%
}
\makeatother

\begin{document}

\section{test}\label{sec:test}
\section{test}\label{sec:tes1}
\section{other}

\cf sec test it out
\cf sec:test it out

\cf sec
tes1 it out  % numbers and line break work

\end{document}
Martin Scharrer
  • 262,582
  • @Martin: Wow! Don't you think this ought to be packaged and distributed? It would make the life of so many people sweeter. It would also be cool also to add a flag that would either produce the long version, i.e., Section~1, or the short version Sect.~1... More later. – Yossi Gil Feb 24 '11 at 09:32
  • 1
    @Yossi: I personally think people should stick with explicit \ref{sec:name}. Feel free to turn it into a package. Just mention me somewhere in the manual. – Martin Scharrer Feb 24 '11 at 09:33
  • @Martin: I like the use of \ifcase 0, which expands the following tokens to see whether 0 will be followed by a digit or not. And that next digit continues the expansion until reaching \relax, so the conditionals are fully expanded (no spurious \fi) before \ifcase is evaluated. Neat. – Bruno Le Floch Feb 24 '11 at 10:03
  • @Bruno: I didn't came up with this technique by myself but learned it somewhere (on comp.text.tex I think) – Martin Scharrer Feb 24 '11 at 10:42
  • @Martin: your colon handling mechanism will fail with \usepackage[frenchb]{babel} which makes : active (the second reference will always remain ??). – Philippe Goutet Feb 24 '11 at 10:43
  • Thanks @Philippe, I forgot that. I added a fix for it. Now the colon is taken with the currently used catcode. There are other ways to do this which avoid the use of the eTeX primitive \scantokens, but this solution is quick and simple. – Martin Scharrer Feb 24 '11 at 11:02
  • @Martin: You didn't come up :-) – Hendrik Vogt Feb 24 '11 at 11:04
  • @Martin: small issue. The macro you defined does not take two arguments as in \def\cf #1 #2 {#1~\ref{#1:#2}} – Yossi Gil Feb 24 '11 at 13:14
  • @Yossi: Sure it does. See the example code at the end which works fine for me. The \cf@end macro is executed twice, the first time it adds only a colon to the name and redefines itself to be the real end-macro for the second time a non-suitable character is found. – Martin Scharrer Feb 24 '11 at 13:36
  • I am not sure this is quite what I meant. But, this is unimportant. The important thing is that you cracked it. I will post my rewrite of your code so that you will see what I mean. – Yossi Gil Feb 24 '11 at 15:21
  • @Yossi: Sorry, I totally overlooked the first #1 before \ref. Yes, my macro doesn't do that, but it can be added. – Martin Scharrer Feb 24 '11 at 15:25
  • @Martin: wouldn't it be simpler to change the catcode of ":" to be letter or other? – Yossi Gil Feb 24 '11 at 15:51
  • @Yossi: This wouldn't be reliable. The : might already be read beforehand and the catcode would then already be fixed. Also if the second word ends with a colon, which is not taken as part of the word, it would be placed back to the input stream and might be required to be active. This things are difficult to get 100% right for all possible use cases. – Martin Scharrer Feb 24 '11 at 16:14
  • @Yossi: also I updated my code a while back to handle the first #1 correctly. I forget to inform you right away. – Martin Scharrer Feb 24 '11 at 16:15
1

Here is a rewrite of Martin's code. First the use case:

\documentclass{article}
\usepackage{cf}
\begin{document}
\section{Introduction}
\paragraph{Outline.}
In \cf Section things we shall discuss things, and then continue to discuss
stuff in \cf Section stuff; \cf Section final concludes.
\section{Things are so Important}\label{Section:things}
\section{Stuff is also Important}\label{Section:stuff}
\section{Conclusions}\label{Section:final}
\end{document

which produces enter image description here

now, the rudimentary style file is

\def\cf #1 {%
  \def\kind@cf{#1}%
  \beginIteration@cf
}

\def\beginIteration@cf{%
  \begingroup
  \def\idAccumulator@cf{}%
  \iterateOnNextToken@cf
}

\def\iterateOnNextToken@cf{%
  \futurelet\nextToken@cf
  \examineNextToken@cf    
}

\def\examineNextToken@cf{%
  \ifcase 0% Trick: will expand the next tokens to see if more digits follow. 
    \ifx\nextToken@cf\@sptoken 0\else
    \ifcat a\nextToken@cf 1\else
    \ifcat 0\nextToken@cf 2\fi% test of token is catcode "other"
    \fi\fi
  \relax
    \expandafter\endIteration@cf
  \or
    \expandafter\accumulateCharacter@cf
  \else
    \expandafter\cf@checknum
  \fi
}

\def\accumulateCharacter@cf#1{%
  \edef\idAccumulator@cf{\idAccumulator@cf#1}%
  \iterateOnNextToken@cf
}

% Checks whether the parameter is a number (ASCII 48-57)
\def\cf@checknum#1{%
  \ifcase 0%
    \ifnum`#1>47
    \ifnum`#1<58 1\fi\fi
  \relax
    \def\next{\endIteration@cf#1}%
  \else
    \def\next{\accumulateCharacter@cf{#1}}%
  \fi
  \next
}

\def\endIteration@cf{%
  \edef\label@cf{\kind@cf:\idAccumulator@cf}%
  \kind@cf~\expandafter\ref\expandafter{\label@cf}%
  \endgroup
}
Yossi Gil
  • 15,951