rustlavel_view/lib.rs
1//! rustlavel-view: templating in the spirit of Blade, written from scratch.
2//!
3//! A template is parsed once into an AST and then interpreted. That choice is
4//! what makes [`Engine::with_reload`] possible: with reload on, a designer
5//! edits `resources/views/pages/home.rl.html`, refreshes the browser, and sees
6//! the result without a recompile — the single ergonomic advantage Blade has
7//! over compiled Rust templates, and the one worth keeping.
8//!
9//! ```ignore
10//! use rustlavel_view::{Context, Engine};
11//!
12//! let engine = Engine::default().with_reload(!config.is_production());
13//! let html = engine.render("pages.home", &Context::new().with("title", "Home"))?;
14//! ```
15//!
16//! # The syntax
17//!
18//! ```html
19//! {{ user.name }} escaped output — always, no opt-out
20//! {!! post.body !!} raw output, for HTML you already trust
21//! {{-- a note --}} stripped before rendering
22//!
23//! @if(user.admin) ... @elseif(user.editor) ... @else ... @endif
24//! @foreach(posts as post) {{ loop.index }} {{ post.title }} @endforeach
25//!
26//! @extends("layouts.app")
27//! @section("title", page.name)
28//! @section("body") ... @endsection
29//! @yield("body")
30//! @include("parts.nav")
31//!
32//! @lang("auth.sign_in") a translated phrase
33//! @lang("mail.greeting", "name", user.name) with a `:name` placeholder
34//! ```
35//!
36//! Inside `@foreach`, `loop` carries `index`, `iteration`, `first`, `last`, and
37//! `count`. A structural directive alone on a line takes the line with it, so
38//! the rendered HTML looks like HTML.
39//!
40//! `@include` renders a partial in place, with the scope it was included from —
41//! a partial used inside a loop can read the loop's item and its `loop`
42//! variable, which is what makes `@include` worth having at all.
43//!
44//! # What is missing on purpose
45//!
46//! `@php` does not exist and will not. A template that can run code stops being
47//! reviewable as markup, hides logic from the compiler and from tests, and
48//! turns "just a view change" into a deployment risk. Compute the value in
49//! Rust, put it in the [`Context`], and render it. Templates using `@php` are
50//! rejected with an error that says exactly this.
51//!
52//! Expressions are equally limited — paths, literals, comparisons, `&&`, `||`,
53//! `!` — for the same reason. There is no arithmetic and there are no calls.
54//!
55//! `@lang` looks like the exception and is not. It calls nothing the template
56//! chose: the key is a literal fixed at parse time, the only thing it can reach
57//! is a phrase table, and the worst a wrong key can do is print itself. What
58//! `@php` and calls would let a template do — read the filesystem, hit the
59//! database, decide something — none of that is reachable from here. The
60//! alternative is worse in the way this crate cares about: without it, every
61//! translated string has to be computed in Rust and passed in, which puts the
62//! wording of the page somewhere nobody looks for wording.
63//!
64//! The locale is not a parameter to the directive. It comes from the context
65//! under the reserved name `app_locale`, because one [`Engine`] serves every
66//! request and two people reading at once may be reading different languages —
67//! and because a parsed template is cached and shared, so the words cannot be
68//! decided until the moment the page is written. With no `app_locale` and no
69//! translator, `@lang` prints its key.
70//!
71//! # Escaping
72//!
73//! `{{ }}` escapes `&`, `<`, `>`, `"` and `'`, every time, with no way to turn
74//! it off. Markup that is genuinely trusted asks for [`escape`]'s absence out
75//! loud with `{!! !!}`, which makes the dangerous case the one that stands out
76//! in a diff.
77
78pub mod ast;
79pub mod context;
80pub mod engine;
81pub mod escape;
82pub mod expr;
83pub mod lexer;
84pub mod parser;
85pub mod source;
86pub mod routes;
87pub mod translate;
88pub mod value;
89
90mod render;
91
92pub use ast::{Branch, Node, Template, TemplateRef};
93pub use context::{Context, Scope};
94pub use translate::Translate;
95pub use engine::{DEFAULT_ROOT, EXTENSION, Engine};
96pub use escape::escape;
97pub use expr::{BinaryOp, Expr};
98pub use value::{to_text, truthy};