tmpl

package module
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 31, 2025 License: MIT Imports: 10 Imported by: 5

README ¶

tmpl

tmpl is a developer-friendly wrapper around Go's html/template package, designed to simplify common tasks, enhance type safety, and make complex template setups more maintainable and readable. If you've ever felt frustration dealing with loosely-coupled templates and Go code, tmpl was built specifically for you.

This project attempts to improve the overall template workflow and offers a few helpful utilities for developers building html based applications:

  • Two-way type safety when referencing templates in Go code and vice-versa
  • Nested templates and template fragments
  • Template extensibility through compiler plugins
  • Static analysis utilities such as template parse tree traversal

Roadmap & Idea List

  • Parsing and static analysis of the html in a template
  • Automatic generation of GoLand {{ gotype: }} annotations when using the tmpl CLI
  • Documentation on how to use tmpl.Analyze for parse tree traversal and static analysis of templates

🧰 Installation

go get github.com/tylermmorton/tmpl

🌊 The Workflow

The tmpl workflow starts with a standard html/template. For more information on the syntax, see this useful syntax primer from HashiCorp.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ .Title }} | torque</title>
</head>
<body>
    <form action="/login" method="post">
        <label for="username">Username</label>
        <input type="text" name="username" id="username" value="{{ .Username }}">

        <label for="password">Password</label>
        <input type="password" name="password" id="password" value="{{ .Password }}">

        <button type="submit">Login</button>
    </form>
</body>
Dot Context

To start tying your template to your Go code, declare a struct that represents the "dot context" of the template. The dot context is the value of the "dot" ({{ . }}) in Go's templating language.

In this struct, any exported fields (or methods attached via pointer receiver) will be accessible in your template from the all powerful dot.

type LoginPage struct {
    Title    string // {{ .Title }}
    Username string // {{ .Username }}
    Password string // {{ .Password }}
}
TemplateProvider

To turn your dot context struct into a target for the tmpl compiler, your struct type must implement the TemplateProvider interface:

type TemplateProvider interface {
    TemplateText() string
}

The most straightforward approach is to embed the template into your Go program using the embed package from the standard library.

import (
    _ "embed"
)

var (
    //go:embed login.tmpl.html
    tmplLoginPage string
)

type LoginPage struct { 
    ... 
}

func (*LoginPage) TemplateText() string {
    return tmplLoginPage
}
Compilation

After implementing TemplateProvider you're ready to compile your template and use it in your application.

Currently, it is recommended to compile your template once at program startup using the function tmpl.MustCompile:

var (
    LoginTemplate = tmpl.MustCompile(&LoginPage{})
)

If any of your template's syntax were to be invalid, the compiler will panic on application startup with a detailed error message.

If you prefer to avoid panics and handle the error yourself, use the tmpl.Compile function variant.

The compiler returns a managed tmpl.Template instance. These templates are safe to use from multiple Go routines.

Rendering

After compilation, you may execute your template by calling one of the generic render functions.

type Template[T TemplateProvider] interface {
	Render(w io.Writer, data T, opts ...RenderOption) error
	RenderToChan(ch chan string, data T, opts ...RenderOption) error
	RenderToString(data T, opts ...RenderOption) (string, error)
}
var (
    LoginTemplate = tmpl.MustCompile(&LoginPage{})
)

func main() {
    buf := bytes.Buffer{}
    err := LoginTemplate.Render(&buf, &LoginPage{
        Title:    "Login",
        Username: "",
        Password: "",
    })
    if err != nil {
        panic(err)
    }
	
    fmt.Println(buf.String())
}
Template Functions

tmpl supports multiple ways of providing functions to your templates.

Dot Context Methods

You can define methods on your dot context struct to be used as template functions. These methods must be attached to your struct via pointer receiver. This strategy is useful if your template function depends on a lot of internal state.

type LoginPage struct {
    FirstName string
    LastName  string
}

func (p *LoginPage) FullName() string {
    return fmt.Sprintf("%s %s", p.FirstName, p.LastName)
}
{{ .FullName }}
FuncMapProvider

You can also define template functions on the dot context struct by implementing the FuncMapProvider interface. This is useful for reusing utility functions across multiple templates and packages.

package tmpl

type FuncMapProvider interface {
    TemplateFuncMap() FuncMap
}

Example using the sprig library:

import (
    "github.com/Masterminds/sprig/v3"
)

type LoginPage struct {
    ...
}

func (*LoginPage) TemplateFuncMap() tmpl.FuncMap {
    return sprig.FuncMap()
}

Usage:

{{ "hello!" | upper | repeat 5 }}
Template Nesting

One major advantage of using structs to bind templates is that nesting templates is as easy as nesting structs.

The tmpl compiler knows to recursively look for fields in your dot context struct that also implement the TemplateProvider interface. This includes fields that are embedded, slices or pointers.

A good use case for nesting templates is to abstract the document <head> of the page into a separate template that can now be shared and reused by other pages:

<head>
    <meta charset="UTF-8">
    <title>{{ .Title }} | torque</title>
    
    {{ range .Scripts -}}
        <script src="{{ . }}"></script>
    {{ end -}}
</head>
type Head struct {
    Title   string
    Scripts []string
}

Now, update the LoginPage struct to embed the new Head template.

The name of the template is defined using the tmpl struct tag. If the tag is not present the field name is used instead.

type LoginPage struct {
    Head `tmpl:"head"`
	
    Username string
    Password string
}

Embedded templates can be referenced using the built in {{ template }} directive. Use the name assigned in the struct tag and ensure to pass the dot context value.

<!DOCTYPE html>
<html lang="en">
{{ template "head" .Head }}
<body>
...
</body>
</html>

Finally, update references to LoginPage to include the nested template's dot as well.

var (
    LoginTemplate = tmpl.MustCompile(&LoginPage{})
)

func main() {
    buf := bytes.Buffer{}
    err := LoginTemplate.Render(&buf, &LoginPage{
        Head: &Head{
            Title:   "Login",
            Scripts: []string{ "https://unpkg.com/htmx.org@1.9.2" },
        },
        Username: "",
        Password: "",
    })
    if err != nil {
        panic(err)
    }
	
    fmt.Println(buf.String())
}
Targeting

Sometimes you may want to render a nested template. To do this, use the RenderOption WithTarget in any of the render functions:

func main() {
    buf := bytes.Buffer{}
    err := LoginTemplate.Render(&buf, &LoginPage{
        Title:    "Login",
        Username: "",
        Password: "",
    }, tmpl.WithTarget("head"))
    if err != nil {
        panic(err)
    }
}

Advanced Usage

Template Analysis

The tmpl package provides a static analysis tool for Go templates. This tool can be used to traverse the parse tree of a template and perform custom analysis. The analysis framework is what enables the tmpl compiler to perform static analysis on your templates and provide type safety.

Analyzer

An Analyzer is a function that returns an AnalyzerFunc, which is a visitor-style function that allows you to traverse the parse tree of a template. Analyzers can be provided to tmpl.Compile using the UseAnalyzers option.

In the following example, we search templates for instances of {{ outlet }} and dynamically inject a function. This is how the torque framework uses the tmpl compiler to provide handler wrapping functionality. Example

You may want to do something similar if you want to add new 'built in' directives and functions to your templates.

package main

var outletAnalyzer tmpl.Analyzer  = func(h *tmpl.AnalysisHelper) tmpl.AnalyzerFunc {
	return tmpl.AnalyzerFunc(func(val reflect.Value, node parse.Node) {
		switch node := node.(type) {
		case *parse.IdentifierNode:
			if node.Ident == "outlet" {
				h.AddFunc("outlet", func() string { return "{{ . }}" })
			}
		}
	})
}

var LoginPage = tmpl.MustCompile(&LoginPage{}, tmpl.UseAnalyzers(outletAnalyzer))
AnalysisHelper

The AnalysisHelper allows you to modify the template during analysis. It provides methods to add functions, variables, and other nodes to the template. This is useful for modifying the template during analysis without having to modify the original template.

Analyze

The Analyze function can be used independently of the Compile function and allows you to analyze templates without compiling them. This is useful for static analysis and debugging purposes.

package main

import (
    "fmt"
    "html/template"
	
    "github.com/tylermmorton/tmpl"
)

func main() {
    tmpl.Analyze(&LoginPage{}, tmpl.ParseOptions{}, []tmpl.Analyzer{ ... })
}

Documentation ¶

Index ¶

Constants ¶

This section is empty.

Variables ¶

This section is empty.

Functions ¶

func Traverse ¶

func Traverse(cur parse.Node, visitors ...Visitor)

Traverse is a depth-first traversal utility for all nodes in a text/template/parse.Tree

Types ¶

type AnalysisHelper ¶

type AnalysisHelper struct {
	// contains filtered or unexported fields
}

AnalysisHelper is a struct that contains all the data collected during an analysis of a TemplateProvider.

An Analysis runs in two passes. The first pass collects important contextual information about the template definition tree that can be accessed in the second pass. The second pass is the actual analysis of the template definition tree where errors and warnings are added.

func Analyze ¶

func Analyze(tp TemplateProvider, opts ParseOptions, analyzers []Analyzer) (*AnalysisHelper, error)

Analyze uses reflection on the given TemplateProvider while also parsing the templateProvider text to perform an analysis. The analysis is performed by the given analyzers. The analysis is returned as an AnalysisHelper struct.

func (*AnalysisHelper) AddError ¶

func (h *AnalysisHelper) AddError(node parse.Node, err string)

func (*AnalysisHelper) AddFunc ¶

func (h *AnalysisHelper) AddFunc(name string, fn interface{})

func (*AnalysisHelper) AddWarning ¶

func (h *AnalysisHelper) AddWarning(node parse.Node, err string)

func (*AnalysisHelper) Context ¶

func (h *AnalysisHelper) Context() context.Context

func (*AnalysisHelper) FuncMap ¶

func (h *AnalysisHelper) FuncMap() FuncMap

func (*AnalysisHelper) GetDefinedField ¶

func (h *AnalysisHelper) GetDefinedField(name string) *FieldNode

func (*AnalysisHelper) IsDefinedTemplate ¶

func (h *AnalysisHelper) IsDefinedTemplate(name string) bool

IsDefinedTemplate returns true if the given template name is defined in the analysis target via {{define}}, or defined by any of its embedded templates.

func (*AnalysisHelper) WithContext ¶

func (h *AnalysisHelper) WithContext(ctx context.Context)

type Analyzer ¶

type Analyzer func(res *AnalysisHelper) AnalyzerFunc

Analyzer is a type that parses templateProvider text and performs an analysis

type AnalyzerFunc ¶

type AnalyzerFunc func(val reflect.Value, node parse.Node)

type CompilerOption ¶

type CompilerOption func(opts *CompilerOptions)

CompilerOption is a function that can be used to modify the CompilerOptions

func UseAnalyzers ¶

func UseAnalyzers(analyzers ...Analyzer) CompilerOption

func UseFuncs ¶

func UseFuncs(funcs FuncMap) CompilerOption

func UseParseOptions ¶

func UseParseOptions(parseOpts ParseOptions) CompilerOption

UseParseOptions sets the ParseOptions for the template CompilerOptions. These options are used internally with the html/template package.

type CompilerOptions ¶

type CompilerOptions struct {
	// contains filtered or unexported fields
}

CompilerOptions holds options that control the template compiler

type FieldNode ¶

type FieldNode struct {
	Value       reflect.Value
	StructField reflect.StructField

	Parent   *FieldNode
	Children []*FieldNode
}

func (*FieldNode) FindPath ¶

func (node *FieldNode) FindPath(path []string) *FieldNode

func (*FieldNode) GetKind ¶

func (node *FieldNode) GetKind() reflect.Kind

func (*FieldNode) IsKind ¶

func (node *FieldNode) IsKind(kind reflect.Kind) (reflect.Kind, bool)

type FuncMap ¶

type FuncMap = template.FuncMap

type FuncMapProvider ¶

type FuncMapProvider interface {
	TemplateFuncMap() FuncMap
}

FuncMapProvider is a struct type that returns its corresponding template functions. To be used in conjunction with the TemplateProvider interface.

type ParseOptions ¶

type ParseOptions struct {
	Funcs      FuncMap
	LeftDelim  string
	RightDelim string
}

ParseOptions controls the behavior of the templateProvider parser used by Analyze.

type RenderOption ¶

type RenderOption func(p *RenderProcess)

func WithFuncs ¶

func WithFuncs(funcs template.FuncMap) RenderOption

WithFuncs appends the given Template.FuncMap to the Template's internal func map. These functions become available in the Template during execution

func WithName ¶

func WithName(name string) RenderOption

WithName copies the Template's default parse.Tree and adds it back to the Template under the given name, effectively aliasing the Template.

func WithTarget ¶

func WithTarget(target ...string) RenderOption

WithTarget sets the render Target to the given Template name.

type RenderProcess ¶

type RenderProcess struct {
	Targets  []string
	Template *template.Template
}

type Template ¶

type Template[T TemplateProvider] interface {
	// Render can be used to execute the internal template.
	Render(w io.Writer, data T, opts ...RenderOption) error
	// RenderToChan can be used to execute the internal template and write the result to a channel.
	RenderToChan(ch chan string, data T, opts ...RenderOption) error
	// RenderToString can be used to execute the internal template and return the result as a string.
	RenderToString(data T, opts ...RenderOption) (string, error)
}

func Compile ¶

func Compile[T TemplateProvider](tp T, opts ...CompilerOption) (Template[T], error)

Compile takes the given TemplateProvider, parses the templateProvider text and then recursively compiles all nested templates into one managed Template instance.

func MustCompile ¶

func MustCompile[T TemplateProvider](p T, opts ...CompilerOption) Template[T]

MustCompile is a helper function that wraps Compile and panics if the template fails to compile.

type TemplateProvider ¶

type TemplateProvider interface {
	TemplateText() string
}

TemplateProvider is a struct type that returns its corresponding template text.

type Visitor ¶

type Visitor = func(parse.Node)

Visitor is a function that visits nodes in a parse.Tree traversal

Directories ¶

Path Synopsis
cmd
tmpl command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL