diff --git a/Cargo.lock b/Cargo.lock index 2a6785e..9ce40c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1201,6 +1201,7 @@ dependencies = [ "datastar", "maud", "mime_guess", + "minijinja", "rust-embed", "serde", "tokio", @@ -3405,6 +3406,12 @@ dependencies = [ "libc", ] +[[package]] +name = "memo-map" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b" + [[package]] name = "memoffset" version = "0.9.1" @@ -3430,6 +3437,17 @@ dependencies = [ "unicase", ] +[[package]] +name = "minijinja" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e36f1329330bb1614c94b78632b9ce45dd7d761f3304a1bed07b2990a7c5097" +dependencies = [ + "memo-map", + "self_cell", + "serde", +] + [[package]] name = "minimal-lexical" version = "0.2.1" diff --git a/darm_test/Cargo.toml b/darm_test/Cargo.toml index ce4b473..c08886e 100644 --- a/darm_test/Cargo.toml +++ b/darm_test/Cargo.toml @@ -15,6 +15,7 @@ axum-typed-routing = { git = "https://github.com/jvdwrf/axum-typed-routing", ver datastar = { git = "https://github.com/starfederation/datastar.git", version = "0.1.0" } maud = { version = "0.27.0", features = ["axum"] } mime_guess = "2.0.5" +minijinja = { version = "2.7.0", features = ["loader"] } rust-embed = { version = "8.5.0", features = ["axum", "compression"] } serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.43", features = ["full", "tracing"] } diff --git a/darm_test/TODO.org b/darm_test/TODO.org index 2021d73..ef309f5 100644 --- a/darm_test/TODO.org +++ b/darm_test/TODO.org @@ -1,3 +1,6 @@ +* Planning (newest) +- maud vs hypertext vs minijinja + - try porting to hypertext ? * Todos ** Starter boilerplate *** [X] Server: Axum @@ -5,7 +8,7 @@ **** [X] Nested Router **** [X] Fallbacks *** [X] "embd tmpl": Maud -*** [ ] "html tmpl": minijinja +*** [X] "html tmpl": minijinja - only index yaml for now, test in app wether inline or external templates feel better *** [ ] CSS Basic - UnoCSS diff --git a/darm_test/public/datastar.min.js b/darm_test/public/datastar.min.js index 60ad33c..7341c59 120000 --- a/darm_test/public/datastar.min.js +++ b/darm_test/public/datastar.min.js @@ -1 +1 @@ -css/datastar-1-0-0-beta-7.js \ No newline at end of file +js/datastar-1-0-0-beta-7.js \ No newline at end of file diff --git a/darm_test/public/favicon.ico b/darm_test/public/favicon.ico new file mode 100644 index 0000000..2ba8527 Binary files /dev/null and b/darm_test/public/favicon.ico differ diff --git a/darm_test/public/styles.min.css b/darm_test/public/styles.min.css index fc29658..7e5523d 100644 --- a/darm_test/public/styles.min.css +++ b/darm_test/public/styles.min.css @@ -1 +1 @@ -*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgba(0,0,0,0);--un-ring-shadow:0 0 rgba(0,0,0,0);--un-shadow-inset: ;--un-shadow:0 0 rgba(0,0,0,0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgba(0,0,0,0);--un-ring-shadow:0 0 rgba(0,0,0,0);--un-shadow-inset: ;--un-shadow:0 0 rgba(0,0,0,0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }.static{position:static}.m-1,.m1,[m-1=""],m1{margin:.25rem}.ms,[ms=""]{margin-inline-start:1rem}contents{display:contents}.h1{height:.25rem}.b,[b=""]{border-width:1px}[pe=""]{padding-inline-end:1rem} +*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgba(0,0,0,0);--un-ring-shadow:0 0 rgba(0,0,0,0);--un-shadow-inset: ;--un-shadow:0 0 rgba(0,0,0,0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgba(0,0,0,0);--un-ring-shadow:0 0 rgba(0,0,0,0);--un-shadow-inset: ;--un-shadow:0 0 rgba(0,0,0,0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }.static{position:static}.m-1,.m1,[m-1=""],m1{margin:.25rem}m2{margin:.5rem}m3{margin:.75rem}.ms,[ms=""]{margin-inline-start:1rem}.block{display:block}contents{display:contents}.h1{height:.25rem}.b,[b=""]{border-width:1px}[pe=""]{padding-inline-end:1rem} diff --git a/darm_test/src/main.rs b/darm_test/src/main.rs index 9ef38a2..d8d5e45 100644 --- a/darm_test/src/main.rs +++ b/darm_test/src/main.rs @@ -1,30 +1,23 @@ -use std::sync::Once; +use std::sync::{Arc, Once}; use axum::{ body::Body, extract::State, http::{header, Response, StatusCode, Uri}, - response::IntoResponse, + response::{Html, IntoResponse}, routing::get, }; use axum_typed_routing::{route, TypedRouter}; use maud::{html, Markup}; use rust_embed::RustEmbed; -#[route(GET "/hello")] -async fn hello_world(State(_): State) -> Markup { - html! { - h1 { "Hello, World!" } - } -} - #[allow(unused)] #[route(GET "/item/:id?amount&offset")] async fn item_handler( id: u32, amount: Option, offset: Option, - State(state): State, + State(state): State, // Json(json): Json, ) -> Markup { // todo!("handle request") @@ -41,7 +34,7 @@ async fn item_handler( // within our defined assets directory. This is the directory on our Asset // struct below, where folder = "examples/public/". #[route(GET "/dist/*path")] -async fn static_handler(path: String, _: State) -> impl IntoResponse { +async fn static_handler(path: String, _: State) -> impl IntoResponse { let path = path.trim_start_matches('/').to_string(); StaticFile(path) @@ -51,6 +44,9 @@ fn markup_404(uri: String) -> Markup { html! { h1 { "404" } p { (format!("{uri:?} Not Found")) } + @for i in 0..5 { + div .{"m-" (i)} { (i) } + } } } @@ -58,6 +54,9 @@ fn markup_405() -> Markup { html! { h1 { "404" } p { "Method not allowed!" } + @for i in 1..3 { + div .{"m-" (i)} {} + } } } @@ -82,6 +81,7 @@ where { fn into_response(self) -> Response { let path = self.0.into(); + tracing::debug!(?path); match Asset::get(path.as_str()) { Some(content) => { @@ -109,24 +109,185 @@ pub fn initialize_logger() { }); } +use minijinja::{Environment, Template}; + +#[derive(Clone, Debug)] +struct AppState { + _field: String, + mj_env: Arc>, +} + +#[route(GET "/")] +async fn jinja_index_handler(state: State) -> impl IntoResponse { + RenderTemplate::new("/".to_string(), state.mj_env.clone()) +} + +#[route(GET "/*path")] +async fn jinja_index_handler_path(path: String, state: State) -> impl IntoResponse { + RenderTemplate::new(path, state.mj_env.clone()) +} + +struct RenderTemplate { + tmpl_name: String, + env: Arc>, + ctx: minijinja::Value, + block: Option, +} + +impl RenderTemplate { + fn new(tmpl_name: String, env: Arc>) -> Self { + Self { + tmpl_name, + env, + ctx: minijinja::Value::from(()), + block: None, + } + } + + fn _new_with_ctx( + tmpl_name: String, + env: Arc>, + ctx: minijinja::Value, + ) -> Self { + Self { + tmpl_name, + env, + ctx, + block: None, + } + } +} + +impl IntoResponse for RenderTemplate { + fn into_response(self) -> axum::response::Response { + let path = self.tmpl_name; + let env = self.env; + let ctx = self.ctx; + let block = self.block; + + let res = env.get_template(&path); + + let render = move |template: Template| match block { + None => match template.render(ctx) { + Ok(html) => Html(html).into_response(), + Err(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to render template", + ) + .into_response(), + }, + Some(block) => match template.eval_to_state(ctx).unwrap().render_block(&block) { + Ok(html) => Html(html).into_response(), + Err(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to render block template", + ) + .into_response(), + }, + }; + + match res { + Ok(template) => render(template), + Err(_) => { + let template = env.get_template("404").unwrap(); + let html = render(template); + (StatusCode::NOT_FOUND, html).into_response() + } + } + } +} + +/// Template loader for embedded templates. Uses Symbol’s value as variable is void: rust-embed to embed templates into the binary. +/// For input path example/template +/// it looks up: +/// - $template_dir/example/template/index.html +/// - $template_dir/example/template.html +/// +/// Instead of example/template +/// any of: +/// - example//template/ +/// - example/template/// +/// - example/template.html +/// - example/template/index.html +/// would also resolve to the same 2 templates above +/// TODO Canonilize path with a middleware that redirects any weird variants to the canonical path? +fn template_loader(name: &str) -> Result, minijinja::Error> { + #[derive(RustEmbed)] + #[folder = "src/templates/"] + struct Templ; + // lets extract the "clean path" (i.e. '.html' or '/index.html' suffix) + let mut clean_path = name + .to_string() + .chars() + .fold(String::new(), |mut acc, c| { + if c == '/' && acc.chars().last() != Some('/') { + acc.push(c); + } else if c != '/' { + acc.push(c); + } + + acc + }) + .to_string(); + clean_path = clean_path + .strip_suffix("index.html") + .unwrap_or(&clean_path) + .to_string(); + + clean_path = clean_path + .strip_suffix(".html") + .unwrap_or(&clean_path) + .to_string(); + + let cleaned_path = clean_path.to_string(); + + let files_to_try = vec![ + format!("./{}.html", cleaned_path), + format!("./{}/index.html", cleaned_path), + ]; + + let found_template = files_to_try.into_iter().find_map(|path| { + tracing::info!(?path); + match Templ::get(path.as_str()) { + Some(content) => { + let content = std::str::from_utf8(&content.data).unwrap().to_string(); + Some(content) + } + None => None, + } + }); + + Ok(found_template) +} + #[tokio::main] async fn main() { initialize_logger(); + let mut mj_env = Environment::new(); + + mj_env.set_loader(template_loader); + + let app_state = AppState { + _field: "".to_string(), + mj_env: Arc::new(mj_env), + }; + // TODO pick free port/config let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap(); - let ui_router = axum::Router::new() - .typed_route(item_handler) - .typed_route(hello_world); + let ui_router = axum::Router::new().typed_route(item_handler); let router: axum::Router = axum::Router::new() + // .route("/", get(jinja_index_handler)) .merge(ui_router.clone()) .nest("/ui", ui_router) + .typed_route(jinja_index_handler) + .typed_route(jinja_index_handler_path) .typed_route(static_handler) .fallback_service(get(handle_404)) .method_not_allowed_fallback(handle_405) - .with_state("state".to_string()); + .with_state(app_state); let _ = axum::serve(listener, router.into_make_service()).await; } diff --git a/darm_test/src/templates/404.html b/darm_test/src/templates/404.html new file mode 100644 index 0000000..245cd00 --- /dev/null +++ b/darm_test/src/templates/404.html @@ -0,0 +1 @@ +

Not Found!

diff --git a/darm_test/src/templates/asd/def.html b/darm_test/src/templates/asd/def.html new file mode 100644 index 0000000..c729f9f --- /dev/null +++ b/darm_test/src/templates/asd/def.html @@ -0,0 +1,4 @@ +

Def

+{% block sidebar %} + "Sidebar test" +{% endblock sidebar %} diff --git a/darm_test/src/templates/index.html b/darm_test/src/templates/index.html new file mode 100644 index 0000000..816d6ec --- /dev/null +++ b/darm_test/src/templates/index.html @@ -0,0 +1,26 @@ + + + + + + My Website + + + + + + + + +
+

Welcome to My Website

+
div
+
div
+
div
+ m1 + m2 + m3 +
+ + + diff --git a/darm_test/src/ui/index.html b/darm_test/src/templates/test.html similarity index 80% rename from darm_test/src/ui/index.html rename to darm_test/src/templates/test.html index fe7a4d4..d51fc5e 100644 --- a/darm_test/src/ui/index.html +++ b/darm_test/src/templates/test.html @@ -8,7 +8,8 @@ - + + @@ -19,6 +20,6 @@
div
div - + diff --git a/darm_test/style/uno.css b/darm_test/style/uno.css index 2b0ea4f..9d989ab 100644 --- a/darm_test/style/uno.css +++ b/darm_test/style/uno.css @@ -6,8 +6,11 @@ .m1, [m-1=""], m1{margin:0.25rem;} +m2{margin:0.5rem;} +m3{margin:0.75rem;} .ms, [ms=""]{margin-inline-start:1rem;} +.block{display:block;} contents{display:contents;} .h1{height:0.25rem;} .b, diff --git a/package.json b/package.json index 80bb7ef..5b7936d 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { - "name": "oeko-mono", + "name": "redvault-ai", "description": "", "version": "0.1.1", "author": "Tristan Druyen ", "license": "AGPL", "scripts": { "watch-unocss": "cd ./darm_test && unocss --watch", - "watch-bundle": "npm run build-bundle", + "watch-bundle": "npm run build-bundle; sleep 5; npm run watch-bundle", "build-unocss": "cd ./darm_test && unocss", "build-bundle": "cd ./darm_test && sass -scompressed style/main.scss > public/styles.min.css", "watch-all": "npm run watch-unocss & npm run watch-bundle",