我正在试验 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)*))
}};
}
然后使用宏:
println!("{}",
html!(
<html>[
<head>[
<title>["Some Title"]</title>
]</head>
<body>[
<h1>["This is a header!"]</h1>
]</body>
]</html>
)
);
但是,我真的很想删除多余的左右方括号。我尝试按如下方式进行:
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)*))
}};
}
但是,当我去使用这个宏时:
println!("{}",
html_test!(
<html>
<head>
<title>"Some Title"</title>
</head>
<body>
<h1>"This is a header!"</h1>
</body>
</html>
)
);
我收到错误:局部歧义:多个解析选项:内置 NTs tt('children')或 1 个其他选项。
我知道这个错误的一般解决方案是添加语法来消除歧义(例如添加方括号)。对于这个特定示例,还有其他方法可以解决此问题吗?我知道使用过程宏是一种极端的解决方案,但如果可能的话,我更愿意使用 macro_rules
。
我意识到使用宏来简单地获取包含 HTML 的字符串是多余的,但这只是为了这个问题。潜在地,可以使用宏做更多有趣的事情,例如调用函数来构建表示 HTML 结构的树。
最佳答案
您想让宏真正可用吗?那就不要。实际上,为什么还要在这里使用宏呢?无论你做什么,总有一天你会与 Rust 词法分析器作斗争。只需将 HTML 写成字符串文字,例如:
r##"<html>
<head>
<title>Some Title</title>
</head>
<body>
<h1>This is a header!</h1>
</body>
</html>"##
或者接受宏输入不能匹配实际的 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)* }
};
}
这是通过爬取输入标记、跟踪需要输出的字符串片段(在 $($parts)*
中)和需要关闭的打开标签(在 $($stack)*
中)来实现的。一旦它没有输入,并且堆栈为空,它concat!
将所有部分放在一起,生成一个静态字符串文字。
这有四个问题:
这疯狂地咀嚼递归级别。如果用完了,用户需要全局调高递归限制。
像这样的宏很慢。
错误报告很糟糕。虽然这将检查结束标记是否与相应的开始标记相匹配,但不会在调用的任何特定位置报告问题。
您仍然无法避免使用字符串文字。您不能匹配后跟
<
的表达式或另一个表达式,因此匹配字符串必须是(唯一的)回退规则。
所以您可以删除定界符,但我不推荐这样做。像一个正常人一样引用 HTML。
顺便说一句,这是一个 alternative version of the macro结构略有不同,可以分解出 cmp
宏,并且在不关闭标签的情况下更容易扩展元素。请注意,我没有写这个版本。
关于macros - 克服 Rust 宏中的 "local ambiguity: multiple parsing options:",我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46136169/