``` \__ __/ /_/ /\ \_\ __ \ \/ / __ \_\_\/\/_/_/ STENCIL __/\___\_\/_/___/\__ \/ __/_/\_\__ \/ A Templating package for Go /_/ /\/\ \_\ __/ /\ \__ \_\ \/ /_/ / \ ``` # Stencil - A Templating package for Go [![pipeline status](https://gitlab.com/kylehqcom/stencil/badges/master/pipeline.svg)](https://gitlab.com/kylehqcom/stencil/commits/master) [![coverage report](https://gitlab.com/kylehqcom/stencil/badges/master/coverage.svg)](https://gitlab.com/kylehqcom/stencil/commits/master) [![License MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://gitlab.com/kylehqcom/stencil/blob/master/LICENSE) [![Go Report Card](https://goreportcard.com/badge/gitlab.com/kylehqcom/stencil)](https://goreportcard.com/report/gitlab.com/kylehqcom/stencil) Templating in Go is not always trivial. This package has been designed to take a lot of the *"hurt"* out of rendering your applications template views. Note that I have tried to ensure this package has a well defined domain and structure, with all the versatility required for your applications needs. PR's and community comments are always welcome. If you like this package or are using for your own needs, then let me know via [https://twitter.com/kylehqcom](https://twitter.com/kylehqcom) ## Features - Well defined simple interfaces - Easily extensible to suit your needs - Fully tested - Handles i18n/locales with fallback - Cascading template match rules - Render default Error pages - Packaged *"Decorator"* that handles 95% of use cases - Flash messaging already built in ## Installation ``` // Install with the usual or add to you package manager of choice. go get gitlab.com/kylehqcom/stencil ``` ## Quickstart example If you are wanting to dive straight in, you can take a look at the [_examples](https://gitlab.com/kylehqcom/stencil/tree/master/_examples) directory. This example uses the bundled *"decorator"* to decorate your requests. This (in my opinion) will cover 95% of your use cases. Note that in order to render the index handler example, we assume that you have a folder structure of ``` your/dir/to/templates/ error.html layout.html en/ index.html es/ index.html ``` ```go package main import ( "html/template" "log" "net/http" "gitlab.com/kylehqcom/stencil/decorator" "gitlab.com/kylehqcom/stencil/executor" "gitlab.com/kylehqcom/stencil/loader" "gitlab.com/kylehqcom/stencil/matcher" ) var l = loader.NewFilepathLoader(loader.WithMustParse(true)) func main() { // This example has a yet to be loaded template with a template.func of {{render}}, so we placeholder to ensure parsing l.RegisterFuncs(template.FuncMap{"render": func() string { return "Render called unexpectedly" }}) l.LoadFromFilepath("your/dir/to/templates/*.html") l.LoadFromFilepath("your/dir/to/templates/*/*.html") indexHandler := func(w http.ResponseWriter, r *http.Request) { render(w, r, "index.html", nil) // Note we only refer to the filename to lookup as the PathRoot option is defined in setup } http.HandleFunc("/", indexHandler) log.Println("Enjoy using Stencil") log.Println("Example now running on Port :8080") log.Fatal(http.ListenAndServe(":8080", nil)) } // The placeholded render method from above will get replaced with this render method on template execution func render(w http.ResponseWriter, r *http.Request, name string, data map[string]interface{}, opts ...decorator.Option) { // Pass a query param eg `?locale=es` to render the es template vals := r.URL.Query() loc := vals.Get("locale") if loc != "es" { loc = "en" } d := decorator.NewRequestDecorator( l.Yield(), matcher.NewFilepathMatcher( matcher.WithPathRoot("your/dir/to/templates"), matcher.WithFallbackLocale("en"), matcher.WithLocale(loc), ), executor.NewTemplateExecutor(), decorator.WithBaseTemplate("layout.html"), decorator.WithErrorTemplate("error.html"), ) // Note that we created the decorator "withOptions" but this render method receivers "opts" will take precedence on the Decorate() call. // That way you can have default options defined, but call with different options on render as you require. d.Decorate(w, r, name, data, opts...) } ``` ## Deeper dive The **Stencil** package is built around x3 interfaces. The *Loader*, the *Matcher* and the *Executor*. These x3 interfaces give you all the flexibility to render your templates. Perhaps you need to Load a template from a Datastore on each request, use a custom matcher to ensure you return the correct template from your loaded template collection, or execute your template with a side event on each execute. This is all easily possible. Because this package was built from a web request perspective, it comes bundled with what I have titled a *Decorator*. By using a *decorator.Request* instance, you are ensured you can *render* your loaded templates easily with locales and fallbacks [not to mention Flash messages!](https://gitlab.com/kylehqcom/stencil#flash-messages-bonus-extras) *\*\*Note that although **Stencil** was built from a web perspective, I believe that a web context is not required to make full use of this package.* ### The Loader interface In the normal program flow, the *stencil.Loader.Load()* method should parse the given template name and if successful, add to the `Loader.Collection`. The `stencil.Loader` has the options of `WithAllowDuplicates`, `WithLoadTimeout` and `WithMustParse`. It's recommended that you at least use the `WithMustParse` true when developing to surface errors early. Both allow and must options default false. LoadTimeout defaults to 5 seconds. ```go // Loader is the interface used to "load" templates Loader interface { // Load will read and parse your text, returning an error or adding to a Collection Load(name, text string) error // RegisterFuncs is used to bind funcs to your templates. As the Load method parses your // templates, it may be necessary to register placeholder funcs that are replaced at // execution. Uses the default template.FuncMap for ease of use. RegisterFuncs(template.FuncMap) // Yield will return the collection which allows you access to the loaded templates Yield() *Collection } // Options are all defined behaviours for loading Options struct { // AllowDuplicates defines whether an error should be returned if duplicate found AllowDuplicates bool // LoadTimeout is used to return error on timeout duration is exceeded LoadTimeout time.Duration // MustParse if true, will panic on parse error MustParse bool } ``` Of special note here is the `RegisterFuncs(template.FuncMap)` func. As we need to compile/parse templates prior to executing, you may need to RegisterFunc placeholders to ensure no errors on Load. Example ```go // Layout.html contents Stencil

Welcome to Stencil

{{render}} // Placeholder the render template func which will be replaced at execution time var l = loader.NewFilepathLoader(loader.WithMustParse(true)) l.RegisterFuncs(template.FuncMap{"render": func() string { return "Render called unexpectedly" }}) ``` **Stencil** comes bundled with a `loader.Filepath` so you can take advantage of the `LoadFromFilepath(glob string)` func. This also takes care of the Go template.Templates.ParseGlob() func which does **NOT** respect files of the same name in sub directories and simply overwrites (last one wins). Call `Yield()` on your loader to return your `*stencil.Collection` instance. From here, you can access the default Go `*template.Template` instance, and therefore all the default Go template funcs, through the `*stencil.Collection.T` field. A collection also utilises `sync.Mutex`. This is useful for parallel processing of large quantities of templates in goroutines. ### The Matcher interface The `Matcher` is core to **Stencil** and is responsible for returning a correct Go *template instance, using fallbacks if supplied, or error on not found. Default error templates can be assigned which also have fallbacks. ```go // Matcher is the interface used to "match" templates Matcher interface { // Match will match any pattern string you provide an return a template pointer or error Match(c *Collection, pattern string) (*template.Template, error) } // Options are all defined behaviours for matching Options struct { // PathRoot if set, will be used to prefix all match patterns PathRoot string // PathSeparator is the Separator string used to join match patterns. Defaults to filepath.Separator PathSeparator string // Locale is the string for the current match Locale string // FallbackLocale is used if no match is made on the current Locale match FallbackLocale string } ``` Again, **Stencil** comes bundled with a `matcher.Filepath` so matching template names is made easy. The matcher, based on the match options, will attempt to return a *template.Template instance with the following cascading rules. Example ```go m := matcher.NewFilepathMatcher( matcher.WithPathRoot("src/web/templates/"), matcher.WithLocale("es"), matcher.WithFallbackLocale("en"), ) Calling m.match(c *Collection, "index.html") will check the following in order or preference 1. src/web/templates/es/index.html 2. src/web/templates/en/index.html 3. src/web/templates/index.html 4. index.html ``` The last match can be especially useful if you have a template loaded that has a name outside of your normal directory structure. This allows you to pass in a custom pattern and the matcher will lookup this value without any modifications. ### The Executor interface The `Executor` in **Stencil** serves only as a simple abstraction and follows the Go *template.Template.Execute() func. The abstraction ensures that this package is not bound to the core lib and it also gives you the freedom to manipulate the execution behaviour. ```go // Executor is the interface used to "execute" templates Executor interface { // Execute follows the Go packages Execute method for ease of use Execute(wr io.Writer, t *template.Template, data interface{}) error } ``` ### Flash messages, bonus extras!!! **Stencil** also takes care of your `*http.Request` to `*http.Request` *flash* messages. There are x4 Flash types bundled with **Stencil** ```go const ( // FlashAlert is a Flash Type for Alert messages FlashAlert FlashType = "alert" // FlashError is a Flash Type for Error messages FlashError FlashType = "error" // FlashInfo is a Flash Type for Info messages FlashInfo FlashType = "info" // FlashSuccess is a Flash Type for Success messages FlashSuccess FlashType = "success" ) ``` But of course you can cast your own `FlashType` from a string source. Adding a flash message is as easy as calling the `Add{FlashType}()` or `AddFlash()` methods respectively. Example ```go // Both are equivalent r = stencil.AddFlashSuccess(r *http.Request, "a success message", nil) r = stencil.AddFlash(stencil.FlashSuccess, r *http.Request, "a success message", nil) ``` You can even add flash messages **from outside the Go context** by calling the `BindToRequestQuery` method on your `Flashes` instance. The `BindToRequestQuery` which takes and returns an `*http.Request`, will base64 encode flashes and add as a query param to your request. By default the query param key of `flashes` is used but you can optionally change that too. This is really handy when redirecting users but wanting to display a Flash message on redirect completion. Example ```go r := *http.Request flashes := stencil.GetFlashes(r) r = flashes.BindToRequestQuery(r) // Flashes are now bound on the url.Values of the *http.request as a Base64 Encoded string // On a normal redirect, use the URL.RawQuery with your redirect url. http.Redirect(w, r, fmt.Sprintf("/you/request/uri?%s", r.URL.RawQuery), http.StatusFound) ``` If you are using a `decorator.Request` instance, then everything is taken care of. The decorator will add flash messages to your template `data map[string]interface` under the key of `flashes` by first checking for flash messages on the request query, and then on the requests' context. Note that the key of `flashes` is configurable and you can retrieve the Base64 encoded value and type assert as required. Refer to the `GetFlashes` func of [https://gitlab.com/kylehqcom/stencil/blob/master/flashes.go](https://gitlab.com/kylehqcom/stencil/blob/master/flashes.go) for an example. One last thing around flashes. There are also `FlashContentType` options you can apply. For example, there are times when displaying a string message isn't desirable. Sometimes you may wish to add HTML to your flash message, eg to give a user an href/link. Golang by default escapes all rendered template text so you need to pass either the `WithFlashMessageAsHTML` or the `WithFlashMessageAsJS` flash options when adding/creating. The `WithFlashMessageAsString` is the default option but is available if you need to revert. There are security considerations to be aware of (refer [https://golang.org/src/html/template/content.go](https://golang.org/src/html/template/content.go)), but as long as you trust your own source files, you will be fine. ```go // Example with flash content type example r := *http.Request r = stencil.AddFlashAlert(r,`Flash link`, nil, stencil.WithFlashMessageAsHTML()) ``` ### Next steps From here my best advice would be to take a look at the [_examples](https://gitlab.com/kylehqcom/stencil/tree/master/_examples) directory, especially the [_example/locale_fallback](https://gitlab.com/kylehqcom/stencil/blob/master/_examples/locale_fallback/main.go) which give a good demonstation as to how the bundled `*decorator.Request` works. If you have questions, praise or complaints, I am all ears so let me know via [https://twitter.com/kylehqcom](https://twitter.com/kylehqcom) for feedback. I use this package personally so I may have overlooked aspects outside of my domain. I do hope you find good use out of my efforts however =]