WIP: Complete minijinja support

This commit is contained in:
Tristan D. 2025-03-03 18:26:43 +01:00
parent 6eaad79f9a
commit 4fa9f08cc3
Signed by: tristan
SSH key fingerprint: SHA256:9oFM1J63hYWJjCnLG6C0fxBS15rwNcWwdQNMOHYKJ/4
13 changed files with 240 additions and 22 deletions

18
Cargo.lock generated
View file

@ -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"

View file

@ -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"] }

View file

@ -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

View file

@ -1 +1 @@
css/datastar-1-0-0-beta-7.js
js/datastar-1-0-0-beta-7.js

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -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}

View file

@ -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<String>) -> Markup {
html! {
h1 { "Hello, World!" }
}
}
#[allow(unused)]
#[route(GET "/item/:id?amount&offset")]
async fn item_handler(
id: u32,
amount: Option<u32>,
offset: Option<u32>,
State(state): State<String>,
State(state): State<AppState>,
// Json(json): Json<u32>,
) -> 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<String>) -> impl IntoResponse {
async fn static_handler(path: String, _: State<AppState>) -> 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<Body> {
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<minijinja::Environment<'static>>,
}
#[route(GET "/")]
async fn jinja_index_handler(state: State<AppState>) -> impl IntoResponse {
RenderTemplate::new("/".to_string(), state.mj_env.clone())
}
#[route(GET "/*path")]
async fn jinja_index_handler_path(path: String, state: State<AppState>) -> impl IntoResponse {
RenderTemplate::new(path, state.mj_env.clone())
}
struct RenderTemplate {
tmpl_name: String,
env: Arc<Environment<'static>>,
ctx: minijinja::Value,
block: Option<String>,
}
impl RenderTemplate {
fn new(tmpl_name: String, env: Arc<Environment<'static>>) -> Self {
Self {
tmpl_name,
env,
ctx: minijinja::Value::from(()),
block: None,
}
}
fn _new_with_ctx(
tmpl_name: String,
env: Arc<Environment<'static>>,
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 Symbols 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<Option<String>, 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;
}

View file

@ -0,0 +1 @@
<h1>Not Found! </h1>

View file

@ -0,0 +1,4 @@
<h1>Def</h1>
{% block sidebar %}
"Sidebar test"
{% endblock sidebar %}

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Website</title>
<!-- TODO!!!: Add favico -->
<!-- <link rel="icon" href="./favicon.ico" type="image/x-icon"> -->
<script type="module" src="/dist/datastar.min.js"></script>
<link rel="stylesheet" href="/dist/styles.min.css">
<link rel="icon" href="/dist/favicon.ico" />
</head>
<body>
<main>
<h1>Welcome to My Website</h1>
<div m-1>div</div>
<div class="m-1">div</div>
<div class="m1">div</div>
<m1>m1</m1>
<m2>m2</m2>
<m3>m3</m3>
</main>
<!-- <script src="index.js"></script> -->
</body>
</html>

View file

@ -8,7 +8,8 @@
<!-- TODO!!!: Add favico -->
<!-- <link rel="icon" href="./favicon.ico" type="image/x-icon"> -->
<script type="module" src="/dist/datastar.min.js"></script>
<link rel="stylesheet" href="./dist/styles.min.css">
<link rel="stylesheet" href="/dist/styles.min.css">
<link rel="icon" href="/dist/favicon.ico" />
</head>
<body>
@ -19,6 +20,6 @@
<div class="m1">div</div>
<m1>div</m1>
</main>
<script src="index.js"></script>
<!-- <script src="index.js"></script> -->
</body>
</html>

View file

@ -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,

View file

@ -1,12 +1,12 @@
{
"name": "oeko-mono",
"name": "redvault-ai",
"description": "",
"version": "0.1.1",
"author": "Tristan Druyen <tristan@vault81.mozmail.com>",
"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",