Nunchucks Documentation

Multi-language templating with a Go core, CLI, and WASM bridge.

GitHub Pages serves this docs/ folder.

Packages: npm (@samuelbines/nunchucks) | Go package (pkg.go.dev) | Go source

VS Code extension: BishopCoTech.nunchucks-syntax (search: nunchucks syntax)

npm install @samuelbines/nunchucks
npm link @samuelbines/nunchucks

Nunchucks template

{% extends "layout.njk" %}
{% block body %}
  <h2>{{ user.name | title }}</h2>
  {% if user.active %}
    <p>Status: active</p>
  {% endif %}
{% endblock %}

Pick A Language

Use the selector to view how to render templates from each language/runtime.

env := nunchucks.Configure(nunchucks.ConfigOptions{Path: "views"})
out, err := env.Render("index.njk", map[string]any{
  "user": map[string]any{"name": "sam"},
})
if err != nil {
  panic(err)
}
fmt.Println(out)

Go Details

# CLI
go run ./cmd/nunchucks help
go run ./cmd/nunchucks render -views ./views -template index.njk -data '{"title":"Hello"}'
go run ./cmd/nunchucks precompile -views ./views -out ./public -data '{"title":"Hello"}'

# WASM build
cd go
GOOS=js GOARCH=wasm go build -o wasm/nunchucks.wasm ./cmd/nunchucks-wasm
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" wasm/wasm_exec.js

Examples: go run ./examples/basic and go run ./examples/advanced.

Go and TS are native engines. JS/Python/Dart can use the Go CLI/WASM bridge today.

Quick Start

Package: github.com/SamuelDBines/nunjucks/go

env := nunchucks.Configure(nunchucks.ConfigOptions{Path: "views"})
out, err := env.Render("index.njk", map[string]any{
    "title": "Nunchucks",
    "items": []any{"alpha", "beta"},
})
if err != nil {
    panic(err)
}
fmt.Println(out)

CLI

go run ./cmd/nunchucks help
go run ./cmd/nunchucks render -views ./views -template index.njk -data '{"title":"Hello"}'
go run ./cmd/nunchucks precompile -views ./views -out ./public -data '{"title":"Hello"}'

Examples

cd go
go run ./examples/basic
go run ./examples/advanced

Detailed Example A: Template Composition

# views/layout.njk
<h1>{% block title %}Title{% endblock %}</h1>
<main>{% block body %}{% endblock %}</main>

# views/macros.njk
{% macro badge(text) %}<span class="badge">{{ text | upper }}</span>{% endmacro %}

# views/home.njk
{% extends "layout.njk" %}
{% import "macros.njk" as ui %}
{% block title %}Users{% endblock %}
{% block body %}
  {% for u in users %}
    {{ ui.badge(u.name) }}
  {% endfor %}
{% endblock %}
env := nunchucks.Configure(nunchucks.ConfigOptions{Path: "views"})
out, err := env.Render("home.njk", map[string]any{
  "users": []any{
    map[string]any{"name": "sam"},
    map[string]any{"name": "kai"},
  },
})
if err != nil { panic(err) }

Detailed Example B: Filter + Function Parity

{% set active = users | selectattr("active") %}
Active={{ active | length }}

{% for grp in users | groupby("role", "none") %}
{{ grp.grouper }}={{ grp.list | length }}
{% endfor %}

BestScoreKey={{ scores | dictsort("value", false, true) | first | first }}
Intervals={{ range(0, 10, 3) | join(",") }}
out, err := env.Render("report.njk", map[string]any{
  "users": []any{
    map[string]any{"name":"A","active":true,"role":"Dev"},
    map[string]any{"name":"B","active":false,"role":"ops"},
    map[string]any{"name":"C","active":true,"role":"dev"},
  },
  "scores": map[string]any{"x": 4, "y": 7, "z": 2},
})

Implemented

  • Configure
  • Render(name, ctx)
  • RenderString(src, ctx)
  • include, extends, blocks, super()
  • set, if, for, {{ expr }}
  • macro, import ... as, call
  • raw, verbatim, filter ... endfilter

Expression Support

  • Literals: string, number, bool, nil
  • Dot-path lookup from context or vars
  • Filters: lower, upper

WASM Target

The Go core can be compiled to WebAssembly and consumed from JS runtimes.

cd go
GOOS=js GOARCH=wasm go build -o wasm/nunchucks.wasm ./cmd/nunchucks-wasm
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" wasm/wasm_exec.js

Then load with go/wasm/loader.js and call:

Tags

Current Go runtime support:

{% import "macros.njk" as ui %}
{% set name = "sam" %}
{% if name %}{{ ui.badge(name) }}{% endif %}

Filters

Current Go filters include:

{{ "NUNCHUCKS" | lower }}
{{ "nunchucks" | upper }}
{% filter upper %}hello {{ user.name }}{% endfilter %}

Note: behavior is close to Nunjucks/Jinja for most filters, with a few edge cases still being tightened.

Deep Example 1: Macros + Layout + Call Blocks

Use this when you want reusable UI components and nested content.

# base.njk
<!doctype html>
<html>
  <body>
    <header>{% block header %}Default Header{% endblock %}</header>
    <main>{% block content %}{% endblock %}</main>
  </body>
</html>

# macros.njk
{% macro panel(kind="info") %}
<section class="panel panel-{{ kind }}">
  {{ caller() }}
</section>
{% endmacro %}

# page.njk
{% extends "base.njk" %}
{% import "macros.njk" as ui %}
{% block header %}Account Overview{% endblock %}
{% block content %}
  {% call ui.panel("success") %}
    Hello {{ user.name | title }}
  {% endcall %}
{% endblock %}
env := nunchucks.Configure(nunchucks.ConfigOptions{Path: "views"})
out, err := env.Render("page.njk", map[string]any{
  "user": map[string]any{"name": "samuel"},
})
if err != nil { panic(err) }
fmt.Println(out)

Deep Example 2: Data Shaping (groupby + dictsort + selectattr + range)

Use this when rendering dashboards, reports, and config summaries.

{# users grouped by role (case-insensitive by default) #}
{% for grp in users | groupby("role", "none") %}
Role={{ grp.grouper }} count={{ grp.list | length }}
{% endfor %}

{# active users only #}
Active={{ users | selectattr("active") | length }}

{# reverse sort map by value #}
TopKey={{ scores | dictsort("value", false, true) | first | first }}

{# deterministic ranges from builtin function #}
Steps={{ range(1, 8, 2) | join(",") }}
out, err := env.Render("report.njk", map[string]any{
  "users": []any{
    map[string]any{"name":"Zed","role":"Dev","active":true},
    map[string]any{"name":"Amy","role":"ops","active":false},
    map[string]any{"name":"Bob","role":"dev","active":true},
  },
  "scores": map[string]any{"alpha": 92, "beta": 87, "gamma": 95},
})