···275275276276 cargo_toml["package"]["name"] = toml_edit::value(project_name);
277277278278+ let maudit_intended_version = &cargo_toml["package"]["metadata"]["maudit"]["intended_version"];
279279+280280+ // If the template is using the workspace version, remove the `workspace = true` property
281281+ if let toml_edit::Item::Value(v) = maudit_intended_version {
282282+ cargo_toml["dependencies"]["maudit"] = toml_edit::value(v);
283283+ }
284284+278285 std::fs::write(&cargo_toml_path, cargo_toml.to_string()).unwrap();
279286280287 Ok(())
+23-8
crates/framework/src/build.rs
···1212 errors::BuildError,
1313 logging::print_title,
1414 page::{DynamicRouteContext, FullPage, RenderResult, RouteContext, RouteParams, RouteType},
1515+ route::{
1616+ extract_params_from_raw_route, get_route_file_path, get_route_type_from_route_params,
1717+ get_route_url, ParameterDef,
1818+ },
1519 BuildOptions, BuildOutput,
1620};
1721use colored::{ColoredString, Colorize};
···100104101105 let mut page_count = 0;
102106 for route in routes {
103103- match route.route_type() {
107107+ let params_def = extract_params_from_raw_route(&route.route_raw());
108108+ let route_type = get_route_type_from_route_params(¶ms_def);
109109+ match route_type {
104110 RouteType::Static => {
105111 let route_start = SystemTime::now();
106112 let mut page_assets = assets::PageAssets {
···110116 };
111117112118 let params = RouteParams(FxHashMap::default());
119119+113120 let mut content = Content::new(&content_sources.0);
114121 let mut ctx = RouteContext {
115122 raw_params: ¶ms,
116123 content: &mut content,
117124 assets: &mut page_assets,
118118- current_url: route.url_untyped(¶ms),
125125+ current_url: String::new(), // TODO
119126 };
120127121121- let (file_path, mut file) = create_route_file(*route, ctx.raw_params, &dist_dir)?;
128128+ let (file_path, mut file) =
129129+ create_route_file(*route, ¶ms_def, ctx.raw_params, &dist_dir)?;
122130 let result = route.render_internal(&mut ctx);
123131124132 finish_route(
···129137 route.route_raw(),
130138 )?;
131139132132- info!(target: "build", "{} -> {} {}", route.route(&RouteParams(FxHashMap::default())), file_path.to_string_lossy().dimmed(), format_elapsed_time(route_start.elapsed(), &route_format_options).unwrap());
140140+ info!(target: "build", "{} -> {} {}", get_route_url(&route.route_raw(), ¶ms_def, ¶ms), file_path.to_string_lossy().dimmed(), format_elapsed_time(route_start.elapsed(), &route_format_options).unwrap());
133141134142 build_pages_assets.extend(page_assets.assets);
135143 build_pages_scripts.extend(page_assets.scripts);
···152160 let routes = route.routes_internal(&mut dynamic_route_context);
153161154162 if routes.is_empty() {
155155- info!(target: "build", "{} is a dynamic route, but its implementation of DynamicRoute::routes returned no routes. No pages will be generated for this route.", route.route_raw().to_string().bold());
163163+ info!(target: "build", "{} is a dynamic route, but its implementation of Page::routes returned an empty Vec. No pages will be generated for this route.", route.route_raw().to_string().bold());
156164 continue;
157165 } else {
158166 info!(target: "build", "{}", route.route_raw().to_string().bold());
···170178 raw_params: ¶ms,
171179 content: &mut content,
172180 assets: &mut pages_assets,
173173- current_url: route.url_untyped(¶ms),
181181+ current_url: String::new(), // TODO
174182 };
175183176184 let (file_path, mut file) =
177177- create_route_file(*route, ctx.raw_params, &dist_dir)?;
185185+ create_route_file(*route, ¶ms_def, ctx.raw_params, &dist_dir)?;
186186+178187 let result = route.render_internal(&mut ctx);
179188180189 build_metadata.add_page(
···315324316325fn create_route_file(
317326 route: &dyn FullPage,
327327+ params_def: &Vec<ParameterDef>,
318328 params: &RouteParams,
319329 dist_dir: &Path,
320330) -> Result<(PathBuf, File), Box<dyn std::error::Error>> {
321321- let file_path = dist_dir.join(route.file_path(params));
331331+ let file_path = dist_dir.join(get_route_file_path(
332332+ &route.route_raw(),
333333+ params_def,
334334+ params,
335335+ route.is_endpoint(),
336336+ ));
322337323338 // Create the parent directories if it doesn't exist
324339 if let Some(parent_dir) = file_path.parent() {
+11-18
crates/framework/src/content.rs
···11//! Core functions and structs to define the content sources of your website.
22//!
33-//! Content sources represent the content of your website, such as articles, blog posts, etc. Then, content sources can be passed to [`coronate()`](crate::coronate), through the [`content_sources!`](crate::content_sources) macro, to be loaded. Typically used in [`DynamicRoute`](crate::page::DynamicRoute).
33+//! Content sources represent the content of your website, such as articles, blog posts, etc. Then, content sources can be passed to [`coronate()`](crate::coronate), through the [`content_sources!`](crate::content_sources) macro, to be loaded.
44use std::{any::Any, path::PathBuf};
5566use glob::glob as glob_fs;
···115115/// pub article: String,
116116/// }
117117///
118118-/// impl DynamicRoute<ArticleParams> for Article {
119119-/// fn routes(&self, ctx: &mut DynamicRouteContext) -> Vec<ArticleParams> {
120120-/// let articles = ctx.content.get_source::<ArticleContent>("articles");
121121-///
122122-/// articles.into_params(|entry| ArticleParams {
123123-/// article: entry.id.clone(),
124124-/// })
125125-/// }
126126-/// }
127127-///
128128-/// impl Page for Article {
118118+/// impl Page<ArticleParams> for Article {
129119/// fn render(&self, ctx: &mut RouteContext) -> RenderResult {
130120/// let params = ctx.params::<ArticleParams>();
131121/// let articles = ctx.content.get_source::<ArticleContent>("articles");
132122/// let article = articles.get_entry(¶ms.article);
133123/// article.render().into()
124124+/// }
125125+///
126126+/// fn routes(&self, ctx: &mut DynamicRouteContext) -> Vec<ArticleParams> {
127127+/// let articles = ctx.content.get_source::<ArticleContent>("articles");
128128+///
129129+/// articles.into_params(|entry| ArticleParams {
130130+/// article: entry.id.clone(),
131131+/// })
134132/// }
135133/// }
136134/// ```
···346344/// #[route("/articles/my-article")]
347345/// pub struct Article;
348346///
349349-/// #[derive(Params)]
350350-/// pub struct ArticleParams {
351351-/// pub article: String,
352352-/// }
353353-///
354354-/// impl Page<Markup> for Article {
347347+/// impl Page<RouteParams, Markup> for Article {
355348/// fn render(&self, ctx: &mut RouteContext) -> Markup {
356349/// let articles = ctx.content.get_source::<ArticleContent>("articles");
357350/// let article = articles.get_entry("my-article");
···11//! Core traits and structs to define the pages of your website.
22//!
33-//! Every page must implement the [`Page`] trait, and optionally the [`DynamicRoute`] trait. Then, pages can be passed to [`coronate()`](crate::coronate), through the [`routes!`](crate::routes) macro, to be built.
33+//! Every page must implement the [`Page`] trait. Then, pages can be passed to [`coronate()`](crate::coronate), through the [`routes!`](crate::routes) macro, to be built.
44use crate::assets::PageAssets;
55use crate::content::Content;
66+use crate::route::{extract_params_from_raw_route, get_route_url, guess_if_route_is_endpoint};
67use rustc_hash::FxHashMap;
77-use std::path::PathBuf;
8899/// Represents the result of a page render, can be either text or raw bytes.
1010///
···108108 }
109109}
110110111111-/// Allows to access the content source in a [`DynamicRoute`] implementation.
111111+/// Allows to access the content source in the [`Page::routes`] method.
112112///
113113/// ## Example
114114/// ```rust
···129129/// pub article: String,
130130/// }
131131///
132132-/// impl DynamicRoute<ArticleParams> for Article {
133133-/// fn routes(&self, ctx: &mut DynamicRouteContext) -> Vec<ArticleParams> {
134134-/// let articles = ctx.content.get_source::<ArticleContent>("articles");
135135-///
136136-/// articles.into_params(|entry| ArticleParams {
137137-/// article: entry.id.clone(),
138138-/// })
139139-/// }
140140-/// }
141141-///
142142-/// impl Page for Article {
132132+/// impl Page<ArticleParams> for Article {
143133/// fn render(&self, ctx: &mut RouteContext) -> RenderResult {
144134/// let params = ctx.params::<ArticleParams>();
145135/// let articles = ctx.content.get_source::<ArticleContent>("articles");
146136/// let article = articles.get_entry(¶ms.article);
147137/// article.render().into()
148138/// }
139139+///
140140+/// fn routes(&self, ctx: &mut DynamicRouteContext) -> Vec<ArticleParams> {
141141+/// let articles = ctx.content.get_source::<ArticleContent>("articles");
142142+///
143143+/// articles.into_params(|entry| ArticleParams {
144144+/// article: entry.id.clone(),
145145+/// })
146146+/// }
149147/// }
150148/// ```
151149pub struct DynamicRouteContext<'a> {
···169167/// }
170168/// }
171169/// ```
172172-pub trait Page<T = RenderResult>
170170+pub trait Page<P = RouteParams, T = RenderResult>
173171where
172172+ P: Into<RouteParams>,
174173 T: Into<RenderResult>,
175174{
175175+ fn routes(&self, _ctx: &mut DynamicRouteContext) -> Vec<P> {
176176+ Vec::new()
177177+ }
176178 fn render(&self, ctx: &mut RouteContext) -> T;
177179}
178180···211213 }
212214}
213215214214-/// Must be implemented for every dynamic route of your website.
215215-///
216216-/// Dynamic route allows creating many pages that share the same structure and logic, but with different content. Typically used for a [`ContentSource`](crate::content::ContentSource).
217217-///
218218-/// ## Example
219219-/// ```rust
220220-/// use maudit::page::prelude::*;
221221-///
222222-/// #[route("/tags/[id]")]
223223-/// pub struct Tags;
224224-///
225225-/// #[derive(Params)]
226226-/// struct Params {
227227-/// id: String,
228228-/// }
229229-///
230230-/// impl DynamicRoute for Tags {
231231-/// fn routes(&self, context: &mut DynamicRouteContext) -> Vec<RouteParams> {
232232-/// let tags = vec!["rust", "web", "programming"].iter().map(|tag| Params { id: tag.to_string() }).collect();
233233-/// RouteParams::from_vec(tags)
234234-/// }
235235-/// }
236236-///
237237-/// impl Page for Tags {
238238-/// fn render(&self, ctx: &mut RouteContext) -> RenderResult {
239239-/// let tag = ctx.params::<Params>().id;
240240-/// format!("<h1>Tag: {}</h1>", tag).into()
241241-/// }
242242-/// }
243243-/// ```
244244-pub trait DynamicRoute<P = RouteParams>
245245-where
246246- P: Into<RouteParams>,
247247-{
248248- // Intentionally does not have a default implementation even though it'd be useful in our macros in order to force
249249- // the user to implement it explicitly, even if it's just returning an empty Vec.
250250- fn routes(&self, context: &mut DynamicRouteContext) -> Vec<P>;
251251-}
252252-253216#[doc(hidden)]
217217+#[derive(PartialEq, Eq, Debug)]
254218/// Used internally by Maudit and should not be implemented by the user.
255219/// We expose it because [`maudit_macros::route`] implements it for the user behind the scenes.
256220pub enum RouteType {
···262226/// Used internally by Maudit and should not be implemented by the user.
263227/// We expose it because the derive macro implements it for the user behind the scenes.
264228pub trait InternalPage {
265265- fn route_type(&self) -> RouteType;
266229 fn route_raw(&self) -> String;
267267- fn route(&self, params: &RouteParams) -> String;
268268- fn file_path(&self, params: &RouteParams) -> PathBuf;
269269- fn url_unsafe<P: Into<RouteParams>>(params: P) -> String
270270- where
271271- Self: Sized;
272272- fn url_untyped(&self, params: &RouteParams) -> String;
230230+ fn is_endpoint(&self) -> bool {
231231+ guess_if_route_is_endpoint(&self.route_raw())
232232+ }
273233}
274234275235#[doc(hidden)]
···280240 fn routes_internal(&self, context: &mut DynamicRouteContext) -> Vec<RouteParams>;
281241}
282242243243+pub fn get_page_url<T: Into<RouteParams>>(route: impl FullPage, params: T) -> String {
244244+ let params_defs = extract_params_from_raw_route(&route.route_raw());
245245+ get_route_url(&route.route_raw(), ¶ms_defs, ¶ms.into())
246246+}
247247+283248pub mod prelude {
284249 //! Re-exports of the most commonly used types and traits for defining pages.
285250 //!
···290255 //! use maudit::page::prelude::*;
291256 //! ```
292257 pub use super::{
293293- DynamicRoute, DynamicRouteContext, Page, RenderResult, RouteContext, RouteParams,
258258+ get_page_url, DynamicRouteContext, Page, RenderResult, RouteContext, RouteParams,
294259 };
295260 #[doc(hidden)]
296261 pub use super::{FullPage, InternalPage};
+281
crates/framework/src/route.rs
···11+use std::path::Path;
22+33+use crate::page::{RouteParams, RouteType};
44+55+#[derive(Debug, PartialEq)]
66+pub struct ParameterDef {
77+ key: String,
88+ index: usize,
99+ length: usize,
1010+}
1111+1212+pub fn extract_params_from_raw_route(raw_route: &str) -> Vec<ParameterDef> {
1313+ let mut params = Vec::new();
1414+ let mut start = false;
1515+ let mut escape = false;
1616+ let mut current_value = String::new();
1717+1818+ for (i, c) in raw_route.char_indices() {
1919+ if escape {
2020+ escape = false;
2121+ if start {
2222+ current_value.push(c);
2323+ }
2424+ continue;
2525+ }
2626+2727+ match c {
2828+ '\\' => {
2929+ escape = true;
3030+ }
3131+ '[' => {
3232+ if !escape {
3333+ start = true;
3434+ current_value.clear();
3535+ }
3636+ }
3737+ ']' => {
3838+ if start {
3939+ params.push(ParameterDef {
4040+ key: current_value.clone(),
4141+ index: i - (current_value.len() + 1), // -1 for the starting [
4242+ length: current_value.len() + 2, // +2 for the [ and ]
4343+ });
4444+ start = false;
4545+ }
4646+ }
4747+ _ => {
4848+ if start {
4949+ current_value.push(c);
5050+ }
5151+ }
5252+ }
5353+ }
5454+5555+ params
5656+}
5757+5858+pub fn get_route_type_from_route_params(params_def: &[ParameterDef]) -> RouteType {
5959+ if params_def.is_empty() {
6060+ RouteType::Static
6161+ } else {
6262+ RouteType::Dynamic
6363+ }
6464+}
6565+6666+/// "/articles/[article]" (params: Hashmap {article: "truc"}) -> "articles/truc/index.html"
6767+pub fn get_route_file_path(
6868+ raw_route: &str,
6969+ params_def: &Vec<ParameterDef>,
7070+ params: &RouteParams,
7171+ is_endpoint: bool,
7272+) -> String {
7373+ // Replace every param_def with the value from the params hashmap for said key
7474+ // So, ex: "/articles/[article]" (params: Hashmap {article: "truc"}) -> "/articles/truc"
7575+ let mut route = String::from(raw_route);
7676+7777+ for param_def in params_def {
7878+ let value = params.0.get(¶m_def.key);
7979+8080+ match value {
8181+ Some(value) => {
8282+ route.replace_range(param_def.index..param_def.index + param_def.length, value);
8383+ }
8484+ None => {
8585+ panic!(
8686+ "Route {:?} is missing parameter {:?}",
8787+ raw_route, param_def.key
8888+ );
8989+ }
9090+ }
9191+ }
9292+9393+ let cleaned_raw_route = route.trim_start_matches('/').to_string();
9494+9595+ match is_endpoint {
9696+ true => cleaned_raw_route,
9797+ false => match cleaned_raw_route.is_empty() {
9898+ true => "index.html".to_string(),
9999+ false => format!("{}/index.html", cleaned_raw_route),
100100+ },
101101+ }
102102+}
103103+104104+pub fn get_route_url(
105105+ raw_route: &str,
106106+ params_def: &Vec<ParameterDef>,
107107+ params: &RouteParams,
108108+) -> String {
109109+ // Replace every param_def with the value from the params hashmap for said key
110110+ // So, ex: "/articles/[article]" (params: Hashmap {article: "truc"}) -> "/articles/truc"
111111+ let mut route = String::from(raw_route);
112112+113113+ for param_def in params_def {
114114+ let value = params.0.get(¶m_def.key);
115115+116116+ match value {
117117+ Some(value) => {
118118+ route.replace_range(param_def.index..param_def.index + param_def.length, value);
119119+ }
120120+ None => {
121121+ panic!(
122122+ "Route {:?} is missing parameter {:?}",
123123+ raw_route, param_def.key
124124+ );
125125+ }
126126+ }
127127+ }
128128+129129+ route
130130+}
131131+132132+pub fn guess_if_route_is_endpoint(raw_route: &str) -> bool {
133133+ let real_path = Path::new(&raw_route);
134134+135135+ real_path.extension().is_some()
136136+}
137137+138138+#[cfg(test)]
139139+mod tests {
140140+ use crate::{
141141+ page::{RouteParams, RouteType},
142142+ route::{
143143+ extract_params_from_raw_route, get_route_file_path, get_route_type_from_route_params,
144144+ ParameterDef,
145145+ },
146146+ };
147147+148148+ #[test]
149149+ fn test_extract_params() {
150150+ let input = "/articles/[article]";
151151+ let expected = vec![ParameterDef {
152152+ key: "article".to_string(),
153153+ index: 10,
154154+ length: 9,
155155+ }];
156156+157157+ assert_eq!(extract_params_from_raw_route(input), expected);
158158+ }
159159+160160+ #[test]
161161+ fn test_extract_params_multiple() {
162162+ let input = "/articles/[article]/[id]";
163163+ let expected = vec![
164164+ ParameterDef {
165165+ key: "article".to_string(),
166166+ index: 10,
167167+ length: 9,
168168+ },
169169+ ParameterDef {
170170+ key: "id".to_string(),
171171+ index: 20,
172172+ length: 4,
173173+ },
174174+ ];
175175+176176+ assert_eq!(extract_params_from_raw_route(input), expected);
177177+ }
178178+179179+ #[test]
180180+ fn test_extract_params_no_params() {
181181+ let input = "/articles";
182182+ let expected: Vec<ParameterDef> = Vec::new();
183183+184184+ assert_eq!(extract_params_from_raw_route(input), expected);
185185+ }
186186+187187+ #[test]
188188+ fn test_extract_params_escaped() {
189189+ let input = "/articles/\\[article\\]";
190190+ let expected: Vec<ParameterDef> = Vec::new();
191191+192192+ assert_eq!(extract_params_from_raw_route(input), expected);
193193+ }
194194+195195+ #[test]
196196+ fn test_extract_params_escaped_brackets() {
197197+ let input = "/articles/\\[article\\]/\\[id\\]";
198198+ let expected: Vec<ParameterDef> = Vec::new();
199199+200200+ assert_eq!(extract_params_from_raw_route(input), expected);
201201+ }
202202+203203+ #[test]
204204+ fn test_extract_params_escaped_brackets_with_params() {
205205+ let input = "/articles/\\[article\\]/[id]";
206206+ let expected = vec![ParameterDef {
207207+ key: "id".to_string(),
208208+ index: 22,
209209+ length: 4,
210210+ }];
211211+212212+ assert_eq!(extract_params_from_raw_route(input), expected);
213213+ }
214214+215215+ #[test]
216216+ fn test_route_type_static() {
217217+ let input = "/articles";
218218+ let params_def = extract_params_from_raw_route(input);
219219+ assert_eq!(
220220+ get_route_type_from_route_params(¶ms_def),
221221+ RouteType::Static
222222+ );
223223+ }
224224+225225+ #[test]
226226+ fn test_route_type_dynamic() {
227227+ let input = "/articles/[article]";
228228+ let params_def = extract_params_from_raw_route(input);
229229+ assert_eq!(
230230+ get_route_type_from_route_params(¶ms_def),
231231+ RouteType::Dynamic
232232+ );
233233+ }
234234+235235+ #[test]
236236+ fn test_route_type_dynamic_multiple() {
237237+ let input = "/articles/[article]/[id]";
238238+ let params_def = extract_params_from_raw_route(input);
239239+ assert_eq!(
240240+ get_route_type_from_route_params(¶ms_def),
241241+ RouteType::Dynamic
242242+ );
243243+ }
244244+245245+ #[test]
246246+ fn test_route_type_dynamic_escaped() {
247247+ let input = "/articles/\\[article\\]";
248248+ let params_def = extract_params_from_raw_route(input);
249249+ assert_eq!(
250250+ get_route_type_from_route_params(¶ms_def),
251251+ RouteType::Static
252252+ );
253253+ }
254254+255255+ #[test]
256256+ fn test_route_type_dynamic_mixed_escaped_brackets() {
257257+ let input = "/articles/\\[article\\]/[id]";
258258+ let params_def = extract_params_from_raw_route(input);
259259+ assert_eq!(
260260+ get_route_type_from_route_params(¶ms_def),
261261+ RouteType::Dynamic
262262+ );
263263+ }
264264+265265+ #[test]
266266+ fn test_get_route_file_path() {
267267+ let raw_route = "/articles/[article]";
268268+ let is_endpoint = false;
269269+ let params_def = extract_params_from_raw_route(raw_route);
270270+ let mut params = RouteParams::default();
271271+272272+ params
273273+ .0
274274+ .insert("article".to_string(), "something".to_string());
275275+276276+ assert_eq!(
277277+ get_route_file_path(raw_route, ¶ms_def, ¶ms, is_endpoint),
278278+ "articles/something/index.html"
279279+ );
280280+ }
281281+}
+9-159
crates/macros/src/lib.rs
···11-use std::path::Path;
22-31use proc_macro::TokenStream;
44-use quote::{format_ident, quote};
22+use quote::quote;
53use syn::parse::{self, Parse, ParseStream, Parser as _, Result};
66-use syn::{parse_macro_input, ItemStruct, LitStr};
44+use syn::{parse_macro_input, Expr, ItemStruct};
7586struct Args {
99- path: LitStr,
1010- is_endpoint_file: bool,
77+ path: Expr,
118}
1291310impl Parse for Args {
1411 fn parse(input: ParseStream) -> Result<Self> {
1515- let path = input.parse::<LitStr>()?;
1212+ let path = input.parse()?;
16131717- // If the path ends with a file extension, it is a file, handle any file extensions
1818-1919- let binding = path.value();
2020- let real_path = Path::new(&binding);
2121-2222- Ok(Args {
2323- path,
2424- is_endpoint_file: real_path.extension().is_some(),
2525- })
1414+ Ok(Args { path })
2615 }
2716}
2817···3322 let attrs = syn::parse_macro_input!(attrs as Args);
34233524 let struct_name = &item_struct.ident;
3636-3737- let params = extract_values(&attrs.path.value());
3838-3939- let dynamic_page_impl = match params.is_empty() {
4040- false => quote! {
4141- fn routes_internal(&self, ctx: &mut maudit::page::DynamicRouteContext) -> Vec<maudit::page::RouteParams> {
4242- self.routes(ctx).iter().map(Into::into).collect()
4343- }
4444- },
4545- true => quote! {
4646- fn routes_internal(&self, _: &mut maudit::page::DynamicRouteContext) -> Vec<maudit::page::RouteParams> {
4747- Vec::new()
4848- }
4949- },
5050- };
5151-5252- let path = attrs.path.value();
5353-5454- let list_params = params
5555- .iter()
5656- .map(|v| {
5757- let key = format_ident!("{}", v.key);
5858- quote! { let #key = params.0.get(stringify!(#key)).unwrap().to_string() }
5959- })
6060- .collect::<Vec<_>>();
6161-6262- let path_for_route = make_params_dynamic(&path, ¶ms, 0);
6363- let file_path_for_route = url_to_file_path(&path, attrs.is_endpoint_file, ¶ms);
6464-6565- let route_type = if params.is_empty() {
6666- quote! { maudit::page::RouteType::Static }
6767- } else {
6868- quote! { maudit::page::RouteType::Dynamic }
6969- };
2525+ let path = &attrs.path;
70267127 let expanded = quote! {
7228 impl maudit::page::InternalPage for #struct_name {
7373- fn route_type(&self) -> maudit::page::RouteType {
7474- #route_type
7575- }
7676-7729 fn route_raw(&self) -> String {
7830 #path.to_string()
7931 }
8080-8181- fn route(&self, params: &maudit::page::RouteParams) -> String {
8282- #(#list_params;)*
8383- return format!(#path_for_route);
8484- }
8585-8686- fn file_path(&self, params: &maudit::page::RouteParams) -> std::path::PathBuf {
8787- #(#list_params;)*
8888- std::path::PathBuf::from(format!(#file_path_for_route))
8989- }
9090-9191- fn url_unsafe<P: Into<maudit::page::RouteParams>>(params: P) -> String {
9292- let params = params.into();
9393- #(#list_params;)*
9494- format!(#path_for_route)
9595- }
9696-9797- fn url_untyped(&self, params: &maudit::page::RouteParams) -> String {
9898- #(#list_params;)*
9999- format!(#path_for_route)
100100- }
10132 }
1023310334 impl maudit::page::FullPage for #struct_name {
···10536 self.render(ctx).into()
10637 }
10738108108- #dynamic_page_impl
3939+ fn routes_internal(&self, ctx: &mut maudit::page::DynamicRouteContext) -> Vec<maudit::page::RouteParams> {
4040+ self.routes(ctx).iter().map(Into::into).collect()
4141+ }
10942 }
1104311144 #item_struct
11245 };
1134611447 TokenStream::from(expanded)
115115-}
116116-117117-struct Parameter {
118118- key: String,
119119- index: usize,
120120- length: usize,
121121-}
122122-123123-// Naive implementation to extract dynamic values from a path
124124-fn extract_values(input: &str) -> Vec<Parameter> {
125125- let input = input.trim_matches('"');
126126- let mut values = Vec::new();
127127- let mut start = false;
128128- let mut current_value = String::new();
129129- let mut start_index = 0;
130130-131131- for (i, c) in input.chars().enumerate() {
132132- match c {
133133- '[' => {
134134- start = true;
135135- current_value.clear();
136136- start_index = i;
137137- }
138138- ']' => {
139139- if start {
140140- values.push(Parameter {
141141- key: current_value.clone(),
142142- index: start_index,
143143- length: i - start_index + 1,
144144- });
145145- start = false;
146146- }
147147- }
148148- _ => {
149149- if start {
150150- current_value.push(c);
151151- }
152152- }
153153- }
154154- }
155155-156156- values
157157-}
158158-159159-fn url_to_file_path(url: &str, is_file: bool, params: &[Parameter]) -> String {
160160- let file_path = match is_file {
161161- false => {
162162- // Remove the leading '/' from the URL if it exists
163163- let path_str = url.trim_start_matches('/');
164164-165165- // If the URL is empty (i.e., root), return "index.html"
166166- if path_str.is_empty() {
167167- return "index.html".to_string();
168168- }
169169-170170- format!("{}/index.html", path_str)
171171- }
172172- true => {
173173- // Remove the leading '/' from the URL if it exists
174174- let path_str = url.trim_start_matches('/');
175175-176176- // If the URL is empty (i.e., root), return "index.html"
177177- if path_str.is_empty() {
178178- panic!("Invalid file path");
179179- }
180180-181181- path_str.to_string()
182182- }
183183- };
184184-185185- make_params_dynamic(&file_path, params, 1)
186186-}
187187-188188-fn make_params_dynamic(file_path: &str, params: &[Parameter], offset: usize) -> String {
189189- let mut file_path = file_path.to_string();
190190- for param in params.iter().rev() {
191191- file_path.replace_range(
192192- param.index - offset..param.index + param.length - offset,
193193- &format!("{{{}}}", param.key),
194194- );
195195- }
196196-197197- file_path
19848}
1994920050#[proc_macro_derive(Params)]
···1010 pub page: u128,
1111}
12121313-impl DynamicRoute<Params> for DynamicExample {
1313+impl Page<Params> for DynamicExample {
1414 fn routes(&self, _: &mut DynamicRouteContext) -> Vec<Params> {
1515 (0..1).map(|i| Params { page: i }).collect()
1616 }
1717-}
18171919-impl Page for DynamicExample {
2018 fn render(&self, ctx: &mut RouteContext) -> RenderResult {
2119 let params = ctx.params::<Params>();
2220 let image = ctx.assets.add_image("data/social-card.png");
+1-1
examples/kitchen-sink/src/pages/index.rs
···1313 let script = ctx.assets.add_script("data/some_other_script.js");
1414 let style = ctx.assets.add_style("data/tailwind.css", true);
15151616- let link_to_first_dynamic = DynamicExample::url_unsafe(&DynamicExampleParams { page: 1 });
1616+ let link_to_first_dynamic = get_page_url(DynamicExample, &DynamicExampleParams { page: 1 });
17171818 html! {
1919 head {
+2-2
website/content/docs/assets.md
···5555#[route("/blog")]
5656pub struct Blog;
57575858-impl Page<Markup> for Blog {
5858+impl Page<RouteParams, Markup> for Blog {
5959 fn render(&self, ctx: &mut RouteContext) -> Markup {
6060 let style = ctx.assets.add_style("style.css", false);
6161···111111#[route("/blog")]
112112pub struct Blog;
113113114114-impl Page<Markup> for Blog {
114114+impl Page<RouteParams, Markup> for Blog {
115115 fn render(&self, ctx: &mut RouteContext) -> Markup {
116116 let script = ctx.assets.add_script("script.js");
117117
+18-18
website/content/docs/routing.md
···6677### Static Routes
8899-Maudit uses a simple and intuitive API to define routes and pages. To create a new page, define a struct that implements the `Page` trait, adding the `#[route]` attribute to the struct definition with the path of the route as an argument.
99+To create a new page in your Maudit project, create a struct and implement the `Page` trait for it, adding the `#[route]` attribute to the struct definition with the path of the route as an argument. The path can be any Rust expression, as long as it returns a `String`.
10101111```rust
1212use maudit::page::prelude::*;
···27272828### Ergonomic returns
29293030-The `Page` trait accepts a generic parameter for the return type of the `render` method. This type must implement `Into<RenderResult>`, enabling more ergonomic returns in certain cases.
3030+The `Page` trait accepts a generic parameter in second position for the return type of the `render` method. This type must implement `Into<RenderResult>`, enabling more ergonomic returns in certain cases.
31313232```rust
3333-impl Page<String> for HelloWorld {
3333+impl Page<RouteParams, String> for HelloWorld {
3434 fn render(&self, ctx: &mut RouteContext) -> String {
3535 "Hello, world!".to_string()
3636 }
···6363}
6464```
65656666-In addition to the `Page` trait, dynamic routes must implement the `DynamicRoute` trait for their struct. This trait requires a `routes` function that returns a list of all the possible values for each parameter in the route's path.
6666+In addition to the `render` method, dynamic routes must implement a `routes` method for Page. The `routes` method returns a list of all the possible values for each parameter in the route's path, so that Maudit can generate all the necessary pages.
67676868```rust
6969use maudit::{page::prelude::*, FxHashMap};
···7171#[route("/posts/[slug]")]
7272pub struct Post;
73737474-impl DynamicRoute for Post {
7474+impl Page for Post {
7575+ fn render(&self, ctx: &mut RouteContext) -> RenderResult {
7676+ RenderResult::Text(format!("Hello, {}!", ctx.params.get("slug").unwrap()))
7777+ }
7878+7579 fn routes(&self, ctx: &DynamicRouteContext) -> Vec<RouteParams> {
7680 let mut routes = FxHashMap::default();
7781 routes.insert("slug".to_string(), "hello-world".to_string());
···7983 vec![RouteParams(routes)]
8084 }
8185}
8282-8383-impl Page for Post {
8484- fn render(&self, ctx: &mut RouteContext) -> RenderResult {
8585- RenderResult::Text(format!("Hello, {}!", ctx.params.get("slug").unwrap()))
8686- }
8787-}
8886```
89879088The `RouteParams` type is a [newtype](https://doc.rust-lang.org/rust-by-example/generics/new_types.html) around a `FxHashMap<String, String>`, representing the raw parameters as if they were directly extracted from an URL. If the hashmap contains a key that is not present in the route path, it will be ignored and a warning will be logged during the build process.
···93919492#### Type-safe parameters
95939696-Interacting with HashMaps in Rust can be a bit cumbersome, so Maudit provides the ability to use a struct to define your params and use it in the `DynamicRoute` trait.
9494+Interacting with HashMaps in Rust can be a bit cumbersome, so Maudit provides the ability to use a struct to define your params. This struct must derive the `Params` trait.
97959896```rust
9997#[derive(Params)]
···10199 pub slug: String,
102100}
103101104104-impl<Params> DynamicRoute for Post {
102102+impl Page<Params> for Post {
105103 fn routes(&self, ctx: &DynamicRouteContext) -> Vec<Params> {
106104 vec![Params {
107105 slug: "hello-world".to_string(),
108106 }]
109107 }
108108+109109+ // ...
110110}
111111```
112112113113-This struct can also be used when implementing the `Page` trait, making it possible to access the parameters in a type-safe way.
113113+This struct can also be used inside `render`, making it possible to access the parameters in a type-safe way.
114114115115```rust
116116#[derive(Params)]
···118118 pub slug: String,
119119}
120120121121-impl Page for Post {
121121+impl Page<Params> for Post {
122122+ // ...
123123+122124 fn render(&self, ctx: &mut RouteContext) -> RenderResult {
123125 let params = ctx.params::<Params>();
124126···163165 pub slug: String,
164166}
165167166166-impl DynamicRoute for PostJson {
168168+impl Page<Params> for PostJson {
167169 fn routes(&self, ctx: &DynamicRouteContext) -> Vec<RouteParams> {
168170 let routes = vec![Params { slug: "hello-world".to_string() }];
169171170172 RouteParams::from_vec(routes)
171173 }
172172-}
173174174174-impl Page for PostJson {
175175 fn render(&self, ctx: &mut RouteContext) -> RenderResult {
176176 let params = ctx.params::<Params>();
177177
+2-2
website/content/docs/templating.md
···2121#[route("/")]
2222pub struct Index;
23232424-impl Page<Markup> for Index {
2424+impl Page<RouteParams, Markup> for Index {
2525 fn render(&self, _: &mut RouteContext) -> Markup {
2626 html! {
2727 h1 { "Hello, world!" }
···3939#[route("/")]
4040pub struct Index;
41414242-impl Page<Markup> for Index {
4242+impl Page<RouteParams, Markup> for Index {
4343 fn render(&self, ctx: &mut RouteContext) -> Markup {
4444 let logo = ctx.add_image("./logo.png");
4545
+1-3
website/src/pages/docs.rs
···4444 slug: String,
4545}
46464747-impl DynamicRoute<DocsPageParams> for DocsPage {
4747+impl Page<DocsPageParams> for DocsPage {
4848 fn routes(&self, ctx: &mut DynamicRouteContext) -> Vec<DocsPageParams> {
4949 let content = ctx.content.get_source::<DocsContent>("docs");
5050···5252 slug: entry.id.clone(),
5353 })
5454 }
5555-}
56555757-impl Page for DocsPage {
5856 fn render(&self, ctx: &mut RouteContext) -> RenderResult {
5957 let slug = ctx.params::<DocsPageParams>().slug.clone();
6058 let entry = ctx
+1-4
website/src/pages/index.rs
···1414 let features = [
1515 ("Performant", "Generate a site with thousands of pages in seconds using minimal resources."),
1616 ("Content", "Bring your content to life with built-in support for Markdown, syntax highlighting, and more."),
1717- ("SEO-optimized", "Ensure your site is SEO-friendly by default with built-in support for sitemaps."),
1717+ ("Style your way", "Supports popular CSS frameworks and preprocessors, like Tailwind CSS and Sass."),
1818 ("Powerful routing", "Flexible and powerful routing system allows you to create complex sites with ease."),
1919 ("Ecosystem-ready", "Maudit utilize <a class=\"underline\" href=\"https://rolldown.rs\">Rolldown</a>, a fast bundler for JavaScript and CSS, enabling the usage of TypeScript and the npm ecosystem."),
2020 ("Bring your templates", "Use your preferred templating engine to craft your website's pages. If it renders to HTML, Maudit supports it."),
2121- ("Type-safe routing", "Ensure your links stay correct, even through site refactors."),
2222- ("Style your way", "Supports popular CSS frameworks and preprocessors, like Tailwind CSS and Sass.")
2321 ].map(|(name, description)| {(name, PreEscaped(description))});
24222523 layout(
···5553 p { (description) }
5654 }
5755 }
5858- span.opacity-75.italic.block.mt-1 { "And a lot more!" }
5956 }
6057 }
6158 }