move router crates

This commit is contained in:
Greg Johnston 2024-04-27 11:03:14 -04:00
parent 51d0ed4735
commit 521172ef5f
70 changed files with 1116 additions and 1019 deletions

View file

@ -21,7 +21,7 @@ server_fn = { workspace = true, features = ["axum-no-default"] }
leptos_macro = { workspace = true, features = ["axum"] }
leptos_meta = { workspace = true }
reactive_graph = { workspace = true, features = ["sandboxed-arenas"] }
routing = { workspace = true }
leptos_router = { workspace = true }
#leptos_integration_utils = { workspace = true }
parking_lot = "0.12"
serde_json = "1"

View file

@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.6.12"
version = "0.7.0-preview"
edition = "2021"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
@ -11,73 +11,52 @@ rust-version.workspace = true
[dependencies]
leptos = { workspace = true }
leptos_integration_utils = { workspace = true, optional = true }
leptos_meta = { workspace = true, optional = true }
cached = { version = "0.45.0", optional = true }
cfg-if = "1"
gloo-net = { version = "0.5", features = ["http"] }
lazy_static = "1"
linear-map = { version = "1", features = ["serde_impl"] }
once_cell = "1.18"
regex = { version = "1", optional = true }
url = { version = "2", optional = true }
percent-encoding = "2"
thiserror = "1"
serde_qs = "0.13"
serde = "1"
tracing = "0.1"
any_spawner = { workspace = true }
either_of = { workspace = true }
or_poisoned = { workspace = true }
reactive_graph = { workspace = true }
tachys = { workspace = true, features = ["reactive_graph"] }
url = "2"
js-sys = { version = "0.3" }
wasm-bindgen = { version = "0.2" }
wasm-bindgen-futures = { version = "0.4" }
lru = { version = "0.11", optional = true }
serde_json = "1.0.96"
itertools = "0.12.0"
send_wrapper = "0.6.0"
tracing = { version = "0.1", optional = true }
paste = "1"
once_cell = "1"
send_wrapper = "0.6"
thiserror = "1"
[dependencies.web-sys]
version = "0.3"
features = [
# History/Routing
"History",
"HtmlAnchorElement",
"MouseEvent",
"Url",
# Form
"FormData",
"HtmlButtonElement",
"HtmlFormElement",
"HtmlInputElement",
"SubmitEvent",
"Url",
"UrlSearchParams",
# Fetching in Hydrate Mode
"Headers",
"Request",
"RequestInit",
"RequestMode",
"Response",
"Window",
"Document",
"Window",
"console",
# History/Routing
"History",
"HtmlAnchorElement",
"Location",
"MouseEvent",
"Url",
# Form
"FormData",
"HtmlButtonElement",
"HtmlFormElement",
"HtmlInputElement",
"SubmitEvent",
"Url",
"UrlSearchParams",
# Fetching in Hydrate Mode
"Headers",
"Request",
"RequestInit",
"RequestMode",
"Response",
]
[features]
default = []
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = [
"leptos/ssr",
"dep:cached",
"dep:lru",
"dep:url",
"dep:regex",
"dep:leptos_integration_utils",
"dep:leptos_meta",
]
nightly = ["leptos/nightly"]
[package.metadata.cargo-all-features]
# No need to test optional dependencies as they are enabled by the ssr feature
denylist = ["url", "regex", "nightly"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
tracing = ["dep:tracing"]
ssr = []
nightly = []
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]

View file

@ -1,12 +1,20 @@
use crate::{
Location, NavigateOptions, Params, ParamsError, ParamsMap, RouteContext,
RouterContext,
components::RouterContext,
location::{Location, Url},
navigate::{NavigateOptions, UseNavigate},
params::{Params, ParamsError, ParamsMap},
RouteContext,
};
use leptos::{
request_animation_frame, signal_prelude::*, use_context, window, Oco,
use leptos::{leptos_dom::helpers::window, oco::Oco};
use reactive_graph::{
computed::{ArcMemo, Memo},
owner::use_context,
signal::{ArcReadSignal, ArcRwSignal, ReadSignal},
traits::{Get, Read, With},
};
use std::{rc::Rc, str::FromStr};
use tachys::renderer::Renderer;
/*
/// Constructs a signal synchronized with a specific URL query parameter.
///
/// The function creates a bidirectional sync mechanism between the state encapsulated in a signal and a URL query parameter.
@ -111,76 +119,90 @@ pub fn use_router() -> RouterContext {
);
panic!("You must call use_router() within a <Router/> component");
}
}
/// Returns the current [`RouteContext`], containing information about the matched route.
#[track_caller]
pub fn use_route() -> RouteContext {
use_context::<RouteContext>().unwrap_or_else(|| use_router().base())
}
/// Returns the data for the current route, which is provided by the `data` prop on `<Route/>`.
#[track_caller]
pub fn use_route_data<T: Clone + 'static>() -> Option<T> {
let route = use_context::<RouteContext>()?;
let data = route.inner.data.borrow();
let data = data.clone()?;
let downcast = data.downcast_ref::<T>().cloned();
downcast
}
}*/
/// Returns the current [`Location`], which contains reactive variables
#[track_caller]
pub fn use_location() -> Location {
use_router().inner.location.clone()
let RouterContext { location, .. } =
use_context().expect("Tried to access Location outside a <Router>.");
location
}
#[track_caller]
fn use_params_raw() -> ArcReadSignal<ParamsMap> {
use_context().expect(
"Tried to access params outside the context of a matched <Route>.",
)
}
/// Returns a raw key-value map of route params.
#[track_caller]
pub fn use_params_map() -> Memo<ParamsMap> {
let route = use_route();
route.params()
// TODO this can be optimized in future to map over the signal, rather than cloning
let params = use_params_raw();
Memo::new(move |_| params.get())
}
/// Returns the current route params, parsed into the given type, or an error.
#[track_caller]
pub fn use_params<T>() -> Memo<Result<T, ParamsError>>
where
T: Params + PartialEq,
T: Params + PartialEq + Send + Sync,
{
let route = use_route();
create_memo(move |_| route.params().with(T::from_map))
// TODO this can be optimized in future to map over the signal, rather than cloning
let params = use_params_raw();
Memo::new(move |_| params.with(T::from_map))
}
#[track_caller]
fn use_url_raw() -> ArcRwSignal<Url> {
let RouterContext { current_url, .. } = use_context()
.expect("Tried to access reactive URL outside a <Router> component.");
current_url
}
#[track_caller]
pub fn use_url() -> ReadSignal<Url> {
use_url_raw().read_only().into()
}
/// Returns a raw key-value map of the URL search query.
#[track_caller]
pub fn use_query_map() -> Memo<ParamsMap> {
use_router().inner.location.query
let url = use_url_raw();
Memo::new(move |_| url.with(|url| url.search_params().clone()))
}
/// Returns the current URL search query, parsed into the given type, or an error.
#[track_caller]
pub fn use_query<T>() -> Memo<Result<T, ParamsError>>
where
T: Params + PartialEq,
T: Params + PartialEq + Send + Sync,
{
let router = use_router();
create_memo(move |_| router.inner.location.query.with(|m| T::from_map(m)))
let url = use_url_raw();
Memo::new(move |_| url.with(|url| T::from_map(url.search_params())))
}
/// Resolves the given path relative to the current route.
#[track_caller]
pub fn use_resolved_path(
path: impl Fn() -> String + 'static,
) -> Memo<Option<String>> {
let route = use_route();
create_memo(move |_| {
pub(crate) fn use_resolved_path<R: Renderer + 'static>(
path: impl Fn() -> String + Send + Sync + 'static,
) -> ArcMemo<Option<String>> {
let router = use_context::<RouterContext>()
.expect("called use_resolved_path outside a <Router>");
let matched = use_context::<RouteContext<R>>().map(|route| route.matched);
ArcMemo::new(move |_| {
let path = path();
if path.starts_with('/') {
Some(path)
} else {
route.resolve_path_tracked(&path)
router
.resolve_path(
&path,
matched.as_ref().map(|n| n.get()).as_deref(),
)
.map(|n| n.to_string())
}
})
}
@ -201,31 +223,18 @@ pub fn use_resolved_path(
/// ```
#[track_caller]
pub fn use_navigate() -> impl Fn(&str, NavigateOptions) + Clone {
let router = use_router();
move |to, options| {
let router = Rc::clone(&router.inner);
let to = to.to_string();
if cfg!(any(feature = "csr", feature = "hydrate")) {
request_animation_frame(move || {
#[allow(unused_variables)]
if let Err(e) = router.navigate_from_route(&to, &options) {
leptos::logging::debug_warn!("use_navigate error: {e:?}");
}
});
} else {
leptos::logging::warn!(
"The navigation function returned by `use_navigate` should \
not be called during server rendering."
);
}
}
let cx = use_context::<RouterContext>()
.expect("You cannot call `use_navigate` outside a <Router>.");
move |path: &str, options: NavigateOptions| cx.navigate(path, options)
}
/*
/// Returns a signal that tells you whether you are currently navigating backwards.
pub(crate) fn use_is_back_navigation() -> ReadSignal<bool> {
let router = use_router();
router.inner.is_back.read_only()
}
*/
/// Resolves a redirect location to an (absolute) URL.
pub(crate) fn resolve_redirect_url(loc: &str) -> Option<web_sys::Url> {

View file

@ -1,208 +1,27 @@
#![forbid(unsafe_code)]
//! # Leptos Router
//!
//! Leptos Router is a router and state management tool for web applications
//! written in Rust using the [`Leptos`] web framework.
//! It is ”isomorphic”, i.e., it can be used for client-side applications/single-page
//! apps (SPAs), server-side rendering/multi-page apps (MPAs), or to synchronize
//! state between the two.
//!
//! ## Philosophy
//!
//! Leptos Router is built on a few simple principles:
//! 1. **URL drives state.** For web applications, the URL should be the ultimate
//! source of truth for most of your apps state. (Its called a **Universal
//! Resource Locator** for a reason!)
//!
//! 2. **Nested routing.** A URL can match multiple routes that exist in a nested tree
//! and are rendered by different components. This means you can navigate between siblings
//! in this tree without re-rendering or triggering any change in the parent routes.
//!
//! 3. **Progressive enhancement.** The [`A`] and [`Form`] components resolve any relative
//! nested routes, render actual `<a>` and `<form>` elements, and (when possible)
//! upgrading them to handle those navigations with client-side routing. If youre using
//! them with server-side rendering (with or without hydration), they just work,
//! whether JS/WASM have loaded or not.
//!
//! ## Example
//!
//! ```rust
//! use leptos::*;
//! use leptos_router::*;
//!
//! #[component]
//! pub fn RouterExample() -> impl IntoView {
//! view! {
//!
//! <div id="root">
//! // we wrap the whole app in a <Router/> to allow client-side navigation
//! // from our nav links below
//! <Router>
//! // <nav> and <main> will show on every route
//! <nav>
//! // LR will enhance the active <a> link with the [aria-current] attribute
//! // we can use this for styling them with CSS like `[aria-current] { font-weight: bold; }`
//! <A href="contacts">"Contacts"</A>
//! // But we can also use a normal class attribute like it is a normal component
//! <A href="settings" class="my-class">"Settings"</A>
//! // It also supports signals!
//! <A href="about" class=move || "my-class">"About"</A>
//! </nav>
//! <main>
//! // <Routes/> both defines our routes and shows them on the page
//! <Routes>
//! // our root route: the contact list is always shown
//! <Route
//! path=""
//! view=ContactList
//! >
//! // users like /gbj or /bob
//! <Route
//! path=":id"
//! view=Contact
//! />
//! // a fallback if the /:id segment is missing from the URL
//! <Route
//! path=""
//! view=move || view! { <p class="contact">"Select a contact."</p> }
//! />
//! </Route>
//! // LR will automatically use this for /about, not the /:id match above
//! <Route
//! path="about"
//! view=About
//! />
//! </Routes>
//! </main>
//! </Router>
//! </div>
//! }
//! }
//!
//! type ContactSummary = (); // TODO!
//! type Contact = (); // TODO!()
//!
//! // contact_data reruns whenever the :id param changes
//! async fn contact_data(id: String) -> Contact {
//! todo!()
//! }
//!
//! // contact_list_data *doesn't* rerun when the :id changes,
//! // because that param is nested lower than the <ContactList/> route
//! async fn contact_list_data() -> Vec<ContactSummary> {
//! todo!()
//! }
//!
//! #[component]
//! fn ContactList() -> impl IntoView {
//! // loads the contact list data once; doesn't reload when nested routes change
//! let contacts = create_resource(|| (), |_| contact_list_data());
//! view! {
//!
//! <div>
//! // show the contacts
//! <ul>
//! {move || contacts.read().map(|contacts| view! { <li>"todo contact info"</li> } )}
//! </ul>
//!
//! // insert the nested child route here
//! <Outlet/>
//! </div>
//! }
//! }
//!
//! #[component]
//! fn Contact() -> impl IntoView {
//! let params = use_params_map();
//! let data = create_resource(
//!
//! move || params.with(|p| p.get("id").cloned().unwrap_or_default()),
//! move |id| contact_data(id)
//! );
//! todo!()
//! }
//!
//! #[component]
//! fn About() -> impl IntoView {
//! todo!()
//! }
//! ```
//!
//! ## Module Route Definitions
//! Routes can also be modularized and nested by defining them in separate components, which can be
//! located in and imported from other modules. Components that return `<Route/>` should be marked
//! `#[component(transparent)]`, as in this example:
//! ```rust
//! use leptos::*;
//! use leptos_router::*;
//!
//! #[component]
//! pub fn App() -> impl IntoView {
//! view! {
//! <Router>
//! <Routes>
//! <Route path="/" view=move || {
//! view! { "-> /" }
//! }/>
//! <ExternallyDefinedRoute/>
//! </Routes>
//! </Router>
//! }
//! }
//!
//! // `transparent` here marks the component as returning data (a RouteDefinition), not a view
//! #[component(transparent)]
//! pub fn ExternallyDefinedRoute() -> impl IntoView {
//! view! {
//! <Route path="/some-area" view=move || {
//! view! { <div>
//! <h2>"Some Area"</h2>
//! <Outlet/>
//! </div> }
//! }>
//! <Route path="/path-a/:id" view=move || {
//! view! { <p>"Path A"</p> }
//! }/>
//! <Route path="/path-b/:id" view=move || {
//! view! { <p>"Path B"</p> }
//! }/>
//! </Route>
//! }
//! }
//! ```
//!
//! # Feature Flags
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
//! - `nightly`: On `nightly` Rust, enables the function-call syntax for signal getters and setters.
//!
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in.
//!
//! [`Leptos`]: <https://github.com/leptos-rs/leptos>
#![cfg_attr(feature = "nightly", feature(auto_traits))]
#![cfg_attr(feature = "nightly", feature(negative_impls))]
#![cfg_attr(feature = "nightly", feature(type_name_of_val))]
// to prevent warnings from popping up when a nightly feature is stabilized
#![allow(stable_features)]
mod animation;
mod components;
#[cfg(any(feature = "ssr", doc))]
mod extract_routes;
mod history;
mod hooks;
#[doc(hidden)]
pub mod matching;
mod render_mode;
pub use components::*;
#[cfg(any(feature = "ssr", doc))]
pub use extract_routes::*;
pub use history::*;
pub use hooks::*;
pub use matching::{RouteDefinition, *};
pub use render_mode::*;
extern crate tracing;
pub mod components;
mod flat_router;
mod generate_route_list;
pub mod hooks;
pub mod link;
pub mod location;
mod matching;
mod method;
mod navigate;
mod nested_router;
pub mod params;
//mod router;
mod ssr_mode;
mod static_route;
pub use flat_router::*;
pub use generate_route_list::*;
pub use matching::*;
pub use method::*;
pub use nested_router::*;
//pub use router::*;
pub use ssr_mode::*;
pub use static_route::*;

View file

@ -1,86 +1,452 @@
mod expand_optionals;
mod matcher;
mod resolve_path;
mod route;
mod choose_view;
mod path_segment;
pub(crate) mod resolve_path;
pub use choose_view::*;
pub use path_segment::*;
mod horizontal;
mod nested;
mod vertical;
use crate::{Method, SsrMode};
pub use horizontal::*;
pub use nested::*;
use std::{borrow::Cow, marker::PhantomData};
use tachys::{
renderer::Renderer,
view::{any_view::IntoAny, Render, RenderHtml},
};
pub use vertical::*;
use crate::{Branches, RouteData};
pub use expand_optionals::*;
pub use matcher::*;
pub use resolve_path::*;
pub use route::*;
use std::rc::Rc;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct RouteMatch {
pub path_match: PathMatch,
pub route: RouteData,
#[derive(Debug)]
pub struct Routes<Children, Rndr> {
base: Option<Cow<'static, str>>,
children: Children,
ty: PhantomData<Rndr>,
}
pub(crate) fn get_route_matches(
router_id: usize,
base: &str,
location: String,
) -> Rc<Vec<RouteMatch>> {
#[cfg(feature = "ssr")]
{
use lru::LruCache;
use std::{cell::RefCell, num::NonZeroUsize};
type RouteMatchCache = LruCache<(usize, String), Rc<Vec<RouteMatch>>>;
thread_local! {
static ROUTE_MATCH_CACHE: RefCell<RouteMatchCache> = RefCell::new(LruCache::new(NonZeroUsize::new(32).unwrap()));
impl<Children, Rndr> Clone for Routes<Children, Rndr>
where
Children: Clone,
{
fn clone(&self) -> Self {
Self {
base: self.base.clone(),
children: self.children.clone(),
ty: PhantomData,
}
ROUTE_MATCH_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
Rc::clone(
cache.get_or_insert((router_id, location.clone()), || {
build_route_matches(router_id, base, location)
}),
)
})
}
#[cfg(not(feature = "ssr"))]
build_route_matches(router_id, base, location)
}
fn build_route_matches(
router_id: usize,
base: &str,
location: String,
) -> Rc<Vec<RouteMatch>> {
Rc::new(Branches::with(router_id, base, |branches| {
for branch in branches {
if let Some(matches) = branch.matcher(&location) {
return matches;
}
}
vec![]
}))
}
/// Describes a branch of the route tree.
#[derive(Debug, Clone, PartialEq)]
pub struct Branch {
/// All the routes contained in the branch.
pub routes: Vec<RouteData>,
/// How closely this branch matches the current URL.
pub score: i32,
}
impl Branch {
fn matcher<'a>(&'a self, location: &'a str) -> Option<Vec<RouteMatch>> {
let mut matches = Vec::new();
for route in self.routes.iter().rev() {
match route.matcher.test(location) {
None => return None,
Some(m) => matches.push(RouteMatch {
path_match: m,
route: route.clone(),
}),
}
}
matches.reverse();
Some(matches)
}
}
impl<Children, Rndr> Routes<Children, Rndr> {
pub fn new(children: Children) -> Self {
Self {
base: None,
children,
ty: PhantomData,
}
}
pub fn new_with_base(
children: Children,
base: impl Into<Cow<'static, str>>,
) -> Self {
Self {
base: Some(base.into()),
children,
ty: PhantomData,
}
}
}
impl<Children, Rndr> Routes<Children, Rndr>
where
Rndr: Renderer + 'static,
Children: MatchNestedRoutes<Rndr>,
{
pub fn match_route(&self, path: &str) -> Option<Children::Match> {
let path = match &self.base {
None => path,
Some(base) => {
let (base, path) = if base.starts_with('/') {
(base.trim_start_matches('/'), path.trim_start_matches('/'))
} else {
(base.as_ref(), path)
};
match path.strip_prefix(base) {
Some(path) => path,
None => return None,
}
}
};
let (matched, remaining) = self.children.match_nested(path);
let matched = matched?;
if !(remaining.is_empty() || remaining == "/") {
None
} else {
Some(matched.1)
}
}
pub fn generate_routes(
&self,
) -> (
Option<&str>,
impl IntoIterator<Item = GeneratedRouteData> + '_,
) {
(self.base.as_deref(), self.children.generate_routes())
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct RouteMatchId(pub(crate) u16);
pub trait MatchInterface<R>
where
R: Renderer + 'static,
{
type Child: MatchInterface<R> + MatchParams + 'static;
type View: Render<R> + RenderHtml<R> + Send + 'static;
fn as_id(&self) -> RouteMatchId;
fn as_matched(&self) -> &str;
fn into_view_and_child(
self,
) -> (impl ChooseView<R, Output = Self::View>, Option<Self::Child>);
}
pub trait MatchParams {
type Params: IntoIterator<Item = (Cow<'static, str>, String)>;
fn to_params(&self) -> Self::Params;
}
pub trait MatchNestedRoutes<R>
where
R: Renderer + 'static,
{
type Data;
type View;
type Match: MatchInterface<R> + MatchParams;
fn match_nested<'a>(
&'a self,
path: &'a str,
) -> (Option<(RouteMatchId, Self::Match)>, &str);
fn generate_routes(
&self,
) -> impl IntoIterator<Item = GeneratedRouteData> + '_;
}
#[derive(Default)]
pub(crate) struct GeneratedRouteData {
pub segments: Vec<PathSegment>,
pub ssr_mode: SsrMode,
}
#[cfg(test)]
mod tests {
use super::{NestedRoute, ParamSegment, Routes};
use crate::{MatchInterface, PathSegment, StaticSegment, WildcardSegment};
use std::marker::PhantomData;
use tachys::renderer::dom::Dom;
#[test]
pub fn matches_single_root_route() {
let routes = Routes::<_, Dom>::new(NestedRoute {
segments: StaticSegment("/"),
children: (),
data: (),
view: |_| (),
rndr: PhantomData,
});
let matched = routes.match_route("/");
assert!(matched.is_some());
let matched = routes.match_route("");
assert!(matched.is_some());
let (base, paths) = routes.generate_routes();
assert_eq!(base, None);
let paths = paths.into_iter().collect::<Vec<_>>();
assert_eq!(paths, vec![vec![PathSegment::Static("/".into())]]);
}
#[test]
pub fn matches_nested_route() {
let routes = Routes::new(NestedRoute {
segments: StaticSegment(""),
children: NestedRoute {
segments: (StaticSegment("author"), StaticSegment("contact")),
children: (),
data: (),
view: |_| "Contact Me",
rndr: PhantomData,
},
data: (),
view: |_| "Home",
rndr: PhantomData,
});
// route generation
let (base, paths) = routes.generate_routes();
assert_eq!(base, None);
let paths = paths.into_iter().collect::<Vec<_>>();
assert_eq!(
paths,
vec![vec![
PathSegment::Static("".into()),
PathSegment::Static("author".into()),
PathSegment::Static("contact".into())
]]
);
let matched = routes.match_route("/author/contact").unwrap();
assert_eq!(matched.matched(), "");
assert_eq!(matched.to_child().unwrap().matched(), "/author/contact");
let view = matched.to_view();
assert_eq!(*view, "Home");
assert_eq!(*matched.to_child().unwrap().to_view(), "Contact Me");
}
#[test]
pub fn does_not_match_incomplete_route() {
let routes = Routes::new(NestedRoute {
segments: StaticSegment(""),
children: NestedRoute {
segments: (StaticSegment("author"), StaticSegment("contact")),
children: (),
data: (),
view: "Contact Me",
rndr: PhantomData,
},
data: (),
view: "Home",
rndr: PhantomData,
});
let matched = routes.match_route("/");
assert!(matched.is_none());
}
#[test]
pub fn chooses_between_nested_routes() {
let routes = Routes::new((
NestedRoute {
segments: StaticSegment("/"),
children: (
NestedRoute {
segments: StaticSegment(""),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
NestedRoute {
segments: StaticSegment("about"),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
),
data: (),
view: || (),
rndr: PhantomData,
},
NestedRoute {
segments: StaticSegment("/blog"),
children: (
NestedRoute {
segments: StaticSegment(""),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
NestedRoute {
segments: (StaticSegment("post"), ParamSegment("id")),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
),
data: (),
view: || (),
rndr: PhantomData,
},
));
// generates routes correctly
let (base, paths) = routes.generate_routes();
assert_eq!(base, None);
let paths = paths.into_iter().collect::<Vec<_>>();
assert_eq!(
paths,
vec![
vec![
PathSegment::Static("/".into()),
PathSegment::Static("".into()),
],
vec![
PathSegment::Static("/".into()),
PathSegment::Static("about".into())
],
vec![
PathSegment::Static("/blog".into()),
PathSegment::Static("".into()),
],
vec![
PathSegment::Static("/blog".into()),
PathSegment::Static("post".into()),
PathSegment::Param("id".into())
]
]
);
let matched = routes.match_route("/about").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert!(params.is_empty());
let matched = routes.match_route("/blog").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert!(params.is_empty());
let matched = routes.match_route("/blog/post/42").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert_eq!(params, vec![("id", "42")]);
}
#[test]
pub fn arbitrary_nested_routes() {
let routes = Routes::new_with_base(
(
NestedRoute {
segments: StaticSegment("/"),
children: (
NestedRoute {
segments: StaticSegment("/"),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
NestedRoute {
segments: StaticSegment("about"),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
),
data: (),
view: || (),
rndr: PhantomData,
},
NestedRoute {
segments: StaticSegment("/blog"),
children: (
NestedRoute {
segments: StaticSegment(""),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
NestedRoute {
segments: StaticSegment("category"),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
NestedRoute {
segments: (
StaticSegment("post"),
ParamSegment("id"),
),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
),
data: (),
view: || (),
rndr: PhantomData,
},
NestedRoute {
segments: (
StaticSegment("/contact"),
WildcardSegment("any"),
),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
),
"/portfolio",
);
// generates routes correctly
let (base, _paths) = routes.generate_routes();
assert_eq!(base, Some("/portfolio"));
let matched = routes.match_route("/about");
assert!(matched.is_none());
let matched = routes.match_route("/portfolio/about").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert!(params.is_empty());
let matched = routes.match_route("/portfolio/blog/post/42").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert_eq!(params, vec![("id", "42")]);
let matched = routes.match_route("/portfolio/contact").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert_eq!(params, vec![("any", "")]);
let matched = routes.match_route("/portfolio/contact/foobar").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert_eq!(params, vec![("any", "foobar")]);
}
}
#[derive(Debug)]
pub struct PartialPathMatch<'a, ParamsIter> {
pub(crate) remaining: &'a str,
pub(crate) params: ParamsIter,
pub(crate) matched: &'a str,
}
impl<'a, ParamsIter> PartialPathMatch<'a, ParamsIter> {
pub fn new(
remaining: &'a str,
params: ParamsIter,
matched: &'a str,
) -> Self {
Self {
remaining,
params,
matched,
}
}
pub fn is_complete(&self) -> bool {
self.remaining.is_empty() || self.remaining == "/"
}
pub fn remaining(&self) -> &str {
self.remaining
}
pub fn params(self) -> ParamsIter {
self.params
}
pub fn matched(&self) -> &str {
self.matched
}
}

View file

@ -1,9 +1,5 @@
// Implementation based on Solid Router
// see <https://github.com/solidjs/solid-router/blob/main/src/utils.ts>
use std::borrow::Cow;
#[doc(hidden)]
pub fn resolve_path<'a>(
base: &'a str,
path: &'a str,
@ -17,9 +13,7 @@ pub fn resolve_path<'a>(
let result = if let Some(from_path) = from_path {
if path.starts_with('/') {
base_path
} else if from_path.to_lowercase().find(&base_path.to_lowercase())
!= Some(0)
{
} else if from_path.find(base_path.as_ref()) != Some(0) {
base_path + from_path
} else {
from_path

16
router_macro/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "leptos_router_macro"
edition = "2021"
version.workspace = true
[lib]
proc-macro = true
[dependencies]
proc-macro-error = { version = "1", default-features = false }
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }
[dev-dependencies]
leptos_router = { workspace = true }

76
router_macro/src/lib.rs Normal file
View file

@ -0,0 +1,76 @@
use proc_macro::{TokenStream, TokenTree};
use quote::{quote, ToTokens};
use std::borrow::Cow;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input,
token::Token,
};
#[proc_macro_error::proc_macro_error]
#[proc_macro]
pub fn path(tokens: TokenStream) -> TokenStream {
let mut parser = SegmentParser::new(tokens);
parser.parse_all();
let segments = Segments(parser.segments);
segments.into_token_stream().into()
}
#[derive(Debug, PartialEq)]
struct Segments(pub Vec<Segment>);
#[derive(Debug, PartialEq)]
enum Segment {
Static(Cow<'static, str>),
}
struct SegmentParser {
input: proc_macro::token_stream::IntoIter,
current_str: Option<String>,
segments: Vec<Segment>,
}
impl SegmentParser {
pub fn new(input: TokenStream) -> Self {
Self {
input: input.into_iter(),
current_str: None,
segments: Vec::new(),
}
}
}
impl SegmentParser {
pub fn parse_all(&mut self) {
for input in self.input.by_ref() {
match input {
TokenTree::Literal(lit) => {
Self::parse_str(
lit.to_string()
.trim_start_matches(['"', '/'])
.trim_end_matches(['"', '/']),
&mut self.segments,
);
}
TokenTree::Group(_) => todo!(),
TokenTree::Ident(_) => todo!(),
TokenTree::Punct(_) => todo!(),
}
}
}
pub fn parse_str(current_str: &str, segments: &mut Vec<Segment>) {
let mut chars = current_str.chars();
}
}
impl ToTokens for Segments {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let children = quote! {};
if self.0.len() != 1 {
tokens.extend(quote! { (#children) });
} else {
tokens.extend(children)
}
}
}

View file

@ -0,0 +1,9 @@
use routing::StaticSegment;
use routing_macro::path;
#[test]
fn parses_empty_list() {
let output = path!("");
assert_eq!(output, ());
//let segments: Segments = syn::parse(path.into()).unwrap();
}

80
router_old/Cargo.toml Normal file
View file

@ -0,0 +1,80 @@
[package]
name = "leptos_router"
version = "0.7.0-preview"
edition = "2021"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"
repository = "https://github.com/leptos-rs/leptos"
description = "Router for the Leptos web framework."
rust-version.workspace = true
[dependencies]
leptos = { workspace = true }
leptos_integration_utils = { workspace = true, optional = true }
leptos_meta = { workspace = true, optional = true }
cached = { version = "0.45.0", optional = true }
cfg-if = "1"
gloo-net = { version = "0.5", features = ["http"] }
lazy_static = "1"
linear-map = { version = "1", features = ["serde_impl"] }
once_cell = "1.18"
regex = { version = "1", optional = true }
url = { version = "2", optional = true }
percent-encoding = "2"
thiserror = "1"
serde_qs = "0.12"
serde = "1"
tracing = "0.1"
js-sys = { version = "0.3" }
wasm-bindgen = { version = "0.2" }
wasm-bindgen-futures = { version = "0.4" }
lru = { version = "0.11", optional = true }
serde_json = "1.0.96"
itertools = "0.12.0"
send_wrapper = "0.6.0"
[dependencies.web-sys]
version = "0.3"
features = [
# History/Routing
"History",
"HtmlAnchorElement",
"MouseEvent",
"Url",
# Form
"FormData",
"HtmlButtonElement",
"HtmlFormElement",
"HtmlInputElement",
"SubmitEvent",
"Url",
"UrlSearchParams",
# Fetching in Hydrate Mode
"Headers",
"Request",
"RequestInit",
"RequestMode",
"Response",
"Window",
]
[features]
default = []
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = [
"leptos/ssr",
"dep:cached",
"dep:lru",
"dep:url",
"dep:regex",
"dep:leptos_integration_utils",
"dep:leptos_meta",
]
nightly = ["leptos/nightly"]
[package.metadata.cargo-all-features]
# No need to test optional dependencies as they are enabled by the ssr feature
denylist = ["url", "regex", "nightly"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

1
router_old/Makefile.toml Normal file
View file

@ -0,0 +1 @@
extend = { path = "../cargo-make/main.toml" }

View file

@ -1,20 +1,12 @@
use crate::{
components::RouterContext,
location::{Location, Url},
navigate::{NavigateOptions, UseNavigate},
params::{Params, ParamsError, ParamsMap},
RouteContext,
Location, NavigateOptions, Params, ParamsError, ParamsMap, RouteContext,
RouterContext,
};
use leptos::{leptos_dom::helpers::window, oco::Oco};
use reactive_graph::{
computed::{ArcMemo, Memo},
owner::use_context,
signal::{ArcReadSignal, ArcRwSignal, ReadSignal},
traits::{Get, Read, With},
use leptos::{
request_animation_frame, signal_prelude::*, use_context, window, Oco,
};
use std::{rc::Rc, str::FromStr};
use tachys::renderer::Renderer;
/*
/// Constructs a signal synchronized with a specific URL query parameter.
///
/// The function creates a bidirectional sync mechanism between the state encapsulated in a signal and a URL query parameter.
@ -55,17 +47,6 @@ use tachys::renderer::Renderer;
pub fn create_query_signal<T>(
key: impl Into<Oco<'static, str>>,
) -> (Memo<Option<T>>, SignalSetter<Option<T>>)
where
T: FromStr + ToString + PartialEq,
{
create_query_signal_with_options::<T>(key, NavigateOptions::default())
}
#[track_caller]
pub fn create_query_signal_with_options<T>(
key: impl Into<Oco<'static, str>>,
nav_options: NavigateOptions,
) -> (Memo<Option<T>>, SignalSetter<Option<T>>)
where
T: FromStr + ToString + PartialEq,
{
@ -96,7 +77,7 @@ where
let path = location.pathname.get_untracked();
let hash = location.hash.get_untracked();
let new_url = format!("{path}{qs}{hash}");
navigate(&new_url, nav_options.clone());
navigate(&new_url, NavigateOptions::default());
});
(get, set)
@ -119,90 +100,76 @@ pub fn use_router() -> RouterContext {
);
panic!("You must call use_router() within a <Router/> component");
}
}*/
}
/// Returns the current [`RouteContext`], containing information about the matched route.
#[track_caller]
pub fn use_route() -> RouteContext {
use_context::<RouteContext>().unwrap_or_else(|| use_router().base())
}
/// Returns the data for the current route, which is provided by the `data` prop on `<Route/>`.
#[track_caller]
pub fn use_route_data<T: Clone + 'static>() -> Option<T> {
let route = use_context::<RouteContext>()?;
let data = route.inner.data.borrow();
let data = data.clone()?;
let downcast = data.downcast_ref::<T>().cloned();
downcast
}
/// Returns the current [`Location`], which contains reactive variables
#[track_caller]
pub fn use_location() -> Location {
let RouterContext { location, .. } =
use_context().expect("Tried to access Location outside a <Router>.");
location
}
#[track_caller]
fn use_params_raw() -> ArcReadSignal<ParamsMap> {
use_context().expect(
"Tried to access params outside the context of a matched <Route>.",
)
use_router().inner.location.clone()
}
/// Returns a raw key-value map of route params.
#[track_caller]
pub fn use_params_map() -> Memo<ParamsMap> {
// TODO this can be optimized in future to map over the signal, rather than cloning
let params = use_params_raw();
Memo::new(move |_| params.get())
let route = use_route();
route.params()
}
/// Returns the current route params, parsed into the given type, or an error.
#[track_caller]
pub fn use_params<T>() -> Memo<Result<T, ParamsError>>
where
T: Params + PartialEq + Send + Sync,
T: Params + PartialEq,
{
// TODO this can be optimized in future to map over the signal, rather than cloning
let params = use_params_raw();
Memo::new(move |_| params.with(T::from_map))
}
#[track_caller]
fn use_url_raw() -> ArcRwSignal<Url> {
let RouterContext { current_url, .. } = use_context()
.expect("Tried to access reactive URL outside a <Router> component.");
current_url
}
#[track_caller]
pub fn use_url() -> ReadSignal<Url> {
use_url_raw().read_only().into()
let route = use_route();
create_memo(move |_| route.params().with(T::from_map))
}
/// Returns a raw key-value map of the URL search query.
#[track_caller]
pub fn use_query_map() -> Memo<ParamsMap> {
let url = use_url_raw();
Memo::new(move |_| url.with(|url| url.search_params().clone()))
use_router().inner.location.query
}
/// Returns the current URL search query, parsed into the given type, or an error.
#[track_caller]
pub fn use_query<T>() -> Memo<Result<T, ParamsError>>
where
T: Params + PartialEq + Send + Sync,
T: Params + PartialEq,
{
let url = use_url_raw();
Memo::new(move |_| url.with(|url| T::from_map(url.search_params())))
let router = use_router();
create_memo(move |_| router.inner.location.query.with(|m| T::from_map(m)))
}
/// Resolves the given path relative to the current route.
#[track_caller]
pub(crate) fn use_resolved_path<R: Renderer + 'static>(
path: impl Fn() -> String + Send + Sync + 'static,
) -> ArcMemo<Option<String>> {
let router = use_context::<RouterContext>()
.expect("called use_resolved_path outside a <Router>");
let matched = use_context::<RouteContext<R>>().map(|route| route.matched);
ArcMemo::new(move |_| {
pub fn use_resolved_path(
path: impl Fn() -> String + 'static,
) -> Memo<Option<String>> {
let route = use_route();
create_memo(move |_| {
let path = path();
if path.starts_with('/') {
Some(path)
} else {
router
.resolve_path(
&path,
matched.as_ref().map(|n| n.get()).as_deref(),
)
.map(|n| n.to_string())
route.resolve_path_tracked(&path)
}
})
}
@ -223,18 +190,31 @@ pub(crate) fn use_resolved_path<R: Renderer + 'static>(
/// ```
#[track_caller]
pub fn use_navigate() -> impl Fn(&str, NavigateOptions) + Clone {
let cx = use_context::<RouterContext>()
.expect("You cannot call `use_navigate` outside a <Router>.");
move |path: &str, options: NavigateOptions| cx.navigate(path, options)
let router = use_router();
move |to, options| {
let router = Rc::clone(&router.inner);
let to = to.to_string();
if cfg!(any(feature = "csr", feature = "hydrate")) {
request_animation_frame(move || {
#[allow(unused_variables)]
if let Err(e) = router.navigate_from_route(&to, &options) {
leptos::logging::debug_warn!("use_navigate error: {e:?}");
}
});
} else {
leptos::logging::warn!(
"The navigation function returned by `use_navigate` should \
not be called during server rendering."
);
}
}
}
/*
/// Returns a signal that tells you whether you are currently navigating backwards.
pub(crate) fn use_is_back_navigation() -> ReadSignal<bool> {
let router = use_router();
router.inner.is_back.read_only()
}
*/
/// Resolves a redirect location to an (absolute) URL.
pub(crate) fn resolve_redirect_url(loc: &str) -> Option<web_sys::Url> {

208
router_old/src/lib.rs Normal file
View file

@ -0,0 +1,208 @@
#![forbid(unsafe_code)]
//! # Leptos Router
//!
//! Leptos Router is a router and state management tool for web applications
//! written in Rust using the [`Leptos`] web framework.
//! It is ”isomorphic”, i.e., it can be used for client-side applications/single-page
//! apps (SPAs), server-side rendering/multi-page apps (MPAs), or to synchronize
//! state between the two.
//!
//! ## Philosophy
//!
//! Leptos Router is built on a few simple principles:
//! 1. **URL drives state.** For web applications, the URL should be the ultimate
//! source of truth for most of your apps state. (Its called a **Universal
//! Resource Locator** for a reason!)
//!
//! 2. **Nested routing.** A URL can match multiple routes that exist in a nested tree
//! and are rendered by different components. This means you can navigate between siblings
//! in this tree without re-rendering or triggering any change in the parent routes.
//!
//! 3. **Progressive enhancement.** The [`A`] and [`Form`] components resolve any relative
//! nested routes, render actual `<a>` and `<form>` elements, and (when possible)
//! upgrading them to handle those navigations with client-side routing. If youre using
//! them with server-side rendering (with or without hydration), they just work,
//! whether JS/WASM have loaded or not.
//!
//! ## Example
//!
//! ```rust
//! use leptos::*;
//! use leptos_router::*;
//!
//! #[component]
//! pub fn RouterExample() -> impl IntoView {
//! view! {
//!
//! <div id="root">
//! // we wrap the whole app in a <Router/> to allow client-side navigation
//! // from our nav links below
//! <Router>
//! // <nav> and <main> will show on every route
//! <nav>
//! // LR will enhance the active <a> link with the [aria-current] attribute
//! // we can use this for styling them with CSS like `[aria-current] { font-weight: bold; }`
//! <A href="contacts">"Contacts"</A>
//! // But we can also use a normal class attribute like it is a normal component
//! <A href="settings" class="my-class">"Settings"</A>
//! // It also supports signals!
//! <A href="about" class=move || "my-class">"About"</A>
//! </nav>
//! <main>
//! // <Routes/> both defines our routes and shows them on the page
//! <Routes>
//! // our root route: the contact list is always shown
//! <Route
//! path=""
//! view=ContactList
//! >
//! // users like /gbj or /bob
//! <Route
//! path=":id"
//! view=Contact
//! />
//! // a fallback if the /:id segment is missing from the URL
//! <Route
//! path=""
//! view=move || view! { <p class="contact">"Select a contact."</p> }
//! />
//! </Route>
//! // LR will automatically use this for /about, not the /:id match above
//! <Route
//! path="about"
//! view=About
//! />
//! </Routes>
//! </main>
//! </Router>
//! </div>
//! }
//! }
//!
//! type ContactSummary = (); // TODO!
//! type Contact = (); // TODO!()
//!
//! // contact_data reruns whenever the :id param changes
//! async fn contact_data(id: String) -> Contact {
//! todo!()
//! }
//!
//! // contact_list_data *doesn't* rerun when the :id changes,
//! // because that param is nested lower than the <ContactList/> route
//! async fn contact_list_data() -> Vec<ContactSummary> {
//! todo!()
//! }
//!
//! #[component]
//! fn ContactList() -> impl IntoView {
//! // loads the contact list data once; doesn't reload when nested routes change
//! let contacts = create_resource(|| (), |_| contact_list_data());
//! view! {
//!
//! <div>
//! // show the contacts
//! <ul>
//! {move || contacts.read().map(|contacts| view! { <li>"todo contact info"</li> } )}
//! </ul>
//!
//! // insert the nested child route here
//! <Outlet/>
//! </div>
//! }
//! }
//!
//! #[component]
//! fn Contact() -> impl IntoView {
//! let params = use_params_map();
//! let data = create_resource(
//!
//! move || params.with(|p| p.get("id").cloned().unwrap_or_default()),
//! move |id| contact_data(id)
//! );
//! todo!()
//! }
//!
//! #[component]
//! fn About() -> impl IntoView {
//! todo!()
//! }
//! ```
//!
//! ## Module Route Definitions
//! Routes can also be modularized and nested by defining them in separate components, which can be
//! located in and imported from other modules. Components that return `<Route/>` should be marked
//! `#[component(transparent)]`, as in this example:
//! ```rust
//! use leptos::*;
//! use leptos_router::*;
//!
//! #[component]
//! pub fn App() -> impl IntoView {
//! view! {
//! <Router>
//! <Routes>
//! <Route path="/" view=move || {
//! view! { "-> /" }
//! }/>
//! <ExternallyDefinedRoute/>
//! </Routes>
//! </Router>
//! }
//! }
//!
//! // `transparent` here marks the component as returning data (a RouteDefinition), not a view
//! #[component(transparent)]
//! pub fn ExternallyDefinedRoute() -> impl IntoView {
//! view! {
//! <Route path="/some-area" view=move || {
//! view! { <div>
//! <h2>"Some Area"</h2>
//! <Outlet/>
//! </div> }
//! }>
//! <Route path="/path-a/:id" view=move || {
//! view! { <p>"Path A"</p> }
//! }/>
//! <Route path="/path-b/:id" view=move || {
//! view! { <p>"Path B"</p> }
//! }/>
//! </Route>
//! }
//! }
//! ```
//!
//! # Feature Flags
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
//! - `nightly`: On `nightly` Rust, enables the function-call syntax for signal getters and setters.
//!
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in.
//!
//! [`Leptos`]: <https://github.com/leptos-rs/leptos>
#![cfg_attr(feature = "nightly", feature(auto_traits))]
#![cfg_attr(feature = "nightly", feature(negative_impls))]
#![cfg_attr(feature = "nightly", feature(type_name_of_val))]
// to prevent warnings from popping up when a nightly feature is stabilized
#![allow(stable_features)]
mod animation;
mod components;
#[cfg(any(feature = "ssr", doc))]
mod extract_routes;
mod history;
mod hooks;
#[doc(hidden)]
pub mod matching;
mod render_mode;
pub use components::*;
#[cfg(any(feature = "ssr", doc))]
pub use extract_routes::*;
pub use history::*;
pub use hooks::*;
pub use matching::{RouteDefinition, *};
pub use render_mode::*;
extern crate tracing;

View file

@ -0,0 +1,86 @@
mod expand_optionals;
mod matcher;
mod resolve_path;
mod route;
use crate::{Branches, RouteData};
pub use expand_optionals::*;
pub use matcher::*;
pub use resolve_path::*;
pub use route::*;
use std::rc::Rc;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct RouteMatch {
pub path_match: PathMatch,
pub route: RouteData,
}
pub(crate) fn get_route_matches(
router_id: usize,
base: &str,
location: String,
) -> Rc<Vec<RouteMatch>> {
#[cfg(feature = "ssr")]
{
use lru::LruCache;
use std::{cell::RefCell, num::NonZeroUsize};
type RouteMatchCache = LruCache<(usize, String), Rc<Vec<RouteMatch>>>;
thread_local! {
static ROUTE_MATCH_CACHE: RefCell<RouteMatchCache> = RefCell::new(LruCache::new(NonZeroUsize::new(32).unwrap()));
}
ROUTE_MATCH_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
Rc::clone(
cache.get_or_insert((router_id, location.clone()), || {
build_route_matches(router_id, base, location)
}),
)
})
}
#[cfg(not(feature = "ssr"))]
build_route_matches(router_id, base, location)
}
fn build_route_matches(
router_id: usize,
base: &str,
location: String,
) -> Rc<Vec<RouteMatch>> {
Rc::new(Branches::with(router_id, base, |branches| {
for branch in branches {
if let Some(matches) = branch.matcher(&location) {
return matches;
}
}
vec![]
}))
}
/// Describes a branch of the route tree.
#[derive(Debug, Clone, PartialEq)]
pub struct Branch {
/// All the routes contained in the branch.
pub routes: Vec<RouteData>,
/// How closely this branch matches the current URL.
pub score: i32,
}
impl Branch {
fn matcher<'a>(&'a self, location: &'a str) -> Option<Vec<RouteMatch>> {
let mut matches = Vec::new();
for route in self.routes.iter().rev() {
match route.matcher.test(location) {
None => return None,
Some(m) => matches.push(RouteMatch {
path_match: m,
route: route.clone(),
}),
}
}
matches.reverse();
Some(matches)
}
}

View file

@ -1,5 +1,9 @@
// Implementation based on Solid Router
// see <https://github.com/solidjs/solid-router/blob/main/src/utils.ts>
use std::borrow::Cow;
#[doc(hidden)]
pub fn resolve_path<'a>(
base: &'a str,
path: &'a str,
@ -13,7 +17,9 @@ pub fn resolve_path<'a>(
let result = if let Some(from_path) = from_path {
if path.starts_with('/') {
base_path
} else if from_path.find(base_path.as_ref()) != Some(0) {
} else if from_path.to_lowercase().find(&base_path.to_lowercase())
!= Some(0)
{
base_path + from_path
} else {
from_path

View file

@ -1,53 +0,0 @@
[package]
name = "routing"
edition = "2021"
version.workspace = true
[dependencies]
leptos = { workspace = true }
any_spawner = { workspace = true }
either_of = { workspace = true }
or_poisoned = { workspace = true }
reactive_graph = { workspace = true }
tachys = { workspace = true, features = ["reactive_graph"] }
url = "2"
js-sys = { version = "0.3" }
wasm-bindgen = { version = "0.2" }
tracing = { version = "0.1", optional = true }
paste = "1"
once_cell = "1"
send_wrapper = "0.6"
thiserror = "1"
[dependencies.web-sys]
version = "0.3"
features = [
"Document",
"Window",
"console",
# History/Routing
"History",
"HtmlAnchorElement",
"Location",
"MouseEvent",
"Url",
# Form
"FormData",
"HtmlButtonElement",
"HtmlFormElement",
"HtmlInputElement",
"SubmitEvent",
"Url",
"UrlSearchParams",
# Fetching in Hydrate Mode
"Headers",
"Request",
"RequestInit",
"RequestMode",
"Response",
]
[features]
tracing = ["dep:tracing"]
ssr = []
nightly = []

View file

@ -1,27 +0,0 @@
#![forbid(unsafe_code)]
#![cfg_attr(feature = "nightly", feature(auto_traits))]
#![cfg_attr(feature = "nightly", feature(negative_impls))]
pub mod components;
mod flat_router;
mod generate_route_list;
pub mod hooks;
pub mod link;
pub mod location;
mod matching;
mod method;
mod navigate;
mod nested_router;
pub mod params;
//mod router;
mod ssr_mode;
mod static_route;
pub use flat_router::*;
pub use generate_route_list::*;
pub use matching::*;
pub use method::*;
pub use nested_router::*;
//pub use router::*;
pub use ssr_mode::*;
pub use static_route::*;

View file

@ -1,452 +0,0 @@
mod choose_view;
mod path_segment;
pub(crate) mod resolve_path;
pub use choose_view::*;
pub use path_segment::*;
mod horizontal;
mod nested;
mod vertical;
use crate::{Method, SsrMode};
pub use horizontal::*;
pub use nested::*;
use std::{borrow::Cow, marker::PhantomData};
use tachys::{
renderer::Renderer,
view::{any_view::IntoAny, Render, RenderHtml},
};
pub use vertical::*;
#[derive(Debug)]
pub struct Routes<Children, Rndr> {
base: Option<Cow<'static, str>>,
children: Children,
ty: PhantomData<Rndr>,
}
impl<Children, Rndr> Clone for Routes<Children, Rndr>
where
Children: Clone,
{
fn clone(&self) -> Self {
Self {
base: self.base.clone(),
children: self.children.clone(),
ty: PhantomData,
}
}
}
impl<Children, Rndr> Routes<Children, Rndr> {
pub fn new(children: Children) -> Self {
Self {
base: None,
children,
ty: PhantomData,
}
}
pub fn new_with_base(
children: Children,
base: impl Into<Cow<'static, str>>,
) -> Self {
Self {
base: Some(base.into()),
children,
ty: PhantomData,
}
}
}
impl<Children, Rndr> Routes<Children, Rndr>
where
Rndr: Renderer + 'static,
Children: MatchNestedRoutes<Rndr>,
{
pub fn match_route(&self, path: &str) -> Option<Children::Match> {
let path = match &self.base {
None => path,
Some(base) => {
let (base, path) = if base.starts_with('/') {
(base.trim_start_matches('/'), path.trim_start_matches('/'))
} else {
(base.as_ref(), path)
};
match path.strip_prefix(base) {
Some(path) => path,
None => return None,
}
}
};
let (matched, remaining) = self.children.match_nested(path);
let matched = matched?;
if !(remaining.is_empty() || remaining == "/") {
None
} else {
Some(matched.1)
}
}
pub fn generate_routes(
&self,
) -> (
Option<&str>,
impl IntoIterator<Item = GeneratedRouteData> + '_,
) {
(self.base.as_deref(), self.children.generate_routes())
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct RouteMatchId(pub(crate) u16);
pub trait MatchInterface<R>
where
R: Renderer + 'static,
{
type Child: MatchInterface<R> + MatchParams + 'static;
type View: Render<R> + RenderHtml<R> + Send + 'static;
fn as_id(&self) -> RouteMatchId;
fn as_matched(&self) -> &str;
fn into_view_and_child(
self,
) -> (impl ChooseView<R, Output = Self::View>, Option<Self::Child>);
}
pub trait MatchParams {
type Params: IntoIterator<Item = (Cow<'static, str>, String)>;
fn to_params(&self) -> Self::Params;
}
pub trait MatchNestedRoutes<R>
where
R: Renderer + 'static,
{
type Data;
type View;
type Match: MatchInterface<R> + MatchParams;
fn match_nested<'a>(
&'a self,
path: &'a str,
) -> (Option<(RouteMatchId, Self::Match)>, &str);
fn generate_routes(
&self,
) -> impl IntoIterator<Item = GeneratedRouteData> + '_;
}
#[derive(Default)]
pub(crate) struct GeneratedRouteData {
pub segments: Vec<PathSegment>,
pub ssr_mode: SsrMode,
}
#[cfg(test)]
mod tests {
use super::{NestedRoute, ParamSegment, Routes};
use crate::{MatchInterface, PathSegment, StaticSegment, WildcardSegment};
use std::marker::PhantomData;
use tachys::renderer::dom::Dom;
#[test]
pub fn matches_single_root_route() {
let routes = Routes::<_, Dom>::new(NestedRoute {
segments: StaticSegment("/"),
children: (),
data: (),
view: |_| (),
rndr: PhantomData,
});
let matched = routes.match_route("/");
assert!(matched.is_some());
let matched = routes.match_route("");
assert!(matched.is_some());
let (base, paths) = routes.generate_routes();
assert_eq!(base, None);
let paths = paths.into_iter().collect::<Vec<_>>();
assert_eq!(paths, vec![vec![PathSegment::Static("/".into())]]);
}
#[test]
pub fn matches_nested_route() {
let routes = Routes::new(NestedRoute {
segments: StaticSegment(""),
children: NestedRoute {
segments: (StaticSegment("author"), StaticSegment("contact")),
children: (),
data: (),
view: |_| "Contact Me",
rndr: PhantomData,
},
data: (),
view: |_| "Home",
rndr: PhantomData,
});
// route generation
let (base, paths) = routes.generate_routes();
assert_eq!(base, None);
let paths = paths.into_iter().collect::<Vec<_>>();
assert_eq!(
paths,
vec![vec![
PathSegment::Static("".into()),
PathSegment::Static("author".into()),
PathSegment::Static("contact".into())
]]
);
let matched = routes.match_route("/author/contact").unwrap();
assert_eq!(matched.matched(), "");
assert_eq!(matched.to_child().unwrap().matched(), "/author/contact");
let view = matched.to_view();
assert_eq!(*view, "Home");
assert_eq!(*matched.to_child().unwrap().to_view(), "Contact Me");
}
#[test]
pub fn does_not_match_incomplete_route() {
let routes = Routes::new(NestedRoute {
segments: StaticSegment(""),
children: NestedRoute {
segments: (StaticSegment("author"), StaticSegment("contact")),
children: (),
data: (),
view: "Contact Me",
rndr: PhantomData,
},
data: (),
view: "Home",
rndr: PhantomData,
});
let matched = routes.match_route("/");
assert!(matched.is_none());
}
#[test]
pub fn chooses_between_nested_routes() {
let routes = Routes::new((
NestedRoute {
segments: StaticSegment("/"),
children: (
NestedRoute {
segments: StaticSegment(""),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
NestedRoute {
segments: StaticSegment("about"),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
),
data: (),
view: || (),
rndr: PhantomData,
},
NestedRoute {
segments: StaticSegment("/blog"),
children: (
NestedRoute {
segments: StaticSegment(""),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
NestedRoute {
segments: (StaticSegment("post"), ParamSegment("id")),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
),
data: (),
view: || (),
rndr: PhantomData,
},
));
// generates routes correctly
let (base, paths) = routes.generate_routes();
assert_eq!(base, None);
let paths = paths.into_iter().collect::<Vec<_>>();
assert_eq!(
paths,
vec![
vec![
PathSegment::Static("/".into()),
PathSegment::Static("".into()),
],
vec![
PathSegment::Static("/".into()),
PathSegment::Static("about".into())
],
vec![
PathSegment::Static("/blog".into()),
PathSegment::Static("".into()),
],
vec![
PathSegment::Static("/blog".into()),
PathSegment::Static("post".into()),
PathSegment::Param("id".into())
]
]
);
let matched = routes.match_route("/about").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert!(params.is_empty());
let matched = routes.match_route("/blog").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert!(params.is_empty());
let matched = routes.match_route("/blog/post/42").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert_eq!(params, vec![("id", "42")]);
}
#[test]
pub fn arbitrary_nested_routes() {
let routes = Routes::new_with_base(
(
NestedRoute {
segments: StaticSegment("/"),
children: (
NestedRoute {
segments: StaticSegment("/"),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
NestedRoute {
segments: StaticSegment("about"),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
),
data: (),
view: || (),
rndr: PhantomData,
},
NestedRoute {
segments: StaticSegment("/blog"),
children: (
NestedRoute {
segments: StaticSegment(""),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
NestedRoute {
segments: StaticSegment("category"),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
NestedRoute {
segments: (
StaticSegment("post"),
ParamSegment("id"),
),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
),
data: (),
view: || (),
rndr: PhantomData,
},
NestedRoute {
segments: (
StaticSegment("/contact"),
WildcardSegment("any"),
),
children: (),
data: (),
view: || (),
rndr: PhantomData,
},
),
"/portfolio",
);
// generates routes correctly
let (base, _paths) = routes.generate_routes();
assert_eq!(base, Some("/portfolio"));
let matched = routes.match_route("/about");
assert!(matched.is_none());
let matched = routes.match_route("/portfolio/about").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert!(params.is_empty());
let matched = routes.match_route("/portfolio/blog/post/42").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert_eq!(params, vec![("id", "42")]);
let matched = routes.match_route("/portfolio/contact").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert_eq!(params, vec![("any", "")]);
let matched = routes.match_route("/portfolio/contact/foobar").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert_eq!(params, vec![("any", "foobar")]);
}
}
#[derive(Debug)]
pub struct PartialPathMatch<'a, ParamsIter> {
pub(crate) remaining: &'a str,
pub(crate) params: ParamsIter,
pub(crate) matched: &'a str,
}
impl<'a, ParamsIter> PartialPathMatch<'a, ParamsIter> {
pub fn new(
remaining: &'a str,
params: ParamsIter,
matched: &'a str,
) -> Self {
Self {
remaining,
params,
matched,
}
}
pub fn is_complete(&self) -> bool {
self.remaining.is_empty() || self.remaining == "/"
}
pub fn remaining(&self) -> &str {
self.remaining
}
pub fn params(self) -> ParamsIter {
self.params
}
pub fn matched(&self) -> &str {
self.matched
}
}