File System Resources
standout-render supports file-based templates and stylesheets that can be hot-reloaded during development and embedded into release binaries. This workflow combines the rapid iteration of interpreted languages with the distribution simplicity of compiled binaries.
The Development Workflow
During development, you want to:
- Edit a template or stylesheet
- Re-run your program
- See changes immediately
During release, you want:
- A single binary with no external dependencies
- No file paths to manage
- No risk of missing assets
standout-render supports both modes with the same code.
Hot Reload
In debug builds (debug_assertions enabled), file-based templates are re-read from disk on each render. This means:
- Edit
templates/report.jinja→ re-run → see changes - No recompilation needed
#![allow(unused)] fn main() { use standout_render::{Renderer, Theme}; let mut renderer = Renderer::new(Theme::new())?; renderer.add_template_dir("./templates")?; // In debug: reads from disk each time // In release: content was scanned once at registration let output = renderer.render("report", &data)?; }
How It Works
Renderer tracks the source of each template name:
- Inline (
add_template) and embedded (with_embedded,with_embedded_source) content: always cached, never re-read. - File-based (
add_template_dir): path recorded; in debug builds the file is re-read before each render, so edits are visible without recompiling.
Supported Extensions
Templates
| Extension | Priority |
|---|---|
.jinja | 1 (highest) |
.jinja2 | 2 |
.j2 | 3 |
.stpl | 4 |
.txt | 5 (lowest) |
If both report.jinja and report.txt exist in the same directory, report.jinja is used. A lookup tries the given name exactly first; if that carries one of the extensions above, it also tries the name with that extension stripped.
Stylesheets
| Extension | Format |
|---|---|
.css | CSS syntax |
.yaml | YAML syntax |
.yml | YAML syntax |
Embedding Resources
For release builds, embed resources into the binary at compile time with embed_templates! / embed_styles!. Each macro reads matching files under the given directory and returns an EmbeddedTemplates / EmbeddedStyles value (both are EmbeddedSource<R>, differing only in the resource kind).
#![allow(unused)] fn main() { use standout_render::{embed_templates, embed_styles, Renderer, Theme}; let templates = embed_templates!("src/templates"); let styles = embed_styles!("src/styles"); let mut renderer = Renderer::new(Theme::new())?; renderer.with_embedded_source(templates); }
EmbeddedSource::should_hot_reload() is true in debug builds when the original source directory (recorded at compile time) still exists on disk. What that buys depends on which consumer takes the value, and the two differ:
App::builder().templates(embedded)/.styles(embedded)callEmbeddedSource::into_registry, which undershould_hot_reload()walks the source directory and registers the entries as file-backed. A debug build therefore re-reads them per render, exactly likeadd_template_dir. See the standout framework docs for that wiring.Renderer::with_embedded_sourcebuilds that registry and then copies every resolved entry in withadd_inline. The content is snapshotted at registration, so aRendererdoes not re-read the source directory afterwards —should_hot_reload()changes nothing a caller can observe on this path.
Hybrid Approach
Combine embedded defaults with an optional file directory. The directory adds names; it cannot replace one, because tier 1 is consulted first (see Resolution Priority) and with_embedded_source has already put every embedded name there:
#![allow(unused)] fn main() { use standout_render::{embed_templates, Renderer, Theme}; use std::path::Path; let embedded = embed_templates!("src/templates"); let mut renderer = Renderer::new(Theme::new())?; renderer.with_embedded_source(embedded); // Only reached for names not already resolved above if Path::new("./templates").exists() { renderer.add_template_dir("./templates")?; } }
So ./templates supplies templates the binary does not embed. A same-named file there is not an override: render("report") still resolves the embedded report. To let a directory win over the binary's copy, do not register the embedded source at all when the directory exists.
Resolution Priority
Renderer resolves a name in two tiers:
- Inline and embedded —
add_templateandwith_embedded/with_embedded_sourcewrite into the same namespace. Whichever call registered a name last wins; there's no separate priority between "inline" and "embedded" content. - File-based directories (
add_template_dir) — checked only for a name not already resolved in tier 1.
Registering the same name from two different directories is a collision error, not a silent override — file-based names must be unique across every directory you register.
#![allow(unused)] fn main() { renderer.with_embedded_source(embedded); // Tier 1 renderer.add_template("report", "inline"); // Tier 1 — overwrites "report" if embedded also defined it renderer.add_template_dir("./templates")?; // Tier 2 — only used for names tier 1 doesn't have }
Directory Structure
Recommended project layout:
my-cli/
├── src/
│ ├── main.rs
│ ├── templates/ # Templates for embedding
│ │ ├── list.jinja
│ │ ├── detail.jinja
│ │ └── partials/
│ │ └── header.jinja
│ └── styles/ # Stylesheets for embedding
│ ├── default.css
│ └── colorblind.css
├── templates/ # Extra development templates (gitignored)
└── styles/ # Extra development stylesheets (gitignored)
In main.rs:
#![allow(unused)] fn main() { use std::path::Path; let embedded_templates = embed_templates!("src/templates"); let mut renderer = Renderer::new(theme)?; renderer.with_embedded_source(embedded_templates); // In debug, also pick up local templates the binary does not embed. // Names the binary already embeds keep resolving to the embedded copy. #[cfg(debug_assertions)] { if Path::new("./templates").exists() { renderer.add_template_dir("./templates")?; } } }
Error Handling
Missing Templates
#![allow(unused)] fn main() { match renderer.render("nonexistent", &data) { Ok(output) => println!("{}", output), Err(e) => { // Template not found in any source eprintln!("Template error: {}", e); } } }
Name Collisions
Same-directory collisions use extension priority (.jinja beats .txt, etc. — see the table above).
Collisions across two different directories registered with add_template_dir are reported as RegistryError::Collision, not silently resolved by registration order.
Invalid Content
Template syntax errors are reported with the template name and the underlying engine's message.
API Reference
Renderer
The primary entry point for most applications:
#![allow(unused)] fn main() { use standout_render::{Renderer, Theme}; let mut renderer = Renderer::new(Theme::new())?; // Templates renderer.add_template("name", "content")?; renderer.add_template_dir("./templates")?; renderer.with_embedded_source(embed_templates!("src/templates")); // Render let output = renderer.render("name", &data)?; let count = renderer.template_count(); }
TemplateRegistry
The lower-level registry Renderer builds on internally. Use it directly only when bypassing Renderer:
#![allow(unused)] fn main() { use standout_render::TemplateRegistry; let mut registry = TemplateRegistry::new(); registry.add_inline("greeting", "Hello, {{ name }}!"); registry.add_embedded(embedded_map); // HashMap<String, String> // Query — `get` returns the resolved source, not the raw content let resolved = registry.get("greeting")?; // Result<ResolvedTemplate, RegistryError> let content = registry.get_content("greeting")?; // Result<String, RegistryError> let names: Vec<&str> = registry.names().collect(); }
StylesheetRegistry
#![allow(unused)] fn main() { use standout_render::StylesheetRegistry; let mut registry = StylesheetRegistry::new(); registry.add_dir("./styles")?; registry.add_embedded(embedded_themes); // HashMap<String, Theme> let theme = registry.get("default")?; // Result<Theme, StylesheetError> let exists: bool = registry.contains("default"); let names: Vec<&str> = registry.names().collect(); }
Embed Macros
#![allow(unused)] fn main() { use standout_render::{embed_templates, embed_styles}; // At compile time, reads all matching files and embeds their content let templates = embed_templates!("path/to/templates"); let styles = embed_styles!("path/to/styles"); }