Skip to content
Damask

Traits

Component, Render, Renderer, their async counterparts, and what the derive implements.


Component

Rust
pub trait Component: Render {
    fn default_renderer(&self) -> Box<dyn Renderer>;
    fn render(&self) -> String {}
    fn render_with(&self, slots: Slots<'_>) -> String {}
}

Generated by the derive, which implements default_renderer — a boxed HtmlRenderer — and takes the other two from the trait. render is render_with(Slots::EMPTY); render_with builds the default renderer, calls render_slots, and returns finish(). Object-safe, so &dyn Component and Vec<Box<dyn Component>> work.

render_with is the Rust-side equivalent of <Comp>…</Comp> in a template — see Slots.

Render

Rust
pub trait Render {
    fn render_into(&self, r: &mut dyn Renderer);
    fn render_slots(&self, r: &mut dyn Renderer, slots: Slots<'_>) {}
}

The lower-level half: write yourself into a renderer someone else owns. The derive implements render_slots with the lowered template and points render_into at it with Slots::EMPTY.

The default render_slots forwards to render_into, ignoring its slots. That suits content with no <slot>s of its own — a Fragment, or a hand-written impl — and lets such an impl stay a single method.

Blanket impls, all forwarding both methods:

Impl Behaviour
Render for &T where T: Render + ?Sized renders what it points at — which is what makes the &dyn Render a Slots hands back renderable
Render for Box<T> where T: Render + ?Sized Box<dyn Render> for heterogeneous children
Render for Option<T> where T: Render None renders nothing, so {@render slots.get("x")} needs no guard

Render is object-safe too.

AsyncComponent and AsyncRender

Rust
pub type RenderFuture<'a> = Pin<Box<dyn Future<Output = ()> + Send + 'a>>;

pub trait AsyncRender: Sync {
    fn render_into_async<'a>(&'a self, r: &'a mut dyn Renderer) -> RenderFuture<'a>;
    fn render_slots_async<'a>(&'a self, r: &'a mut dyn Renderer, slots: Slots<'a>)
        -> RenderFuture<'a> {}
}

pub trait AsyncComponent: AsyncRender {
    fn default_renderer(&self) -> Box<dyn Renderer>;
    fn render_async(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>> {}
    fn render_with_async(&self, slots: Slots<'_>)
        -> Pin<Box<dyn Future<Output = String> + Send + '_>> {}
}

The same two traits with values replaced by boxed futures, implemented by the derive instead of Component/Render for a template that contains .await — see Async templates. Boxed because an async fn in a trait is not dyn-safe and these stay object-safe like the rest; that allocation is why the derive only reaches for them when a template actually awaits.

Blanket impls run one way only:

Impl Effect
AsyncRender for T where T: Render + Sync + ?Sized every sync component is async-renderable — its future has nothing to poll
AsyncComponent for T where T: Component + Sync + ?Sized render_async / render_with_async work on a sync component too

There is no impl in the other direction: rendering an async component synchronously would mean blocking on a future inside the caller’s own executor.

RenderFuture is Send so a server executor can drive a render. That is what Renderer: Send, AsyncRender: Sync and a slot fill’s Sync are for — one bound per thing the future holds across an await.

Renderer

The output buffer and the escaping policy, in one object-safe trait. Components are compiled against &mut dyn Renderer, so a component compiled once is driven by any renderer.

Renderer: Send, because an async render holds &mut dyn Renderer across its awaits and a future holding one is Send only if the renderer is. A buffer is Send unless it was built from something deliberately not.

Method Backs Default
write_raw(&str) tags and already-safe content required
write_escaped(&dyn Display) { … } required
finish(self: Box<Self>) -> String producing the output required
write_text(&str) literal text between tags forwards to write_raw
write_display_raw(&dyn Display) {@html … } formats through write_raw
push_indent(usize) / pop_indent(usize) layout no-op
close_line(usize) layout no-op
set_verbatim(bool) <pre>, <textarea>, <script>, <style> no-op

Everything with a default exists so that a renderer written before those hooks did stays correct without being touched — and so a minimal renderer is three methods.

write_text is separate from write_raw because it is the only markup a renderer may lay out: it is the whitespace between elements, which HTML renders as a single space however much of it there is. A tag’s own bytes are never re-laid-out, since the only newline one can contain is inside an attribute value, and re-indenting a multi-line title would change what it says.

write_display_raw defaults to formatting the value into a String and passing it to write_raw. A buffer-backed renderer should override it to write in place instead.

push_indent / pop_indent exist because indentation is a property of the call site, not of the component: one compiled render_into serves every place a component is used, and those sit at different depths, so the depth cannot be baked into a component’s literals. The caller — which knows its own depth statically — opens the levels, and the renderer carries the running total.

close_line(depth) sets the indentation already written for the current line to depth levels below that running total, because what comes next is the end tag of an element at that depth. Only the run-time side can get this right: the last thing an element writes before its end tag may come from a {#if} that rendered nothing, leaving a separator written for a child — a level too deep — and which branch ran is not known until it runs. It does nothing where the line is not open, so <span>Wi-Fi</span> gains no break.

set_verbatim(bool) enters and leaves a region where whitespace is significant. It nests, because such an element can contain a component containing more of them.

Why a child renders through its parent’s renderer

{@render} and component elements write into the same renderer as their parent. Escaping is therefore a property of the output, not of the component: a component cannot escape differently depending on who called it.

Prelude

Rust
use damask::prelude::*;

Brings in Component (trait and derive), Render, Renderer, Slot, Slots, DEFAULT_SLOT, fragment, HtmlRenderer, StringRenderer, Whitespace, the async set AsyncComponent, AsyncRender, RenderFuture and fragment_async, and the attribute traits Attr, AttrSpread, AttrSet, Attrs, IntoAttrValue, TokenItem, TokenList.

The attribute traits are documented where the syntax they back is: Attr and AttrSpread in Attributes, TokenItem and TokenList in Token lists, AttrSet, IntoAttrValue and Attrs in Attribute groups.