mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
move router crates
This commit is contained in:
parent
51d0ed4735
commit
521172ef5f
70 changed files with 1116 additions and 1019 deletions
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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 app’s state. (It’s 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 you’re 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::*;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
16
router_macro/Cargo.toml
Normal 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
76
router_macro/src/lib.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
9
router_macro/tests/path.rs
Normal file
9
router_macro/tests/path.rs
Normal 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
80
router_old/Cargo.toml
Normal 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
1
router_old/Makefile.toml
Normal file
|
@ -0,0 +1 @@
|
|||
extend = { path = "../cargo-make/main.toml" }
|
|
@ -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
208
router_old/src/lib.rs
Normal 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 app’s state. (It’s 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 you’re 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;
|
86
router_old/src/matching/mod.rs
Normal file
86
router_old/src/matching/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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 = []
|
|
@ -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::*;
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue