use crate::{components::RouterContext, hooks::use_resolved_path};
use leptos::{children::Children, oco::Oco, prelude::*};
use reactive_graph::{computed::ArcMemo, owner::use_context};
use std::{borrow::Cow, rc::Rc};
pub trait ToHref {
fn to_href(&self) -> Box<dyn Fn() -> String + '_>;
}
impl ToHref for &str {
fn to_href(&self) -> Box<dyn Fn() -> String> {
let s = self.to_string();
Box::new(move || s.clone())
}
}
impl ToHref for String {
fn to_href(&self) -> Box<dyn Fn() -> String> {
let s = self.clone();
Box::new(move || s.clone())
}
}
impl ToHref for Cow<'_, str> {
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
let s = self.to_string();
Box::new(move || s.clone())
}
}
impl ToHref for Oco<'_, str> {
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
let s = self.to_string();
Box::new(move || s.clone())
}
}
impl ToHref for Rc<str> {
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
let s = self.to_string();
Box::new(move || s.clone())
}
}
impl<F> ToHref for F
where
F: Fn() -> String + 'static,
{
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
Box::new(self)
}
}
#[component]
pub fn A<H>(
href: H,
#[prop(optional, into)]
target: Option<Oco<'static, str>>,
#[prop(optional)]
exact: bool,
#[prop(optional)]
strict_trailing_slash: bool,
#[prop(default = true)]
scroll: bool,
children: Children,
) -> impl IntoView + 'static
where
H: ToHref + Send + Sync + 'static,
{
fn inner(
href: ArcMemo<String>,
target: Option<Oco<'static, str>>,
exact: bool,
children: Children,
strict_trailing_slash: bool,
scroll: bool,
) -> impl IntoView {
let RouterContext { current_url, .. } =
use_context().expect("tried to use <A/> outside a <Router/>.");
let is_active = {
let href = href.clone();
move || {
let path = normalize_path(&href.read());
current_url.with(|loc| {
let loc = loc.path();
if exact {
loc == path
} else {
is_active_for(&path, loc, strict_trailing_slash)
}
})
}
};
view! {
<a
href=move || href.get()
target=target
aria-current=move || if is_active() { Some("page") } else { None }
data-noscroll=!scroll
>
{children()}
</a>
}
}
let href = use_resolved_path(move || href.to_href()());
inner(href, target, exact, children, strict_trailing_slash, scroll)
}
fn is_active_for(
href: &str,
location: &str,
strict_trailing_slash: bool,
) -> bool {
let mut href_f = href.split('/');
std::iter::zip(location.split('/'), href_f.by_ref())
.enumerate()
.all(|(c, (loc_p, href_p))| {
loc_p == href_p || href_p.is_empty() && c > 1
})
&& match href_f.next() {
None => true,
Some("") => !strict_trailing_slash,
_ => false,
}
}
fn normalize_path(path: &str) -> String {
if path.is_empty() {
return String::new();
}
let mut del = 0;
let mut it = path
.split(['?', '#'])
.next()
.unwrap_or_default()
.split(['/'])
.rev()
.peekable();
let init = if it.peek() == Some(&"..") {
String::from("/")
} else {
String::new()
};
let mut path = it
.filter(|v| {
if *v == ".." {
del += 1;
false
} else if *v == "." {
false
} else if del > 0 {
del -= 1;
false
} else {
true
}
})
.fold(init, |mut p, v| {
p.reserve(v.len() + 1);
p.insert(0, '/');
p.insert_str(0, v);
p
});
path.truncate(path.len().saturating_sub(1));
if !path.starts_with('/') {
path.insert(0, '/');
}
path
}
#[cfg(test)]
mod tests {
use super::{is_active_for, normalize_path};
#[test]
fn is_active_for_matched() {
[false, true].into_iter().for_each(|f| {
assert!(is_active_for("/", "/", f));
assert!(is_active_for("/item", "/item", f));
assert!(is_active_for("/item", "/item/", f));
assert!(is_active_for("/item/", "/item/", f));
assert!(is_active_for("/item", "/item/one", f));
assert!(is_active_for("/item", "/item/one/", f));
assert!(is_active_for("/item/", "/item/one", f));
assert!(is_active_for("/item/", "/item/one/", f));
assert!(is_active_for("/item/1", "/item/1", f));
assert!(is_active_for("/item/1", "/item/1/", f));
assert!(is_active_for("/item/1/", "/item/1/", f));
assert!(is_active_for("/item/1", "/item/1/two", f));
assert!(is_active_for("/item/1", "/item/1/three/four/", f));
assert!(is_active_for("/item/1/", "/item/1/three/four", f));
assert!(is_active_for("/item/1/", "/item/1/two/", f));
assert!(is_active_for("/item/1/two/three", "/item/1/two/three", f));
assert!(is_active_for(
"/item/1/two/three/444",
"/item/1/two/three/444/",
f
));
assert!(is_active_for(
"/item/1/two/three/444/FIVE/final/",
"/item/1/two/three/444/FIVE/final/",
f
));
assert!(is_active_for(
"/item/1/two/three",
"/item/1/two/three/three/two/1/item",
f
));
assert!(is_active_for(
"/item/1/two/three/444",
"/item/1/two/three/444/just_one_more/",
f
));
assert!(is_active_for(
"/item/1/two/three/444/final/",
"/item/1/two/three/444/final/just/kidding",
f
));
assert!(is_active_for(
"/item/////",
"/item/one/two/three/four/",
f
));
assert!(is_active_for(
"/item/////",
"/item/1/two/three/three/two/1/item",
f
));
assert!(is_active_for(
"/item/1///three//1",
"/item/1/two/three/three/two/1/item",
f
));
assert!(is_active_for(
"/item//foo",
"/item/this_is_not_empty/foo/bar/baz",
f
));
});
assert!(is_active_for("/item/", "/item", false));
assert!(is_active_for("/item/1/", "/item/1", false));
assert!(is_active_for(
"/item/1/two/three/444/FIVE/",
"/item/1/two/three/444/FIVE",
false
));
}
#[test]
fn is_active_for_mismatched() {
[false, true].into_iter().for_each(|f| {
assert!(!is_active_for("/", "/item", f));
assert!(!is_active_for("/", "/somewhere/", f));
assert!(!is_active_for("/", "/else/where", f));
assert!(!is_active_for("/", "/no/where/", f));
assert!(!is_active_for("/somewhere", "/", f));
assert!(!is_active_for("/somewhere/", "/", f));
assert!(!is_active_for("/else/where", "/", f));
assert!(!is_active_for("/no/where/", "/", f));
assert!(!is_active_for("/level", "/item", f));
assert!(!is_active_for("/level", "/item/", f));
assert!(!is_active_for("/level/", "/item", f));
assert!(!is_active_for("/level/", "/item/", f));
assert!(!is_active_for("/item/one", "/item", f));
assert!(!is_active_for("/item/one/", "/item", f));
assert!(!is_active_for("/item/one", "/item/", f));
assert!(!is_active_for("/item/one/", "/item/", f));
assert!(!is_active_for("/item/1/two", "/item/1", f));
assert!(!is_active_for("/item/1/three/four/", "/item/1", f));
assert!(!is_active_for("/item/1/three/four", "/item/", f));
assert!(!is_active_for("/item/1/two/", "/item/", f));
assert!(!is_active_for(
"/item/1/two/three/three/two/1/item",
"/item/1/two/three",
f
));
assert!(!is_active_for(
"/item/1/two/three/444/just_one_more/",
"/item/1/two/three/444",
f
));
assert!(!is_active_for(
"/item/1/two/three/444/final/just/kidding",
"/item/1/two/three/444/final/",
f
));
assert!(!is_active_for(
"//////",
"/item/1/two/three/three/two/1/item",
f
));
assert!(!is_active_for(
"/item/1/two/three/three/two/1/item",
"//////",
f
));
assert!(!is_active_for(
"/item/one/two/three/four/",
"/item/////",
f
));
assert!(!is_active_for(
"/item/one/two/three/four/",
"/item////four/",
f
));
});
assert!(!is_active_for("/item/", "/item", true));
assert!(!is_active_for("/item/1/", "/item/1", true));
assert!(!is_active_for(
"/item/1/two/three/444/FIVE/",
"/item/1/two/three/444/FIVE",
true
));
}
#[test]
fn normalize_path_test() {
assert!(normalize_path("") == "".to_string());
assert!(normalize_path("/") == "/".to_string());
assert!(normalize_path("/some") == "/some".to_string());
assert!(normalize_path("/some/") == "/some/".to_string());
assert!(normalize_path("/some/../another") == "/another".to_string());
assert!(
normalize_path("/one/two/../three/../../four")
== "/four".to_string()
);
assert!(normalize_path("/one/two/..") == "/one/".to_string());
assert!(normalize_path("/one/two/../") == "/one/".to_string());
assert!(normalize_path("/..") == "/".to_string());
assert!(normalize_path("/../") == "/".to_string());
assert!(
normalize_path("/one/../../two/three") == "/two/three".to_string()
);
assert!(
normalize_path("/one/../../two/three/")
== "/two/three/".to_string()
);
}
}