gotmpl 0.2.0

A Rust reimplementation of Go's text/template library
Documentation
# gotmpl

[![Test](https://github.com/phsym/gotmpl-rs/actions/workflows/test.yaml/badge.svg)](https://github.com/phsym/gotmpl-rs/actions/workflows/test.yaml)
[![MSRV](https://img.shields.io/badge/MSRV-1.89-blue)](https://releases.rs/docs/1.89.0/)

A Rust port of Go's [`text/template`](https://pkg.go.dev/text/template).

Supports the full template syntax (pipelines, control flow, custom functions, template
composition, whitespace trimming) with Go compatible output. `no_std` compatible.

**No `unsafe`, no panics**: the crate `#![forbid(unsafe_code)]` and denies `panic!`,
`unwrap`, `expect`, `unreachable!`, `todo!`, and `unimplemented!` at the lint level.
User-provided functions that panic are caught under `std`; see `no_std` notes below.

## Quick start

```rust
use gotmpl::{Template, tmap};

let data = tmap! { "Name" => "World" };
let result = Template::new("hello")
    .parse("Hello, {{.Name}}!")
    .unwrap()
    .execute_to_string(&data)
    .unwrap();
assert_eq!(result, "Hello, World!");
```

## Template syntax

Actions are delimited by `{{` and `}}` (configurable via `.delims()`).
The syntax follows Go's `text/template` spec.

### Data access

```text
{{.}}              Current context (dot)
{{.Name}}          Field access on dot
{{.User.Email}}    Nested field access
{{$}}              Top-level data (root context)
{{$x}}             Variable access
{{$x.Name}}        Field access on variable
```

### Pipelines

```text
{{.Name | printf "%s!"}}
{{"hello" | len | printf "%d chars"}}
```

### Control flow

```text
{{if .Condition}}...{{end}}
{{if .Cond}}...{{else}}...{{end}}
{{if eq .X 1}}...{{else if eq .X 2}}...{{else}}...{{end}}

{{range .Items}}...{{end}}
{{range .Items}}...{{else}}empty{{end}}
{{range $i, $v := .Items}}{{$i}}: {{$v}}{{end}}
{{range .Items}}{{if eq . 3}}{{break}}{{end}}{{.}}{{end}}
{{range .Items}}{{if eq . 3}}{{continue}}{{end}}{{.}}{{end}}
{{range 5}}{{.}} {{end}}

{{with .Value}}...{{end}}
{{with .Value}}...{{else}}fallback{{end}}
```

### Variables

```text
{{$x := .Name}}            Declare variable
{{$x = "new value"}}       Assign to existing variable
{{$i, $v := range .List}}  Range with index and value (declaration)
{{$i, $v = range .List}}   Range with index and value (assignment)
```

### Template composition

```text
{{define "name"}}...{{end}}        Define a named template
{{template "name" .}}              Invoke a named template
{{block "name" .}}default{{end}}   Define and invoke (with default)
```

### Comments and whitespace trimming

```text
{{/* This is a comment */}}
{{- .X}}     Trim whitespace before
{{.X -}}     Trim whitespace after
{{- .X -}}   Trim both sides
```

## Built-in functions

| Function | Description |
|----------|-------------|
| `print` | Concatenate args (spaces between non-string adjacent args) |
| `printf` | Formatted output (`%s`, `%d`, `%f`, `%v`, `%q`, `%x`, `%o`, `%b`, `%e`, `%g`, `%t`, `%c`) |
| `println` | Print with spaces between args, trailing newline |
| `len` | Length of string, list, or map |
| `index` | Index into list or map: `index .List 0`, `index .Map "key"` |
| `slice` | Slice a list or string: `slice .List 1 3` |
| `call` | Call a function value: `call .Func arg1 arg2` |
| `eq`, `ne`, `lt`, `le`, `gt`, `ge` | Comparison operators. `eq` supports multi-arg: `eq .X 1 2 3` |
| `and`, `or` | Short-circuit logic, return the deciding value |
| `not` | Boolean negation |
| `html`, `js`, `urlquery` | Escape for HTML, JavaScript, URL query |

## Custom functions

Register functions before parsing:

```rust
use gotmpl::{Template, tmap};
use gotmpl::Value;

let result = Template::new("test")
    .func("upper", |args| {
        match args.first() {
            Some(Value::String(s)) => Ok(Value::String(s.to_uppercase().into())),
            _ => Ok(Value::Nil),
        }
    })
    .parse("{{.Name | upper}}")
    .unwrap()
    .execute_to_string(&tmap! { "Name" => "hello" })
    .unwrap();
assert_eq!(result, "HELLO");
```

## Function values and `call`

`Value::Function` allows passing callable values through templates:

```rust
extern crate alloc;
use alloc::sync::Arc;
use gotmpl::{Template, tmap};
use gotmpl::{Value, ValueFunc};

let adder: ValueFunc = Arc::new(|args| {
    let sum: i64 = args.iter().filter_map(|a| a.as_int()).sum();
    Ok(Value::Int(sum))
});

let result = Template::new("test")
    .func("getAdder", move |_| Ok(Value::Function(adder.clone())))
    .parse("{{call (getAdder) 3 4}}")
    .unwrap()
    .execute_to_string(&tmap!{})
    .unwrap();
assert_eq!(result, "7");
```

## Options

```rust
use gotmpl::{Template, MissingKey};

let tmpl = Template::new("t")
    .missing_key(MissingKey::Error)   // error on missing map keys
    .delims("<<", ">>")              // custom delimiters
    .parse("<< .Name >>")
    .unwrap();
```

`MissingKey` implements `FromStr`, so you can parse from strings (useful for
config files or CLI args):

```rust
use gotmpl::MissingKey;

let mk: MissingKey = "error".parse().unwrap();
```

| `MissingKey` variant | `FromStr` value | Behavior |
|----------------------|-----------------|----------|
| `Invalid` (default) | `"invalid"`, `"default"` | Return `<no value>` |
| `ZeroValue`          | `"zero"` | Return `<no value>` |
| `Error`              | `"error"` | Return an error |

## Number literals

Go-compatible number literal syntax:

```text
{{42}}          Decimal
{{3.14}}        Float
{{0xFF}}        Hexadecimal
{{0o77}}        Octal
{{0377}}        Octal (legacy leading zero)
{{0b1010}}      Binary
{{1_000_000}}   Underscore separators
{{'a'}}         Character literal (97)
{{0x1.ep+2}}    Hex float (7.5)
```

## Data model

Template data uses the `Value` enum:

| Variant | Rust type | Go equivalent |
|---------|-----------|---------------|
| `Nil` | n/a | `nil` |
| `Bool(bool)` | `bool` | `bool` |
| `Int(i64)` | `i64` | `int` |
| `Float(f64)` | `f64` | `float64` |
| `String(Arc<str>)` | `String` | `string` |
| `List(Arc<[Value]>)` | `Vec<Value>` | `[]any` |
| `Map(Arc<BTreeMap<Arc<str>, Value>>)` | `BTreeMap` | `map[string]any` |
| `Function(ValueFunc)` | `Arc<dyn Fn>` | `func(...)` |

The `tmap!` macro builds data maps:

```rust
use gotmpl::{tmap, ToValue};

let data = tmap! {
    "Name" => "Alice",
    "Age" => 30i64,
    "Scores" => vec![95i64, 87, 92],
    "Address" => tmap! {
        "City" => "Paris",
    },
};
```

## `no_std` support

The crate works in `no_std` environments (requires `alloc`). Disable the default `std`
feature:

```toml
[dependencies]
gotmpl = { version = "0.1", default-features = false }
```

Without `std`, `execute_fmt` and `execute_to_string` are available. The `io::Write`-based
`execute`/`execute_template` methods and `parse_files` require the `std` feature.
User-defined functions that panic will propagate instead of being caught.

## Differences from Go

Rust has no runtime reflection, so:

- **No struct field access**: use `Value::Map` instead
- **No method calls**: register functions via `.func()`
- **No pointer/interface indirection**: `Value` is always concrete
- **No complex numbers, channels, or `iter.Seq`**
- **NaN comparisons** return an error instead of Go's silently wrong results

## Go cross-check

The test suite can optionally run every template through Go's `text/template` and assert
output parity:

```sh
cargo test --features go-crosscheck
```

A Go helper (`tests/testdata/go_crosscheck.go`) is compiled once per test run. It
receives templates and typed data as JSON on stdin, executes via Go's `text/template`, and
prints the result.