maud-extensions 0.4.0

Component, inline CSS/JS, and font helper macros for Maud views.
Documentation
# maud-extensions

[![crates.io](https://img.shields.io/crates/v/maud-extensions.svg)](https://crates.io/crates/maud-extensions)
[![docs.rs](https://img.shields.io/docsrs/maud-extensions)](https://docs.rs/maud-extensions)
[![license](https://img.shields.io/crates/l/maud-extensions.svg)](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.4.x]#whats-new-in-04x
- [Quick Start]#quick-start
- [component!]#component
- [Slots]#slots
- [Runtime Injection]#runtime-injection
- [Macro Reference]#macro-reference
- [Runtime Slot API]#runtime-slot-api
- [Font Helpers]#font-helpers
- [Migration Guide (0.3 -> 0.4)]#migration-guide-03---04
- [Migration Guide (0.2 -> 0.3)]#migration-guide-02---03
- [License]#license

## Install

```bash
cargo add maud-extensions
cargo add maud-extensions-runtime # needed for slots + `.in_slot("name")`
```

## What's New in 0.4.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.
- Slot flow simplified to runtime APIs: `slot()`, `named_slot("...")`, and
  `.with_children(...)` + `.in_slot("...")`.

## 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

## Slots

Use runtime slot functions inside your component template, then pass children
through `.with_children(...)`. Unannotated children go to the default slot, and
named content is tagged with `.in_slot("name")`.

```rust
use maud::{Markup, Render, html};
use maud_extensions_runtime::{InSlotExt, WithChildrenExt, named_slot, slot};

struct Card;

impl Render for Card {
    fn render(&self) -> Markup {
        html! {
            article class="card" {
                header class="card-header" { (named_slot("header")) }
                section class="card-body" { (slot()) }
            }
        }
    }
}

struct CardHeader<'a> {
    title: &'a str,
}

impl<'a> Render for CardHeader<'a> {
    fn render(&self) -> Markup {
        html! { h2 { (self.title) } }
    }
}

let view = html! {
    (Card.with_children(html! {
        (CardHeader { title: "Status" }.in_slot("header"))
        p { "All systems operational" }
    }))
};
```

Rules:
- `slot()` renders the default slot.
- `named_slot("name")` renders a named slot.
- `.in_slot("name")` assigns a child component to that named slot.
- `.with_children(html! { ... })` provides child content for slot resolution.
- missing named slots render empty content.
- extra provided named slots are ignored.

## 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.

## Runtime Slot API

From `maud-extensions-runtime`:

- `slot()`
  - Render default slot content for the current slotted component context.
- `named_slot("name")`
  - Render named slot content.
- `WithChildrenExt::with_children(html! { ... })`
  - Attach child content to a component value before rendering.
- `InSlotExt::in_slot("name")`
  - Mark child content for a named slot.

## 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.3 -> 0.4)

### 1. Replace slot macros with runtime functions

Old:

```rust
use maud_extensions::slot;

html! {
    (slot!())
    (slot!("header"))
}
```

New:

```rust
use maud_extensions_runtime::{named_slot, slot};

html! {
    (slot())
    (named_slot("header"))
}
```

### 2. Replace `use_component!` with `.with_children(...)`

Old:

```rust
use maud_extensions::use_component;
use maud_extensions_runtime::InSlotExt;

html! {
    (use_component!(
        Card,
        {
            (Title.in_slot("header"))
            p { "Body" }
        }
    ))
}
```

New:

```rust
use maud_extensions_runtime::{InSlotExt, WithChildrenExt};

html! {
    (Card.with_children(html! {
        (Title.in_slot("header"))
        p { "Body" }
    }))
}
```

## 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