Skip to content
Damask

Compile-time components for Rust

Components with real slots.

A component is a Rust struct and an HTML template that share a filename. The template declares as many named slots as it likes — with fallback content, forwarding, and a way to ask what the caller filled — and the struct never changes. Callers fill them the way they fill a web component’s.

TOML
[dependencies]
damask = "0.11"

Rust 1.88 or newer. No build script, no configuration.

card.rs
use damask::Component;

#[derive(Component)]
pub struct Card {
    pub title: String,
    pub tone: Tone,
}
card.dmk
<article class={@tokens("card", self.tone.skin())}>
  <h3>{self.title}</h3>
  <slot/>
  <footer>
    <slot name="meta">Just now</slot>
  </footer>
</article>
page.dmk
<Card title="Damask 0.11" tone={Tone::Note}>
  <p>Templates can await.</p>
</Card>
compiles to
rendered
<article class="card card--note">
  <h3>Damask 0.11</h3>
  <p>Templates can await.</p>
  <footer>Just now</footer>
</article>

Why Damask

Slots, not children

Most engines hand a component one anonymous block of content. A Damask template declares as many named slots as it wants: <slot> marks the place, its body is the fallback, and slot="…" on a direct child fills it. Several children may name the same slot, and a <slot> placed where a fill goes forwards a slot of your own straight through.

Damask
<Frame title={self.heading.clone()}>
  <p>{self.body}</p>
  <span slot="footer">© {self.year}</span>
  <a slot="footer" href="/about">About</a>
</Frame>

Two files, no wiring

button.rs and button.dmk, side by side. Damask finds the template next to the struct, so there is no path attribute, no templates directory, no registry and no build script — and editing the template triggers a rebuild on its own. It needs Rust 1.88, which is when a macro first gained the ability to ask where it was written.

Rust
// button.rs — paired with button.dmk
#[derive(Component)]
pub struct Button {
    pub label: String,
    pub disabled: bool,
}

A missing prop is a compile error

Component tags are checked like the struct literals they become. Rename a field and every call site that still passes the old one stops the build, with the field named in the message. A prop typed Option<_> may be left out, and arrives as None.

Damask
<Button label="Save" disabled={self.locked}/>
<Button label="Cancel"/>   <!-- error: missing `disabled` -->

Attributes that know they are HTML

On an element, attr={expr} asks the value’s type how to appear. A bool renders a bare attribute or nothing at all, and an Option renders nothing when it is None — because in HTML it is the presence of disabled that disables a control, and disabled="false" disables it too.

Damask
<input title="row {self.n}"
       disabled={self.locked}
       placeholder={self.hint}/>

Await where the data is

A component whose data has not arrived yet writes .await in its markup, and the derive compiles that template to an async render instead — no attribute, nothing to configure, and no cost at all to the templates that don’t. The future is Send, so a request handler can await one, and a plain sync child still drops into an awaiting parent unchanged.

Damask
<ul class="feed">
  {#for deploy in self.store.recent(5).await}
    <li>{deploy.service}</li>
  {/for}
</ul>

One component, any renderer

Renderer owns the output buffer and the escaping policy, and components are compiled against &mut dyn Renderer — so one compiled component writes through the HTML renderer, a pretty-printing one, or one you wrote for a format Damask has never heard of. A child always writes through its parent’s renderer, which is what makes escaping a property of the output rather than of the component.

Rust
let mut r: Box<dyn Renderer> = Box::new(MyRenderer::new());
component.render_into(r.as_mut());
let out = r.finish();

The template is the markup

There is no separate expression language to learn. A { … } tag holds a Rust block, so it sees whatever is in scope — self, an impl method beside the struct, a use you wrote three lines up. Control flow is the Rust you already know, spelled {#if} and {#for} so that a template still reads as markup.

Slots are matched by name when the page renders, and that is the price of keeping them off the struct: a template may add or drop a <slot> without touching a single field, and in exchange a misspelled slot name renders nothing rather than failing the build. Fills are borrowed rather than owned, so content passed from a caller stays on the caller’s stack and can borrow the caller’s data without being boxed.