WIP: Complete minijinja support
This commit is contained in:
parent
6eaad79f9a
commit
4fa9f08cc3
13 changed files with 240 additions and 22 deletions
18
Cargo.lock
generated
18
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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
|
||||
|
|
2
darm_test/public/datastar.min.js
vendored
2
darm_test/public/datastar.min.js
vendored
|
@ -1 +1 @@
|
|||
css/datastar-1-0-0-beta-7.js
|
||||
js/datastar-1-0-0-beta-7.js
|
BIN
darm_test/public/favicon.ico
Normal file
BIN
darm_test/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
2
darm_test/public/styles.min.css
vendored
2
darm_test/public/styles.min.css
vendored
|
@ -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}
|
||||
|
|
|
@ -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 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<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;
|
||||
}
|
||||
|
|
1
darm_test/src/templates/404.html
Normal file
1
darm_test/src/templates/404.html
Normal file
|
@ -0,0 +1 @@
|
|||
<h1>Not Found! </h1>
|
4
darm_test/src/templates/asd/def.html
Normal file
4
darm_test/src/templates/asd/def.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<h1>Def</h1>
|
||||
{% block sidebar %}
|
||||
"Sidebar test"
|
||||
{% endblock sidebar %}
|
26
darm_test/src/templates/index.html
Normal file
26
darm_test/src/templates/index.html
Normal 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>
|
|
@ -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>
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Reference in a new issue