summaryrefslogtreecommitdiff
path: root/macros/latex/contrib/xpeek/xpeek.dtx
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
committerNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
commite0c6872cf40896c7be36b11dcc744620f10adf1d (patch)
tree60335e10d2f4354b0674ec22d7b53f0f8abee672 /macros/latex/contrib/xpeek/xpeek.dtx
Initial commit
Diffstat (limited to 'macros/latex/contrib/xpeek/xpeek.dtx')
-rw-r--r--macros/latex/contrib/xpeek/xpeek.dtx700
1 files changed, 700 insertions, 0 deletions
diff --git a/macros/latex/contrib/xpeek/xpeek.dtx b/macros/latex/contrib/xpeek/xpeek.dtx
new file mode 100644
index 0000000000..007ca4e6e7
--- /dev/null
+++ b/macros/latex/contrib/xpeek/xpeek.dtx
@@ -0,0 +1,700 @@
+% \iffalse meta-comment
+% !TeX program = pdflatex
+% !TeX encoding = utf-8
+% !TeX spellcheck = en_US
+%<*internal>
+\begingroup
+%</internal>
+%<*install>
+\input{l3docstrip.tex}
+\keepsilent
+\askforoverwritefalse
+\preamble
+----------------------------------------------------------------
+
+xpeek: Define commands that peek ahead in the input stream
+
+Copyright © 2012 by Joel C. Salomon.
+
+This work consists of the file xpeek.dtx,
+ and the derived files xpeek.ins,
+ xpeek.pdf, &
+ xpeek.sty.
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is at ‹http://www.latex-project.org/lppl.txt›.
+
+This work is “maintained” (per LPPL maintenance status) by
+Joel C. Salomon ‹joelcsalomon@gmail.com›.
+
+----------------------------------------------------------------
+\endpreamble
+\declarepreamble\minimalpreamble
+\endpreamble
+\postamble
+\endpostamble
+\usedir{tex/latex/xpeek}
+\generate{
+ \file{\jobname.sty}{\from{\jobname.dtx}{package}}
+}
+\nopreamble\nopostamble
+\generate{
+ \file{\jobname.tests}{\from{\jobname.dtx}{tests}}
+}
+\generate{
+ \file{\jobname-test.tex}{\from{\jobname.dtx}{testsuite,tests}}
+}
+%</install>
+%<install>\endbatchfile
+%<*internal>
+\usepreamble\minimalpreamble
+\usepostamble\defaultpostamble
+\usedir{source/latex/xpeek}
+\generate{
+ \file{\jobname.ins}{\from{\jobname.dtx}{install}}
+}
+\endgroup
+%</internal>
+%<*driver>
+\pdfobjcompresslevel=0
+\RequirePackage[utf8]{inputenc}
+\documentclass[full]{l3doc}
+\usepackage{xpeek}
+\usepackage{xspace}
+\usepackage[all]{foreign}
+\usepackage{qstest}
+
+^^A Set up formatting options
+
+^^A Format URLs surrounded by guillemets
+\DeclareUrlCommand\url
+ {\def\UrlLeft##1\UrlRight{\text{‹}\href{##1}{##1}\text{›}}}
+\DeclareUrlCommand\email
+ {\def\UrlLeft##1\UrlRight{\text{‹}\href{mailto:##1}{##1}\text{›}}}
+\urlstyle{same}
+
+^^A Make Unicode curly quotes (“”) behave correctly
+\AtBeginDocument{
+ \sfcode\csname\encodingdefault\string\textquotedblright\endcsname=0
+}
+\xspaceaddexceptions{”}
+
+^^A Define indented verbatim environment
+\DefineVerbatimEnvironment{IVerbatim}{Verbatim}%
+ {xleftmargin=2.5\rmemwidth, gobble=4}
+\newlength\rmemwidth
+\AtBeginDocument{
+ \setlength\rmemwidth{\fontdimen6\font}
+}
+
+^^A Enable line continuations in `syntax` environments
+\NewDocumentCommand \fixsyntaxlines {}
+ {\def\^^M{\unskip\space\ignorespaces}}
+
+\begin{document}
+ \DocInput{\jobname.dtx}
+\end{document}
+%</driver>
+% \fi
+%
+% \changes{Version 0.1}{2012/08/09}{First useful version}
+%
+% \GetFileInfo{\jobname.sty}
+%
+% \DoNotIndex
+% {
+% \ProvidesExplPackage, \RequirePackage, \NewDocumentCommand,
+% \ExplSyntaxOn, \ExplSyntaxOff
+% \documentclass, \usepackage,
+% \begin, \end,
+% \xspace,
+% \bool_new:N, \bool_set_true:N, \bool_set_false:N, \bool_if:NTF,
+% \hbox_set:Nn, \box_wd:N, \l_tmpa_box, \l_tmpb_box,
+% \cs_new_protected:Npn,
+% \dim_use:N,
+% \peek_after:Nw,
+% \prg_new_protected_conditional:Npnn,
+% \prg_return_true:, \prg_return_false:,
+% \tl_new:N, \tl_clear:N, \tl_const:Nn, \tl_set:Nn,
+% \tl_put_right:Nn,
+% \tl_map_inline:Nn, \tl_map_break:,
+% \c_empty_tl,
+% \token_if_eq_charcode:NNT,
+% \xsp, \c_xsp_exceptions_tl,
+% }
+%
+% \NewDocumentCommand \releasenote {}
+% {This file describes release \fileversion, last revised \filedate.}
+% \title{The \pkg{\jobname} package\thanks\releasenote}
+%
+% \urldef{\urlxthinsp}{\url}{http://tex.stackexchange.com/a/59542/2966}
+% \urldef{\urlxflw}{\url}{http://tex.stackexchange.com/q/63568/2966}
+% \urldef{\urlxign}{\url}{http://tex.stackexchange.com/q/63971/2966}
+% \urldef{\urlltxl}
+% {\url}{http://thread.gmane.org/gmane.comp.tex.latex.latex3/2894}
+% \NewDocumentCommand \acknote {}
+% {The original code is due to Enrico “egreg” Gregorio,
+% based on an answer he gave to a question of mine on
+% \href{http://tex.stackexchange.com}{\TeX.SX};
+% see \urlxthinsp.
+% Enrico, Joseph Wright, Bruno Le Floch, \& Clemens Niederberger
+% helped iron out the implementation;
+% Bruno also wrote the initial version of the documentation
+% and the near-final version of much of the code.
+% See \urlxflw, \urlxign, \& \urlltxl.}
+% \author{Joel C. Salomon\\\email{joelcsalomon@gmail.com}\thanks\acknote}
+% \date{Released \filedate}
+%
+% \maketitle
+%
+% \begin{abstract}
+% The \pkg{\jobname} package provides functions
+% for defining \& managing commands which,
+% like the familiar \cs{xspace},
+% “peek” ahead at what follows them in the input stream.
+% They compare this token against a stored list
+% and choose an action depending on whether there was a match.
+% \end{abstract}
+%
+% \tableofcontents
+%
+% \begin{documentation}
+%
+% \section{Introduction}
+%
+% This package provides functions for defining \cs{xspace}-like commands.
+% Such commands “peek” ahead into the input stream
+% to see what follows their invocations;
+% their behavior varies depending on what it is that comes next.
+%
+% Probably the most common \LaTeX\ command of this sort is \cs{textit}.
+% It looks ahead at the token following its argument
+% and decides whether to insert italic correction afterward.
+% Table~\ref{tab:itcor} compares the use of \cs{textit}
+% against using the font-switch command \cs{itshape}
+% with \& without explicit italic correction.
+% \begin{table}[hbp]\centering
+% \begin{tabular}{|lll|}
+% \hline
+% |{\itshape half}hearted| &
+% {\itshape half}hearted &
+% (overlap)\\
+% |{\itshape half\/}hearted| &
+% {\itshape half\/}hearted &
+% (correct)\\
+% |\textit{half}hearted| &
+% \textit{half}hearted &
+% (correctly including italic correction)\\
+% ^^A There’s a bug in the package `trace` which breaks suppression
+% ^^A of italic correction; see latex3/svn-mirror#96 for details.
+% ^^A As a temporary work-around, include `\nocorr` where needed.
+% |by {\itshape half}.| &
+% by {\itshape half}. &
+% (correct)\\
+% |by {\itshape half\/}.| &
+% by {\itshape half\/}. &
+% (too wide)\\
+% |by \textit{half}.| &
+% by \textit{half\nocorr}. &
+% (correctly omitting italic correction)\\
+% \hline
+% \end{tabular}
+% \caption{Automatic italic correction with \cs{textit}}
+% \label{tab:itcor}
+% \end{table}
+% Notice how \cs{textit} seems to just “do the right thing”
+% when it comes to inserting or omitting italic correction.
+%
+% It is, unfortunately, not hard to confuse \cs{textit}.
+% As an example, consider the following code:
+% \begin{IVerbatim}
+% \NewDocumentCommand \naif {} {\textit{na\"\i f}}
+% He was a true \naif.
+% \end{IVerbatim}
+% This yields
+% \begin{quote}
+% \NewDocumentCommand \naif {} {\textit{na\"\i f\nocorr}}
+% He was a true \naif.
+% \end{quote}
+% So far, so good.
+% But using this macro in middle of a sentence
+% may produce results the naïve user does not expect.
+% For example, try
+% \begin{IVerbatim}
+% He was such a \naif he expected this example to work.
+% \end{IVerbatim}
+% yielding
+% \begin{quote}
+% \NewDocumentCommand \naif {} {\textit{na\"\i f}}
+% He was such a \naif he expected this example to work.
+% \end{quote}
+% Oops.
+% We forgot that command words eat the spaces that follow them.
+%
+% This is usually the point where \TeX perts explain
+% about following command words with explicit spaces,
+% \eg, writing “|such a \naif\ he |…”,
+% or following such commands with an empty group,
+% “|such a \naif{} he |…”, \eg.
+%
+% Others, more pragmatically-inclined,
+% will mention the \pkg{xspace} package.
+% This is the sort of use-case it was developed for, after all,
+% so let’s try it:
+% \begin{IVerbatim}
+% \NewDocumentCommand \naif {} {\textit{na\"\i f}\xspace}
+% He was such a \naif he expected this example to work.
+% \end{IVerbatim}
+% This produces
+% \begin{quote}
+% \NewDocumentCommand \naif {} {\textit{na\"\i f}\xspace}
+% He was such a \naif he expected this example to work.
+% \end{quote}
+% Even “such a \foreign{naïf}” can get the right result sometimes.
+%
+% But you see, there’s a kinda hitch.^^A
+% \footnote{Cue Malcolm Reynolds: “Don’t complicate things.”}
+% Recall the first time we used the macro:
+% \begin{IVerbatim}
+% He was a true \naif.
+% \end{IVerbatim}
+% Now it yields this:
+% \begin{quote}
+% \NewDocumentCommand \naif {} {\textit{na\"\i f}\xspace}
+% He was a true \naif.
+% \end{quote}
+% Notice that ugly space between the \textit{f} and the period?
+% That’s italic correction being added where it doesn’t belong.
+%
+% The trouble lies in how \LaTeX\ implements the \cs{textit} logic.
+% The macro is clever enough to look ahead,
+% seeking for short punctuation (\eg, the comma \& period)
+% which should not be preceded by italic correction.
+% What the macro finds first, though,
+% is that invocation of \cs{xspace} we added.
+% That’s not one of the “no italic correction” tokens,
+% so the correction gets added and the sentence looks ugly.
+%
+% That’s where this package comes in.
+%
+% (As a side note, Phillip G. Ratcliffe’s \pkg{foreign} package,
+% bundled with both MiK\TeX\ and \TeX~Live
+% and available from CTAN at \url{http://ctan.org/pkg/foreign},
+% is an excellent tool for this sort of foreign-word macro
+% and was one of the inspirations for this package.)
+%
+% Commands generated via the tools the \pkg{xpeek} package provides
+% can peek ahead in the input stream,
+% like \cs{textit} and \cs{xspace},
+% checking whether the next token is on their match-list.
+% What \pkg{xpeek} adds is
+% the ability to ignore certain tokens during that peek-ahead.
+% These ignored tokens are not lost;
+% the scan-ahead routine saves them
+% and they are restored when the command executes.
+%
+% \section{Documentation}
+%
+% At this point in the development of \pkg{xpeek},
+% only one set of follower-dependent functions is defined.
+% Later versions will provide tools for defining such function sets.
+%
+% \subsection{Match- \& Ignore-Lists}
+%
+% \begin{variable}{\g_xpeek_matchlist_tl, \g_xpeek_ignorelist_tl}
+% \begin{syntax}
+% \cs{tl_gput_right:Nn} \cs{g_xpeek_\meta{list}_tl} \Arg{tokens}
+% \cs{tl_gremove_all:Nn} \cs{g_xpeek_\meta{list}_tl} \Arg{tokens}
+% \end{syntax}
+% These two token-lists are the heart of \pkg{xpeek}.
+%
+% The \textit{match-list} is similar to \pkg{xspace}’s exceptions list
+% or \LaTeX’s \cs{nocorrlist} list for suppressing italic correction.
+%
+% The functionality enabled by the \emph{ignore-list},
+% on the other hand,
+% is the \foreign{raison d’être} of this package.
+% For example,
+% if \LaTeX’s \cs{textit} had an ignore-list
+% and the token \cs{xspace} were in that list,
+% then the definition
+% \begin{IVerbatim}[gobble=6]
+% \NewDocumentCommand \naif {} {\textit{na\"\i f}\xspace}
+% \end{IVerbatim}
+% from the example above
+% would have worked properly:
+% \cs{textit} would have ignored the \cs{xspace},
+% looked past it \& seen the period,
+% and therefore omitted italic correction.
+% When that was done, \cs{xspace} would execute;
+% it too would see the period,
+% and therefore would not insert a space.
+% \end{variable}
+%
+% \subsection{Peek-ahead}
+%
+% \begin{function}{\xpeek_after:nw}
+% \begin{syntax}
+% \cs{xpeek_after:nw} \Arg{code} \meta{token}
+% \end{syntax}
+% Similar to \pkg{expl3}’s \cs{peek_after:Nw},
+% this sets the test variable \cs{l_peek_token} to \meta{token}
+% then executes the \Arg{code}.
+% The \meta{token} remains in the input stream.
+% \end{function}
+%
+% \subsection{Collecting Ignored Tokens}
+%
+% \begin{function}{\xpeek_collect_do:nn}
+% \begin{syntax}\fixsyntaxlines
+% \cs{xpeek_collect_do:nn} \Arg{ignore-list} \Arg{operation} \
+% \meta{the rest of the input-stream}
+% \end{syntax}
+% This collects all leading tokens in the input stream
+% which are in the ignore-list,
+% and passes them as an argument to the operation.
+% For example, the invocation
+% \begin{IVerbatim}[gobble=6]
+% \xpeek_collect_do:nn { abc } { \foo \bar } caada
+% \end{IVerbatim}
+% will collect the leading “\texttt{caa}” from the input stream
+% and pass those tokens as an argument to
+% the operation “|\foo \bar|”.
+% \Ie, the code above is equivalent to this:
+% \begin{IVerbatim}[gobble=6]
+% \foo \bar { caa } da
+% \end{IVerbatim}
+% (Note that it’s \cs{bar} \emph{within} the \Arg{operation}
+% that gets the collected tokens as an argument.)
+% \end{function}
+%
+% \subsection{Searching Through Token-Lists}
+%
+% \begin{function}{\xpeek_if_in:NNTF}
+% \begin{syntax}\fixsyntaxlines
+% \cs{xpeek_if_in:NNTF} \cs{\meta{haystack}} \cs{\meta{needle}} \
+% \Arg{true-code} \Arg{false-code}
+% \end{syntax}
+% Among the variants of \cs{tl_if_in:Nn(TF)} defined in \pkg{expl3},
+% the form \cs{tl_if_in:NNTF} is missing.
+% But that’s the form needed in this package,
+% in constructions like this:
+% \begin{IVerbatim}[gobble=6, commandchars=+\[\]]
+% \xpeek_if_in:NNTF \g_xpeek_matchlist_tl \l_peek_token
+% +marg[true-code] +marg[false-code]
+% \end{IVerbatim}
+% So we define it ourselves.
+% \end{function}
+%
+% \section{Example}
+%
+% As a demonstration,
+% here’s an example of using the \pkg{xpeek} facilities
+% to implement correct automatic italic correction \ala \cs{textit}.
+%
+% We set the match-list equal to \LaTeX’s default \cs{nocorrlist};
+% as mentioned above, \cs{xspace} needs to be in the ignore-list:
+% \begin{IVerbatim}
+% \tl_gset:Nn \g_xpeek_matchlist_tl { , . }
+% \tl_gset:Nn \g_xpeek_ignorelist_tl { \xspace }
+% \end{IVerbatim}
+% (Note that this does not require \cs{xspace} to be defined;
+% it merely ensure that
+% the macro will work correctly
+% if it is defined.)
+%
+% Next, we declare the new italicization command \cs{xit}.
+% It calls \cs{xpeek_collect_do:nn} to collect
+% any instances of \cs{xspace} that might follow the invocation.
+% These are saved, but ignored for the time being.
+%
+% It’s within the \Arg{code} argument to \cs{xpeek_collect_do:nn},
+% between \cs{group_begin:} and \cs{group_end:},
+% that the italicization happens.
+% Before we return to the default font, though,
+% the code checks whether \cs{l_peek_token} is in the ignore-list;
+% it then decides whether to include italic correction.
+%
+% Recall next that the last command within the \Arg{code}
+% is what will get the collected tokens as an argument.
+% Since we want to keep \cs{xspace} if it’s there,
+% we finish up the code with \cs{use:n},
+% which simply puts its argument into the input stream.
+%
+% Here’s the code:
+% \begin{IVerbatim}
+% \NewDocumentCommand \xit {m}
+% {
+% \xpeek_collect_do:nn \g_xpeek_ignorelist_tl
+% {
+% \group_begin:
+% \itshape #1
+% \xpeek_if_in:NNTF \g_xpeek_matchlist_tl \l_peek_token
+% { } { \/ }
+% \group_end:
+% \use:n
+% }
+% }
+% \end{IVerbatim}
+%
+% Finally, define the command we tried to make work in the introduction,
+% and see whether we’ve made it work:
+% \begin{IVerbatim}
+% \NewDocumentCommand \naif {} {\xit{na\"\i f}\xspace}
+% He was a true \naif.
+% He was such a \naif he expected this example to work.
+% \end{IVerbatim}
+% The code above yields this result:
+% \begin{quote}
+% \ExplSyntaxOn
+% \tl_gset:Nn \g_xpeek_matchlist_tl { , . }
+% \tl_gset:Nn \g_xpeek_ignorelist_tl { \xspace }
+% \NewDocumentCommand \xit {m}
+% {
+% \xpeek_collect_do:nn \g_xpeek_ignorelist_tl
+% {
+% \group_begin:
+% \itshape #1
+% \xpeek_if_in:NNTF \g_xpeek_matchlist_tl \l_peek_token
+% { } { \/ }
+% \group_end:
+% \use:n
+% }
+% }
+% \ExplSyntaxOff
+% \NewDocumentCommand \naif {} {\xit{na\"\i f}\xspace}
+% He was a true \naif.
+% He was such a \naif he expected this example to work.
+% \end{quote}
+% And work it does.
+%
+% \end{documentation}
+%
+% \begin{implementation}
+%
+% \section{\pkg{\jobname} Implementation}
+%
+% \begin{macrocode}
+%<*package>
+% \end{macrocode}
+%
+% \begin{macrocode}
+%<@@=xpeek>
+% \end{macrocode}
+%
+% \begin{macrocode}
+\RequirePackage{expl3, xparse}
+% \end{macrocode}
+%
+% \begin{macrocode}
+\ProvidesExplPackage
+ {xpeek} {2012/08/15} {0.2}
+ {Define commands that peek ahead in the input stream}
+% \end{macrocode}
+%
+% \subsection{Match- \& Ignore-Lists}
+%
+% \begin{variable}{\g_xpeek_matchlist_tl, \g_xpeek_ignorelist_tl}
+% Define the lists.
+% \begin{macrocode}
+\tl_new:N \g_xpeek_matchlist_tl
+\tl_new:N \g_xpeek_ignorelist_tl
+% \end{macrocode}
+% \end{variable}
+%
+% \subsection{Peek-ahead}
+%
+% \begin{macro}[internal]{\xpeek_after:nw}
+% \begin{variable}[aux]{\l_@@_code_tl}
+% Define a function (AKA “token-list”) to hold the code passed in.
+% \begin{macrocode}
+\tl_new:N \l_@@_code_tl
+% \end{macrocode}
+% Store the argument in \cs{l_@@_code_tl},
+% then defer to \cs{peek_after:Nw}
+% \begin{macrocode}
+\cs_new_protected:Npn \xpeek_after:nw #1
+ {
+ \tl_set:Nn \l_@@_code_tl {#1}
+ \peek_after:Nw \l_@@_code_tl
+ }
+% \end{macrocode}
+% \end{variable}
+% \end{macro}
+%
+% \subsection{Collecting Ignored Tokens}
+%
+% \begin{macro}{\xpeek_collect_do:nn}
+% \begin{macro}[aux]{\@@_collect_do:nnn}
+% \begin{variable}[aux]{\l_@@_collected_tokens_tl}
+% \begin{arguments}
+% \item Tokens to collect from the input stream
+% \item The operation to be performed on the collected tokens
+% \item The next token on the input stream
+% (for \cs{@@_collect_do:nnn}).
+% \end{arguments}\noindent
+% Define a list to save collected tokens in.
+% \begin{macrocode}
+\tl_new:N \l_@@_collected_tokens_tl
+% \end{macrocode}
+% Clear list and defer to auxiliary function.
+% \begin{macrocode}
+\cs_new_protected:Npn \xpeek_collect_do:nn #1#2
+ {
+ \tl_clear:N \l_@@_collected_tokens_tl
+ \@@_collect_do:nnn {#1} {#2} {}
+ }
+% \end{macrocode}
+% Recursively consume tokens from the input stream
+% so long as they match the collect-list,
+% then apply the operation to the collected tokens.
+% The last matching token will not yet have been collected,
+% so pass it to the operation as well.
+% (If there were no matching tokens,
+% |#3| will be the empty argument
+% that was passed by \cs{xpeek_collect_do:nn}.)
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_collect_do:nnn #1#2#3
+ {
+ \xpeek_after:nw
+ {
+ \xpeek_if_in:NNTF #1 \l_peek_token
+ {
+ \tl_put_right:Nn \l_@@_collected_tokens_tl {#3}
+ \@@_collect_do:nnn {#1} {#2}
+ }
+ {
+ #2 { \l_@@_collected_tokens_tl #3 }
+ }
+ }
+ }
+% \end{macrocode}
+% \end{variable}
+% \end{macro}
+% \end{macro}
+%
+% \subsection{Searching Through Token-Lists}
+%
+% \begin{macro}{\xpeek_if_in:NNTF}
+% \begin{variable}[aux]{\l_@@_bool}
+% \begin{arguments}
+% \item Token-list to search through (the “haystack”)
+% \item Token to search for (the “needle”)
+% \end{arguments}\noindent
+% Define an internal flag.
+% \begin{macrocode}
+\bool_new:N \l_@@_bool
+% \end{macrocode}
+% Map the “haystack”, searching for the “needle”.
+% \begin{macrocode}
+\prg_new_protected_conditional:Npnn \xpeek_if_in:NN #1#2 { TF }
+ {
+ \bool_set_false:N \l_@@_bool
+ \tl_map_inline:Nn #1
+ {
+ \token_if_eq_charcode:NNT #2 ##1
+ {
+ \bool_set_true:N \l_@@_bool
+ \tl_map_break:
+ }
+ }
+ \bool_if:NTF \l_@@_bool
+ { \prg_return_true: } { \prg_return_false: }
+ }
+% \end{macrocode}
+% \end{variable}
+% \end{macro}
+%
+% \begin{macrocode}
+%</package>
+% \end{macrocode}
+%
+% \section{Test Suite}
+%
+% \changes{Version 0.2}{2012/08/15}{Added test-suite}
+%
+% ^^A Reset line numbers in code listings
+% \makeatletter
+% \c@CodelineNo 0
+% \makeatother
+%
+% The test suite below is run automatically
+% when this document is produced.
+% It can also be run separately by executing
+% \begin{IVerbatim}[commandchars=+\[\]]
+% latex +jobname-test
+% \end{IVerbatim}
+% at the command prompt.
+%
+% \input{\jobname.tests}
+%
+% \begin{macrocode}
+%<testsuite>\documentclass{article}
+%<testsuite>\usepackage{xparse, expl3, xpeek, qstest}
+%<testsuite>\begin{document}
+%<*tests>
+% \end{macrocode}
+%
+% \subsection{Set-Up: Wrapping \cs{Expect}}
+%
+% \begin{macro}[internal]{\ExpectIdenticalWidths}
+% Since the commands \pkg{xpeek} helps produce are not expandable,
+% directly comparing their outputs is not feasible.
+% Instead, typeset two versions into boxes
+% and have \pkg{qstest} compare these boxes’ widths.
+% (Thanks to Heiko Oberdiek for this technique;
+% see \url{http://tex.stackexchange.com/q/67192/2966}.)
+% \begin{macrocode}
+\ExplSyntaxOn
+\NewDocumentCommand \ExpectIdenticalWidths { m m }
+ {
+ \hbox_set:Nn \l_tmpa_box {#1}
+ \hbox_set:Nn \l_tmpb_box {#2}
+ \Expect
+ * {\dim_use:N \box_wd:N \l_tmpa_box}
+ * {\dim_use:N \box_wd:N \l_tmpb_box}
+ }
+\ExplSyntaxOff
+% \end{macrocode}
+% \end{macro}
+%
+% \subsection{Emulating \cs{xspace}}
+%
+% Define a simple analogue to \cs{xspace}.
+% \begin{macrocode}
+\ExplSyntaxOn
+\tl_const:Nn \c_xsp_exceptions_tl { ,;:.!? }
+\NewDocumentCommand \xsp {}
+ {
+ \xpeek_collect_do:nn \c_empty_tl
+ {
+ \xpeek_if_in:NNTF \c_xsp_exceptions_tl \l_peek_token
+ { } { ~ }
+ }
+ }
+\ExplSyntaxOff
+% \end{macrocode}
+% Test \cs{xsp}, ensuring that it is space-factor–agnostic.
+% \begin{macrocode}
+\begin{qstest}{Emulating \xspace}{xpeek}
+ \ExpectIdenticalWidths{foo bar}{foo\xsp bar}
+ \ExpectIdenticalWidths{foo. bar}{foo\xsp. bar}
+ \ExpectIdenticalWidths{FOO. bar}{FOO\xsp. bar}
+ \ExpectIdenticalWidths{foo. bar}{foo.\xsp bar}
+ \ExpectIdenticalWidths{FOO. bar}{FOO.\xsp bar}
+\end{qstest}
+% \end{macrocode}
+%
+% \begin{macrocode}
+%</tests>
+%<testsuite>\end{document}
+% \end{macrocode}
+% N.B\@. The stand-alone test-suite will not produce any output,
+% only a log file.
+%
+% \end{implementation}
+%
+% \PrintChanges
+%
+% \PrintIndex