diff --git a/README.md b/README.md index 8e9caee..24b593a 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,13 @@ We solve this problem by adding a simple [template comment](https://golang.org/p This comment is removed by `html/template` in the final output, but tells `got` to load this child template inside `mobilelayout.html`. +## Subfolders + +Simple sites can easily fit everything under the `pages`, `includes`, and `layouts` namespaces. However, for better organization of your HTML snippets you can also use subfolders. For example, you might have custom sidebar modules you wish to isolate to their own files. Including items in subfolders (and beyond) is as easy as regular files. Imagine you had the following file: + + /templates/includes/sidebar/active_users.html + +You can access this in your templates using the string `includes/sidebar/active_users`. ## Benchmarks @@ -82,11 +89,6 @@ This library adds almost no overhead to `html/template` for rendering templates. This library is as fast as `html/template` because the organizational sorting and inheritance calculations are performed on the initial load. -## Roadmap - -- Template Functions -- Allow registering functions to provide global template variables: (nonces, session data, etc...) - ## Template Functions (Recommended) diff --git a/files.go b/files.go new file mode 100644 index 0000000..ae773c8 --- /dev/null +++ b/files.go @@ -0,0 +1,50 @@ +package got + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +// locate templates in possibly nested subfolders +func findTemplatesRecursively(path string, extension string) (paths []string, err error) { + err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + if err == nil { + if strings.Contains(path, extension) { + paths = append(paths, path) + } + } + return err + }) + return +} + +// Handles reading templates files in the given directory + ending path +func loadTemplateFiles(dir, path, extension string) (templates map[string][]byte, err error) { + var files []string + files, err = findTemplatesRecursively(filepath.Join(dir, path), extension) + if err != nil { + return + } + + templates = make(map[string][]byte) + + for _, path = range files { + var b []byte + b, err = ioutil.ReadFile(path) + if err != nil { + return + } + + // Convert "templates/layouts/base.html" to "layouts/base" + // For subfolders the extra folder name is included: + // "templates/includes/sidebar/ad1.html" to "includes/sidebar/ad1" + name := strings.TrimPrefix(filepath.Clean(path), filepath.Clean(dir)+"/") + name = strings.TrimSuffix(name, filepath.Ext(name)) + + templates[name] = b + } + + return +} diff --git a/functions.go b/functions.go new file mode 100644 index 0000000..d968d3b --- /dev/null +++ b/functions.go @@ -0,0 +1,90 @@ +package got + +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "encoding/base32" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "html/template" + "strings" + "time" +) + +// Functions I've found to be required in most every web-site template engine +// Many borrowed from https://github.com/Masterminds/sprig + +// DefaultFunctions for templates +var DefaultFunctions = template.FuncMap{ + "title": strings.Title, + "upper": strings.ToUpper, + "lower": strings.ToLower, + "trim": strings.TrimSpace, + // Display singluar or plural based on count + "plural": func(one, many string, count int) string { + if count == 1 { + return one + } + return many + }, + // Current Date (Local server time) + "date": func() string { + return time.Now().Format("2006-01-02") + }, + // Current Unix timestamp + "unixtimestamp": func() string { + return fmt.Sprintf("%d", time.Now().Unix()) + }, + // json encodes an item into a JSON string + "json": func(v interface{}) string { + output, _ := json.Marshal(v) + return string(output) + }, + // Allow unsafe injection into HTML + "noescape": func(a ...interface{}) template.HTML { + return template.HTML(fmt.Sprint(a...)) + }, + // Allow unsafe URL injections into HTML + "noescapeurl": func(u string) template.URL { + return template.URL(u) + }, + // Modern Hash + "sha256": func(input string) string { + hash := sha256.Sum256([]byte(input)) + return hex.EncodeToString(hash[:]) + }, + // Legacy + "sha1": func(input string) string { + hash := sha1.Sum([]byte(input)) + return hex.EncodeToString(hash[:]) + }, + // Gravatar + "md5": func(input string) string { + hash := md5.Sum([]byte(input)) + return hex.EncodeToString(hash[:]) + }, + // Popular encodings + "base64encode": func(v string) string { + return base64.StdEncoding.EncodeToString([]byte(v)) + }, + "base64decode": func(v string) string { + data, err := base64.StdEncoding.DecodeString(v) + if err != nil { + return err.Error() + } + return string(data) + }, + "base32encode": func(v string) string { + return base32.StdEncoding.EncodeToString([]byte(v)) + }, + "base32decode": func(v string) string { + data, err := base32.StdEncoding.DecodeString(v) + if err != nil { + return err.Error() + } + return string(data) + }, +} diff --git a/templates.go b/templates.go index a919ee6..d7c276d 100644 --- a/templates.go +++ b/templates.go @@ -4,11 +4,9 @@ import ( "bytes" "fmt" "html/template" - "io/ioutil" "net/http" "path/filepath" "regexp" - "strings" ) // Children define the base template using comments: { /* use basetemplate */ } @@ -47,19 +45,6 @@ func (t *NotFoundError) Error() string { // // Template for displaying errors // var ErrorTemplate = template.Must(template.New("error").Parse(errorTemplateHTML)) -// FindTemplates in path recursively -// func FindTemplates(path string, extension string) (paths []string, err error) { -// err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error { -// if err == nil { -// if strings.Contains(path, extension) { -// paths = append(paths, path) -// } -// } -// return err -// }) -// return -// } - // Templates Collection type Templates struct { Extension string @@ -68,18 +53,6 @@ type Templates struct { Functions template.FuncMap } -// DefaultFunctions for templates -var DefaultFunctions = template.FuncMap{ - // Allow unsafe injection into HTML - "noescape": func(a ...interface{}) template.HTML { - return template.HTML(fmt.Sprint(a...)) - }, - "title": strings.Title, - "upper": strings.ToUpper, - "lower": strings.ToLower, - "trim": strings.TrimSpace, -} - // New Templates collection func New(templatesDir, extension string) (*Templates, error) { t := &Templates{ @@ -89,56 +62,41 @@ func New(templatesDir, extension string) (*Templates, error) { Functions: DefaultFunctions, } - return t, t.Load() + return t, t.load() } -func LoadTemplateFiles(dir, path string) (templates map[string][]byte, err error) { - var files []string - files, err = filepath.Glob(filepath.Join(dir, path)) - if err != nil { - return - } - - templates = make(map[string][]byte) - - for _, path = range files { - // fmt.Printf("Loading: %s\n", path) - var b []byte - b, err = ioutil.ReadFile(path) - if err != nil { - return - } - - // Convert "templates/layouts/base.html" to "layouts/base" - name := strings.TrimPrefix(filepath.Clean(path), filepath.Clean(dir)+"/") - name = strings.TrimSuffix(name, filepath.Ext(name)) - - // fmt.Printf("%q = %q\n", name, b) - templates[name] = b +// Funcs function map for templates +func (t *Templates) Funcs(functions template.FuncMap) *Templates { + for _, tmpl := range t.Templates { + tmpl.Funcs(functions) } - return + return t } -func (t *Templates) Load() (err error) { +// Handles loading required templates +func (t *Templates) load() (err error) { // Child pages to render var pages map[string][]byte - pages, err = LoadTemplateFiles(t.Dir, "pages/*"+t.Extension) + // pages, err = loadTemplateFiles(t.Dir, "pages/*"+t.Extension) + pages, err = loadTemplateFiles(t.Dir, "pages/", t.Extension) if err != nil { return } // Shared templates across multiple pages (sidebars, scripts, footers, etc...) var includes map[string][]byte - includes, err = LoadTemplateFiles(t.Dir, "includes/*"+t.Extension) + // includes, err = loadTemplateFiles(t.Dir, "includes/*"+t.Extension) + includes, err = loadTemplateFiles(t.Dir, "includes", t.Extension) if err != nil { return } // Layouts used by pages var layouts map[string][]byte - layouts, err = LoadTemplateFiles(t.Dir, "layouts/*"+t.Extension) + // layouts, err = loadTemplateFiles(t.Dir, "layouts/*"+t.Extension) + layouts, err = loadTemplateFiles(t.Dir, "layouts", t.Extension) if err != nil { return } @@ -149,7 +107,7 @@ func (t *Templates) Load() (err error) { matches := parentRegex.FindSubmatch(b) basename := filepath.Base(name) - tmpl, err = template.New(basename).Parse(string(b)) + tmpl, err = template.New(basename).Funcs(t.Functions).Parse(string(b)) // Uses a layout if len(matches) == 2 { @@ -165,7 +123,6 @@ func (t *Templates) Load() (err error) { if len(includes) > 0 { for name, src := range includes { - // fmt.Printf("\tAdding:%s = %s\n", name, string(src)) _, err = tmpl.New(name).Parse(string(src)) if err != nil { return diff --git a/templates_test.go b/templates_test.go index 495e0a9..17770fd 100644 --- a/templates_test.go +++ b/templates_test.go @@ -24,7 +24,8 @@ type templateFile struct { var testingTemplateFiles = []templateFile{ // We have two pages each using a different parent layout - {"pages/home.html", `{{define "content"}}home {{.Name}}{{end}} {{/* use one */}}`}, + // One of the pages is using a subfolder + {"pages/home/home.html", `{{define "content"}}home {{.Name}}{{end}} {{/* use one */}}`}, {"pages/about.html", `{{define "content"}}about {{.Name}}{{end}}{{/* use two */}}`}, // We have two different layouts (using two different styles) {"layouts/one.html", `Layout 1: {{.Name}} {{block "content" .}}{{end}} {{block "includes/sidebar" .}}{{end}}`}, @@ -254,7 +255,7 @@ func BenchmarkNativeTemplates(b *testing.B) { t := template.New("") var by []byte - for _, name := range []string{"pages/home", "layouts/one", "includes/sidebar"} { + for _, name := range []string{"pages/home/home", "layouts/one", "includes/sidebar"} { by, err = ioutil.ReadFile(filepath.Join(dir, name+".html")) if err != nil { b.Error(err)