Skip to main content

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}