Skip to content
Snippets Groups Projects
template.go 2.6 KiB
Newer Older
package template

import (
	"bytes"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"text/template"

	"git.autistici.org/ai3/go-common/mail/mdtext"
godog's avatar
godog committed
	bf "github.com/russross/blackfriday/v2"
)

var (
	// TemplateDirectory points at the directory containing templates.
	TemplateDirectory = "/etc/ai/templates/mail"

	// DefaultLanguage is the fallback language.
	DefaultLanguage = "en"

	// Global, lazily-initialized, shared template registry.
	templates      *template.Template
	templateLoadMx sync.Mutex

	// Line width of email plain text bodies.
	emailLineWidth = 75
)

func init() {
	if d := os.Getenv("MAIL_TEMPLATE_DIR"); d != "" {
		TemplateDirectory = d
	}
}

func loadTemplates() (err error) {
	templateLoadMx.Lock()
	defer templateLoadMx.Unlock()
	if templates != nil {
		return
	}
	templates, err = template.ParseGlob(filepath.Join(TemplateDirectory, "*.??.md"))
	return
}

// SetTemplateDirectory can be used to (re)set the TemplateDirectory
// once the program has started, so it's mostly useful for tests.
func SetTemplateDirectory(d string) {
	templateLoadMx.Lock()
	templates = nil
	TemplateDirectory = d
	templateLoadMx.Unlock()
}

func findTemplate(name, lang string) *template.Template {
	if lang == "" {
		lang = DefaultLanguage
	}
	tpl := templates.Lookup(fmt.Sprintf("%s.%s.md", name, lang))
	if tpl == nil && lang != DefaultLanguage {
		return findTemplate(name, DefaultLanguage)
	}
	return tpl
}

// Template represents a templated message body.
type Template struct {
	body []byte
}

// New loads a template with the specified name and language,
// and renders it with the given values.
//
// Templates are Markdown files loaded from the TemplateDirectory
// (which can be overridden at runtime by setting the environment
// variable MAIL_TEMPLATE_DIR), and must follow the <name>.<lang>.md
// naming pattern. Such templates can then be rendered to plain text
// or HTML.
//
// If a template with the desired language does not exist, we fall
// back to using DefaultLanguage.
func New(name, lang string, values map[string]interface{}) (*Template, error) {
	if err := loadTemplates(); err != nil {
		return nil, err
	}
	tpl := findTemplate(name, lang)
	if tpl == nil {
		return nil, errors.New("template not found")
	}

	var buf bytes.Buffer
	if err := tpl.Execute(&buf, values); err != nil {
		return nil, err
	}

	return &Template{
		body: buf.Bytes(),
	}, nil
}

// Text renders the template body to plain text.
func (t *Template) Text() []byte {
	return bf.Run(t.body, bf.WithRenderer(mdtext.NewTextRenderer(emailLineWidth)))
}

// HTML renders the template body to HTML.
func (t *Template) HTML() []byte {
	return bf.Run(t.body)
}