# maud-extensions
[](https://crates.io/crates/maud-extensions)
[](https://docs.rs/maud-extensions)
[](https://github.com/eboody/maud-extensions)
Proc macros for Maud that make inline CSS/JS and component-style authoring simpler.
This crate includes bundled copies of
[gnat/surreal](https://github.com/gnat/surreal) and
[gnat/css-scope-inline](https://github.com/gnat/css-scope-inline). Check those
repos to see what these two tiny JS files can do and how to use them.
## Why use it?
- Define component-local `js()` and `css()` helpers with `js!` / `css!`.
- Wrap markup with `component!` and auto-inject JS/CSS helpers.
- Emit direct `<script>` / `<style>` blocks when needed.
- Bundle `surreal.js` and `css-scope-inline.js` with zero path setup.
- Embed fonts as base64 `@font-face` CSS.
## Table of Contents
- [Install](#install)
- [What's New in 0.3.x](#whats-new-in-03x)
- [Quick Start](#quick-start)
- [component!](#component)
- [Runtime Injection](#runtime-injection)
- [Macro Reference](#macro-reference)
- [Font Helpers](#font-helpers)
- [Migration Guide (0.2 -> 0.3)](#migration-guide-02---03)
- [License](#license)
## Install
```bash
cargo add maud-extensions
```
## What's New in 0.3.x
- New `component!` macro for auto-injecting JS/CSS helpers into one root element.
- Swapped JS/CSS macro naming so `js!`/`css!` define local helpers and
`inline_js!`/`inline_css!` emit direct tags.
- Bundled runtime helper `surreal_scope_inline!()` with no path setup.
- Explicit compile-time shape checks for `component!` input.
## Quick Start
This example shows the single-file component pattern: `js!` at the top,
`component!` markup in `Render`, and `css!` at the bottom.
```rust
use maud::{html, Markup, Render};
use maud_extensions::{component, css, js, surreal_scope_inline};
// Component behavior at file scope.
js! {
me().class_add("ready");
}
struct StatusCard<'a> {
message: &'a str,
}
impl<'a> Render for StatusCard<'a> {
fn render(&self) -> Markup {
component! {
article class="status-card" {
h2 { "System status" }
p class="message" { (self.message) }
}
}
}
}
// Component styles at file scope.
css! {
me {
border: 1px solid #ddd;
border-radius: 10px;
padding: 12px;
transition: border-color 160ms ease-in;
}
me.ready {
border-color: #16a34a;
}
me .message {
margin: 0;
opacity: 0.85;
}
}
struct Page;
impl Render for Page {
fn render(&self) -> Markup {
html! {
head {
// Inject bundled `surreal.js` + `css-scope-inline.js`.
(surreal_scope_inline!())
}
body {
(StatusCard { message: "All systems operational" })
}
}
}
}
```
## `component!`
`component!` wraps one top-level Maud element and appends the JS/CSS helpers
generated by `js!` and `css!` inside that root element automatically.
```rust
use maud::{Markup, Render};
use maud_extensions::{component, css, js};
js! {
me().class_add("ready");
}
struct Card;
impl Render for Card {
fn render(&self) -> Markup {
component! {
section class="card" {
p { "Hello" }
}
}
}
}
css! {
me { border: 1px solid #ddd; }
}
```
Equivalent output shape:
- root element content
- then `(js())`
- then `(css())`
Rules:
- input must be exactly one top-level element with a `{ ... }` body
- `js! { ... }` and `css! { ... }` must be present in scope (empty is valid: `js! {}` / `css! {}`)
- a clean pattern is one component per module/file with `js!` above and `css!` below the `Render` impl
- trailing `;` is allowed
- invalid root shapes fail at compile time with guidance
- if a helper is missing, the compiler error points at a required internal helper symbol;
add the corresponding `js! { ... }` or `css! { ... }` call
## Runtime Injection
Use bundled runtime scripts with no filesystem setup:
```rust
use maud_extensions::surreal_scope_inline;
maud::html! {
(surreal_scope_inline!())
}
```
Need custom files instead? Use `js_file!` / `css_file!` (`include_str!` behavior):
```rust
use maud_extensions::js_file;
maud::html! {
(js_file!(concat!(env!("CARGO_MANIFEST_DIR"), "/static/vendor/custom-runtime.js")))
}
```
## Macro Reference
- `js! { ... }` / `js!("...")`
- Generate local `fn js() -> maud::Markup` and the hidden helper used by `component!`.
- `css! { ... }` / `css!("...")`
- Generate local `fn css() -> maud::Markup` and the hidden helper used by `component!`.
- `component! { ... }`
- Wrap one root element and inject helpers emitted by `js!` / `css!` at the end of its body.
- `inline_js! { ... }` / `inline_js!("...")`
- Emit `<script>` markup directly.
- Validate JS via `swc_ecma_parser`.
- `inline_css! { ... }` / `inline_css!("...")`
- Emit `<style>` markup directly.
- Validate CSS via `cssparser`.
- `js_file!("path")` / `css_file!("path")`
- Emit `<script>` / `<style>` tags from file contents.
- `surreal_scope_inline!()`
- Emit bundled `surreal.js` and `css-scope-inline.js`.
- `font_face!(...)` / `font_faces!(...)`
- Embed font files as base64 `@font-face` CSS.
## Font Helpers
`font_face!` and `font_faces!` embed font files as base64 data URLs. Because
this macro expands at the call site, the consuming crate must include `base64`
if you use these macros.
```rust
use maud_extensions::font_face;
maud::html! {
(font_face!(
"../static/fonts/JetBrainsMono.woff2",
"JetBrains Mono"
))
}
```
## Migration Guide (0.2 -> 0.3)
### 1. Rename JS/CSS macro usage
The JS/CSS macro names were intentionally swapped:
- old `js!` -> new `inline_js!`
- old `css!` -> new `inline_css!`
- old `inline_js!` -> new `js!`
- old `inline_css!` -> new `css!`
### 2. Move to `component!` for root injection
Old pattern:
```rust
inline_js! { me().class_add("ready"); }
let view = maud::html! {
article {
"Hello"
(js())
(css())
}
};
inline_css! { me { color: red; } }
```
New pattern:
```rust
js! { me().class_add("ready"); }
let view = component! {
article {
"Hello"
}
};
css! { me { color: red; } }
```
### 3. Keep runtime scripts explicit in layout/page shell
```rust
maud::html! {
head {
(surreal_scope_inline!())
}
}
```
### 4. Update assumptions in your codebase
- `component!` requires exactly one top-level element with a body block.
- `component!` expects `js!` and `css!` calls in scope (empty blocks are allowed).
- defining `fn js()` / `fn css()` manually is not enough; use `js!` / `css!` so `component!` sees required helpers.
- `font_face!`/`font_faces!` still require `base64` in the consuming crate.
- `js_file!`/`css_file!` paths are resolved from the calling source file context.
## License
MIT OR Apache-2.0