From d298b626d3ae21a39a8ec4426373369ac94edf9f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 3 Nov 2021 00:35:56 -0400 Subject: [PATCH] wip: add router --- Cargo.toml | 3 +- docs/guide/src/concepts/interactivity.md | 2 - packages/core-macro/src/lib.rs | 42 +++- packages/core-macro/src/router.rs | 247 +++++++++++++++++++++++ packages/core-macro/src/rsx/body.rs | 3 +- packages/core/src/component.rs | 4 +- packages/core/src/context.rs | 9 +- packages/router/Cargo.toml | 39 ++++ packages/router/examples/blog.rs | 28 +++ packages/router/examples/v2.rs | 88 ++++++++ packages/router/src/lib.rs | 140 +++++++++++++ packages/router/src/platform.rs | 1 + packages/router/src/platform/desktop.rs | 0 packages/router/src/platform/mobile.rs | 0 packages/router/src/platform/web.rs | 0 packages/router/src/service.rs | 0 packages/router/src/utils.rs | 31 +++ 17 files changed, 617 insertions(+), 20 deletions(-) create mode 100644 packages/core-macro/src/router.rs create mode 100644 packages/router/Cargo.toml create mode 100644 packages/router/examples/blog.rs create mode 100644 packages/router/examples/v2.rs create mode 100644 packages/router/src/lib.rs create mode 100644 packages/router/src/platform.rs create mode 100644 packages/router/src/platform/desktop.rs create mode 100644 packages/router/src/platform/mobile.rs create mode 100644 packages/router/src/platform/web.rs create mode 100644 packages/router/src/service.rs create mode 100644 packages/router/src/utils.rs diff --git a/Cargo.toml b/Cargo.toml index d265c0644..9a066caa8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ description = "Core functionality for Dioxus - a concurrent renderer-agnostic Vi [dependencies] dioxus-core = { path = "./packages/core", version = "0.1.3" } +dioxus-router = { path = "./packages/router", optional = true } dioxus-core-macro = { path = "./packages/core-macro", optional = true } dioxus-html = { path = "./packages/html", optional = true } dioxus-web = { path = "./packages/web", optional = true } @@ -24,10 +25,10 @@ core = ["macro", "hooks", "html"] macro = ["dioxus-core-macro"] hooks = ["dioxus-hooks"] html = ["dioxus-html"] +router = ["dioxus-router"] # utilities atoms = [] -router = [] # targets ssr = ["dioxus-ssr"] diff --git a/docs/guide/src/concepts/interactivity.md b/docs/guide/src/concepts/interactivity.md index f8e5e68e2..a7403933c 100644 --- a/docs/guide/src/concepts/interactivity.md +++ b/docs/guide/src/concepts/interactivity.md @@ -173,8 +173,6 @@ In general, Dioxus should be plenty fast for most use cases. However, there are Don't worry - Dioxus is fast. But, if your app needs *extreme performance*, then take a look at the `Performance Tuning` in the `Advanced Guides` book. - - ## Moving On This overview was a lot of information - but it doesn't tell you everything! diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index a132148c8..ce36fb83c 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -5,6 +5,7 @@ use syn::parse_macro_input; pub(crate) mod htm; pub(crate) mod ifmt; pub(crate) mod props; +pub(crate) mod router; pub(crate) mod rsx; #[proc_macro] @@ -82,12 +83,9 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token /// }} /// /// // Matching -/// // Matching will throw a Rust error about "no two closures are the same type" -/// // To fix this, call "render" method or use the "in" syntax to produce VNodes. -/// // There's nothing we can do about it, sorry :/ (unless you want *really* unhygenic macros) /// {match true { -/// true => rsx!(cx, h1 {"Top text"}), -/// false => cx.render(rsx!( h1 {"Bottom text"})) +/// true => rsx!(h1 {"Top text"}), +/// false => rsx!(h1 {"Bottom text"}) /// }} /// /// // Conditional rendering @@ -95,11 +93,11 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token /// // You can convert a bool condition to rsx! with .then and .or /// {true.then(|| rsx!(div {}))} /// -/// // True conditions need to be rendered (same reasons as matching) +/// // True conditions /// {if true { -/// rsx!(cx, h1 {"Top text"}) +/// rsx!(h1 {"Top text"}) /// } else { -/// rsx!(cx, h1 {"Bottom text"}) +/// rsx!(h1 {"Bottom text"}) /// }} /// /// // returning "None" is a bit noisy... but rare in practice @@ -186,3 +184,31 @@ pub fn rsx(s: TokenStream) -> TokenStream { Ok(s) => s.to_token_stream().into(), } } + +/// Derive macro used to mark an enum as Routable. +/// +/// This macro can only be used on enums. Every varient of the macro needs to be marked +/// with the `at` attribute to specify the URL of the route. It generates an implementation of +/// `yew_router::Routable` trait and `const`s for the routes passed which are used with `Route` +/// component. +/// +/// # Example +/// +/// ``` +/// # use yew_router::Routable; +/// #[derive(Debug, Clone, Copy, PartialEq, Routable)] +/// enum Routes { +/// #[at("/")] +/// Home, +/// #[at("/secure")] +/// Secure, +/// #[at("/404")] +/// NotFound, +/// } +/// ``` +#[proc_macro_derive(Routable, attributes(at, not_found))] +pub fn routable_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + use router::{routable_derive_impl, Routable}; + let input = parse_macro_input!(input as Routable); + routable_derive_impl(input).into() +} diff --git a/packages/core-macro/src/router.rs b/packages/core-macro/src/router.rs new file mode 100644 index 000000000..684d2dc7d --- /dev/null +++ b/packages/core-macro/src/router.rs @@ -0,0 +1,247 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::{Data, DeriveInput, Fields, Ident, LitStr, Variant}; + +const AT_ATTR_IDENT: &str = "at"; +const NOT_FOUND_ATTR_IDENT: &str = "not_found"; + +pub struct Routable { + ident: Ident, + ats: Vec, + variants: Punctuated, + not_found_route: Option, +} + +impl Parse for Routable { + fn parse(input: ParseStream) -> syn::Result { + let DeriveInput { ident, data, .. } = input.parse()?; + + let data = match data { + Data::Enum(data) => data, + Data::Struct(s) => { + return Err(syn::Error::new( + s.struct_token.span(), + "expected enum, found struct", + )) + } + Data::Union(u) => { + return Err(syn::Error::new( + u.union_token.span(), + "expected enum, found union", + )) + } + }; + + let (not_found_route, ats) = parse_variants_attributes(&data.variants)?; + + Ok(Self { + ident, + variants: data.variants, + ats, + not_found_route, + }) + } +} + +fn parse_variants_attributes( + variants: &Punctuated, +) -> syn::Result<(Option, Vec)> { + let mut not_founds = vec![]; + let mut ats: Vec = vec![]; + + let mut not_found_attrs = vec![]; + + for variant in variants.iter() { + if let Fields::Unnamed(ref field) = variant.fields { + return Err(syn::Error::new( + field.span(), + "only named fields are supported", + )); + } + + let attrs = &variant.attrs; + let at_attrs = attrs + .iter() + .filter(|attr| attr.path.is_ident(AT_ATTR_IDENT)) + .collect::>(); + + let attr = match at_attrs.len() { + 1 => *at_attrs.first().unwrap(), + 0 => { + return Err(syn::Error::new( + variant.span(), + format!( + "{} attribute must be present on every variant", + AT_ATTR_IDENT + ), + )) + } + _ => { + return Err(syn::Error::new_spanned( + quote! { #(#at_attrs)* }, + format!("only one {} attribute must be present", AT_ATTR_IDENT), + )) + } + }; + + let lit = attr.parse_args::()?; + ats.push(lit); + + for attr in attrs.iter() { + if attr.path.is_ident(NOT_FOUND_ATTR_IDENT) { + not_found_attrs.push(attr); + not_founds.push(variant.ident.clone()) + } + } + } + + if not_founds.len() > 1 { + return Err(syn::Error::new_spanned( + quote! { #(#not_found_attrs)* }, + format!("there can only be one {}", NOT_FOUND_ATTR_IDENT), + )); + } + + Ok((not_founds.into_iter().next(), ats)) +} + +impl Routable { + fn build_from_path(&self) -> TokenStream { + let from_path_matches = self.variants.iter().enumerate().map(|(i, variant)| { + let ident = &variant.ident; + let right = match &variant.fields { + Fields::Unit => quote! { Self::#ident }, + Fields::Named(field) => { + let fields = field.named.iter().map(|it| { + //named fields have idents + it.ident.as_ref().unwrap() + }); + quote! { Self::#ident { #(#fields: params.get(stringify!(#fields))?.parse().ok()?,)* } } + } + Fields::Unnamed(_) => unreachable!(), // already checked + }; + + let left = self.ats.get(i).unwrap(); + quote! { + #left => ::std::option::Option::Some(#right) + } + }); + + quote! { + fn from_path(path: &str, params: &::std::collections::HashMap<&str, &str>) -> ::std::option::Option { + match path { + #(#from_path_matches),*, + _ => ::std::option::Option::None, + } + } + } + } + + fn build_to_path(&self) -> TokenStream { + let to_path_matches = self.variants.iter().enumerate().map(|(i, variant)| { + let ident = &variant.ident; + let mut right = self.ats.get(i).unwrap().value(); + + match &variant.fields { + Fields::Unit => quote! { Self::#ident => ::std::string::ToString::to_string(#right) }, + Fields::Named(field) => { + let fields = field + .named + .iter() + .map(|it| it.ident.as_ref().unwrap()) + .collect::>(); + + for field in fields.iter() { + // :param -> {param} + // so we can pass it to `format!("...", param)` + right = right.replace(&format!(":{}", field), &format!("{{{}}}", field)) + } + + quote! { + Self::#ident { #(#fields),* } => ::std::format!(#right, #(#fields = #fields),*) + } + } + Fields::Unnamed(_) => unreachable!(), // already checked + } + }); + + quote! { + fn to_path(&self) -> ::std::string::String { + match self { + #(#to_path_matches),*, + } + } + } + } +} + +pub fn routable_derive_impl(input: Routable) -> TokenStream { + let Routable { + ats, + not_found_route, + ident, + .. + } = &input; + + let from_path = input.build_from_path(); + let to_path = input.build_to_path(); + + let not_found_route = match not_found_route { + Some(route) => quote! { ::std::option::Option::Some(Self::#route) }, + None => quote! { ::std::option::Option::None }, + }; + + let cache_thread_local_ident = Ident::new( + &format!("__{}_ROUTER_CURRENT_ROUTE_CACHE", ident), + ident.span(), + ); + + quote! { + ::std::thread_local! { + #[doc(hidden)] + #[allow(non_upper_case_globals)] + static #cache_thread_local_ident: ::std::cell::RefCell<::std::option::Option<#ident>> = ::std::cell::RefCell::new(::std::option::Option::None); + } + + #[automatically_derived] + impl ::dioxus::router::Routable for #ident { + #from_path + #to_path + + fn routes() -> ::std::vec::Vec<&'static str> { + ::std::vec![#(#ats),*] + } + + fn not_found_route() -> ::std::option::Option { + #not_found_route + } + + fn current_route() -> ::std::option::Option { + #cache_thread_local_ident.with(|val| ::std::clone::Clone::clone(&*val.borrow())) + } + + fn recognize(pathname: &str) -> ::std::option::Option { + ::std::thread_local! { + static ROUTER: ::dioxus::router::__macro::Router = ::dioxus::router::__macro::build_router::<#ident>(); + } + let route = ROUTER.with(|router| ::dioxus::router::__macro::recognize_with_router(router, pathname)); + { + let route = ::std::clone::Clone::clone(&route); + #cache_thread_local_ident.with(move |val| { + *val.borrow_mut() = route; + }); + } + route + } + + fn cleanup() { + #cache_thread_local_ident.with(move |val| { + *val.borrow_mut() = ::std::option::Option::None; + }); + } + } + } +} diff --git a/packages/core-macro/src/rsx/body.rs b/packages/core-macro/src/rsx/body.rs index 404d5083e..34c16fb86 100644 --- a/packages/core-macro/src/rsx/body.rs +++ b/packages/core-macro/src/rsx/body.rs @@ -49,9 +49,8 @@ impl ToTokens for CallBody { match &self.custom_context { // The `in cx` pattern allows directly rendering Some(ident) => out_tokens.append_all(quote! { - #ident.render(dioxus::prelude::LazyNodes::new(move |__cx: NodeFactory| -> VNode { + #ident.render(NodeFactory::annotate_lazy(move |__cx: NodeFactory| -> VNode { use dioxus_elements::{GlobalAttributes, SvgAttributes}; - #inner })) }), diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs index 26c920596..5c7b4c36c 100644 --- a/packages/core/src/component.rs +++ b/packages/core/src/component.rs @@ -15,7 +15,7 @@ use crate::innerlude::{Context, Element, VNode}; /// ```rust /// struct State {} /// -/// fn Example((cx, props): Component) -> DomTree { +/// fn Example((cx, props): Scope) -> DomTree { /// // ... /// } /// ``` @@ -26,7 +26,7 @@ use crate::innerlude::{Context, Element, VNode}; /// name: &'a str /// } /// -/// fn Example<'a>((cx, props): Component<'a, State>) -> DomTree<'a> { +/// fn Example<'a>((cx, props): Scope<'a, State>) -> DomTree<'a> { /// // ... /// } /// ``` diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index ca5d9cf25..95953ff16 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -132,12 +132,11 @@ impl<'src> Context<'src> { (self.scope.shared.submit_task)(task) } - /// This hook enables the ability to expose state to children further down the VirtualDOM Tree. + /// This method enables the ability to expose state to children further down the VirtualDOM Tree. /// - /// This is a hook, so it should not be called conditionally! + /// This is a "fundamental" operation and should only be called during initialization of a hook. /// - /// The init method is ran *only* on first use, otherwise it is ignored. However, it uses hooks (ie `use`) - /// so don't put it in a conditional. + /// For a hook that provides the same functionality, use `use_provide_state` and `use_consume_state` instead. /// /// When the component is dropped, so is the context. Be aware of this behavior when consuming /// the context via Rc/Weak. @@ -148,7 +147,7 @@ impl<'src> Context<'src> { /// struct SharedState(&'static str); /// /// static App: FC<()> = |(cx, props)|{ - /// cx.provide_state(SharedState("world")); + /// cx.use_hook(|_| cx.provide_state(SharedState("world")), |_| {}, |_| {}); /// rsx!(cx, Child {}) /// } /// diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml new file mode 100644 index 000000000..93a811a76 --- /dev/null +++ b/packages/router/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "dioxus-router" +version = "0.0.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-core = { path = "../core", version = "0.1.3", default-features = false } +dioxus-core-macro = { path = "../core-macro", version = "0.1" } +dioxus-html = { path = "../html", version = "0.0.0", default-features = false } + +web-sys = { version = "0.3", features = [ + "Attr", + "Document", + "History", + "HtmlBaseElement", + "Event", + "NamedNodeMap", + "Url", + "UrlSearchParams", + "Window", +], optional = true } + +serde = "1" + +serde_urlencoded = "0.7" + +wasm-bindgen = "0.2" +js-sys = "0.3" +gloo = "0.4" +route-recognizer = "0.3.1" + + +[features] +default = ["web"] +web = ["web-sys"] +desktop = [] +mobile = [] diff --git a/packages/router/examples/blog.rs b/packages/router/examples/blog.rs new file mode 100644 index 000000000..fa282363c --- /dev/null +++ b/packages/router/examples/blog.rs @@ -0,0 +1,28 @@ +use dioxus::prelude::*; + +fn main() {} + +enum Routes { + Home, + Blog, +} + +static App: FC<()> = |(cx, props)| { + let route = use_router::(cx); + + let content = match route { + Routes::Home => rsx!(Home {}), + Routes::Blog => rsx!(Blog {}), + }; + + cx.render(rsx! { + {content} + }) +}; + +fn use_router

(cx: Context) -> &P { + todo!() +} + +static Home: FC<()> = |(cx, props)| todo!(); +static Blog: FC<()> = |(cx, props)| todo!(); diff --git a/packages/router/examples/v2.rs b/packages/router/examples/v2.rs new file mode 100644 index 000000000..6bea4b58e --- /dev/null +++ b/packages/router/examples/v2.rs @@ -0,0 +1,88 @@ +use dioxus::prelude::*; +use dioxus_router::{use_router, Link, Routable}; + +fn main() { + dbg!(App as *const fn()); +} + +#[derive(PartialEq, Debug, Clone)] +pub enum Route { + // #[at("/")] + Home, + + // #[at("/:id")] + AllUsers { page: u32 }, + + // #[at("/:id")] + User { id: u32 }, + + // #[at("/:id")] + BlogList { page: u32 }, + + // #[at("/:id")] + BlogPost { post_id: u32 }, + + // #[at("/404")] + // #[not_found] + NotFound, +} + +static App: FC<()> = |(cx, props)| { + let route = use_router(cx, Route::parse)?; + + cx.render(rsx! { + nav { + Link { to: Route::Home, "Go home!" } + Link { to: Route::AllUsers { page: 0 }, "List all users" } + Link { to: Route::BlogList { page: 0 }, "Blog posts" } + } + {match route { + Route::Home => rsx!("Home"), + Route::AllUsers { page } => rsx!("All users - page {page}"), + Route::User { id } => rsx!("User - id: {id}"), + Route::BlogList { page } => rsx!("Blog posts - page {page}"), + Route::BlogPost { post_id } => rsx!("Blog post - post {post_id}"), + Route::NotFound => rsx!("Not found"), + }} + footer {} + }) +}; + +impl Route { + // Generate the appropriate route from the "tail" end of the URL + fn parse(url: &str) -> Self { + use Route::*; + match url { + "/" => Home, + "/users" => AllUsers { page: 1 }, + "/users/:page" => AllUsers { page: 1 }, + "/users/:page/:id" => User { id: 1 }, + "/blog" => BlogList { page: 1 }, + "/blog/:page" => BlogList { page: 1 }, + "/blog/:page/:id" => BlogPost { post_id: 1 }, + _ => NotFound, + } + } +} + +impl Routable for Route { + fn from_path(path: &str, params: &std::collections::HashMap<&str, &str>) -> Option { + todo!() + } + + fn to_path(&self) -> String { + todo!() + } + + fn routes() -> Vec<&'static str> { + todo!() + } + + fn not_found_route() -> Option { + todo!() + } + + fn recognize(pathname: &str) -> Option { + todo!() + } +} diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs new file mode 100644 index 000000000..378935546 --- /dev/null +++ b/packages/router/src/lib.rs @@ -0,0 +1,140 @@ +mod utils; + +use std::{cell::RefCell, collections::HashMap, rc::Rc}; + +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +use dioxus_core_macro::{format_args_f, rsx, Props}; +use dioxus_html as dioxus_elements; +use wasm_bindgen::JsValue; +use web_sys::Event; + +use crate::utils::fetch_base_url; + +pub struct RouterService { + history: RefCell>, + base_ur: RefCell>, +} + +impl RouterService { + fn push_route(&self, r: R) { + self.history.borrow_mut().push(r); + } + + fn get_current_route(&self) -> &str { + todo!() + } + + fn update_route_impl(&self, url: String, push: bool) { + let history = web_sys::window().unwrap().history().expect("no history"); + let base = self.base_ur.borrow(); + let path = match base.as_ref() { + Some(base) => { + let path = format!("{}{}", base, url); + if path.is_empty() { + "/".to_string() + } else { + path + } + } + None => url, + }; + + if push { + history + .push_state_with_url(&JsValue::NULL, "", Some(&path)) + .expect("push history"); + } else { + history + .replace_state_with_url(&JsValue::NULL, "", Some(&path)) + .expect("replace history"); + } + let event = Event::new("popstate").unwrap(); + + web_sys::window() + .unwrap() + .dispatch_event(&event) + .expect("dispatch"); + } +} + +/// This hould only be used once per app +/// +/// You can manually parse the route if you want, but the derived `parse` method on `Routable` will also work just fine +pub fn use_router(cx: Context, cfg: impl FnOnce(&str) -> R) -> Option { + // for the web, attach to the history api + cx.use_hook( + |f| { + // + use gloo::events::EventListener; + + let base_url = fetch_base_url(); + + let service: RouterService = RouterService { + history: RefCell::new(vec![]), + base_ur: RefCell::new(base_url), + }; + + cx.provide_state(service); + + let regenerate = cx.schedule_update(); + + // when "back" is called by the user, we want to to re-render the component + let listener = EventListener::new(&web_sys::window().unwrap(), "popstate", move |_| { + // + regenerate(); + }); + }, + |f| { + // + }, + |f| { + // + }, + ); + + let router = use_router_service::(cx)?; + Some(cfg(router.get_current_route())) +} + +pub fn use_router_service(cx: Context) -> Option<&Rc>> { + cx.use_hook( + |_| cx.consume_state::>(), + |f| f.as_ref(), + |f| {}, + ) +} + +#[derive(Props)] +pub struct LinkProps<'a, R: Routable> { + to: R, + children: ScopeChildren<'a>, +} + +pub fn Link<'a, R: Routable>((cx, props): Scope<'a, LinkProps<'a, R>>) -> Element { + let service = use_router_service::(cx)?; + cx.render(rsx! { + a { + href: format_args!("{}", props.to.to_path()), + onclick: move |_| service.push_route(props.to.clone()), + {&props.children}, + } + }) +} + +pub trait Routable: Sized + Clone + 'static { + /// Converts path to an instance of the routes enum. + fn from_path(path: &str, params: &HashMap<&str, &str>) -> Option; + + /// Converts the route to a string that can passed to the history API. + fn to_path(&self) -> String; + + /// Lists all the available routes + fn routes() -> Vec<&'static str>; + + /// The route to redirect to on 404 + fn not_found_route() -> Option; + + /// Match a route based on the path + fn recognize(pathname: &str) -> Option; +} diff --git a/packages/router/src/platform.rs b/packages/router/src/platform.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/packages/router/src/platform.rs @@ -0,0 +1 @@ + diff --git a/packages/router/src/platform/desktop.rs b/packages/router/src/platform/desktop.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/router/src/platform/mobile.rs b/packages/router/src/platform/mobile.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/router/src/platform/web.rs b/packages/router/src/platform/web.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/router/src/utils.rs b/packages/router/src/utils.rs new file mode 100644 index 000000000..46764ebab --- /dev/null +++ b/packages/router/src/utils.rs @@ -0,0 +1,31 @@ +use wasm_bindgen::JsCast; +use web_sys::window; + +pub fn fetch_base_url() -> Option { + match window() + .unwrap() + .document() + .unwrap() + .query_selector("base[href]") + { + Ok(Some(base)) => { + let base = JsCast::unchecked_into::(base).href(); + + let url = web_sys::Url::new(&base).unwrap(); + let base = url.pathname(); + + let base = if base != "/" { + strip_slash_suffix(&base) + } else { + return None; + }; + + Some(base.to_string()) + } + _ => None, + } +} + +pub(crate) fn strip_slash_suffix(path: &str) -> &str { + path.strip_suffix('/').unwrap_or(path) +}