我正在尝试使用Rust,macro_rules并希望创建一个可以解析HTML语法的宏,并简单地将HTML作为字符串回显.下面的宏大部分都在那里:
macro_rules! html {
() => ("");
($text:tt) => {{
format!("{}", $text)
}};
(<$open:ident>[$($children:tt)*]</$close:ident>$($rest:tt)*) => {{
format!("<{}>{}</{}>{}",
stringify!($open),
html!($($children)*),
stringify!($close),
html!($($rest)*))
}};
}
Run Code Online (Sandbox Code Playgroud)
然后使用宏:
println!("{}",
html!(
<html>[
<head>[
<title>["Some Title"]</title>
]</head>
<body>[
<h1>["This is a header!"]</h1>
]</body>
]</html>
)
);
Run Code Online (Sandbox Code Playgroud)
但是,我真的想删除无关的开合方括号.我尝试这样做如下:
macro_rules! html_test {
() => ("");
($text:tt) => {{
format!("{}", $text)
}};
(<$open:ident>$($children:tt)*</$close:ident>$($rest:tt)*) => {{
format!("<{}>{}</{}>{}",
stringify!($open),
html!($($children)*),
stringify!($close),
html!($($rest)*))
}};
}
Run Code Online (Sandbox Code Playgroud)
但是,当我去使用这个宏时:
println!("{}",
html_test!(
<html>
<head>
<title>"Some Title"</title>
</head>
<body>
<h1>"This is a header!"</h1>
</body>
</html>
)
);
Run Code Online (Sandbox Code Playgroud)
我明白了 error: local ambiguity: multiple parsing options: built-in NTs tt ('children') or 1 other option.
我知道这个错误的一般解决方案是添加语法来消除案例歧义(例如添加方括号).对于这个具体的例子,还有其他方法解决这个问题吗?我知道使用过程宏将是一个极端的解决方案,但我宁愿尽可能使用macro_rules.
我意识到使用宏来简单地获取包含HTML的字符串是过度的,但它仅仅是为了这个问题.可能,人们可以使用宏做更多有趣的事情,例如调用函数来构建表示HTML结构的树.
你想让宏实际可用吗?那就不要.实际上,为什么甚至在这里使用宏呢?无论你做什么,你都会在某些时候与Rust lexer作战.只需在字符串文字中写下HTML:
r##"<html>
<head>
<title>Some Title</title>
</head>
<body>
<h1>This is a header!</h1>
</body>
</html>"##
Run Code Online (Sandbox Code Playgroud)
那或者接受宏输入无法匹配实际的HTML语法,关闭标签,继续前进.
你还在这里?哦,所以你不关心可用性或性能?无论成本如何,你真的希望语法略有改进吗?*卷起袖子*
小心你想要的.
您需要使用增量解析器,它允许您绕过一些模糊的解析问题.而不是尝试匹配非分隔的组(您不能这样做),而是递归匹配唯一的前缀.这样做会导致:
macro_rules! html_test {
(@soup {$($parts:expr,)*}, [], ) => {
concat!($($parts),*)
};
(@soup $parts:tt, [$head:ident $($stack:ident)*], ) => {
compile_error!(
concat!(
"unexpected end of HTML; the following elements need closing: ",
stringify!($head),
$(",", stringify!($stack),)*
"."
)
)
};
(@soup {$($parts:tt)*}, [$ex_close:ident $($stack:ident)*], </$got_close:ident> $($tail:tt)*) => {
{
macro_rules! cmp {
($ex_close) => {
html_test!(
@soup
{$($parts)* "</", stringify!($ex_close), ">",},
[$($stack)*], $($tail)*
)
};
($got_close) => {
compile_error!(
concat!(
"closing element mismatch: expected `",
stringify!($ex_close),
"`, got `",
stringify!($got_close),
"`"
)
)
};
}
cmp!($got_close)
}
};
(@soup {$($parts:tt)*}, $stack:tt, <img $($tail:tt)*) => {
html_test!(@tag {$($parts)* "<img",}, $stack, $($tail)*)
};
(@soup {$($parts:tt)*}, [$($stack:ident)*], <$open:ident $($tail:tt)*) => {
html_test!(
@tag
{$($parts)* "<", stringify!($open),},
[$open $($stack)*],
$($tail)*
)
};
(@soup {$($parts:tt)*}, $stack:tt, $text:tt $($tail:tt)*) => {
html_test!(@soup {$($parts)* $text,}, $stack, $($tail)*)
};
(@tag {$($parts:tt)*}, $stack:tt, > $($tail:tt)*) => {
html_test!(@soup {$($parts)* ">",}, $stack, $($tail)*)
};
(@tag {$($parts:tt)*}, $stack:tt, $name:ident=$value:tt $($tail:tt)*) => {
html_test!(
@tag
{$($parts)* " ", stringify!($name), "=", stringify!($value),},
$stack, $($tail)*
)
};
($($tts:tt)*) => {
html_test! { @soup {}, [], $($tts)* }
};
}
Run Code Online (Sandbox Code Playgroud)
这通过爬行输入标记,跟踪需要输出的字符串(in $($parts)*)以及需要关闭(in $($stack)*)的打开标记来工作.一旦它没有输入,并且堆栈为空,它将concat!所有部分放在一起,产生一个静态字符串文字.
这有四个问题:
这通过疯狂的递归水平来咀嚼.如果用完,用户将需要全局提升递归限制.
像这样的宏很慢.
错误报告很糟糕.虽然这将检查结束标记是否与相应的开始标记匹配,但是在调用中的任何特定位置都不会报告问题.
您仍然无法避免需要使用字符串文字.您无法匹配后跟的表达式<或其他表达式,因此匹配字符串必须是(唯一)回退规则.
所以你可以删除分隔符,但我不推荐它.只需引用HTML就像一个理智的人.
顺便说一句,这里是宏的替代版本,其结构略有不同,可以将cmp宏分解出来,并且更容易扩展元素而不关闭标记.请注意,我没有写这个版本.