README.md 11.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12
```
     \__    __/
     /_/ /\ \_\
    __ \ \/ / __
    \_\_\/\/_/_/                 STENCIL
__/\___\_\/_/___/\__
  \/ __/_/\_\__ \/      A Templating package for Go
    /_/ /\/\ \_\
     __/ /\ \__
     \_\ \/ /_/
     /        \
```
Kyle Clarke's avatar
Kyle Clarke committed
13

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
# 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() {
  // In this example we know that a loaded template has the 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 as the PathRoot is defined below
  }

  http.HandleFunc("/", indexHandler)
  log.Println("Enjoy using Stencil")
  log.Println("Example now running on Port :8080")
  log.Fatal(http.ListenAndServe(":8080", nil))
}

func render(w http.ResponseWriter, r *http.Request, name string, data map[string]interface{}, opts ...decorator.Option) {
  // Pass ?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 receivers "opts" will take precedence on the Decorate() call.
  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` and `WithMustParse`. It's recommended that you at least use the `WithMustParse` true when developing to surface errors early. Both options default false.

```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

  // 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
<!DOCTYPE html>
<html>
  <head>
    <title>Stencil</title>
  </head>
  <h1>Welcome to Stencil</h1>
  <body>
    {{render}}
  </body>
</html>

// 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.

### The Matcher interface

The `Matcher` is core to **Stencil** and is responsible for returning a correct Go *template instance, using fallbacks if need be.

```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)

// Now continue with your request as normal
```

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.

### What next?

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 =]