Renderers and escaping
The built-ins, the whitespace features, and writing your own.
HtmlRenderer
The default, and what Component::render builds for an HTML template. It
HTML-escapes everything that goes through { … } and leaves {@html … } alone.
let mut r: Box<dyn damask::Renderer> = Box::new(damask::HtmlRenderer::new());
component.render_into(r.as_mut());
let out = r.finish();| Item | Purpose |
|---|---|
HtmlRenderer::new() |
the whitespace policy the crate features chose |
HtmlRenderer::with_whitespace(ws) |
that choice made explicitly |
.as_str() |
borrow the output built so far |
finish() |
consume a Box<Self> and return the String |
It is a thin newtype over StringRenderer, and derives Debug, Clone and
Default.
StringRenderer
The generic String-backed buffer the built-ins are made of, and the shortest
path to a renderer for a different output format:
use damask::renderers::{StringRenderer, Whitespace, escape_html, escape_none};
let html = StringRenderer::with_escape(escape_html);
let raw = StringRenderer::with_escape(escape_none).with_whitespace(Whitespace::Pretty);An escape policy is a pub type EscapeFn = fn(&str, &mut String), which appends
to the output rather than returning — so text with no special characters costs a
single push_str. Two are provided:
escape_html- Replaces
&,<,>,"and'with their entities, copying runs of ordinary characters in bulk. escape_none- Copies through unchanged. Right for a format with no metacharacters, wrong for almost everything else.
Alongside with_escape and with_whitespace, StringRenderer offers as_str()
and into_string(). Escaped values are formatted straight into the buffer
through the policy, without a full intermediate String.
Whitespace
HtmlRenderer::new() emits each template’s bytes exactly as written. Two crate
features change that:
damask = { version = "0.5", features = ["pretty"] }pretty- Re-indents the output — two spaces per level — so “view source” on a page you are debugging is readable.
minify- Replaces each newline and the indentation after it with the single space it already renders as.
Neither alters the rendered document, only its source. Every variant produces the same page: HTML collapses a run of whitespace containing a newline to one space wherever whitespace is insignificant, so resizing such a run cannot change what a browser draws. None of them ever adds a newline where the template had none, or removes one that separated two things written on separate lines.
They are features rather than arguments because Component::render builds its
own renderer: there is no call site to pass a choice at, and threading one
through every component would put a formatting concern in every template. Cargo
features are additive, so minify wins when both are enabled — one dependency
asking for pretty must not un-minify another’s release build.
The Whitespace enum is the same choice at the type level, for a renderer you
build yourself:
| Variant | Effect |
|---|---|
AsWritten |
the templates’ own bytes; each component starts at column 0 |
Pretty |
indent each component’s markup to the depth of its call site |
Minified |
one space per newline run |
Whitespace::default() reads the features, which is what new() uses.
What layout never touches
An escaped value and a {@html … } value are data: a newline inside a hostname
or a log line belongs to the value, and moving it would be the renderer editing
content. So is a tag’s own bytes, whose only possible newline is inside an
attribute value.
Verbatim regions
A renderer that lays its output out must leave the content of <pre>,
<textarea>, <script> and <style> alone — a space added there is a space the
reader gets. That is what set_verbatim is for, and it nests, because such an
element can contain a component containing more of them.
Writing your own
Implement Renderer to change escaping, target a different sink, or stream.
Components are compiled against &mut dyn Renderer, so any renderer drives any
component — including ones from other crates.
Three methods are required: write_raw, write_escaped, finish. Everything
else has a default, so the smallest useful renderer is those three:
use damask::Renderer;
use std::fmt::Display;
#[derive(Default)]
pub struct UpcaseRenderer {
buf: String,
}
impl Renderer for UpcaseRenderer {
fn write_raw(&mut self, s: &str) {
self.buf.push_str(&s.to_uppercase());
}
fn write_escaped(&mut self, value: &dyn Display) {
self.buf.push_str(&value.to_string().to_uppercase());
}
fn finish(self: Box<Self>) -> String {
self.buf
}
}Implement write_display_raw too if the renderer is buffer-backed, so
{@html … } writes in place instead of allocating a String first, and the
layout hooks if it formats. See Traits for what each
one is for.
Renderer requires Send, which a String-backed one like the above satisfies
without saying anything. An async render holds
&mut dyn Renderer across its awaits, and a future holding one is Send — the
condition for a server executor driving it — only if the renderer is. A renderer
over a deliberately non-Send sink is what this rules out.
Escaping rules of thumb
{ … }escapes.{@html … }does not. Only reach for the second with content you produced or that is already escaped — a child’s.render(), markdown compiled at build time.- Attribute values are escaped by the same policy, including the interpolated parts of a quoted value, and both halves of a spread map.
{...expr}over a&'static stris not escaped; over a[(K, V)]map it is. That asymmetry is the whole design: the'staticbound is what keeps request-derived data out of the unescaped path.