Skip to content

Instantly share code, notes, and snippets.

@logrusorgru
Last active November 18, 2023 10:50
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save logrusorgru/abd846adb521a6fb39c7405f32fec0cf to your computer and use it in GitHub Desktop.
Save logrusorgru/abd846adb521a6fb39c7405f32fec0cf to your computer and use it in GitHub Desktop.
Golang load HTML templates
//
// Copyright (c) 2018 Konstanin Ivanov <kostyarin.ivanov@gmail.com>.
// All rights reserved. This program is free software. It comes without
// any warranty, to the extent permitted by applicable law. You can
// redistribute it and/or modify it under the terms of the Do What
// The Fuck You Want To Public License, Version 2, as published by
// Sam Hocevar. See below for more details.
//
//
// DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
// Version 2, December 2004
//
// Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
//
// Everyone is permitted to copy and distribute verbatim or modified
// copies of this license document, and changing it is allowed as long
// as the name is changed.
//
// DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
// TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
//
// 0. You just DO WHAT THE FUCK YOU WANT TO.
//
// ************************************************************************** //
// //
// This gist shows a convenient way to load and use HTML templates in Golang //
// web-applications. The way includes: //
// //
// - recursive loading, unlike (html/template).ParseGlob does it //
// - short (Rails-like) template name without shared prefix (dir) //
// and without file extension //
// //
// For example, there is a tree of templates //
// views/ //
// static/ //
// home.html //
// about.html //
// privacypolicy.html //
// help.html //
// user/ //
// new.html //
// edit.html //
// show.html //
// form.html //
// layout/ //
// head.html //
// foot.html //
// //
// Thus, the home.html can include head.html and foor.html following way //
// //
// {{ template "layout/head" . }} //
// //
// <h1> Home page </h1> //
// <!-- other content of the home.html //
// //
// {{ template "layout/foot" . }} //
// //
// This is acceptable for user/new and user/edit which can include user/form //
// along with the layout/head and layout/foot. //
// //
// ************************************************************************** //
// A Tmpl implements keeper, loader and reloader for HTML templates
type Tmpl struct {
*template.Template // root template
}
// NewTmpl creates new Tmpl.
func NewTmpl() (tmpl *Tmpl) {
tmpl = new(Tmpl)
tmpl.Template = template.New("") // unnamed root template
return
}
// SetFuncs sets template functions to underlying templates
func (t *Tmpl) SetFuncs(funcMap template.FuncMap) {
t.Template = t.Template.Funcs(funcMap)
}
// Load templates. The dir argument is a directory to load templates from.
// The ext argument is extension of tempaltes.
func (t *Tmpl) Load(dir, ext string) (err error) {
// get absolute path
if dir, err = filepath.Abs(dir); err != nil {
return fmt.Errorf("getting absolute path: %w", err)
}
var root = t.Template
var walkFunc = func(path string, info os.FileInfo, err error) (_ error) {
// handle walking error if any
if err != nil {
return err
}
// skip all except regular files
// TODO (kostyarin): follow symlinks (?)
if !info.Mode().IsRegular() {
return
}
// filter by extension
if filepath.Ext(path) != ext {
return
}
// get relative path
var rel string
if rel, err = filepath.Rel(dir, path); err != nil {
return err
}
// name of a template is its relative path
// without extension
rel = strings.TrimSuffix(rel, ext)
rel = strings.Join(strings.Split(rel, string(os.PathSeparator)), "/")
// load or reload
var (
nt = root.New(rel)
b []byte
)
if b, err = ioutil.ReadFile(path); err != nil {
return err
}
_, err = nt.Parse(string(b))
return err
}
if err = filepath.Walk(dir, walkFunc); err != nil {
return
}
t.Template = root // set or replace (does it needed?)
return
}
// Render is equal to ExecuteTemplate.
//
// DEPRECATED: use Go native ExeuteTempalte instead.
func (t *Tmpl) Render(w io.Writer, name string, data interface{}) error {
return t.ExecuteTemplate(w, name, data)
}
@frederikhors
Copy link

Can I ask you how to use it?

@logrusorgru
Copy link
Author

Can I ask you how to use it?

For a directory structure, for example,

templates/
     one/
        one-a.html
        one-b.html
    two/
        two-a.html
        two-b.html

Load templates

var templs, err = NeTempl("./templates/", ".html", false)
if err != nil {
    // handle error
}

Use the templates

// the w is a io.Writer, can be a http.ResponseWriter for example

err = templs.Render(w, "one/one-a", map[string]interface{
    "key": "value, for example"
})
if err != nil {
     // rendering or writing error
}

Everything is described in the load.go header.

cc @frederikhors

@frederikhors
Copy link

frederikhors commented Dec 27, 2021

Thank you for your quick answer.

I'm using the precious code as you indicated but I have this error:

panic: NewTmpl: template: tests/templates/test.tmpl:1: function "title" not defined

I think the error is correct because if one of the templates contains the following code:

func {{title .EntityName}} ...

it doesn't know how to interpret "title" and I can't even use:

templs.Funcs(funcMap)

after the NewTempl() because there is already the call to Load in it that uses nt.Parse(string(b)).

Do you think a change is needed?

Should we also pass funcs in NewTempl()?

@logrusorgru
Copy link
Author

panic: NewTmpl: template: tests/templates/test.tmpl:1: function "title" not defined

Do you think a change is needed?

Should we also pass funcs in NewTempl()?

Hm. May be. Pass funcs in the NewTempl or move the Load out of the NewTempl and load outside, when functions are set. Also, I would change Funcs to SetFuncs for a better naming. And I would get rid out of the development stuff (the .devel and all related).

@logrusorgru
Copy link
Author

@frederikhors , I've updated the load.go. And I've not tested it.

var templs = NeTempl()

// may be not 100% correct here
templs.SetFuncs(template.FuncMap{
    "title": strings.Title,
})

var err = templs.Load("./templates/", ".html")
if err != nil {
    // handle error
}

Use the templates

// the w is a io.Writer, can be a http.ResponseWriter for example

err = templs.ExecuteTemplatte(w, "one/one-a", map[string]interface{
    "key": "value, for example"
})
if err != nil {
     // rendering or writing error
}

@frederikhors
Copy link

frederikhors commented Dec 27, 2021

You forgot t.dir on line https://gist.github.com/logrusorgru/abd846adb521a6fb39c7405f32fec0cf#file-load-go-L136.

Why did you remove the develop part? Do you find it no longer useful?

Plus I'm having a problem because I'm on Windows I think.

All the templates it finds have the key like: templates\customDir\subdir\file.

And even if I use path.Join() everywhere it won't find them unless I point to them using \ instead of / (example "one\\one-a" instead of "one/one-a").

This is very strange. Do you understand why?

@logrusorgru
Copy link
Author

logrusorgru commented Dec 27, 2021

This is very strange. Do you understand why?

It uses relative filesystem path as a template name. You can add this line (below), to convert Windows-like paths to UNIX-like for a template name. The line is

		rel = strings.TrimSuffix(rel, ext)
		rel = strings.Join(strings.Split(rel, os.PathSeparator), "/") // additional line

This way, all template names will be UNIX-like (e.g. /-separated). I've added this to the load.go.

Why did you remove the develop part? Do you find it no longer useful?

Yes, I think it useless.

@frederikhors
Copy link

The error with this new code now is:

image

@frederikhors
Copy link

Maybe we should use: string(os.PathSeparator).

@logrusorgru
Copy link
Author

Yep

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment