diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs index ca9bf690b..8fa5aeeed 100644 --- a/packages/router-macro/src/lib.rs +++ b/packages/router-macro/src/lib.rs @@ -5,6 +5,7 @@ use nest::{Nest, NestId}; use proc_macro::TokenStream; use quote::{__private::Span, format_ident, quote, ToTokens}; use route::Route; +use segment::RouteSegment; use syn::{parse::ParseStream, parse_macro_input, Ident, Token}; use proc_macro2::TokenStream as TokenStream2; @@ -51,12 +52,16 @@ struct RouteEnum { routes: Vec, nests: Vec, layouts: Vec, + site_map: Vec, } impl RouteEnum { fn parse(data: syn::ItemEnum) -> syn::Result { let name = &data.ident; + let mut site_map = Vec::new(); + let mut site_map_stack: Vec> = Vec::new(); + let mut routes = Vec::new(); let mut layouts: Vec = Vec::new(); @@ -106,10 +111,43 @@ impl RouteEnum { }; let nest = attr.parse_args_with(parser)?; + // add the current segment to the site map stack + let segments: Vec<_> = nest + .segments + .iter() + .map(|seg| { + let segment_type = seg.into(); + SiteMapSegment { + segment_type, + children: Vec::new(), + } + }) + .collect(); + if !segments.is_empty() { + site_map_stack.push(segments); + } + nests.push(nest); nest_stack.push(NestId(nest_index)); } else if attr.path.is_ident("end_nest") { nest_stack.pop(); + // pop the current nest segment off the stack and add it to the parent or the site map + if let Some(segment) = site_map_stack.pop() { + let children = site_map_stack + .last_mut() + .map(|seg| &mut seg.last_mut().unwrap().children) + .unwrap_or(&mut site_map); + + // Turn the list of segments in the segments stack into a tree + let mut iter = segment.into_iter().rev(); + let mut current = iter.next().unwrap(); + for mut segment in iter { + segment.children.push(current); + current = segment; + } + + children.push(current); + } } else if attr.path.is_ident("layout") { let parser = |input: ParseStream| { let bang: Option = input.parse().ok(); @@ -149,6 +187,16 @@ impl RouteEnum { let route = Route::parse(active_nests, active_layouts, variant.clone())?; + // add the route to the site map + if let Some(segment) = SiteMapSegment::new(&route.segments) { + let parent = site_map_stack.last_mut(); + let children = match parent { + Some(parent) => &mut parent.last_mut().unwrap().children, + None => &mut site_map, + }; + children.push(segment); + } + routes.push(route); } @@ -157,6 +205,7 @@ impl RouteEnum { routes, nests, layouts, + site_map, }; Ok(myself) @@ -276,6 +325,7 @@ impl RouteEnum { fn routable_impl(&self) -> TokenStream2 { let name = &self.name; + let site_map = &self.site_map; let mut layers = Vec::new(); @@ -304,6 +354,10 @@ impl RouteEnum { quote! { impl dioxus_router::routable::Routable for #name where Self: Clone { + const SITE_MAP: &'static [dioxus_router::routable::SiteMapSegment] = &[ + #(#site_map,)* + ]; + fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a> { let myself = self.clone(); match level { @@ -311,12 +365,10 @@ impl RouteEnum { #index_iter => { match myself { #layers - // _ => panic!("Route::render called with invalid level {}", level), _ => None } }, )* - // _ => panic!("Route::render called with invalid level {}", level), _ => None } } @@ -339,3 +391,76 @@ impl ToTokens for RouteEnum { )); } } + +struct SiteMapSegment { + pub segment_type: SegmentType, + pub children: Vec, +} + +impl SiteMapSegment { + fn new(segments: &[RouteSegment]) -> Option { + let mut current = None; + // walk backwards through the new segments, adding children as we go + for segment in segments.iter().rev() { + let segment_type = segment.into(); + let mut segment = SiteMapSegment { + segment_type, + children: Vec::new(), + }; + // if we have a current segment, add it as a child + if let Some(current) = current.take() { + segment.children.push(current) + } + current = Some(segment); + } + current + } +} + +impl ToTokens for SiteMapSegment { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let segment_type = &self.segment_type; + let children = &self.children; + + tokens.extend(quote! { + dioxus_router::routable::SiteMapSegment { + segment_type: #segment_type, + children: &[ + #(#children,)* + ] + } + }); + } +} + +enum SegmentType { + Static(String), + Dynamic(String), + CatchAll(String), +} + +impl ToTokens for SegmentType { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + SegmentType::Static(s) => { + tokens.extend(quote! { dioxus_router::routable::SegmentType::Static(#s) }) + } + SegmentType::Dynamic(s) => { + tokens.extend(quote! { dioxus_router::routable::SegmentType::Dynamic(#s) }) + } + SegmentType::CatchAll(s) => { + tokens.extend(quote! { dioxus_router::routable::SegmentType::CatchAll(#s) }) + } + } + } +} + +impl<'a> From<&'a RouteSegment> for SegmentType { + fn from(value: &'a RouteSegment) -> Self { + match value { + segment::RouteSegment::Static(s) => SegmentType::Static(s.to_string()), + segment::RouteSegment::Dynamic(s, _) => SegmentType::Dynamic(s.to_string()), + segment::RouteSegment::CatchAll(s, _) => SegmentType::CatchAll(s.to_string()), + } + } +} diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index b057ae853..6446bf41a 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -47,6 +47,7 @@ pub mod prelude { pub use crate::components::*; pub use crate::contexts::*; pub use crate::hooks::*; + pub use crate::routable::*; pub use crate::router_cfg::RouterConfiguration; pub use dioxus_router_macro::Routable; } diff --git a/packages/router/src/routable.rs b/packages/router/src/routable.rs index a7c5f907d..71ab8ffc6 100644 --- a/packages/router/src/routable.rs +++ b/packages/router/src/routable.rs @@ -3,7 +3,7 @@ #![allow(non_snake_case)] use dioxus::prelude::*; -use std::str::FromStr; +use std::{fmt::Display, str::FromStr}; /// An error that occurs when parsing a route #[derive(Debug, PartialEq)] @@ -95,6 +95,9 @@ impl> FromRouteSegments for I { /// Something that can be routed to pub trait Routable: std::fmt::Display + std::str::FromStr + Clone + 'static { + /// The error that can occur when parsing a route + const SITE_MAP: &'static [SiteMapSegment]; + /// Render the route at the given level fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>; } @@ -124,3 +127,54 @@ where self.render(cx, level) } } + +/// A type erased map of the site structurens +#[derive(Debug, Clone, PartialEq)] +pub struct SiteMapSegment { + /// The type of the route segment + pub segment_type: SegmentType, + /// The children of the route segment + pub children: &'static [SiteMapSegment], +} + +impl SiteMapSegment { + /// Take a map of the site structure and flatten it into a vector of routes + pub fn flatten(&self) -> Vec> { + let mut routes = Vec::new(); + self.flatten_inner(&mut routes, Vec::new()); + routes + } + + fn flatten_inner(&self, routes: &mut Vec>, current: Vec) { + let mut current = current; + current.push(self.segment_type.clone()); + if self.children.is_empty() { + routes.push(current); + } else { + for child in self.children { + child.flatten_inner(routes, current.clone()); + } + } + } +} + +/// The type of a route segment +#[derive(Debug, Clone, PartialEq)] +pub enum SegmentType { + /// A static route segment + Static(&'static str), + /// A dynamic route segment + Dynamic(&'static str), + /// A catch all route segment + CatchAll(&'static str), +} + +impl Display for SegmentType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + SegmentType::Static(s) => write!(f, "/{}", s), + SegmentType::Dynamic(s) => write!(f, "/:{}", s), + SegmentType::CatchAll(s) => write!(f, "/:...{}", s), + } + } +}