这是我遇到的一个问题,我在网上找不到答案,所以我打算在这里发表一个问答式的帖子。
问题
使用 tex 原语,\string
可以将以下标记转换为其自身的字符串表示。例如,\string\macro
将导致(不可扩展的)字符串\macro
。
但是\string
只对下一个标记有效。但我想将一个完整的标记集字符串化,该标记集可以通过宏中的参数获得。
事实证明,字符串化某些输入的问题可以比使用更优雅地解决,\string
这就是为什么你不会在接受的答案中找到它。\string
不过,还有其他答案描述了一种方法。
答案1
[这是我的回答的第二部分。
由于答案字符数的限制,我不得不将此答案分为两部分。
这部分包含例程的编码示例\UDCollectverbarg
。
第 1 部分包含大量有关 LaTeX 中工作原理的解释。 ]
\UDcollectverbarg
我可以提供具有以下语法的宏:
\UDcollectverbarg{⟨^^M-replacement⟩}{⟨non-optional 1⟩}{⟨non-optional 2⟩}⟨verbatimized argument⟩
得出的结果是:
⟨non-optional 1⟩{⟨non-optional 2⟩{⟨verbatimized argument⟩}}
,其中每个字符^^M
表示⟨verbatimized argument⟩
一行的结束,被标记序列替换⟨^^M-replacement⟩
。
没有可选参数:
非可选参数是必需的。如果它们由多个标记组成,则必须将它们嵌套到 catcode-1/2 字符对/括号中。如果需要读取和标记化,这将在未更改的类别代码 制度
下进行。
⟨逐字论证⟩也是必需的。它将在逐字类别代码制度下读取和标记。如果它的第一个字符是括号,则将“假定”该参数嵌套在括号中。否则,将假定该参数的结尾由第一个字符分隔 - 就像的参数一样\verb
。
空行不会被忽略。
我选择这种语法是因为通过这种语法,您可以通过嵌套调用第一个非可选参数来收集第二个非可选参数\UDcollectverbarg
中的逐字参数。\UDcollectverbarg
例如,
\UDcollectverbarg{<^^M-replacement>}%
{\UDcollectverbarg{<^^M-replacement>}{\UDcollectverbarg{<^^M-replacement>}{<actionA>}}}% <- Mandatory 1
{<actionB>}% <- Mandatory 2
<verbatimized argument 1><verbatimized argument 2><verbatimized argument 3>
产量:
\UDcollectverbarg{<^^M-replacement>}{\UDcollectverbarg{<^^M-replacement>}{<actionA>}}% <- Mandatory 1
{<actionB>{<verbatimized argument 1>}}% <- Mandatory 2
<verbatimized argument 2><verbatimized argument 3>
产量:
\UDcollectverbarg{<^^M-replacement>}{<actionA>}% <- Mandatory 1
{<actionB>{<verbatimized argument 1>}{<verbatimized argument 2>}}% <- Mandatory 2
<verbatimized argument 3>
产量:
<actionA>{<actionB>{<verbatimized argument 1>}{<verbatimized argument 2>}{<verbatimized argument 3>}}
假设<actionA>
= \@firstofone
:
\@firstofone{<actionB>{<verbatimized argument 1>}{<verbatimized argument 2>}{<verbatimized argument 3>}}
产量:
<actionB>{<verbatimized argument 1>}{<verbatimized argument 2>}{<verbatimized argument 3>}
%% Copyright (C) 2007 - 2019 by Ulrich Diez ([email protected])
%%
%% This work may be distributed and/or modified under the
%% conditions of the LaTeX Project Public Licence (LPPL), either
%% version 1.3 of this license or (at your option) any later
%% version. (The latest version of this license is in:
%% http://www.latex-project.org/lppl.txt
%% and version 1.3 or later is part of all distributions of LaTeX
%% version 1999/12/01 or later.)
%% The author of this work is Ulrich Diez.
%% This work has the LPPL maintenance status 'not maintained'.
%% Usage of any/every component of this work is at your own risk.
%% There is no warranty - neither for probably included
%% documentation nor for any other part/component of this work.
%% If something breaks, you usually may keep the pieces.
\errorcontextlines=10000
%%<-------------------- code for \UDcollectverbarg -------------------->
\makeatletter
%%......................................................................
%% Check whether argument is empty:
%%......................................................................
%% \UD@CheckWhetherNull{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is empty>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is not empty>}%
%% The gist of this macro comes from Robert R. Schneck's \ifempty-macro:
%% <https://groups.google.com/forum/#!original/comp.text.tex/kuOEIQIrElc/lUg37FmhA74J>
\newcommand\UD@CheckWhetherNull[1]{%
\romannumeral0\expandafter\@secondoftwo\string{\expandafter
\@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
\@secondoftwo\string}\expandafter\@firstoftwo\expandafter{\expandafter
\@secondoftwo\string}\expandafter\expandafter\@firstoftwo{ }{}%
\@secondoftwo}{\expandafter\expandafter\@firstoftwo{ }{}\@firstoftwo}%
}%
%%......................................................................
\begingroup
\catcode`\^^M=12 %
\@firstofone{%
\endgroup%
\newcommand\UDEndlreplace[2]{\romannumeral0\@UDEndlreplace{#2}#1^^M\relax{}}%
\newcommand*\@UDEndlreplace{}%
\long\def\@UDEndlreplace#1#2^^M#3\relax#4#5{%
\UD@CheckWhetherNull{#3}%
{ #5{#4#2}}{\@UDEndlreplace{#1}#3\relax{#4#2#1}{#5}}%
}%
}%
\newcommand\UDcollectverbarg[3]{%
\@bsphack
\begingroup
\let\do\@makeother % <- this and the next line switch to
\dospecials % verbatim-category-code-régime.
\catcode`\{=1 % <- give opening curly brace the usual catcode so a
% curly-brace-balanced argument can be gathered in
% case of the first thing of the verbatimized-argument
% being a curly opening brace.
\catcode`\ =10 % <- give space the usual catcode so \UD@collectverbarg
% cannot catch a space as its 4th undelimited argument.
% (Its 4th undelimited argument denotes the verbatim-
% syntax-delimiter in case of not gathering a
% curly-brace-nested argument.)
\kernel@ifnextchar\bgroup
{% seems a curly-brace-nested argument is to be caught:
\catcode`\}=2 % <- give closing curly brace the usual catcode also.
\UD@collectverbarg{#1}{#2}{#3}{}%
}{% seems an argument with verbatim-syntax-delimiter is to be caught:
\do\{ % <- give opening curly brace the verbatim-catcode again.
\UD@collectverbarg{#1}{#2}{#3}%
}%
}%
\newcommand\UD@collectverbarg[4]{%
\do\ % % <- Now that \UD@collectverbarg has the delimiter or
% emptiness in its 4th arg, give space the
% verbatim-catcode again.
\catcode`\^^M=12 % <- Give the carriage-return-character the verbatim-catcode.
\long\def\@tempb##1#4{%
\edef\@tempb{##1}%
\@onelevel@sanitize\@tempb % <- Turn characters into their "12/other"-pendants.
% This may be important with things like the
% inputenc-package which may make characters
% active/which give them catcode 13(active).
\expandafter\UDEndlreplace\expandafter{\@tempb}{#1}{\def\@tempb}% <- this starts
% the loop for replacing endline-characters.
\expandafter\UD@@collectverbarg\expandafter{\@tempb}{#2}{#3}% <- this "spits
% out the result.
}%
\@tempb
}%
\newcommand\UD@@collectverbarg[3]{%
\endgroup
\@esphack
#2{#3{#1}}%
}%
%%<---------------- end of code for \UDcollectverbarg ----------------->
% As a usage-example let's now define a macro \CodeAndResult which
% collects a verbatim-argument and does both print it wrapped into a
% verbatim*-environment and execute it.
% This time the eTeX primitive \scantokens is used.
% Basically \CodeAndResult is a wrapper for calling \UDcollectverbarg and
% passing the verbatimized argument to \@CodeAndResult
\newcommand\CodeAndResult{%
\UDcollectverbarg{^^J}{\@firstofone}{\@CodeAndResult}%
}%
\begingroup
\newcommand\@CodeAndResult[1]{%
\endgroup
\newcommand\@CodeAndResult[1]{%
\par\noindent \underline{Code:}
\scantokens{\begin{verbatim*}^^J##1^^J#1}%
\par\noindent \underline{Result:}
\scantokens{##1}%
}%
}%
\UDcollectverbarg{^^J}{\@firstofone}{\@CodeAndResult}|\end{verbatim*}|%
\makeatother
\documentclass{article}
\begin{document}
\noindent\hrulefill
% Test with verbatim-delimiter-syntax:
\CodeAndResult|\csname @firstofone\endcsname{\LaTeX} is funny.|
\noindent\hrulefill
% Test with brace-nested-syntax:
\CodeAndResult{\csname @firstofone\endcsname{\TeX} is funny, too.}
\noindent\hrulefill
% Test with verbatim-delimiter-syntax and linebreaks:
\CodeAndResult|Both
\csname @firstofone\endcsname{\LaTeX}
and
\csname @firstofone\endcsname{\TeX}
are funny.% This is a comment.|
\noindent\hrulefill
% Test with brace-nested-syntax and linebreaks:
\CodeAndResult{Both
\csname @firstofone\endcsname{\TeX}
and
\csname @firstofone\endcsname{\LaTeX}
are funny.% This is a comment.}
\noindent\hrulefill
\end{document}
答案2
如果我另一个答案中的建议被分为第1部分和第2部分,对您没有帮助,并且您仍然希望有一个采用无分隔/括号嵌套参数并应用于\string
其每个标记的例程,这是可行的。
但是您需要面对这样一个事实:使用宏您实际上只能根据参数来执行操作,而不能根据标记来执行操作。
如果参数本身包含嵌套在花括号中的内容,则需要考虑到这一点。
需要考虑的另一个方面是,(La)TeX 将跳过/静默删除未分隔/括号嵌套的宏参数之间出现的显式空格标记。
因此,在每次迭代中连续/迭代地应用于\string
其参数的每个标记的例程需要检查剩余参数的第一个标记是否是显式空格标记或左括号标记。
如果这两种可能性都不存在,则可以将第一个标记从参数中取出并\string
应用于它。
如果第一个标记是显式空间标记,则需要将其取出(这需要除了取出未限定参数之外的另一个技巧),并且如果您愿意,您可以应用于\string
该显式空间标记,但这不会产生任何区别,因为应用于\string
显式空间标记会产生显式空间标记。
如果第一个标记是开括号标记,则参数本身的第一个组件是嵌套在括号中的东西。因此,您需要将应用于参数的例程也应用于该组件。此外,您还需要将周围的括号拉出来。
为了得出周围的括号,您可以将参数加倍,然后从其中一个副本中取出第一个组件。
然后,您可以让 LaTeX 迭代地从提取的第一个组件和其中一个副本中的第一个组件中删除内容,直到提取的第一个组件为空。在这种情况下,就到达了副本内部的右括号。
您将需要一些支撑破解技巧。
%% Copyright (C) 2019 by Ulrich Diez ([email protected])
%%
%% This work may be distributed and/or modified under the
%% conditions of the LaTeX Project Public Licence (LPPL), either
%% version 1.3 of this license or (at your option) any later
%% version. (The latest version of this license is in:
%% http://www.latex-project.org/lppl.txt
%% and version 1.3 or later is part of all distributions of LaTeX
%% version 1999/12/01 or later.)
%% The author of this work is Ulrich Diez.
%% This work has the LPPL maintenance status 'not maintained'.
%% Usage of any/every component of this work is at your own risk.
%% There is no warranty - neither for probably included
%% documentation nor for any other part/component of this work.
%% If something breaks, you usually may keep the pieces.
\errorcontextlines=10000
\documentclass{article}
\makeatletter
%%=============================================================================
%% Paraphernalia:
%% \UD@firstoftwo, \UD@secondoftwo,
%% \UD@PassFirstToSecond, \UD@Exchange, \UD@removespace
%% \UD@CheckWhetherNull, \UD@CheckWhetherBrace,
%% \UD@CheckWhetherLeadingSpace, \UD@ExtractFirstArg
%%=============================================================================
\newcommand\UD@firstoftwo[2]{#1}%
\newcommand\UD@secondoftwo[2]{#2}%
\newcommand\UD@PassFirstToSecond[2]{#2{#1}}%
\newcommand\UD@Exchange[2]{#2#1}%
\newcommand\UD@removespace{}\UD@firstoftwo{\def\UD@removespace}{} {}%
%%-----------------------------------------------------------------------------
%% Check whether argument is empty:
%%.............................................................................
%% \UD@CheckWhetherNull{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is empty>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is not empty>}%
%%
%% The gist of this macro comes from Robert R. Schneck's \ifempty-macro:
%% <https://groups.google.com/forum/#!original/comp.text.tex/kuOEIQIrElc/lUg37FmhA74J>
\newcommand\UD@CheckWhetherNull[1]{%
\romannumeral0\expandafter\UD@secondoftwo\string{\expandafter
\UD@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
\UD@secondoftwo\string}\expandafter\UD@firstoftwo\expandafter{\expandafter
\UD@secondoftwo\string}\expandafter\expandafter\UD@firstoftwo{ }{}%
\UD@secondoftwo}{\expandafter\expandafter\UD@firstoftwo{ }{}\UD@firstoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument's first token is a catcode-1-character
%%.............................................................................
%% \UD@CheckWhetherBrace{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked has leading
%% catcode-1-token>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked has no leading
%% catcode-1-token>}%
\newcommand\UD@CheckWhetherBrace[1]{%
\romannumeral0\expandafter\UD@secondoftwo\expandafter{\expandafter{%
\string#1.}\expandafter\UD@firstoftwo\expandafter{\expandafter
\UD@secondoftwo\string}\expandafter\expandafter\UD@firstoftwo{ }{}%
\UD@firstoftwo}{\expandafter\expandafter\UD@firstoftwo{ }{}\UD@secondoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether brace-balanced argument starts with a space-token
%%.............................................................................
%% \UD@CheckWhetherLeadingSpace{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case <argument
%% which is to be checked>'s 1st token is a
%% space-token>}%
%% {<Tokens to be delivered in case <argument
%% which is to be checked>'s 1st token is not
%% a space-token>}%
\newcommand\UD@CheckWhetherLeadingSpace[1]{%
\romannumeral0\UD@CheckWhetherNull{#1}%
{\expandafter\expandafter\UD@firstoftwo{ }{}\UD@secondoftwo}%
{\expandafter\UD@secondoftwo\string{\UD@CheckWhetherLeadingSpaceB.#1 }{}}%
}%
\newcommand\UD@CheckWhetherLeadingSpaceB{}%
\long\def\UD@CheckWhetherLeadingSpaceB#1 {%
\expandafter\UD@CheckWhetherNull\expandafter{\UD@secondoftwo#1{}}%
{\UD@Exchange{\UD@firstoftwo}}{\UD@Exchange{\UD@secondoftwo}}%
{\UD@Exchange{ }{\expandafter\expandafter\expandafter\expandafter
\expandafter\expandafter\expandafter}\expandafter\expandafter
\expandafter}\expandafter\UD@secondoftwo\expandafter{\string}%
}%
%%-----------------------------------------------------------------------------
%% Extract first inner undelimited argument:
%%
%% \UD@ExtractFirstArg{ABCDE} yields {A}
%%
%% \UD@ExtractFirstArg{{AB}CDE} yields {AB}
%%.............................................................................
\newcommand\UD@RemoveTillUD@SelDOm{}%
\long\def\UD@RemoveTillUD@SelDOm#1#2\UD@SelDOm{{#1}}%
\newcommand\UD@ExtractFirstArg[1]{%
\romannumeral0%
\UD@ExtractFirstArgLoop{#1\UD@SelDOm}%
}%
\newcommand\UD@ExtractFirstArgLoop[1]{%
\expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
{ #1}%
{\expandafter\UD@ExtractFirstArgLoop\expandafter{\UD@RemoveTillUD@SelDOm#1}}%
}%
%%-----------------------------------------------------------------------------
%% In case an argument's first token is an opening brace, stringify that and
%% add another opening brace before that and remove everything behind the
%% matching closing brace:
%% \UD@StringifyOpeningBrace{{Foo}bar} yields {{Foo} whereby the second
%% opening brace is stringified:
%%.............................................................................
\newcommand\UD@StringifyOpeningBrace[1]{%
\romannumeral0%
\expandafter\UD@ExtractFirstArgLoop\expandafter{%
\romannumeral0\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter\expandafter
\expandafter {%
\expandafter\UD@firstoftwo
\expandafter{%
\expandafter}%
\romannumeral0\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter\string
\expandafter}%
\string#1%
\UD@SelDOm}%
}%
%%-----------------------------------------------------------------------------
%% In case an argument's first token is an opening brace, remove everything till
%% finding the corresponding closing brace. Then stringify that closing brace:
%% \UD@StringifyClosingBrace{{Foo}bar} yields: {}bar} whereby the first closing
%% brace is stringified:
%%.............................................................................
\newcommand\UD@StringifyClosingBrace[1]{%
\romannumeral0\expandafter\expandafter\expandafter
\UD@StringifyClosingBraceloop
\UD@ExtractFirstArg{#1}{#1}%
}%
\newcommand\UD@CheckWhetherStringifiedOpenBraceIsSpace[1]{%
%% This can happen when character 32 (space) has catcode 1...
\expandafter\UD@CheckWhetherLeadingSpace\expandafter{%
\romannumeral0\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter\UD@secondoftwo
\expandafter{%
\expandafter}%
\expandafter{%
\romannumeral0\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter\UD@firstoftwo
\expandafter{%
\expandafter}%
\romannumeral0\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter\string
\expandafter}%
\string#1%
}%
}%
\newcommand\UD@TerminateStringifyClosingBraceloop[2]{%
\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter\expandafter
\expandafter{%
\expandafter\string
\romannumeral0\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter#1%
\string#2%
}%
}%
\newcommand\UD@StringifyClosingBraceloopRemoveElement[4]{%
\expandafter\UD@PassFirstToSecond\expandafter{\expandafter
{\romannumeral0\expandafter\UD@secondoftwo\string}{}%
\UD@CheckWhetherStringifiedOpenBraceIsSpace{#4}{%
\UD@Exchange{\UD@removespace}%
}{%
\UD@Exchange{\UD@firstoftwo\expandafter{\expandafter}}%
}{%
\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter#1%
\romannumeral0\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter
}%
\string#4%
}{\expandafter\UD@StringifyClosingBraceloop\expandafter{#2#3}}%
}%
\newcommand\UD@StringifyClosingBraceloop[2]{%
\UD@CheckWhetherNull{#1}{%
\UD@CheckWhetherStringifiedOpenBraceIsSpace{#2}{%
\UD@TerminateStringifyClosingBraceloop{\UD@removespace}%
}{%
\UD@TerminateStringifyClosingBraceloop{\UD@firstoftwo\expandafter{\expandafter}}%
}%
{#2}%
}{%
\UD@CheckWhetherLeadingSpace{#1}{%
\UD@StringifyClosingBraceloopRemoveElement
{\UD@removespace}{\UD@removespace}%
}{%
\UD@StringifyClosingBraceloopRemoveElement
{\UD@firstoftwo\expandafter{\expandafter}}{\UD@firstoftwo{}}%
}%
{#1}{#2}%
}%
}%
%%-----------------------------------------------------------------------------
%% Apply <action> to the stringification of each token of the argument:
%%
%% \StringifyNAct{<action>}{<token 1><token 2>...<token n>}
%%
%% yields: <action>{<stringification of token 1>}%
%% <action>{<stringification of token 2>}%
%% ...
%% <action>{<stringification of token n>}%
%%
%% whereby "stringification of token" means the result of applying \string
%% to the token in question.
%% Due to \romannumeral-expansion the result is delivered after two
%% \expandafter-chains.
%% If you leave <action> empty, you can apply a loop on the list formed by
%% {<stringification of token 1>}%
%% {<stringification of token 2>}%
%% ...
%% {<stringification of token n>}%
%%.............................................................................
\newcommand\StringifyNAct{%
\romannumeral0\StringifyNActLoop{}%
}%
%%.............................................................................
%% \StringifyNActLoop{{<stringification of token 1>}...{<stringification of token k-1>}}%
%% {<action>}%
%% {<token k>...<token n>}
%%.............................................................................
\newcommand\StringifyNActLoop[3]{%
\UD@CheckWhetherNull{#3}{%
\UD@firstoftwo{ }{}#1%
}{%
\UD@CheckWhetherBrace{#3}{%
\expandafter\expandafter\expandafter\UD@Exchange
\expandafter\expandafter\expandafter{%
\UD@StringifyClosingBrace{#3}%
}{%
\expandafter\StringifyNActLoop\expandafter{%
\romannumeral0%
\expandafter\expandafter\expandafter\UD@Exchange
\expandafter\expandafter\expandafter{\UD@StringifyOpeningBrace{#3}}{\StringifyNActLoop{#1}{#2}}%
}{#2}%
}%
}{%
\UD@CheckWhetherLeadingSpace{#3}{%
\expandafter\UD@PassFirstToSecond\expandafter{\UD@removespace#3}{%
\StringifyNActLoop{#1#2{ }}{#2}%
}%
}{%
\expandafter\UD@PassFirstToSecond\expandafter{\UD@firstoftwo{}#3}{%
\expandafter\StringifyNActLoop\expandafter{%
\romannumeral0%
\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\UD@PassFirstToSecond
\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter{%
\expandafter\expandafter\expandafter\string
\expandafter\UD@Exchange
\romannumeral0\UD@ExtractFirstArgLoop{#3\UD@SelDOm}{}%
}{ #1#2}%
}%
{#2}%
}%
}%
}%
}%
}%
%%.............................................................................
%% Now a routine which you can apply as <action> within \StringifyNAct:
%%.............................................................................
\newcommand\printstringifiedtoken[1]{%
\UD@CheckWhetherLeadingSpace{#1}{%
An \fbox{\texttt{explicit space token}}%
}{%
The token \fbox{\texttt{#1}}%
}
was stringified.\\
}%
%%.............................................................................
%% Now a routine which you can apply when prefering iterating on the result
%% of \StringifyNAct
%%.............................................................................
\newcommand\printstringifiedtokenloop[1]{%
\ifx\relax#1\expandafter\@gobble\else\expandafter\@firstofone\fi
{\printstringifiedtoken{#1}\printstringifiedtokenloop}%
}%
\makeatother
\begin{document}
\begin{verbatim*}
\noindent
\StringifyNAct{\printstringifiedtoken}{%
\textbf{\csname @firstofone\endcsname{\LaTeX} is funny.}
}
\end{verbatim*}
yields:\bigskip
\noindent
\StringifyNAct{\printstringifiedtoken}{%
\textbf{\csname @firstofone\endcsname{\LaTeX} is funny.}
}
(The last explicit space token is due to the \verb|\endlinechar|-thingie
while the state of \LaTeX's reading-apparatus is in state M (middle of line)
after a curly closing brace. It also is in that state after an opening
curly brace.)
\newpage
\vspace*{-1.5cm}
\begin{verbatim*}
\noindent
\expandafter\expandafter
\expandafter\printstringifiedtokenloop
\StringifyNAct{}{%
\textbf{\csname @firstofone\endcsname{\LaTeX} is funny.}
}%
\relax
\end{verbatim*}
yields:\bigskip
\noindent
\expandafter\expandafter
\expandafter\printstringifiedtokenloop
\StringifyNAct{}{%
\textbf{\csname @firstofone\endcsname{\LaTeX} is funny.}
}%
\relax
(The last explicit space token is due to the \verb|\endlinechar|-thingie
while the state of \LaTeX's reading-apparatus is in state M (middle of line)
after a curly closing brace. It also is in that state after an opening
curly brace.)
\end{document}
答案3
[这是我的回答的第一部分。
由于答案字符数的限制,我不得不将此答案分为两部分。
这部分包含大量有关 LaTeX 中工作原理的解释。
第 2 部分包含例程的编码示例\UDCollectverbarg
。]
似乎您希望基于一组标记来创建 .dvi 或 .pdf 输出或外部文本文件,其内容看起来像导致这些标记产生的 tex 源代码。
由于 LaTeX “消化” .tex-input/tex 源代码的方式,不可能准确地从一组标记推断出这些标记产生的 tex 源代码的外观。
这与 LaTeX 在读取/处理 tex 源代码以形成标记时的行为方式有关:
LaTeX 确实会逐行读取 tex 源代码,逐个字符地处理每一行以形成要插入到标记流中进行进一步处理的标记(字符标记、控制序列标记)。
(控制序列标记有两种:
控制字标记是控制序列标记,其名称由类别代码 11(字母)的单个字符或多个字符组成。例如,\e
和\LaTeX
。
控制符号标记的名称由不属于类别代码 11(字母)的单个字符组成。例如\!
,,,\?
。\7
)
LaTeX 对输入行所做的第一件事(甚至在开始产生标记之前)是删除行右端的所有空格字符(空格字符的代码点编号在 UTF-8 和 ASCII 中都是 32,这是 LaTeX 的两种可能的内部字符编码)。之后,它会在行的右端插入一个字符,其代码点编号等于整数参数 的值\endlinechar
。通常 的值为\endlinechar
13,这是许多编码中回车符的代码点编号,例如在 ASCII 和 UTF-8 中。ASCII 是老式 TeX 引擎的内部字符编码。UTF-8 是较新的 TeX 引擎(如 LuaTeX 和 XeTeX)的内部字符编码。
然后 LaTeX 将其读取装置的状态切换为状态 N(换行)。
(LaTeX 有一个读取装置。它可以有以下三种状态之一:
状态 N:换行。此状态表示 LaTeX 正在开始处理另一行输入。
状态 M:行的中间。此状态表示 LaTeX 正在处理一行输入中的某个字符。
状态 S:跳过空格。此状态表示 LaTeX 应跳过类别代码为 10(空格)的字符,而不是在标记流中插入显式空格标记(字符代码 32,类别代码 10(空格))。)
然后 LaTeX 开始逐个字符地查看该行。
在 LaTeX 中,每个字符都有一个所谓的类别代码。
字符的类别代码会影响 LaTeX 在输入中遇到该字符时执行的操作。
例如,当 LaTeX 在输入中找到类别代码为 0(转义)的字符时,LaTeX 将开始从当前行的后续字符中收集控制序列标记的名称,然后将该控制序列标记插入到标记流中。通常,反斜杠字符\
是唯一类别代码为 0(转义)的字符。
找到这样的字符后,LaTeX 将立即开始收集控制序列标记的名称。
如果下一个字符的类别代码不是 11(字母),LaTeX 会将下一个字符作为控制符号标记的名称,并停止收集并将相应的控制符号标记插入到标记流中,并将读取装置切换到状态 M。
如果下一个字符的类别代码是 11(字母),LaTeX 会将下一个字符作为控制字标记名称的第一个字符,并继续收集(此时处于收集的中间位置),直到到达行尾或文件末尾,或遇到类别代码不是 11(字母)的字符,该字符将不被视为相关控制字标记名称的一部分,而将被视为需要单独查看的内容。
然后,LaTeX 会将迄今为止收集的字符作为控制字标记的名称,并将相应的控制字标记插入到标记流中,并将读取装置切换到状态 S。
例如,当输入中的 LaTeX 发现类别代码为 11(字母)的字符而未收集控制字标记的名称时,LaTeX 将在标记流中插入一个字符标记,其类别为 11(字母),其字符代码等于该字符在 LaTeX 内部字符编码中的代码点编号(根据底层引擎,可能是 ASCII 或 UTF-8)。
当输入中的 LaTeX 发现类别代码为 11(字母)的字符而未收集控制字标记的名称时,它将把该字符作为该控制字标记名称的一部分并继续收集。
例如,当 LaTeX 在输入中找到类别代码为 12(其他)的字符,而此时尚未开始收集控制序列标记的名称,则 LaTeX 会将一个字符标记插入到标记流中,该标记的类别为 12(其他),其字符代码等于该字符在 LaTeX 内部字符编码(根据底层引擎的不同,可能是 ASCII 或 UTF-8)中的代码点编号,并将读取装置切换到状态 M。
例如,当 LaTeX 在输入中找到类别代码为 12(其他)的字符,而此时尚未开始收集控制序列标记的名称,则 LaTeX 会将这个字符作为控制符号标记的名称,将相应的控制符号标记插入到标记流中,并将读取装置切换到状态 M。
顺便一提:
在将非空格字符标记或名称不是由类别代码 10(空格)的字符组成的控制符号标记标记并插入到标记流中后,LaTeX 总是将读取装置切换到状态 M。
在对控制符号标记进行标记并将其插入到标记流中(其名称由类别代码 10(空格)的字符或显式空格标记或控制字标记组成)之后,LaTeX 总是将读取装置切换到状态 S。
当开始处理另一行 TeX 输入时,LaTeX 总是将读取装置切换到状态 N。
例如,当 LaTeX 在输入中找到类别代码为 10(空格)的字符时 — — 通常空格字符(ASCII 和 UTF-8 中的代码点 32)和水平制表符(ASCII 和 UTF-8 中的代码点 9)是类别代码为 10(空格)的仅有的字符,基本上有两种可能性:
可能性 1:
LaTeX 可能正处于收集控制序列标记的名称的开始:在这种情况下,LaTeX 会将空格字符作为该控制序列标记的名称,从而将控制符号标记\
(控制空格)插入到标记流中并将读取装置切换到状态 S。
可能性 2:
LaTeX 可能还没有开始收集控制序列标记的名称。
如果它正处于收集控制字标记名称的过程中,那么到目前为止收集到的字符将形成该控制字标记的名称,并且该控制字标记将被插入到标记流中,并且读取装置将切换到状态 S。
进一步的操作取决于读取装置的状态:
在状态 S 下,LaTeX 将忽略空格字符,不会在标记流中为其插入任何标记。(现在您明白了为什么导致将控制字标记插入标记流的字符序列后面的输入中的空格不会导致将空格标记插入到标记流中。)
在状态 N 下,LaTeX 将忽略该空格字符,不会在标记流中为其插入任何标记。
在状态 M 下,LaTeX 将在标记流中插入一个明确的空格标记(类别 10(空格)和字符代码 32 的字符标记)。
无论如何,在遇到类别代码 10(空格)的字符后,LaTeX 都会切换到状态 S。
这意味着,对于类别代码为 10(空格)的几个连续字符,第一个字符后面的字符将始终被跳过,并且不会产生任何标记,因为它们总是会发现读取装置由于其前辈的处理而切换到状态 S。
这个概念的另一个效果是,在 tex 源代码中出现在行首的类别代码 10(空格)的后续字符序列不会导致任何标记的产生:第一个不会产生任何标记,因为读取设备处于状态 N。后续的不会产生任何标记,因为读取设备处于状态 S。
这就是为什么通常可以使用空格字符和/或制表符从左侧缩进宏代码等,以提高可读性。
例如,当 LaTeX 在输入中发现一个类别代码为 14(注释)的字符,而此时正处于收集控制序列标记名称的开始阶段,则 LaTeX 会将名称与该字符相对应的控制符号标记插入到标记流中,并将读取装置切换到状态 M。(例外:如果该字符是空格字符,则将获得控制空格\
,读取装置将切换到状态 S。)
当 LaTeX 在输入中发现一个类别代码为 14(注释)的字符,而此时正处于收集控制序列标记名称的开始阶段,则 LaTeX 将停止处理当前行,并删除该行的后续字符。如果正处于收集控制字标记名称的过程中,则到目前为止收集的字符将组成该控制字标记的名称,并将相应的控制字标记插入到标记流中。
当 LaTeX 停止处理当前行时,它将继续开始处理下一行输入(如果存在)。当抓取下一行进行处理时,读取装置的状态将切换到状态N。通常%
是类别代码14(注释)的唯一字符。
例如,当 LaTeX 在输入中发现一个分类代码为 5(行尾)的字符,而此时正处于收集控制序列标记名称的开始位置,则 LaTeX 会将名称与该字符相对应的控制符号标记插入到标记流中,并将读取装置切换到状态 M。(例外:如果该字符是空格字符,则将获得控制空格\
,读取装置将切换到状态 S。)
当 LaTeX 在输入中发现一个分类代码为 5(行尾)的字符,而此时并非正处于收集控制序列标记名称的开始位置,则 LaTeX 将停止处理当前行,并删除该行的后续字符。如果正处于收集控制字标记名称的过程中,则到目前为止收集到的字符将组成该控制字标记的名称,并将相应的控制字标记插入到标记流中,读取装置将切换到状态 S。
这种情况下下一步的操作取决于读取装置的状态:如果处于状态 N,则插入
标记。 如果处于状态 M,则会插入显式空格标记(类别 10(空格)和字符代码 32 的字符标记)。 如果处于状态 S,则不会插入任何标记。 在这三种情况下,LaTeX 都会开始处理下一行,从而将读取装置切换到状态 N。\par
上面说过,LaTeX 根据 的值\endlinechar
在每个行尾插入一个字符,该字符通常\endlinechar
具有值 13,这表示回车符的代码点。现在补充一点,通常回车符的类别代码为 5。这意味着通常每个行尾都用类别代码 5(行尾)的回车符来处理。因此,空行意味着在该空行的开头插入类别代码 5(行尾)的回车符,这反过来意味着在读取设备仍处于状态 N 时处理插入的字符,这反过来意味着插入一个 -token \par
。这就是为什么通常空行具有与 相同的效果\par
。
经过这个小插曲,我们看到,一个标记序列不一定与 tex 源代码中的所有字符相似,这些字符的读取和标记化导致了该标记序列的产生:
- 在许多情况下,空格和制表符根本不会产生标记。
- 空行可能会产生
\par
-token,而不会\string\par
产生换行符,但是\
,p
,a
。r
- 类别代码 14(注释)的字符根本不产生标记。
- 类别代码 9(忽略)的字符根本不产生标记。
此外,-primitive 的输出\string
不一定类似于 tex 源代码:
如果\string
将 应用于一个明确的字符标记,则结果将是一个具有相同字符代码的字符标记,但是——在\string
应用于不具有字符代码 32(32 是空格字符的代码点的数量)的字符标记的情况下——属于类别 12(其他)或——在应用于具有字符代码 32 的字符标记的情况下\string
——属于类别 10(空格)。
如果\string
应用于控制序列标记,则结果将是一系列字符标记:
如果整数参数\escapechar
在所用引擎的内部字符编码的代码点范围内具有正值,则会交付一个字符标记,其字符代码等于的值\escapechar
且其类别为 12(other)(例外:在\escapechar
表示空格字符的情况下,类别为 10(space) )。通常\escapechar
具有值 92,即反斜杠字符的编码点的编号。然后是一系列字符标记,每个字符标记表示控制序列标记名称的字符,字符代码是该字符在 LaTeX 内部字符编码中的代码点编号,类别为 12(other)(如果所讨论的代码点不表示空格字符)或 10(space)(如果代码点表示空格字符)。
如果\string
将 应用于无名控制序列标记,则结果将是序列\csname\endcsname
。
因此我建议采取相反的方向:
创建一个宏,切换到逐字类别代码制度,然后通过让 LaTeX 读取并标记包含 tex 源代码的文件中输入来收集其参数。
“切换到逐字分类代码制度”意味着以某种方式更改输入字符的分类代码,从而导致构成参数的 tex 源代码片段的每个字符(在读取和标记后)在字符标记方面具有对应项。在
逐字分类代码制度下,例如,反斜杠没有类别代码 0(转义),但有类别代码 12(其他),因此不会导致收集控制序列标记的名称,但会产生类别代码 12(其他)的反斜杠字符标记。这样,在收集参数时不会产生任何控制序列。只会产生字符标记。
在逐字分类代码制度下,例如,空格字符没有类别代码 10(空格),但有类别代码 12(其他)。因此,它将被视为普通事物,此后读取装置不会切换到状态 S,而是切换到状态 M,导致连续空格不会“折叠成单个空格标记”。
当 LaTeX 在逐字类别代码制度下读取和标记输入时,构成相关输入的 tex 源代码片段的每个字符在读取和标记后都会有一个字符标记的对应部分。
您可以将通过这种方式标记化的参数“馈送”给 -primitive \scantokens
。
-primitive\scantokens
随 eTeX 扩展一起出现。
-primitive\scantokens
让 LaTeX 表现得好像它会以非扩展方式写入构成其⟨平衡文本⟩到外部文件,然后通过 加载该文件\input
。
在该操作的后半部分,即\input
-part 中,事物根据\scantokens
执行时有效的类别代码机制进行 (重新) 标记。如果这是正常的类别代码机制,则 (重新) 标记也可能产生控制序列标记等。
在进一步的部分,即\write
-part 中,所有微妙的规则都适用,这些规则始终适用于 TeX 将标记写入外部文本文件/屏幕。(例如哈希加倍。字符标记的字符代码等于整数参数的值,这\newlinechar
导致 LaTeX 继续在外部文本文件/屏幕上的新行开头写入后续内容。)
切换到逐字类别代码制度然后收集其参数的宏 - 您应该如何使用这样的宏?
由于宏旨在收集在逐字分类代码制度下要标记化的参数,因此应该可以让它收集左花括号和右花括号参数匹配的参数和这些括号不匹配的参数。
在另一种情况下,可以应用与任何普通强制参数相同的语法 - 即,只需将参数嵌套在花括号内。在后一种情况下,需要应用
LaTeX 的 -macro 语法,其中使用字符来分隔参数,而该字符不在参数内出现。 因此,让宏检测在逐字分类代码制度下要读取和标记化的文本的第一个标记是否是花括号是一个好主意。如果是,则在应用另一种情况的语法时收集该参数的剩余标记。如果不是,则在应用后一种情况的语法时收集该参数的剩余标记。\verb
从今以后,在逐字类别代码制度下被标记的参数将被称为⟨逐字论证⟩,无论是使用进一步的语法还是后一种语法来收集。
处理的另一个问题是⟨逐字论证⟩是对线尾的处理:
上面\endlinechar
提到了-thingie:
上面说了,LaTeX 根据 的值\endlinechar
在每个行末插入一个字符,该字符通常具有值 13,表示回车符的代码点。然后补充了一条信息,通常回车符具有类别代码 5(行末),根据读取设备的状态,这可能导致插入的“行末符”被跳过,或者可能导致-token 或显式空格 token\endlinechar
的出现。\par
当您输入文本时,即当您创建 tex 源代码时,回车符不会出现在行的中间。对于您用于创建/输入/查看 tex 源代码的软件来说,它是一个表示行尾的字符。
在正常类别代码制度下,其中^
-character 具有类别代码 7(数学上标),您可以在 tex 源代码中应用^^
-notation 来表示某些无法在键盘上轻松输入的字符。
在正常类别代码制度下,您可以在 TeX 源代码中使用字符序列^^M
来表示回车符。 (M
是字母表中的第
^^
13 个字母;回车符的代码点编号为 13...) -notation 将在读取和处理出现它的输入行时进行转换,甚至在将标记放入标记流之前,甚至在收集控制序列标记的名称时也是如此。
^^
-notation 在逐字类别代码制度下不可用,因为在该制度下,的类别代码^
被切换为 12(其他)。
有关^^
-notation 的更精确解释可以在 TeXbook 中找到。
用于收集⟨逐字论证⟩可以将回车符的类别代码切换为 12(其他)。这样,您便可获得行尾的明确回车符标记。(明确回车符标记是类别 12(其他)和字符代码 13 的字符标记。13 是 ASCII 和 UTF-8 中回车符的代码点编号,UTF-8 是 (La)TeX 引擎可能的内部字符编码。)
这样,您便可获得明确的回车符标记仅有的表示线的末端。
因此在 LaTeX 中⟨逐字论证⟩回车符标记只能由于 -thingie 而产生\endlinechar
,因此它们可用于表示 tex 源代码中出现行结束符的位置。
因此,宏的一个优点是可以收集⟨逐字论证⟩\endlinechar
将是一个附加参数,您可以在其中提供标记,通过该标记可以替换明确的回车符标记(由于 -thingie,它只能在行尾出现)。
例如,您可以使用此功能将显式回车符标记替换为显式换行符标记。(显式换行符标记是类别 12(其他)和字符代码 10 的字符标记。10 是换行符在 ASCII 和 UTF-8 中的代码点编号,(La)TeX 引擎可能的内部字符编码。是J
字母表中的第10 个字母。在 tex 源代码中,您可以在正常类别代码制度下通过 表示换行符^^J
。)
为什么要这样做?原因与 LaTeX 的整数参数有关\newlinechar
:当 LaTeX 将未展开的标记写入文本文件或屏幕时,它将为字符代码等于 值的显式字符标记创建换行符,\newlinechar
而不是将相应的字符写入文件。
通常的值为\newlinechar
10。因此,通常,在将未扩展的标记写入文本文件或屏幕时,可以使用明确的换行符标记来表示 LaTeX 应在新行开头继续书写的位置。
当你打算通过⟨逐字论证⟩至\scantokens
,您可以使用此功能将所有表示行结束的显式回车符标记替换为显式换行符标记。效果是,当\scantokens
执行其未展开的书写部分时,它将在这些位置“写入”换行符。
答案4
解决方案
为了将整个 token 列表字符串化,我必须以某种方式将\string
命令递归应用于 token 列表中的所有 token。我实现这一点的方法是
\def\stringify#1{%
\def\stringifiedInput{}%
\stringifyGrab#1\relax<!;!>%
}
\def\stringifyGrab#1#2<!;!>{%
\apptocmd{\stringifiedInput}{\string#1}{}{}%
%
\noexpandarg
\IfStrEq{#2}{\relax}{%
\stringifiedInput%
}{%
\stringifyGrab#2<!;!>%
}%
}
递归循环由宏完成。请注意,此操作需要\stringifyGrab
packagefsetoolbox
和。xstring
\stringify{\This is a \test for all \hrule and \vrules}
这可以通过以下方法测试
\Thisisa\testforall\hruleand\vrules
。如您所见,所有空格都消失了。我不知道如何防止这种情况发生,所以如果您有解决方案,请随时发表评论
正如 Ulrike Fischer 在下面的评论中指出的那样,一种更简单的方法(保留空间)是使用宏\detokenize{...}
。