mirror of
https://github.com/askama-rs/askama.git
synced 2025-09-29 05:51:32 +00:00
Merge pull request #313 from Kijewski/pr-salvo
Add salvo example by translating the axum example
This commit is contained in:
commit
32b976216c
6
.github/workflows/rust.yml
vendored
6
.github/workflows/rust.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
|||||||
- run: |
|
- run: |
|
||||||
set -eu
|
set -eu
|
||||||
for PKG in \
|
for PKG in \
|
||||||
examples/actix-web-app examples/axum-app examples/poem-app examples/rocket-app examples/warp-app fuzzing \
|
examples/actix-web-app examples/axum-app examples/poem-app examples/rocket-app examples/salvo-app examples/warp-app fuzzing \
|
||||||
rinja rinja_derive rinja_derive_standalone rinja_parser \
|
rinja rinja_derive rinja_derive_standalone rinja_parser \
|
||||||
testing testing-alloc testing-no-std
|
testing testing-alloc testing-no-std
|
||||||
do
|
do
|
||||||
@ -115,7 +115,7 @@ jobs:
|
|||||||
- run: |
|
- run: |
|
||||||
set -eu
|
set -eu
|
||||||
for PKG in \
|
for PKG in \
|
||||||
examples/actix-web-app examples/axum-app examples/poem-app examples/rocket-app examples/warp-app fuzzing \
|
examples/actix-web-app examples/axum-app examples/poem-app examples/rocket-app examples/salvo-app examples/warp-app fuzzing \
|
||||||
rinja rinja_derive rinja_derive_standalone rinja_parser \
|
rinja rinja_derive rinja_derive_standalone rinja_parser \
|
||||||
testing testing-alloc testing-no-std
|
testing testing-alloc testing-no-std
|
||||||
do
|
do
|
||||||
@ -158,7 +158,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
package: [
|
package: [
|
||||||
examples/actix-web-app, examples/axum-app, examples/poem-app, examples/rocket-app, examples/warp-app, fuzzing,
|
examples/actix-web-app, examples/axum-app, examples/poem-app, examples/rocket-app, examples/salvo-app, examples/warp-app, fuzzing,
|
||||||
rinja, rinja_derive, rinja_derive_standalone, rinja_parser,
|
rinja, rinja_derive, rinja_derive_standalone, rinja_parser,
|
||||||
testing, testing-alloc, testing-no-std,
|
testing, testing-alloc, testing-no-std,
|
||||||
]
|
]
|
||||||
|
@ -196,7 +196,6 @@ impl ResponseError for AppError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This is your error handler
|
|
||||||
impl IntoResponse for AppError {
|
impl IntoResponse for AppError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
#[derive(Debug, Template)]
|
#[derive(Debug, Template)]
|
||||||
@ -278,6 +277,66 @@ impl<'r> Responder<'r, 'static> for AppError {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Salvo
|
||||||
|
|
||||||
|
[](
|
||||||
|
https://github.com/rinja-rs/rinja/tree/master/examples/salvo-app "our salvo example web-app"
|
||||||
|
)
|
||||||
|
[](
|
||||||
|
https://crates.io/crates/salvo "crates.io: salvo"
|
||||||
|
)
|
||||||
|
|
||||||
|
To convert the `String` to an HTML response, you can use
|
||||||
|
[`Text::Html(_)`](https://docs.rs/salvo/0.76.0/salvo/prelude/enum.Text.html#variant.Html).
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use salvo::writing::Text;
|
||||||
|
use salvo::{Scribe, handler};
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
async fn handler() -> Result<impl Scribe, AppError> {
|
||||||
|
…
|
||||||
|
Ok(Text::Html(template.render()?))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To implement your own error type, you can use this boilerplate code:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use rinja::Template;
|
||||||
|
use salvo::http::StatusCode;
|
||||||
|
use salvo::writing::Text;
|
||||||
|
use salvo::{Response, Scribe};
|
||||||
|
|
||||||
|
#[derive(Debug, displaydoc::Display, thiserror::Error)]
|
||||||
|
enum AppError {
|
||||||
|
/// could not render template
|
||||||
|
Render(#[from] rinja::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scribe for AppError {
|
||||||
|
fn render(self, res: &mut Response) {
|
||||||
|
#[derive(Debug, Template)]
|
||||||
|
#[template(path = "error.html")]
|
||||||
|
struct Tmpl { … }
|
||||||
|
|
||||||
|
res.status_code(match &self {
|
||||||
|
AppError::Render(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
});
|
||||||
|
let tmpl = Tmpl { … };
|
||||||
|
if let Ok(body) = tmpl.render() {
|
||||||
|
Text::Html(body).render(res);
|
||||||
|
} else {
|
||||||
|
Text::Plain("Something went wrong").render(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Warp
|
## Warp
|
||||||
|
|
||||||
[ -> StatusCode {
|
fn status_code(&self) -> StatusCode {
|
||||||
match &self {
|
match self {
|
||||||
AppError::NotFound => StatusCode::NOT_FOUND,
|
AppError::NotFound => StatusCode::NOT_FOUND,
|
||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ enum AppError {
|
|||||||
|
|
||||||
impl ResponseError for AppError {
|
impl ResponseError for AppError {
|
||||||
fn status(&self) -> StatusCode {
|
fn status(&self) -> StatusCode {
|
||||||
match &self {
|
match self {
|
||||||
AppError::NotFound => StatusCode::NOT_FOUND,
|
AppError::NotFound => StatusCode::NOT_FOUND,
|
||||||
AppError::Render(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
AppError::Render(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# rinja + warp example web app
|
# rinja + rocket example web app
|
||||||
|
|
||||||
This is a simple web application that uses rinja as template engine, and
|
This is a simple web application that uses rinja as template engine, and
|
||||||
[rocket](https://crates.io/crates/rocket) as web framework.
|
[rocket](https://crates.io/crates/rocket) as web framework.
|
||||||
|
1
examples/salvo-app/.rustfmt.toml
Symbolic link
1
examples/salvo-app/.rustfmt.toml
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../.rustfmt.toml
|
31
examples/salvo-app/Cargo.toml
Normal file
31
examples/salvo-app/Cargo.toml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
[package]
|
||||||
|
name = "salvo-app"
|
||||||
|
version = "0.3.5"
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
# This is an example application that uses both rinja as template engine,
|
||||||
|
# and salvo as your web-framework.
|
||||||
|
[dependencies]
|
||||||
|
rinja = { version = "0.3.5", path = "../../rinja" }
|
||||||
|
salvo = { version = "0.76.0", default-features = false, features = ["http1", "logging", "server"] }
|
||||||
|
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread"] }
|
||||||
|
|
||||||
|
# serde and strum are used to parse (deserialize) and generate (serialize) information
|
||||||
|
# between web requests, e.g. to share the selected display language.
|
||||||
|
serde = { version = "1.0.217", features = ["derive"] }
|
||||||
|
strum = { version = "0.26.3", features = ["derive"] }
|
||||||
|
|
||||||
|
# These depenendies are simply used for a better user experience, having access logs in the
|
||||||
|
# console, and error messages if anything goes wrong, e.g. if the port is already in use.
|
||||||
|
displaydoc = "0.2.5"
|
||||||
|
pretty-error-debug = "0.3.1"
|
||||||
|
thiserror = "2.0.11"
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-subscriber = "0.3.19"
|
||||||
|
|
||||||
|
# In a real application you would not need this section. It is only used in here, so that this
|
||||||
|
# example can have a more lenient MSRV (minimum supported rust version) than rinja as a whole.
|
||||||
|
[workspace]
|
||||||
|
members = ["."]
|
1
examples/salvo-app/LICENSE-APACHE
Symbolic link
1
examples/salvo-app/LICENSE-APACHE
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../LICENSE-APACHE
|
1
examples/salvo-app/LICENSE-MIT
Symbolic link
1
examples/salvo-app/LICENSE-MIT
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../LICENSE-MIT
|
17
examples/salvo-app/README.md
Normal file
17
examples/salvo-app/README.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# rinja + salvo example web app
|
||||||
|
|
||||||
|
This is a simple web application that uses rinja as template engine, and
|
||||||
|
[salvo](https://crates.io/crates/salvo) as web framework.
|
||||||
|
It lets the user of the web page select a display language, and asks for their name.
|
||||||
|
The example shows the interaction between both projects, and serves as an example to use
|
||||||
|
basic rinja features such as base templates to a unified layout skeleton for your page,
|
||||||
|
and less boilerplate in your template code.
|
||||||
|
|
||||||
|
To run the example execute `cargo run` in this folder.
|
||||||
|
Once the project is running, open <http://127.0.0.1:8080/> in your browser.
|
||||||
|
To gracefully shut does the server, type ctrl+C in your terminal.
|
||||||
|
|
||||||
|
The files of the project contain comments for you to read.
|
||||||
|
The recommended reading order is "templates/_layout.html", "templates/index.html",
|
||||||
|
"Cargo.toml", "src/main.rs". Also please have a look at our [book](https://rinja.readthedocs.io/),
|
||||||
|
which explains rinja's features in greater detail.
|
1
examples/salvo-app/_typos.toml
Symbolic link
1
examples/salvo-app/_typos.toml
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../_typos.toml
|
1
examples/salvo-app/deny.toml
Symbolic link
1
examples/salvo-app/deny.toml
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../deny.toml
|
199
examples/salvo-app/src/main.rs
Normal file
199
examples/salvo-app/src/main.rs
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
|
|
||||||
|
use rinja::Template;
|
||||||
|
use salvo::catcher::Catcher;
|
||||||
|
use salvo::conn::TcpListener;
|
||||||
|
use salvo::http::StatusCode;
|
||||||
|
use salvo::logging::Logger;
|
||||||
|
use salvo::macros::Extractible;
|
||||||
|
use salvo::writing::{Redirect, Text};
|
||||||
|
use salvo::{Listener, Request, Response, Router, Scribe, Server, Service, handler};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tracing::Level;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Error> {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(Level::DEBUG)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let router = Router::new()
|
||||||
|
.push(Router::new().get(start_handler))
|
||||||
|
.push(Router::with_path("{lang}/index.html").get(index_handler))
|
||||||
|
.push(Router::with_path("{lang}/greet-me.html").get(greeting_handler));
|
||||||
|
let server = Service::new(router)
|
||||||
|
.catcher(Catcher::default().hoop(not_found_handler))
|
||||||
|
.hoop(Logger::new());
|
||||||
|
|
||||||
|
// In a real application you would most likely read the configuration from a config file.
|
||||||
|
let acceptor = TcpListener::new((IpAddr::V4(Ipv4Addr::LOCALHOST), 8080))
|
||||||
|
.try_bind()
|
||||||
|
.await
|
||||||
|
.map_err(Error::Bind)?;
|
||||||
|
|
||||||
|
Server::new(acceptor)
|
||||||
|
.try_serve(server)
|
||||||
|
.await
|
||||||
|
.map_err(Error::Run)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(displaydoc::Display, thiserror::Error, pretty_error_debug::Debug)]
|
||||||
|
enum Error {
|
||||||
|
/// could not bind socket
|
||||||
|
Bind(#[source] salvo::Error),
|
||||||
|
/// could not run server
|
||||||
|
Run(#[source] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Thanks to this type, your user can select the display language of your page.
|
||||||
|
///
|
||||||
|
/// The same type is used by salvo as part of the URL, and in rinja to select what content to show,
|
||||||
|
/// and also as an HTML attribute in `<html lang=`. To make it possible to use the same type for
|
||||||
|
/// three different use cases, we use a few derive macros:
|
||||||
|
///
|
||||||
|
/// * `Default` to have a default/fallback language.
|
||||||
|
/// * `Debug` is not strictly needed, but it might aid debugging.
|
||||||
|
/// * `Clone` + `Copy` so that we can pass the language by value.
|
||||||
|
/// * `PartialEq` so that we can use the type in comparisons with `==` or `!=`.
|
||||||
|
/// * `serde::Deserialize` so that salvo can parse the type in incoming URLs.
|
||||||
|
/// * `strum::Display` so that rinja can write the value in templates.
|
||||||
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Deserialize, strum::Display)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
enum Lang {
|
||||||
|
#[default]
|
||||||
|
en,
|
||||||
|
de,
|
||||||
|
fr,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This enum contains any error that could occur while handling an incoming request.
|
||||||
|
///
|
||||||
|
/// In a real application you would most likely have multiple error sources, e.g. database errors,
|
||||||
|
#[derive(Debug, displaydoc::Display, thiserror::Error)]
|
||||||
|
enum AppError {
|
||||||
|
/// not found
|
||||||
|
NotFound,
|
||||||
|
/// could not extract information from request
|
||||||
|
Extract(#[from] salvo::http::ParseError),
|
||||||
|
/// could not render template
|
||||||
|
Render(#[from] rinja::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is your error handler
|
||||||
|
impl Scribe for AppError {
|
||||||
|
fn render(self, res: &mut Response) {
|
||||||
|
// It uses a rinja template to display its content.
|
||||||
|
// The member `lang` is used by "_layout.html" which "error.html" extends. Even though it
|
||||||
|
// is always the fallback language English in here, "_layout.html" expects to be able to
|
||||||
|
// access this field, so you have to provide it.
|
||||||
|
#[derive(Debug, Template)]
|
||||||
|
#[template(path = "error.html")]
|
||||||
|
struct Tmpl {
|
||||||
|
lang: Lang,
|
||||||
|
err: AppError,
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status_code(match &self {
|
||||||
|
AppError::NotFound => StatusCode::NOT_FOUND,
|
||||||
|
AppError::Extract(_) => StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
AppError::Render(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
});
|
||||||
|
let tmpl = Tmpl {
|
||||||
|
lang: Lang::default(),
|
||||||
|
err: self,
|
||||||
|
};
|
||||||
|
if let Ok(body) = tmpl.render() {
|
||||||
|
Text::Html(body).render(res);
|
||||||
|
} else {
|
||||||
|
Text::Plain("Something went wrong").render(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is your "Error: 404 - not found" handler
|
||||||
|
#[handler]
|
||||||
|
async fn not_found_handler() -> impl Scribe {
|
||||||
|
AppError::NotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is the first page your user hits, meaning it does not contain language information,
|
||||||
|
/// so we redirect them.
|
||||||
|
#[handler]
|
||||||
|
async fn start_handler() -> impl Scribe {
|
||||||
|
Redirect::found("/en/index.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is the first localized page your user sees.
|
||||||
|
///
|
||||||
|
/// It has arguments in the path that need to be parsable using `serde::Deserialize`; see `Lang`
|
||||||
|
/// for an explanation. And also query parameters (anything after `?` in the incoming URL).
|
||||||
|
#[handler]
|
||||||
|
async fn index_handler(req: &mut Request) -> Result<impl Scribe, AppError> {
|
||||||
|
/// This type collects the URL params, i.e. the `"/{lang}/"` part
|
||||||
|
#[derive(Debug, Deserialize, Extractible)]
|
||||||
|
#[salvo(extract(default_source(from = "param")))]
|
||||||
|
struct Params {
|
||||||
|
lang: Lang,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This type collects the query parameter `?name=` (if present)
|
||||||
|
#[derive(Debug, Deserialize, Extractible)]
|
||||||
|
#[salvo(extract(default_source(from = "query")))]
|
||||||
|
struct Query {
|
||||||
|
#[serde(default)]
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let Params { lang } = req.extract().await?;
|
||||||
|
let Query { name } = req.extract().await?;
|
||||||
|
|
||||||
|
// In the template we both use `{% match lang %}` and `{% if lang !=`, the former to select the
|
||||||
|
// text of a specific language, e.g. in the `<title>`; and the latter to display references to
|
||||||
|
// all other available languages except the currently selected one.
|
||||||
|
// The field `name` will contain the value of the query parameter of the same name.
|
||||||
|
// In `IndexHandlerQuery` we annotated the field with `#[serde(default)]`, so if the value is
|
||||||
|
// absent, an empty string is selected by default, which is visible to the user an empty
|
||||||
|
// `<input type="text" />` element.
|
||||||
|
#[derive(Debug, Template)]
|
||||||
|
#[template(path = "index.html")]
|
||||||
|
struct Tmpl {
|
||||||
|
lang: Lang,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let template = Tmpl { lang, name };
|
||||||
|
Ok(Text::Html(template.render()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is the final page of this example application.
|
||||||
|
///
|
||||||
|
/// Like `index_handler` it contains a language in the URL, and a query parameter to read the user's
|
||||||
|
/// provided name. In here, the query argument `name` has no default value, so salvo will show
|
||||||
|
/// an error message if absent.
|
||||||
|
#[handler]
|
||||||
|
async fn greeting_handler(req: &mut Request) -> Result<impl Scribe, AppError> {
|
||||||
|
#[derive(Debug, Deserialize, Extractible)]
|
||||||
|
#[salvo(extract(default_source(from = "param")))]
|
||||||
|
struct Params {
|
||||||
|
lang: Lang,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Extractible)]
|
||||||
|
#[salvo(extract(default_source(from = "query")))]
|
||||||
|
struct Query {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let Params { lang } = req.extract().await?;
|
||||||
|
let Query { name } = req.extract().await?;
|
||||||
|
|
||||||
|
#[derive(Debug, Template)]
|
||||||
|
#[template(path = "greet.html")]
|
||||||
|
struct Tmpl {
|
||||||
|
lang: Lang,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let template = Tmpl { lang, name };
|
||||||
|
Ok(Text::Html(template.render()?))
|
||||||
|
}
|
56
examples/salvo-app/templates/_layout.css
Normal file
56
examples/salvo-app/templates/_layout.css
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{#-
|
||||||
|
This file is included by "_layout.html".
|
||||||
|
You can still use template syntax (such as this comment) in here.
|
||||||
|
-#}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background-color: #eee;
|
||||||
|
color: #111;
|
||||||
|
font-size: 62.5%;
|
||||||
|
min-height: 100vh;
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
line-height: 1.2em;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #fff;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
max-width: 40em;
|
||||||
|
margin: 1em auto;
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
h1 { font-size: 2.4rem; }
|
||||||
|
h2 { font-size: 2.2rem; }
|
||||||
|
h3 { font-size: 2.0rem; }
|
||||||
|
a:link, a:visited {
|
||||||
|
color: #36c;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:active, a:hover, a:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 0.3em;
|
||||||
|
}
|
||||||
|
#lang-select {
|
||||||
|
font-size: 80%;
|
||||||
|
width: max-content;
|
||||||
|
margin: 2em 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
#lang-select li {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: auto;
|
||||||
|
margin: .25em 0 0 0;
|
||||||
|
padding: 0 1em;
|
||||||
|
text-align: center;
|
||||||
|
list-style-type: none;
|
||||||
|
border-left: 0.1rem solid currentColor;
|
||||||
|
}
|
||||||
|
#lang-select li:first-of-type {
|
||||||
|
border-left: 0 none transparent;
|
||||||
|
}
|
||||||
|
#lang-select li:last-of-type {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
67
examples/salvo-app/templates/_layout.html
Normal file
67
examples/salvo-app/templates/_layout.html
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
{#-
|
||||||
|
This is the basic layout of our example application.
|
||||||
|
It is the core skeleton shared between all pages.
|
||||||
|
It expects the struct of any template that `{% extends %}` this layout to contain
|
||||||
|
(at least) a field `lang: Lang`, so it can be used in the `<html lang=` attribute.
|
||||||
|
-#}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{lang}}">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
{#-
|
||||||
|
A base template can contain `blocks`, which my be overridden templates that use
|
||||||
|
this base template. A block may contain a default content, if the extending
|
||||||
|
template does want to / need to override the content of a block.
|
||||||
|
|
||||||
|
E.g. maybe you would like to have "Rinja example application" as default title for
|
||||||
|
your pages, then simply add this text (without quotation marks) in the block!
|
||||||
|
|
||||||
|
The default content can be as complex as you need it to be.
|
||||||
|
E.g. it may contain any nodes like `{% if … %}`, and even other blocks.
|
||||||
|
~#}
|
||||||
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
|
||||||
|
<meta http-equiv="expires" content="Sat, 01 Dec 2001 00:00:00 GMT" />
|
||||||
|
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
|
||||||
|
{#-
|
||||||
|
In a real application you most likely would want to link style sheets,
|
||||||
|
any JavaScripts etc. using e.g. `actix-files`, instead of embedding the content
|
||||||
|
in your generated HTML.
|
||||||
|
|
||||||
|
As you can see, this comment starts with `-`, which will tells the comment
|
||||||
|
to strip all white spaces before it, until it finds the first non-white space
|
||||||
|
character, a `>`.
|
||||||
|
|
||||||
|
The comment is also terminated with `~`. This also strips white spaces, but
|
||||||
|
will leave one space, or a newline character, if the stripped content contained
|
||||||
|
a newline.
|
||||||
|
~#}
|
||||||
|
<style>
|
||||||
|
/*<![CDATA[*/
|
||||||
|
{%~ include "_layout.css" ~%}
|
||||||
|
/*]]>*/
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{%~ block content %}{% endblock ~%}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
{%- macro lang_select(page, query="") -%}
|
||||||
|
<ul id="lang-select">
|
||||||
|
{%- if lang != Lang::en -%}
|
||||||
|
<li><a href="/en/{{ page }}.html{{ query }}">This page in English</a></li>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if lang != Lang::de -%}
|
||||||
|
<li><a href="/de/{{ page }}.html{{ query }}">Diese Seite auf deutsch.</a></li>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if lang != Lang::fr -%}
|
||||||
|
<li><a href="/fr/{{ page }}.html{{ query }}">Cette page est en français.</a></li>
|
||||||
|
{%- endif -%}
|
||||||
|
</ul>
|
||||||
|
{%- endmacro lang_select -%}
|
27
examples/salvo-app/templates/error.html
Normal file
27
examples/salvo-app/templates/error.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{% extends "_layout.html" %}
|
||||||
|
|
||||||
|
{%- block title -%}
|
||||||
|
{%- match err -%}
|
||||||
|
{% when AppError::NotFound -%} 404: Not Found
|
||||||
|
{% when AppError::Extract(_) -%} 422: Unprocessable Entity
|
||||||
|
{% when AppError::Render(_) -%} 500: Internal Server Error
|
||||||
|
{%- endmatch -%}
|
||||||
|
{%- endblock -%}
|
||||||
|
|
||||||
|
{%- block content -%}
|
||||||
|
<h1>
|
||||||
|
{%- match err -%}
|
||||||
|
{% when AppError::NotFound -%} 404: Not Found
|
||||||
|
{% when AppError::Extract(_) -%} 422: Unprocessable Entity
|
||||||
|
{% when AppError::Render(_) -%} 500: Internal Server Error
|
||||||
|
{%- endmatch -%}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{%- match err -%}
|
||||||
|
{% when AppError::NotFound -%}
|
||||||
|
{% when AppError::Extract(_) -%} <pre>{{ err }}</pre>
|
||||||
|
{% when AppError::Render(err) -%} <pre>{{ err }}</pre>
|
||||||
|
{%- endmatch -%}
|
||||||
|
|
||||||
|
<h2><a href="/">Back to the first page.</a></h2>
|
||||||
|
{%- endblock -%}
|
43
examples/salvo-app/templates/greet.html
Normal file
43
examples/salvo-app/templates/greet.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{% extends "_layout.html" %}
|
||||||
|
|
||||||
|
{%- block title -%}
|
||||||
|
{%- match lang -%}
|
||||||
|
{%- when Lang::en -%} Hello, {{name}}!
|
||||||
|
{%- when Lang::de -%} Hallo, {{name}}!
|
||||||
|
{%- when Lang::fr -%} Bonjour, {{name}}!
|
||||||
|
{%- endmatch -%}
|
||||||
|
{%- endblock -%}
|
||||||
|
|
||||||
|
{%- block content -%}
|
||||||
|
<h1>
|
||||||
|
{%- match lang -%}
|
||||||
|
{%- when Lang::en -%} Hello!
|
||||||
|
{%- when Lang::de -%} Hallo!
|
||||||
|
{%- when Lang::fr -%} Bonjour!
|
||||||
|
{%- endmatch -%}
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
{%- match lang -%}
|
||||||
|
{%- when Lang::en -%}
|
||||||
|
Hello, <strong>{{name}}</strong>, nice to meet you! {#-~#}
|
||||||
|
I'm a <a href="https://rinja.readthedocs.io/">Rinja</a> example application.
|
||||||
|
{%- when Lang::de -%}
|
||||||
|
Hallo, <strong>{{name}}</strong>, schön dich kennenzulernen! {#-~#}
|
||||||
|
Ich bin eine <a href="https://rinja.readthedocs.io/">Rinja</a>-Beispielanwendung.
|
||||||
|
{%- when Lang::fr -%}
|
||||||
|
Bonjour, <strong>{{name}}</strong>, ravi de vous rencontrer ! {#-~#}
|
||||||
|
Je suis une application d'exemple de <a href="https://rinja.readthedocs.io/">Rinja</a>.
|
||||||
|
{%- endmatch -%}
|
||||||
|
</p>
|
||||||
|
<h2>
|
||||||
|
<a href="/{{ lang }}/index.html?name={{ name | urlencode }}">
|
||||||
|
{%- match lang -%}
|
||||||
|
{%- when Lang::en -%} Back to the first page.
|
||||||
|
{%- when Lang::de -%} Zurück zur ersten Seite.
|
||||||
|
{%- when Lang::fr -%} Retour à la première page.
|
||||||
|
{%- endmatch -%}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{%- call lang_select("greet-me", name|urlencode|fmt("?name={}")) -%}
|
||||||
|
{%- endblock -%}
|
84
examples/salvo-app/templates/index.html
Normal file
84
examples/salvo-app/templates/index.html
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
{% extends "_layout.html" %}
|
||||||
|
|
||||||
|
{%- block title -%}
|
||||||
|
{#-
|
||||||
|
In here you can see how to use the language URL compment to select the text to display.
|
||||||
|
-#}
|
||||||
|
{%- match lang -%}
|
||||||
|
{%- when Lang::en -%} Hello!
|
||||||
|
{%- when Lang::de -%} Hallo!
|
||||||
|
{%- when Lang::fr -%} Bonjour!
|
||||||
|
{%- endmatch -%}
|
||||||
|
{%- endblock -%}
|
||||||
|
|
||||||
|
{%- block content -%}
|
||||||
|
<h1>
|
||||||
|
{%- match lang -%}
|
||||||
|
{%- when Lang::en -%} Hello!
|
||||||
|
{%- when Lang::de -%} Hallo!
|
||||||
|
{%- when Lang::fr -%} Bonjour!
|
||||||
|
{%- endmatch -%}
|
||||||
|
</h1>
|
||||||
|
<form
|
||||||
|
method="GET"
|
||||||
|
action="/{{ lang }}/greet-me.html"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{#-
|
||||||
|
If your text contains long lines, you may want to split them,
|
||||||
|
so you as a developer have an easier time reading them.
|
||||||
|
|
||||||
|
If you don't want to end up with a multitude of spaces in the
|
||||||
|
generated content, you can use empty comments as seen below: `#-~#`.
|
||||||
|
This would strip the space before the comment, and leave a newline
|
||||||
|
character after the comment. Another option would be `#~-#`,
|
||||||
|
so that single space remains.
|
||||||
|
-#}
|
||||||
|
{%- match lang -%}
|
||||||
|
{%- when Lang::en -%}
|
||||||
|
I would like to say <em>hello</em>. {#-~#}
|
||||||
|
Would you please tell me your name?
|
||||||
|
{%- when Lang::de -%}
|
||||||
|
Ich möchte dir gerne <em>hallo</em> sagen. {#-~#}
|
||||||
|
Bitte nenne mir doch deinen Namen!
|
||||||
|
{%- when Lang::fr -%}
|
||||||
|
Je voudrais vous dire <em>bonjour</em>. {#-~#}
|
||||||
|
Pourriez-vous me donner votre nom ?
|
||||||
|
{%- endmatch -%}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
{%- match lang -%}
|
||||||
|
{%- when Lang::en -%} My name is
|
||||||
|
{%- when Lang::de -%} Ich heiße
|
||||||
|
{%- when Lang::fr -%} Je m'appelle
|
||||||
|
{%- endmatch -%}:
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="{{name}}"
|
||||||
|
name="name"
|
||||||
|
required
|
||||||
|
style="width: 10em"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
<button type="submit">
|
||||||
|
{%- match lang -%}
|
||||||
|
{%- when Lang::en -%} Greet me, then!
|
||||||
|
{%- when Lang::de -%} Dann begrüße mich!
|
||||||
|
{%- when Lang::fr -%} Saluons-nous !
|
||||||
|
{%- endmatch -%}
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{#-
|
||||||
|
The called macro is defined in base template "_layout.html",
|
||||||
|
and used to display the language selection footer.
|
||||||
|
~#}
|
||||||
|
{%- call lang_select("index") -%}
|
||||||
|
{%- endblock -%}
|
1
examples/salvo-app/tomlfmt.toml
Symbolic link
1
examples/salvo-app/tomlfmt.toml
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../tomlfmt.toml
|
Loading…
x
Reference in New Issue
Block a user