Skip to main content

tablo_test/protocol/
search.rs

1//! Reads the panel's global search out of a rendered page: `[data-global-search]` around the
2//! topbar's box and its form, `section[data-search-group]` for each resource's results, and
3//! `a[data-search-hit]` for each result, with its title in the first `span` and its details as
4//! `dt`/`dd` pairs.
5
6use super::dom::{attr_value, strip_text, tags_where};
7
8/// One resource's results.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct SearchGroup {
11    /// The resource's plural label, which heads the results.
12    pub label: String,
13    pub results: Vec<SearchResult>,
14}
15
16/// One record a global search lists.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct SearchResult {
19    pub title: String,
20    /// The page the result opens.
21    pub href: String,
22    /// Each `(label, value)` under the title.
23    pub details: Vec<(String, String)>,
24}
25
26/// The results `html` lists, by resource in document order: the results page's, or the topbar
27/// box's.
28pub fn search_results(html: &str) -> Vec<SearchGroup> {
29    tags_where(html, |tag, attrs| {
30        tag == "section" && attr_value(attrs, "data-search-group").is_some()
31    })
32    .into_iter()
33    .map(|(_, attrs, inner)| SearchGroup {
34        label: attr_value(&attrs, "data-search-group").unwrap_or_default(),
35        results: tags_where(&inner, |tag, attrs| {
36            tag == "a" && attr_value(attrs, "data-search-hit").is_some()
37        })
38        .into_iter()
39        .map(|(_, attrs, inner)| SearchResult {
40            title: tags_where(&inner, |tag, _| tag == "span")
41                .first()
42                .map(|(_, _, text)| strip_text(text))
43                .unwrap_or_default(),
44            href: attr_value(&attrs, "href").unwrap_or_default(),
45            details: tags_where(&inner, |tag, _| tag == "dt")
46                .into_iter()
47                .zip(tags_where(&inner, |tag, _| tag == "dd"))
48                .map(|((_, _, label), (_, _, value))| {
49                    (
50                        strip_text(&label).trim_end_matches(':').to_string(),
51                        strip_text(&value),
52                    )
53                })
54                .collect(),
55        })
56        .collect(),
57    })
58    .collect()
59}
60
61/// Where the topbar's search box submits its term, or `None` when the page shows no box.
62pub fn search_box(html: &str) -> Option<String> {
63    let (_, _, inner) = tags_where(html, |tag, attrs| {
64        tag == "div" && attr_value(attrs, "data-global-search").is_some()
65    })
66    .into_iter()
67    .next()?;
68    tags_where(&inner, |tag, _| tag == "form")
69        .first()
70        .and_then(|(_, attrs, _)| attr_value(attrs, "action"))
71}