maud-extensions
Proc macros for Maud that make component-style view code, bundled browser runtime helpers, and typed shell components easier to author.
This crate has three main jobs:
- file-scoped component helpers with
js!,css!, andcomponent! - direct emitters and bundled runtimes such as
inline_js!(),surreal_scope_inline!(), andsurreal_scope_signals_inline!() - typed builder generation for layout and shell components with
#[derive(ComponentBuilder)]
Signals support stays JS-first: Maud markup provides anchors, while js!
owns signals, effects, and DOM binding. The companion crate
maud-extensions-runtime
still provides the older string-based slot transport, but for new shell and
layout components the preferred path is ComponentBuilder.
Install
Support policy:
- MSRV: Rust 1.85
- supported Maud version: 0.27
Choose A Workflow
Use component! when the component owns its own DOM, local CSS, and local JS:
use ;
use ;
js!
css!
css!
let page = html! ;
css! { ... } still defines the default css() helper that component!
injects automatically. If the same scope needs extra stylesheet helpers, use
css! { "card_border", { ... } } to generate a named function such as
card_border(). Use raw!(r#"..."#) inside css! or inline_css! as an
escape hatch for CSS fragments that are not valid Rust token syntax, such as
single-quoted selectors or font-family values.
For larger token or theme stylesheets, prefer wrapping the non-Rust CSS slice in
one raw! fragment instead of trying to escape it piecemeal:
use css;
Use #[derive(ComponentBuilder)] for new shell and layout components with
props and named content regions. This is the preferred path when the regions
can be expressed as typed fields:
use ;
use ComponentBuilder;
let view = new
.tone
.header
.body
.action
.action
.render;
Use the runtime slot API from maud-extensions-runtime only when you really
need open caller-owned child structure, or when you are keeping an existing
slot-based component. That API is the lower-level string-based transport layer;
see examples/slots.rs and the
maud-extensions-runtime docs
when you actually need it.
ComponentBuilder
ComponentBuilder is the preferred API for new typed shell and layout
components. It doesn't try to invent new Maud syntax. It generates a normal
Rust builder from the component struct you already want to render.
What it generates:
Type::new()andType::builder()- one setter per field
maybe_field(option)helpers forOption<T>fields using the exact field type#[builder(each = "...")]item setters forVec<T>fields.build()once all required fields are present.render()on the complete builder when the component implementsRenderFrom<CompleteBuilder> for Type
Field rules:
- plain fields are required
Option<T>fields are optionalOption<T>fields also get amaybe_field(Option<T>)helperVec<T>fields default to empty and can use#[builder(each = "...")]#[builder(default)]makes a non-Option, non-Vecfield useDefault#[slot]and#[slot(default)]record the component's content-region contract for this builder-core layer and for later syntax sugar
Markup ergonomics:
- regular setters for fields written as
Markup,maud::Markup, or::maud::Markupaccept anyimpl Render - that applies to single-markup fields, optional markup fields, and repeated
Vec<Markup>item setters maybe_field(...)helpers for optional markup fields takeOption<Markup>
Current limits:
- named structs only
- at most one
#[slot(default)]field .build()is still required when you need the concrete component value- there is no
compose!macro or block syntax yet - the builder offers a consuming
.render()convenience instead of implementingRender
Runtime Slots
The runtime slot API is still supported, but it is no longer the aspirational surface for new shell and layout components.
Why it exists:
- it works for fully open caller-owned child structure
- it keeps existing slot-based components working
- it provides one generic transport path over plain
Render
Why it is lower-level:
- slot names are stringly
- child transport goes through
.with_children(...) - the slot contract stays outside the type system
- missing or extra named slots are runtime behavior, not builder-shape errors
Use it when openness is the point. Otherwise prefer ComponentBuilder.
Signals
Signals support is intentionally JS-first. Render stable DOM anchors in Maud,
then create signals and bindings in js!.
use ;
use ;
js!
;
css!
let page = html! ;
Supported v1 binders:
bindText(source)bindAttr(name, source)bindClass(name, source)bindShow(source)
Rules:
- binders live on
window.mxand on Surreal-sugared handles such asme(".count") sourcecan be a Signals object or a function- function sources run inside
mx.effect(...) - binder cleanup is scoped to
component!roots surreal_scope_signals_inline!()is the supported runtime include when a page usescomponent!,js!, and Signals binders together
Runtime And Direct Emitters
Bundled runtime macros:
surreal_scope_inline!()- emits bundled
surreal.jsandcss-scope-inline.js
- emits bundled
signals_inline!()- emits bundled
@preact/signals-coreand the Maud Signals adapter
- emits bundled
surreal_scope_signals_inline!()- emits
surreal.js,css-scope-inline.js,@preact/signals-core, and the Maud Signals adapter in the right order
- emits
Direct emitters:
inline_js! { ... }/inline_css! { ... }- emit direct
<script>/<style>tags
- emit direct
js_file!(...)/css_file!(...)- inline file contents using
include_str!-style paths
- inline file contents using
font_face!(...)/font_faces!(...)- emit base64
@font-faceCSS without adding another dependency
- emit base64
Manual composition rule:
- if you emit
surreal_scope_inline!()andsignals_inline!()separately, putsurreal_scope_inline!()first so the Signals adapter can extend Surreal before componentjs!blocks run
Limits And Guarantees
component!performs compile-time shape checks over the token stream it sees; it only checks the token shape the macro can observecomponent!accepts exactly one top-level element with a body blockjs!andcss!must both be in scope forcomponent!, even if one is emptyinline_js!validates JavaScript with SWC before generating markupinline_css!runs a lightweight CSS syntax check before generating markup- token-style
css!/inline_css!only see Rust-tokenizable input; useraw!(r#"..."#)orcss_file!(...)for arbitrary CSS fragments - slot runtime helpers fail closed outside
.with_children(...) - malformed slot transport markers fail closed into default-slot content
- runtime slots are a lower-level transport layer; for new shell/layout
components prefer
ComponentBuilder ComponentBuilderobserves lexical type forms, not full type resolution, so type aliases toMarkup,Option, orVecare treated as ordinary fields
Read Next
- examples/component_card.rs
- examples/signals_counter.rs
- examples/runtime_injection.rs
- examples/slots.rs
- tests/component_builder.rs
- docs.rs for
maud-extensions - docs.rs for
maud-extensions-runtime
License
MIT OR Apache-2.0