Odin version of Go html template package ported over using Claude code.
Its doing most of things i need at the momemnt.
- Context-aware auto-escaping -- automatically applies the correct escaping based on where a value appears (HTML body, attribute, JS, CSS, URL)
- Full template language -- if/else, range, with, variables, pipelines, define/template/block
- Safe content types --
Safe_HTML,Safe_URL,Safe_CSS,Safe_JS, etc. to bypass escaping for trusted content - Custom functions -- register your own template functions via
Func_Map - Layout composition --
block/definefor template inheritance (layouts, partials) - 19 built-in functions --
and,or,not,eq,ne,lt,le,gt,ge,print,printf,println,len,index,call,html,js,urlquery - Zero dependencies beyond Odin's core library
package main
import "core:fmt"
import "ohtml"
main :: proc() {
Page :: struct {
title: string,
name: string,
}
data := Page{title = "Hello", name = "World"}
result, err := ohtml.render("page", "<h1>{{.title}}</h1><p>{{.name}}</p>", data)
if err.kind != .None {
fmt.eprintln("error:", err.msg)
return
}
defer delete(result)
fmt.println(result)
// Output: <h1>Hello</h1><p>World</p>
}| Proc | Description |
|---|---|
render(name, text, data) -> (string, Error) |
Parse, escape, and execute in one call |
render_raw(name, text, data) -> (string, Error) |
Same but without auto-escaping (trusted templates only) |
template_new(name) -> ^Template |
Create a new template |
template_parse(t, text) -> (^Template, Error) |
Parse template source |
template_funcs(t, funcs) |
Register custom functions |
escape_template(t) -> Error |
Run the auto-escaping pass |
execute(t, writer, data) -> Error |
Execute template, write to io.Writer |
execute_to_string(t, data) -> (string, Error) |
Execute template, return string |
template_lookup(t, name) -> ^Template |
Look up a named sub-template |
template_destroy(t) |
Free all resources |
All custom functions use the uniform signature:
Template_Func :: #type proc(args: []any) -> (any, Error)
Func_Map :: map[string]Template_Funcupper_fn :: proc(args: []any) -> (ohtml.any, ohtml.Error) {
s := args[0].(string)
return strings.to_upper(s), {}
}
t := ohtml.template_new("page")
funcs := ohtml.Func_Map{"upper" = upper_fn}
ohtml.template_funcs(t, funcs)
ohtml.template_parse(t, `{{.name | upper}}`)Bypass auto-escaping for trusted content using distinct string types:
| Type | Use |
|---|---|
Safe_HTML |
Trusted HTML markup |
Safe_CSS |
Trusted CSS rules |
Safe_JS |
Trusted JavaScript code |
Safe_JS_Str |
Trusted JS string literal |
Safe_URL |
Trusted URL |
Safe_Srcset |
Trusted srcset attribute |
Safe_HTML_Attr |
Trusted HTML attribute |
Page :: struct {
bio: ohtml.Safe_HTML,
}
data := Page{bio = ohtml.Safe_HTML(`<em>bold</em>`)}
// {{.bio}} renders as <em>bold</em> without escaping{{.title}} -- field access
{{.user.name}} -- nested field
{{.}} -- current value (dot)
{{if .show}}visible{{end}}
{{if .admin}}Admin{{else}}User{{end}}
{{if eq .role "admin"}}...{{else if eq .role "editor"}}...{{else}}...{{end}}
{{range .items}}{{.}}{{end}}
{{range $i, $v := .items}}[{{$i}}] {{$v}}{{end}}
{{range .items}}...{{else}}No items.{{end}}
{{with .user}}Name: {{.name}}{{end}}
{{with .user}}...{{else}}No user.{{end}}
{{$name := "Odin"}}{{$name}}
{{.name | printf "Hello, %s!"}}
{{.items | len}}
{{/* Layout defines default blocks */}}
{{block "content" .}}Default content{{end}}
{{/* Page overrides them */}}
{{define "content"}}Page-specific content{{end}}
{{/* Call a named template */}}
{{template "header" .}}
{{if eq .x 1}}...{{end}}
{{if ne .x .y}}...{{end}}
{{if lt .age 18}}...{{end}}
{{if and .admin .active}}...{{end}}
{{if or .a .b}}...{{end}}
{{if not .disabled}}...{{end}}
{{print "a" "b"}} -- "a b"
{{printf "%s is %d" .name .age}} -- formatted
{{println "line"}} -- with newline
{{- .x}} -- trim whitespace before
{{.x -}} -- trim whitespace after
{{- .x -}} -- trim both sides
{{/* This produces no output */}}
The escaping pass analyzes each template action's HTML context and injects the appropriate escaper:
| Context | Example | Escaping |
|---|---|---|
| HTML body | <p>{{.x}}</p> |
&, <, >, ", ' encoded |
| HTML attribute | <div title="{{.x}}"> |
Attribute-safe encoding |
| JavaScript | <script>var x = "{{.x}}";</script> |
JS string escaping |
| CSS | <style>{{.x}}</style> |
CSS hex escaping |
| URL | <a href="{{.x}}"> |
Percent-encoding, javascript: blocked |
Dangerous protocols like javascript: are automatically neutralized.
Combine a layout with page-specific content by concatenating sources (layout first, then page):
layout_src := read_file("layout.html") // has {{block "content" .}}...{{end}}
page_src := read_file("page.html") // has {{define "content"}}...{{end}}
combined := strings.concatenate({layout_src, page_src})
t := ohtml.template_new("page")
ohtml.template_parse(t, combined)
ohtml.escape_template(t)
result, _ := ohtml.execute_to_string(t, data)The layout's block provides defaults; the page's define overrides them.
examples/ecomm/ -- a full web app using odin-http with:
- Product listing, detail, and cart pages
- Login and forgot-password flows
- Layout/page template composition
- A
/capabilitypage demonstrating every template feature
odin run examples/ecomm
# Visit http://localhost:8080odin test testsMIT