yew_router/
utils.rs

1use std::cell::RefCell;
2
3use wasm_bindgen::JsCast;
4
5pub(crate) fn strip_slash_suffix(path: &str) -> &str {
6    path.strip_suffix('/').unwrap_or(path)
7}
8
9static BASE_URL_LOADED: std::sync::Once = std::sync::Once::new();
10thread_local! {
11    static BASE_URL: RefCell<Option<String>> = RefCell::new(None);
12}
13
14// This exists so we can cache the base url. It costs us a `to_string` call instead of a DOM API
15// call. Considering base urls are generally short, it *should* be less expensive.
16pub fn base_url() -> Option<String> {
17    BASE_URL_LOADED.call_once(|| {
18        BASE_URL.with(|val| {
19            *val.borrow_mut() = fetch_base_url();
20        })
21    });
22    BASE_URL.with(|it| it.borrow().as_ref().map(|it| it.to_string()))
23}
24
25pub fn fetch_base_url() -> Option<String> {
26    match gloo::utils::document().query_selector("base[href]") {
27        Ok(Some(base)) => {
28            let base = base.unchecked_into::<web_sys::HtmlBaseElement>().href();
29
30            let url = web_sys::Url::new(&base).unwrap();
31            let base = url.pathname();
32
33            let base = if base != "/" {
34                strip_slash_suffix(&base)
35            } else {
36                return None;
37            };
38
39            Some(base.to_string())
40        }
41        _ => None,
42    }
43}
44
45#[cfg(target_arch = "wasm32")]
46pub fn compose_path(pathname: &str, query: &str) -> Option<String> {
47    gloo::utils::window()
48        .location()
49        .href()
50        .ok()
51        .and_then(|base| web_sys::Url::new_with_base(pathname, &base).ok())
52        .map(|url| {
53            url.set_search(query);
54            format!("{}{}", url.pathname(), url.search())
55        })
56}
57
58#[cfg(not(target_arch = "wasm32"))]
59pub fn compose_path(pathname: &str, query: &str) -> Option<String> {
60    let query = query.trim();
61
62    if !query.is_empty() {
63        Some(format!("{pathname}?{query}"))
64    } else {
65        Some(pathname.to_owned())
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use gloo::utils::document;
72    use wasm_bindgen_test::wasm_bindgen_test as test;
73    use yew_router::prelude::*;
74    use yew_router::utils::*;
75
76    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
77
78    #[derive(Debug, Clone, Copy, PartialEq, Routable)]
79    enum Routes {
80        #[at("/")]
81        Home,
82        #[at("/no")]
83        No,
84        #[at("/404")]
85        NotFound,
86    }
87
88    #[test]
89    fn test_base_url() {
90        document().head().unwrap().set_inner_html(r#""#);
91
92        assert_eq!(fetch_base_url(), None);
93
94        document()
95            .head()
96            .unwrap()
97            .set_inner_html(r#"<base href="/base/">"#);
98        assert_eq!(fetch_base_url(), Some("/base".to_string()));
99
100        document()
101            .head()
102            .unwrap()
103            .set_inner_html(r#"<base href="/base">"#);
104        assert_eq!(fetch_base_url(), Some("/base".to_string()));
105    }
106
107    #[test]
108    fn test_compose_path() {
109        assert_eq!(compose_path("/home", ""), Some("/home".to_string()));
110        assert_eq!(
111            compose_path("/path/to", "foo=bar"),
112            Some("/path/to?foo=bar".to_string())
113        );
114        assert_eq!(
115            compose_path("/events", "from=2019&to=2021"),
116            Some("/events?from=2019&to=2021".to_string())
117        );
118    }
119}