templar

package module
v0.0.17 Latest Latest
Warning

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

Go to latest
Published: Apr 17, 2025 License: MIT Imports: 11 Imported by: 1

README

Templar: Go Template Loader

Go Reference

Templar is a powerful extension to Go's standard templating libraries that adds dependency management, simplifies template composition, and solves common pain points in template organization.

Why Templar?

Templar is designed to integrate smoothly with Go's standard templating libraries while solving common issues:

  1. Minimal Learning Curve: If you know Go templates, you already know 99% of Templar.
  2. Zero New Runtime Syntax: The include directives are processed before rendering (variable based inclusion in the works).
  3. Flexible and Extensible: Create custom loaders for any template source (file loader for now, more in the works).
  4. Production Ready: Handles complex dependencies, prevents cycles, and provides clear error messages (and aiming to get better at this).

Background

Go's built-in templating libraries (text/template and html/template) are powerful but have limitations when working with complex template structures:

  1. No Native Dependency Management: When templates reference other templates, you must manually ensure they're loaded in the correct order.

  2. Global Template Namespace: All template definitions share a global namespace, making it challenging to use different versions of the same template in different contexts.

  3. Brittle Template Resolution: In large applications, templates often load differently in development vs. production environments.

  4. Verbose Template Loading: Loading templates with their dependencies typically requires repetitive boilerplate code:

// Standard approach - verbose and error-prone
func renderIndexPage(w http.ResponseWriter, r *http.Request) {
  t := template.ParseFiles("Base1.tmpl", "a2.tmpl", "IndexPage.tmpl")
  t.Execute(w, data)
}

func renderProductListPage(w http.ResponseWriter, r *http.Request) {
  t := template.ParseFiles("AnotherBase.tmpl", "a2.tmpl", "ProductListPage.tmpl")
  t.Execute(w, data)
}

Proposal

Templar solves these problems by providing:

  1. Dependency Declaration: Templates can declare their own dependencies using a simple include syntax:
{{# include "base.tmpl" #}}
{{# include "components/header.tmpl" #}}

<div class="content">
  {{ template "content" . }}
</div>
  1. Automatic Template Loading: Templar automatically loads and processes all dependencies:
// With Templar - clean and maintainable
func renderIndexPage(w http.ResponseWriter, r *http.Request) {
  tmpl := loadTemplate("IndexPage.tmpl")  // Dependencies automatically handled
  tmpl.Execute(w, data)
}
  1. Flexible Template Resolution: Multiple loaders can be configured to find templates in different locations.

  2. Template Reuse: The same template name can be reused in different contexts without conflict.

Getting Started

Basic Example
package main

import (
    "os"
    "github.com/panyam/templar"
)

func main() {
  // Create a template group
  group := templar.NewTemplateGroup()
  
  // Create a filesystem loader that searches multiple directories
  group.Loader = templar.NewFileSystemLoader(
      "templates/",
      "templates/shared/",
  )
  
  // Load a root template (dependencies handled automatically)
  rootTemplate := group.MustLoad("pages/homepage.tmpl", "")

  // Prepare data for the template
  data := map[string]any{
    "Title": "Home Page",
    "User": User{
      ID:   1,
      Name: "John Doe",
    },
    "Updates": []Update{
      {Title: "New Feature Released", Date: "2023-06-15"},
      {Title: "System Maintenance", Date: "2023-06-10"},
      {Title: "Welcome to our New Site", Date: "2023-06-01"},
    },
    "Featured": FeaturedContent{
      Title:       "Summer Sale",
      Description: "Get 20% off on all products until July 31st!",
      URL:         "/summer-sale",
    },
  }

  // Render the template to stdout (for this example)
  if err = group.RenderHtmlTemplate(os.Stdout, rootTemplate[0], "", data, nil); err != nil {
    fmt.Printf("Error rendering template: %v\n", err)
  }
}

Key Features

1. Template Dependencies

In your templates, use the {{# include "path/to/template" #}} directive to include dependencies:

{{# include "layouts/base.tmpl" #}}
{{# include "components/navbar.tmpl" #}}

{{ define "content" }}
  <h1>Welcome to our site</h1>
  <p>This is the homepage content.</p>
{{ end }}
2. Multiple Template Loaders

Templar allows you to configure multiple template loaders with fallback behavior:

// Create a list of loaders to search in order
loaderList := &templar.LoaderList{}

// Add loaders in priority order
loaderList.AddLoader(templar.NewFileSystemLoader("app/templates/"))
loaderList.AddLoader(templar.NewFileSystemLoader("shared/templates/"))

// Set a default loader as final fallback
loaderList.DefaultLoader = templar.NewFileSystemLoader("default/templates/")
3. Template Groups

Template groups manage collections of templates and their dependencies:

group := templar.NewTemplateGroup()
group.Loader = loaderList
group.AddFuncs(map[string]any{
    "formatDate": func(t time.Time) string {
        return t.Format("2006-01-02")
    },
})

Advanced Usage

Conditional Template Loading

You can implement conditional template loading based on application state:

folder := "desktop"
if isMobile {
  folder = "mobile"
}
tmpl, err := loader.Load(fmt.Sprintf("%s/homepage.tmpl", folder))
Dynamic Templates

Generate templates dynamically and use them immediately:

dynamicTemplate := &templar.Template{
    Name:      "dynamic-template",
    RawSource: []byte(`Hello, {{.Name}}!`),
}

group.RenderTextTemplate(w, dynamicTemplate, "", map[string]any{"Name": "World"}, nil)

Comparison with Other Solutions

Feature Standard Go Templates Templar
Dependency Management
Self-describing Templates (*)
Standard Go Template Syntax
Supports Cycles Prevention (**)
HTML Escaping
Template Grouping (***) ⚠️ Partial

*: Self-describing here refers to a template specifying all the dependencies it needs so a template author can be clear about what is required and include them instead of hoping they exist somehow. **: Cycles are caught by the preprocessor and is clearer. ***: Grouping in standard templates is done in code by the template user instead of the author.

Other alternatives
  • Pongo2 is amazing for its reverence for Django syntax.
  • Templ is amazing as a typed template library and being able to perform compile time validations of templates.

My primary goal here was to have as much alignment with Go's template stdlib. Beyond this library for managing dependencies, the goal itself was to have strict adherence to Go's templating syntax. Using the same Go template syntax also allows extra features during preprocessing of templates. (eg using same set of variables for both pre-processing as well as for final rendering).

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var TemplateNotFound = errors.New("template not found")

TemplateNotFound is returned when a template could not be found by a loader.

Functions

This section is empty.

Types

type FileSystemLoader

type FileSystemLoader struct {
	// Folders is a list of directories to search for templates.
	Folders []string

	// Extensions is a list of file extensions to consider as templates.
	Extensions []string
}

FileSystemLoader loads templates from the file system based on a set of directories and file extensions.

func NewFileSystemLoader

func NewFileSystemLoader(folders ...string) *FileSystemLoader

NewFileSystemLoader creates a new file system loader that will search in the provided folders for template files. By default, it recognizes files with .tmpl, .tmplus, and .html extensions.

func (*FileSystemLoader) Load

func (g *FileSystemLoader) Load(name string, cwd string) (template []*Template, err error)

Load attempts to find and load a template with the given name. If the name includes an extension, only files with that extension are considered. Otherwise, files with any of the loader's recognized extensions are searched. If cwd is provided, it's used for resolving relative paths. Returns the loaded templates or TemplateNotFound if no matching templates were found.

type LoaderList

type LoaderList struct {
	// DefaultLoader is used as a fallback if no other loaders succeed.
	DefaultLoader TemplateLoader
	// contains filtered or unexported fields
}

LoaderList is a composite loader that tries multiple loaders in sequence and returns the first successful match.

func (*LoaderList) AddLoader

func (t *LoaderList) AddLoader(loader TemplateLoader) *LoaderList

AddLoader adds a new loader to the list of loaders to try. Returns the updated LoaderList for method chaining.

func (*LoaderList) Load

func (t *LoaderList) Load(name string, cwd string) (matched []*Template, err error)

Load attempts to load a template with the given name by trying each loader in sequence. It returns the first successful match, or falls back to the DefaultLoader if all others fail. If cwd is provided, it's used for resolving relative paths. Returns TemplateNotFound if no loader can find the template.

type Template

type Template struct {
	// Name is an identifier for this template.
	Name string

	// RawSource contains the original, unprocessed template content.
	RawSource []byte

	// ParsedSource contains the template content after preprocessing.
	ParsedSource string

	// Path is the file path for this template if it was loaded from a file.
	Path string

	// Status indicates whether the template has been loaded and parsed.
	Status int

	// AsHtml determines whether the content should be treated as HTML (with escaping)
	// or as plain text.
	AsHtml bool

	// Error contains any error encountered during template processing.
	Error error

	// Metadata stores extracted information from the template (e.g., FrontMatter).
	Metadata map[string]any
	// contains filtered or unexported fields
}

Template is the basic unit of rendering that manages content and dependencies.

func (*Template) AddDependency

func (t *Template) AddDependency(another *Template) bool

AddDependency adds another template as a dependency of this template. It returns false if the dependency would create a cycle, true otherwise.

func (*Template) Dependencies

func (t *Template) Dependencies() []*Template

Dependencies returns all templates that this template directly depends on.

func (*Template) WalkTemplate

func (root *Template) WalkTemplate(loader TemplateLoader, handler func(template *Template) error) (err error)

WalkTemplate processes a template and its dependencies recursively. It starts from the root template, processes all includes with the {{# include "..." #}} directive, and calls the provided handler function on each template in the dependency tree. The loader is used to resolve and load included templates.

type TemplateGroup

type TemplateGroup struct {

	// Funcs contains template functions available to all templates in this group.
	Funcs map[string]any

	// Loader is used to resolve and load template dependencies.
	Loader TemplateLoader
	// contains filtered or unexported fields
}

TemplateGroup manages a collection of templates and their dependencies, providing methods to process and render them.

func NewTemplateGroup

func NewTemplateGroup() *TemplateGroup

NewTemplateGroup creates a new empty template group with initialized internals.

func (*TemplateGroup) AddFuncs

func (t *TemplateGroup) AddFuncs(funcs map[string]any) *TemplateGroup

AddFuncs adds template functions to this group, making them available to all templates. Returns the template group for method chaining.

func (*TemplateGroup) MustLoad added in v0.0.12

func (t *TemplateGroup) MustLoad(pattern string, cwd string) []*Template

Calls the underlying Loader to load templates matching a pattern and optional using a cwd for relative paths. Panics if an error is encountered. Returns matching templates or an error if no templates were found.

func (*TemplateGroup) NewHtmlTemplate

func (t *TemplateGroup) NewHtmlTemplate(name string, funcs map[string]any) (out *htmpl.Template)

NewHtmlTemplate creates a new HTML template with the given name. The template will have access to the group's functions and any additional functions provided.

func (*TemplateGroup) NewTextTemplate

func (t *TemplateGroup) NewTextTemplate(name string, funcs map[string]any) (out *ttmpl.Template)

NewTextTemplate creates a new TEXT template with the given name. The template will have access to the group's functions and any additional functions provided.

func (*TemplateGroup) PreProcessHtmlTemplate

func (t *TemplateGroup) PreProcessHtmlTemplate(root *Template, funcs htmpl.FuncMap) (out *htmpl.Template, err error)

PreProcessHtmlTemplate processes a HTML template and its dependencies, creating an html/template that can be used for rendering. It handles template dependencies recursively. Returns the processed template and any error encountered.

func (*TemplateGroup) PreProcessTextTemplate

func (t *TemplateGroup) PreProcessTextTemplate(root *Template, funcs ttmpl.FuncMap) (out *ttmpl.Template, err error)

PreProcessTextTemplate processes a template and its dependencies, creating a text/template that can be used for rendering. It handles template dependencies recursively. Returns the processed template and any error encountered.

func (*TemplateGroup) RenderHtmlTemplate

func (t *TemplateGroup) RenderHtmlTemplate(w io.Writer, root *Template, entry string, data any, funcs map[string]any) (err error)

RenderHtmlTemplate renders a template as HTML to the provided writer. It processes the template with its dependencies, executes it with the given data, and applies any additional template functions provided. If entry is specified, it executes that specific template within the processed template.

func (*TemplateGroup) RenderTextTemplate

func (t *TemplateGroup) RenderTextTemplate(w io.Writer, root *Template, entry string, data any, funcs map[string]any) (err error)

RenderTextTemplate renders a template as plain text to the provided writer. It processes the template with its dependencies, executes it with the given data, and applies any additional template functions provided. If entry is specified, it executes that specific template within the processed template.

type TemplateLoader

type TemplateLoader interface {
	// Load attempts to load templates matching the given pattern.
	// If cwd is not empty, it's used as the base directory for relative paths.
	// Returns matching templates or an error if no templates were found.
	Load(pattern string, cwd string) (template []*Template, err error)
}

TemplateLoader defines an interface for loading template content by name or pattern.

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

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