我正在开发一个小型解析器,它接受免费的用户输入并将某些输入解释为命令。例如,解析器将其解释+
为\oplus
或[
为“以括号作为分隔符启动预配置数组”。解析器最终将能够方便地插入语言学中使用的某种数据结构(称为 AVM),而 CTAN 上目前没有这种数据结构的软件包。
解析器目前基于循环遍历输入标记列表 (wit \tl_map_inline:nn
)。但循环遍历用户输入中的空格和控制序列让我头疼。例如,用户输入可能包含:
Hello World
\textit{Hello World}
由于\tl_map_inline:nn
循环遍历标记列表的项目,输出将变成“HelloWorld”和“H“elloWorld”。
当然,受保护的输入
Hello{~}World
{\textit{Hello World}}
将给出所需的结果,但用户不太可能以这种方式输入。此外,上面的两个输入确实完全不同:Hello{~}World
是一个包含 11 个项目的标记列表,但{\textit{Hello World}}
只有 1 个。在映射中,我想将命令的内容解析\textit
为单个字母,而不是单个标记(因为它的参数可能包含解析器应该敏感的字符,就像+
上面提到的那样)。
这让我想到,也许有比使用标记列表和映射更好的方法来实现解析器。如果标记列表是最好的方法,那么有哪些方法可以:
- 准备用户输入,以便将空格保留为项目?(也许用 替换它们
~
,但是怎么做呢?) - 正确地将控制序列转发到输出并能够访问其参数所组成的标记?
在这个答案,@egreg 建议将用户输入作为序列接收,在每个空格处将其拆分为标记列表,然后解析标记列表(在每个 tl 后向输出添加一个空格)。这种方法可以应用于示例中的命令吗\textit
?
(物品和代币之间的差异真的是问题的核心吗?)
这是我的方法的基本框架(实际还包含一个模式开关,以便用户可以禁用替换,例如,[
以便仍然可以在解析器范围内输入带有可选参数的命令):
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\parse}{+m}{
\avm_parse:n { #1 }
}
\tl_new:N \l_avm_output_tl
\cs_new:Nn \avm_parse:n {
\tl_clear:N \l_avm_output_tl
\tl_map_inline:nn {#1} {
\tl_put_right:Nn \l_avm_output_tl {##1}
}
\tl_use:N \l_avm_output_tl
}
\ExplSyntaxOff
\begin{document}
\noindent
\parse{Hello World}\\
\parse{\textit{Hello World}}\\
\parse{Hello{~}World}\\
\parse{{\textit{Hello{~}World}}}
\end{document}
答案1
可以解析整个标记列表,同时保留空格和组,但这比“正常”\tl_map_inline:nn
解析要费力得多(请参阅这里以获得更完整的解释)。问题是\tl_map_inline:nn
,正如其文档中interface3
所述,代币,但在项目XX
,并删除外括号。 、X X
、X{X}
和全部{XX}{XX}
都是两个项目的列表(在最后一个中,每个项目都有两个标记)。 这种空格和括号删除发生在 TeX 的低级别,因为当它抓取一个参数时, 和 全部都会\next X
被\next{X}
抓取\next <spaces>X
为X
参数:这就是定义物品。
迭代适当的代币,您需要特别检查空格和括号的情况。expl3
有三个条件函数,\tl_if_head_is_space:n(TF)
用于检查参数标记列表中的第一个标记是否为空格、\tl_if_head_is_group:n(TF)
标记分组列表的情况以及所有其他情况。 这允许您有条件地处理参数标记列表中的空格和组,从而在处理过程中保留它们。 's (以前的)、、 和其他一些\tl_if_head_is_N_type:n(TF)
方法也使用此方法。expl3
\text_uppercase:n
\tl_upper_case:n
\tl_count_tokens:n
\tl_reverse:n
这里我定义了一个\avm_parse:n
函数,让它迭代参数的每个标记,如果标记是 -type N
,它会检查当前标记是 a+
还是 a [
(使用),并调用相应的函数来处理每种情况。您可以更改和\str_case:nnF
的定义以执行您期望的操作(我制作了produce和produce (还解析了with )。\__avm_special_plus:
\__avm_special_lbrack:w
+
\ensuremath{\oplus}
[<tokens>]
\textbf{<tokens>}
<tokens>
\avm_parse:n
代码如下:
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewExpandableDocumentCommand {\parse} {+m}
{ \avm_parse:n {#1} }
\cs_new:Npn \avm_parse:n #1
{ \exp_args:No \exp_not:o { \__avm_parse:n {#1} } }
\cs_new:Npn \__avm_parse:n #1
{
\exp:w
\group_align_safe_begin:
\__avm_parse_loop:w #1
\q_recursion_tail \q_recursion_stop
\__avm_result:n { }
}
\cs_new:Npn \__avm_end:w \__avm_result:n #1
{
\group_align_safe_end:
\exp_end:
#1
}
\cs_new:Npn \__avm_parse_loop:w #1 \q_recursion_stop
{
\tl_if_head_is_N_type:nTF {#1}
{ \__avm_N_type:N }
{
\tl_if_head_is_group:nTF {#1}
{ \__avm_group:nw }
{ \__avm_space:w }
}
#1 \q_recursion_stop
}
\cs_new:Npn \__avm_N_type:N #1
{
\quark_if_recursion_tail_stop_do:Nn #1
{ \__avm_end:w }
\__avm_parse_specials:N #1
}
\cs_new:Npn \__avm_parse_specials:N #1
{
\str_case:nnF {#1}
{
{ + }{ \__avm_special_plus: }
{ [ }{ \__avm_special_lbrack:w }
}
{ \__avm_non_special:N #1 }
}
\cs_new:Npn \__avm_group:nw #1
{ \exp_args:NNo \exp_args:No \__avm_group:n { \__avm_parse:n {#1} } }
\cs_new:Npn \__avm_group:n #1 { \__avm_add_result:nw { {#1} } }
\exp_last_unbraced:NNo
\cs_new:Npn \__avm_space:w \c_space_tl { \__avm_add_result:nw { ~ } }
\cs_new:Npn \__avm_add_result:nw #1 #2 \q_recursion_stop \__avm_result:n #3
{ \__avm_parse_loop:w #2 \q_recursion_stop \__avm_result:n {#3 #1} }
\cs_new:Npn \__avm_non_special:N #1 { \__avm_add_result:nw {#1} }
%
\cs_new:Npn \__avm_special_plus:
{ \__avm_add_result:nw { \ensuremath { \oplus } } }
\cs_new:Npn \__avm_special_lbrack:w #1 ]
{
\exp_args:No \__avm_add_result:nw
{ \exp:w \exp_args:NNf \exp_end: \textbf { \avm_parse:n {#1} } }
}
\ExplSyntaxOff
\begin{document}
\noindent
\parse{Hello World}\\
\parse{\textit{Hello World}}\\
\parse{Hello{~}World}\\
\parse{{\textit{Hello{~}World}}}\\
\parse{Hello [World]}\\
\parse{\textit{Hell[o W]orld}}\\
\parse{Hell[o{~+}W]orld}\\
\end{document}
示例的输出如下:
答案2
我发现递归方法是可以理解的,但比函数系列更难理解analysis
;保存的字符码并不容易{}
。
备注:在简单情况下,如果正则表达式替换就足够了,就使用它。而且它更快。
替代方法包括
- 使用正则表达式暂时用一些占位符替换所有空格,处理后再替换回来。
tokcycle
或etl
包裹。
\tl_analysis_map_inline
不用多说,\f
\a
使用的示例在实际代码中不要重新定义重要的 LaTeX 宏,例如,等等:
\ExplSyntaxOn
\def \f #1 {
\tl_build_begin:N \a
\tl_analysis_map_inline:nn {#1} {
\int_compare:nNnTF {##2} = {`[} {
\tl_build_put_right:Nn \a {
\iftrue \noexpand\textbf{ \else } \fi % this will x-expand to '\textbf{'
}
} {
\int_compare:nNnTF {##2} = {`]} {
\tl_build_put_right:Nn \a {
\iffalse { \else } \fi % this will x-expand to '}'
}
} {
\int_compare:nNnTF {##2} = {`+} {
\tl_build_put_right:Nn \a { $\noexpand \oplus$ }
} {
\tl_build_put_right:Nn \a {##1}
}
}
}
}
\tl_build_end:N \a
\tl_set:Nx \a {\a}
}
\ExplSyntaxOff
\f{a [Hello + world] b}
\a
答案中还演示了构建部分不平衡中间标记列表的方法。(请参阅我的其他答案中的解释这里)
然而,这种方法的缺点是,必须手动计算左括号和右括号的数量
- 抓取一件物品,或者
- 仅在顶层操作。
(虽然tokcycle
包可以自动执行此操作,但它会忽略 的字符码{}
)