forme 0.1.1

Compile-time HTML template engine — plain HTML templates with tpl-* directives generate type-safe Rust rendering functions
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
# forme

A compile-time HTML template engine for Rust. Write templates as plain `.html` files with `tpl-*` directives, and `forme` generates type-safe Rust rendering functions that write HTML via `std::fmt::Write`.

## Why forme?

Most template engines either invent a custom syntax (Jinja2-style `{% %}` blocks) or embed HTML inside Rust macros. Both break standard HTML tooling. `forme` takes a different approach: templates are **plain HTML** with data-binding expressed as attributes. Your HTML editor, linter, and formatter keep working. Designers can open templates in a browser and see the fallback content.

## Features

- **Plain HTML templates** -- no custom syntax to learn; templates are valid HTML with `tpl-*` attributes
- **Compile-time code generation** -- templates are compiled to Rust functions checked by `rustc`
- **Type-safe arguments** -- template parameters are Rust types (`&str`, `Vec<String>`, custom structs)
- **Zero-allocation rendering** -- generated code writes directly to any `std::fmt::Write` implementor
- **Template composition** -- include and nest templates with argument passing and slot-based content
- **Optional JS/TS transform pipeline** -- preprocess custom elements into standard HTML (e.g. Bootstrap/Stimulus components)
- **Config file support** -- define build targets in a `formefile.ts` with full access to environment variables

## Quick Start

### 1. Write a template

```html
<!-- templates/card.html -->
<tpl-arg name="title" type="&str"/>
<tpl-arg name="content" type="&str" default='""'/>

<div class="card">
    <h3 tpl-text="title">Card Title</h3>
    <p tpl-if="!content.is_empty()" tpl-text="content">Placeholder</p>
</div>
```

### 2. Generate Rust code

```bash
forme --source templates --output src/generated.rs
```

This produces a file with a rendering function for each template:

```rust,ignore
pub fn render_card(
    out: &mut impl std::fmt::Write,
    title: &str,
    content: &str,
) -> std::fmt::Result {
    write!(out, "<div class=\"card\">")?;
    write!(out, "<h3>")?;
    forme::html_escape(out, &(title))?;
    write!(out, "</h3>")?;
    if !content.is_empty() {
        write!(out, "<p>")?;
        forme::html_escape(out, &(content))?;
        write!(out, "</p>")?;
    }
    write!(out, "</div>")?;
    Ok(())
}
```

### 3. Use in your Rust code

```rust,ignore
mod generated;
use generated::render_card;

fn main() {
    let mut html = String::new();
    render_card(&mut html, "Hello", "World").unwrap();
    println!("{}", html);
}
```

Add `forme` as a dependency for the `html_escape` function:

```toml
[dependencies]
forme = "0.1"
```

## Installation

```bash
# Install the CLI binary from crates.io
cargo install forme

# Or from source
git clone https://github.com/DmitryBochkarev/forme
cd forme
cargo install --path .
```

After installation, the `forme` binary is available in your `$PATH`.

## Build Integration

You can compile templates automatically during `cargo build` using a build script. There are two approaches:

### Library API (recommended)

Add `forme` as a **build dependency** and call the library API directly -- no need to install the `forme` binary:

**`Cargo.toml`:**

```toml
[dependencies]
forme = "0.1"

[build-dependencies]
forme = "0.1"
```

**`build.rs`:**

```rust,ignore
fn main() {
    forme::Builder::new("templates", "src/generated.rs")
        .rerun_if_changed(true)
        .build()
        .expect("forme template compilation failed");
}
```

This approach is simpler -- no external binary required, and `cargo:rerun-if-changed` directives are emitted automatically for every template file.

### CLI in build.rs

Alternatively, install the `forme` binary and invoke it from `build.rs`:

**Project structure:**

```text
my-app/
├── build.rs
├── Cargo.toml
├── formefile.ts              # or pass --source/--output in build.rs
├── templates/
│   ├── card.html
│   └── page.html
└── src/
    ├── main.rs
    ├── types.rs
    └── generated.rs        # auto-generated by forme
```

**`build.rs`:**

```rust,ignore
use std::process::Command;

fn main() {
    // Re-run if any template changes
    println!("cargo:rerun-if-changed=templates");
    println!("cargo:rerun-if-changed=formefile.ts");

    let status = Command::new("forme")
        // Option A: use a config file
        .args(["--config", "formefile.ts"])
        // Option B: pass flags directly
        // .args(["--source", "templates", "--output", "src/generated.rs"])
        .status()
        .expect("failed to run forme -- is it installed? (cargo install --path <forme-repo>)");

    if !status.success() {
        panic!("forme template compilation failed");
    }
}
```

**`src/main.rs`:**

```rust,ignore
mod types;
mod generated;

use generated::render_card;

fn main() {
    let mut html = String::new();
    render_card(&mut html, "Title", "Content").unwrap();
    println!("{}", html);
}
```

With this setup, `cargo build` regenerates templates automatically whenever the source templates or config change.

## Usage

```bash
# Basic usage
forme --source <template-dir> --output <output-file.rs>

# With a config file
forme --config formefile.ts

# Auto-discover formefile.ts in current directory
forme

# With JS/TS transform script
forme --source templates --output src/generated.rs --transform-script components.ts

# With custom escape function
forme --source templates --output src/generated.rs --escape-func "my_crate::escape"

# Initialize a new config file
forme init
```

### CLI Options

| Flag | Short | Description |
|------|-------|-------------|
| `--source <DIR>` | `-s` | Source template directory |
| `--output <FILE>` | `-o` | Output Rust file path |
| `--transform-script <FILE>` | `-t` | Optional JS/TS transform script |
| `--escape-func <FUNC>` | `-e` | Custom escape function (default: `forme::html_escape`) |
| `--config <FILE>` | `-c` | Path to config file |

## Configuration

Instead of passing CLI flags, you can define build configuration in a `formefile.ts` (or `formefile.js`). Run `forme init` to scaffold one.

```typescript
export function config(ctx: FormeContext): FormeConfig {
  return {
    source: "templates",
    output: "src/generated.rs",
  };
}

interface FormeContext {
  cwd: string;
  env: Record<string, string>;
  cli: {
    source?: string;
    output?: string;
    transform_script?: string;
    escape_func?: string;
  };
}

interface FormeConfig {
  source: string;
  output: string;
  transform_script?: string;
  escape_func?: string;
}
```

The config function receives the current working directory, all environment variables, and any CLI overrides. It can return a single config or an array for multi-target builds.

**Using environment variables:**

```typescript
export function config(ctx: FormeContext): FormeConfig {
  const env = ctx.env.APP_ENV || "development";
  return {
    source: `templates/${env}`,
    output: "src/generated.rs",
    escape_func: env === "production" ? "my_crate::strict_escape" : undefined,
  };
}
```

## Template Directives

All directives use the `tpl-` prefix and contain Rust expressions.

### Template Arguments (`<tpl-arg>`)

Declare the parameters your template expects. These become function arguments in the generated Rust code.

```html
<tpl-arg name="title" type="&str"/>
<tpl-arg name="items" type="&[Item]"/>
<tpl-arg name="user" type="Option<&User>"/>
```

Use the `default` attribute for optional arguments:

```html
<tpl-arg name="content" type="&str" default='""'/>
```

When referencing custom types, use module paths relative to the generated file:

```html
<tpl-arg name="items" type="&[super::types::Item]"/>
```

### Conditional Rendering (`tpl-if`)

Show or hide an element based on a boolean Rust expression:

```html
<header tpl-if="show_header">
    <h1>Welcome</h1>
</header>

<div tpl-if="user.is_some()">
    User is logged in
</div>

<p tpl-if="items.is_empty()">No items found.</p>
```

The entire element and its children are omitted when the expression is `false`.

### Text Content (`tpl-text`)

Replace an element's children with HTML-escaped text from a Rust expression:

```html
<h1 tpl-text="title">Fallback Title</h1>

<span tpl-text='format!("Hello, {}!", name)'>Hello!</span>
```

The original text inside the element serves as fallback content visible when previewing the template in a browser. Use single quotes for the attribute when the expression contains double quotes.

### Raw HTML (`tpl-html`, `tpl-outer-html`)

Insert unescaped HTML content. Use with caution -- the content is not escaped.

`tpl-html` replaces the element's **children**:

```html
<div tpl-html="content_html">Fallback</div>
<!-- Renders: <div>{content_html}</div> -->
```

`tpl-outer-html` replaces the **entire element** (including the tag itself):

```html
<div tpl-outer-html="content_html">Fallback</div>
<!-- Renders: {content_html} (no wrapping <div>) -->
```

### Dynamic Attributes (`tpl-attr:*`)

Set attribute values dynamically using Rust expressions:

```html
<div tpl-attr:class='format!("status-{}", status)'
     tpl-attr:id="element_id">
    Content
</div>

<a tpl-attr:href='format!("/user/{}", user.id)'>Profile</a>
```

### Optional Attributes (`tpl-optional-attr:*`)

Conditionally include a boolean attribute. The attribute is rendered only when the expression is `true`:

```html
<button tpl-optional-attr:disabled="!is_enabled">Click me</button>

<input type="checkbox" tpl-optional-attr:checked="user.is_admin">
```

### Repeating Elements (`tpl-repeat`)

Loop over a collection. The syntax is `tpl-repeat="variable in iterator"`:

```html
<ul>
    <li tpl-repeat="item in items.iter()">
        <span tpl-text="item.name">Item</span>
    </li>
</ul>
```

With enumeration:

```html
<li tpl-repeat="(idx, item) in items.iter().enumerate()">
    <span tpl-text='format!("{}. {}", idx + 1, item.name)'>Item</span>
</li>
```

With filtering:

```html
<div tpl-repeat="item in items.iter().filter(|i| i.in_stock)">
    <p tpl-text="item.name">Product</p>
</div>
```

### Template Composition (`tpl-template`, `tpl-include`)

Include other templates as reusable components. Pass arguments with `tpl-arg:name` attributes.

**`tpl-template`** -- wrapping mode (replaces the element's children with the rendered template):

```html
<div tpl-template="card.html"
     tpl-arg:title='&"Card Title"'
     tpl-arg:content='&"Body text"'>
</div>
<!-- Renders: <div>{card.html output}</div> -->
```

**`tpl-include`** -- inline mode (replaces the entire element):

```html
<div tpl-include="card.html"
     tpl-arg:title='&"Card Title"'>
</div>
<!-- Renders: {card.html output} (no wrapping <div>) -->
```

Include the `.html` extension in template names. Use `&` to pass string literals or owned values as references.

### Slot Content (`tpl-slot:*`, `tpl-slot-items:*`)

Slots let you pass rendered HTML blocks to a template, rather than simple expressions.

**`tpl-slot:name`** renders children into a single `&str`:

```html
<div tpl-template="card.html" tpl-arg:title="&u.name">
    <template tpl-slot:content_html>
        <p><strong>Name:</strong> <span tpl-text="u.name">?</span></p>
        <p><strong>Email:</strong> <span tpl-text="u.email">?</span></p>
    </template>
</div>
```

**`tpl-slot-items:name`** renders each direct child as a separate `String`, collected into a `Vec<String>`:

```html
<template tpl-include="header.html">
    <template tpl-slot-items:top_items>
        <a href="/home">Home</a>
        <a href="/about">About</a>
    </template>
    <template tpl-slot-items:menu_items>
        <a href="/settings">Settings</a>
    </template>
</template>
```

Children inside slots can use any directive (`tpl-repeat`, `tpl-if`, `tpl-text`, etc.).

### Combining Directives

Multiple directives can be used on the same element:

```html
<button tpl-if="is_visible"
        tpl-optional-attr:disabled="!is_enabled"
        tpl-attr:class='format!("btn-{}", style)'
        tpl-text="label">
    Click
</button>
```

Directives are processed in a fixed order: `tpl-repeat` > `tpl-if` > `tpl-text` / `tpl-html` / `tpl-outer-html` > `tpl-attr:*` > `tpl-optional-attr:*` > `tpl-template` / `tpl-include`.

### Directive Reference

| Directive | Syntax | Purpose |
|-----------|--------|---------|
| `<tpl-arg>` | `<tpl-arg name="x" type="T"/>` | Declare a template argument |
| `<tpl-arg>` (default) | `<tpl-arg name="x" type="T" default='val'/>` | Argument with default value |
| `tpl-if` | `tpl-if="bool_expr"` | Conditional rendering |
| `tpl-repeat` | `tpl-repeat="var in iterator"` | Loop over a collection |
| `tpl-text` | `tpl-text="expr"` | Text content (HTML-escaped) |
| `tpl-html` | `tpl-html="expr"` | Raw HTML (replaces children) |
| `tpl-outer-html` | `tpl-outer-html="expr"` | Raw HTML (replaces entire element) |
| `tpl-attr:name` | `tpl-attr:class="expr"` | Dynamic attribute value |
| `tpl-optional-attr:name` | `tpl-optional-attr:disabled="bool"` | Conditional boolean attribute |
| `tpl-template` | `tpl-template="file.html"` | Include template (wrapping) |
| `tpl-include` | `tpl-include="file.html"` | Include template (inline) |
| `tpl-arg:name` | `tpl-arg:title="expr"` | Pass argument to included template |
| `tpl-slot:name` | `tpl-slot:content` | Pass slot as `&str` |
| `tpl-slot-items:name` | `tpl-slot-items:children` | Pass slot items as `Vec<String>` |

For more examples of each directive, see [`examples/README.md`](examples/README.md).

## Generated Code

Understanding what `forme` produces helps you reason about performance and debug issues.

**Template:**

```html
<tpl-arg name="items" type="&[Item]"/>

<ul tpl-if="!items.is_empty()">
    <li tpl-repeat="item in items.iter()" tpl-text="item.name">placeholder</li>
</ul>
```

**Generated Rust:**

```rust,ignore
pub fn render_list(
    out: &mut impl std::fmt::Write,
    items: &[Item],
) -> std::fmt::Result {
    if !items.is_empty() {
        write!(out, "<ul>")?;
        for item in items.iter() {
            write!(out, "<li>")?;
            forme::html_escape(out, &(item.name))?;
            write!(out, "</li>")?;
        }
        write!(out, "</ul>")?;
    }
    Ok(())
}
```

Key observations:
- Each `tpl-arg` becomes a function parameter with the declared Rust type
- `tpl-if` becomes a Rust `if` block
- `tpl-repeat` becomes a `for` loop
- `tpl-text` calls `forme::html_escape()` (or your custom escape function)
- `tpl-html` / `tpl-outer-html` write content directly without escaping
- The generated file starts with `#![cfg_attr(rustfmt, rustfmt::skip)]` so `cargo fmt` skips it
- Template file names map to function names: `card.html` becomes `render_card`

## JS/TS Transform Pipeline

The optional `--transform-script` flag enables a preprocessing step that transforms HTML elements before template compilation. This is useful for mapping component shorthand to framework-specific HTML with the right CSS classes and data attributes.

**How it works:** For each HTML element, `forme` calls the `processor.elementHeader()` function exported by your script. The function receives the element's tag name, attributes, and metadata, and returns a (potentially modified) element.

**Interface:**

```typescript
export interface Processor {
  elementHeader: (element: JsElement) => JsElement;
}

interface JsElement {
  name: string;           // tag name
  attributes: JsAttr[];   // HTML attributes
  is_void: boolean;       // void element flag
}
```

**Minimal example:**

```typescript
// my-transforms.ts
export const processor: Processor = {
  elementHeader: function(element: JsElement): JsElement {
    // Convert <card> to <div class="card">
    if (element.name === "card") {
      element.name = "div";
      element.attributes.push({
        name: "class",
        conditions: { ty: "Empty", expr: "", list: [] },
        values: [{ ty: "Text", conditions: { ty: "Empty", expr: "", list: [] }, content: "card" }],
      });
    }
    return element;
  },
};
```

```bash
forme --source templates --output src/generated.rs --transform-script my-transforms.ts
```

The included `examples/components.ts` provides a full reference implementation with transforms for Bootstrap and Stimulus.js components (buttons, forms, cards, modals, etc.).

## Examples

The [`examples/`](examples/) directory contains a full showcase exercising most template features. See [`examples/README.md`](examples/README.md) for detailed documentation.

```bash
# Generate the example templates
cargo run -- --config examples/formefile.ts

# Run the showcase
cargo run --example showcase_example
```

The showcase demonstrates `tpl-if`, `tpl-repeat`, `tpl-text`, `tpl-attr`, `tpl-include`, `tpl-template` with slots, and nested template composition.

## Troubleshooting

### "Found 0 template file(s)"

Ensure template files have `.html` or `.htm` extensions and the `--source` path is correct.

### Type errors in generated code

- Verify `<tpl-arg>` types match your Rust types exactly
- Check module paths are relative to the generated file (e.g. `super::types::Item`, not `types::Item`)
- All variables in template expressions must come from `<tpl-arg>` declarations

### Attributes not appearing in output

- Use `tpl-attr:name="expr"` syntax (not `tpl-attr-name`)
- For boolean attributes, use `tpl-optional-attr:name="bool_expr"`
- Inspect the generated `.rs` file to see what code was produced

### Quote / escaping issues

- Use **single quotes** for the HTML attribute when the Rust expression contains double quotes: `tpl-text='format!("hi {}", name)'`
- Avoid inline `<style>` tags with CSS -- curly braces `{}` conflict with Rust `format!` strings. Use external stylesheets instead.

### Generated code not updating

If using `build.rs`, ensure `cargo:rerun-if-changed` includes both the template directory and the config file.

## License

MIT