millipede_html/selectors.rs
1/// Declares lazily parsed, process-wide CSS selector accessors.
2///
3/// Each generated function parses its CSS literal at most once and returns the same
4/// `&'static` selector thereafter. Invalid selectors panic on first use.
5///
6/// # Example
7///
8/// ```
9/// millipede_html::selectors! {
10/// pub title_sel = "title";
11/// product = "article.product_pod h3 a";
12/// }
13///
14/// struct HandlerContext {
15/// html: millipede_html::scraper::Html,
16/// }
17///
18/// fn handler(ctx: &HandlerContext) {
19/// for title in ctx.html.select(title_sel()) {
20/// println!("{}", title.text().collect::<String>());
21/// }
22/// let _products = ctx.html.select(product()).count();
23/// }
24///
25/// let ctx = HandlerContext {
26/// html: millipede_html::scraper::Html::parse_document(
27/// "<title>Millipede</title><article class='product_pod'><h3><a>Item</a></h3></article>",
28/// ),
29/// };
30/// handler(&ctx);
31/// ```
32#[macro_export]
33macro_rules! selectors {
34 ($( $vis:vis $name:ident = $css:literal; )+) => { $(
35 $vis fn $name() -> &'static $crate::scraper::Selector {
36 static SELECTOR: ::std::sync::OnceLock<$crate::scraper::Selector> = ::std::sync::OnceLock::new();
37 SELECTOR.get_or_init(|| {
38 $crate::scraper::Selector::parse($css)
39 .unwrap_or_else(|error| panic!("invalid CSS selector {:?}: {error:?}", $css))
40 })
41 }
42 )+ };
43}
44
45#[cfg(test)]
46mod tests {
47 crate::selectors! {
48 test_selector = "a.detail[href]";
49 invalid_selector = "a[";
50 }
51
52 #[test]
53 fn returns_same_static_selector() {
54 assert!(std::ptr::eq(test_selector(), test_selector()));
55 }
56
57 #[test]
58 #[should_panic(expected = "invalid CSS selector")]
59 fn invalid_selector_panics_on_first_use() {
60 let _ = invalid_selector();
61 }
62}