From 4fa9f08cc35bf0376134c15101c83105ccc7d31a Mon Sep 17 00:00:00 2001 From: Tristan Druyen Date: Mon, 3 Mar 2025 18:26:43 +0100 Subject: [PATCH] WIP: Complete minijinja support --- Cargo.lock | 18 ++ darm_test/Cargo.toml | 1 + darm_test/TODO.org | 5 +- darm_test/public/datastar.min.js | 2 +- darm_test/public/favicon.ico | Bin 0 -> 15406 bytes darm_test/public/styles.min.css | 2 +- darm_test/src/main.rs | 191 ++++++++++++++++-- darm_test/src/templates/404.html | 1 + darm_test/src/templates/asd/def.html | 4 + darm_test/src/templates/index.html | 26 +++ .../{ui/index.html => templates/test.html} | 5 +- darm_test/style/uno.css | 3 + package.json | 4 +- 13 files changed, 240 insertions(+), 22 deletions(-) create mode 100644 darm_test/public/favicon.ico create mode 100644 darm_test/src/templates/404.html create mode 100644 darm_test/src/templates/asd/def.html create mode 100644 darm_test/src/templates/index.html rename darm_test/src/{ui/index.html => templates/test.html} (80%) 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 0000000000000000000000000000000000000000..2ba8527cb12f5f28f331b8d361eef560492d4c77 GIT binary patch literal 15406 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 k<4FtN!5) -> 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",