package blocks
import (
"context"
"fmt"
"html/template"
"io"
"io/fs"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"sync"
"github.com/russross/blackfriday/v2"
"github.com/valyala/bytebufferpool"
"golang.org/x/exp/maps"
)
// ExtensionParser type declaration to customize other extension's parsers before passed to the template's one.
type ExtensionParser func([]byte) ([]byte, error)
// ErrNotExist reports whether a template was not found in the parsed templates tree.
type ErrNotExist struct {
Name string
}
// Error implements the `error` interface.
func (e ErrNotExist) Error() string {
return fmt.Sprintf("template '%s' does not exist", e.Name)
}
// Blocks is the main structure which
// holds the necessary information and options
// to parse and render templates.
// See `New` to initialize a new one.
type Blocks struct {
// the file system to load templates from.
// The "rootDir" field can be used to select a specific directory from this file system.
fs fs.FS
rootDir string // it always set to "/" as the RootDir method changes the filesystem to sub one.
layoutDir string // /layouts
layoutFuncs template.FuncMap
tmplFuncs template.FuncMap
defaultLayoutName string // the default layout if it's missing from the `ExecuteTemplate`.
extension string // .html
left, right string // delims.
// extensionHandler can handle other file extensions rathen than the main one,
// The default contains an entry of ".md" for `blackfriday.Run`.
extensionHandler map[string]ExtensionParser // key = extension with dot, value = parser.
// parse the templates on each request.
reload bool
mu sync.RWMutex
bufferPool *bytebufferpool.Pool
// Root, Templates and Layouts can be accessed after `Load`.
Root *template.Template
templatesContents map[string]string
Templates, Layouts map[string]*template.Template
}
// New returns a fresh Blocks engine instance.
// It loads the templates based on the given fs FileSystem (or string).
// By default the layout files should be located at "$rootDir/layouts" sub-directory (see `RootDir` method),
// change this behavior can be achieved through `LayoutDir` method before `Load/LoadContext`.
// To set a default layout name for an empty layout definition on `ExecuteTemplate/ParseTemplate`
// use the `DefaultLayout` method.
//
// The user can customize various options through the Blocks methods.
// The user of this engine MUST call its `Load/LoadWithContext` method once
// before any call of `ExecuteTemplate` and `ParseTemplate`.
//
// Global functions registered through `Register` package-level function
// will be inherited from this engine. To add a function map to this engine
// use its `Funcs` method.
//
// The default extension can be changed through the `Extension` method.
// More extension parsers can be added through the `Extensions` method.
// The left and right delimeters can be customized through its `Delims` method.
// To reload templates on each request (useful for development stage) call its `Reload(true)` method.
//
// Usage:
// New("./views") or
// New(http.Dir("./views")) or
// New(embeddedFS) or New(AssetFile()) for embedded data.
func New(fs interface{}) *Blocks {
v := &Blocks{
fs: getFS(fs),
layoutDir: "/layouts",
extension: ".html",
extensionHandler: map[string]ExtensionParser{
".md": func(b []byte) ([]byte, error) { return blackfriday.Run(b), nil },
},
left: "{{",
right: "}}",
// Root "content" for the default one, so templates without layout can still be rendered.
// Note that, this is parsed, the delims can be configured later on.
Root: template.Must(template.New("root").
Parse(`{{ define "root" }} {{ template "content" . }} {{ end }}`)),
templatesContents: make(map[string]string),
Templates: make(map[string]*template.Template),
Layouts: make(map[string]*template.Template),
reload: false,
bufferPool: new(bytebufferpool.Pool),
}
v.Root.Funcs(translateFuncs(v, builtins))
return v
}
// Reload will turn on the `Reload` setting, for development use.
// It forces the `ExecuteTemplate` to re-parse the templates on each incoming request.
func (v *Blocks) Reload(b bool) *Blocks {
v.reload = b
return v
}
var (
defineStart = func(left string) string {
return fmt.Sprintf("%s define", left)
}
defineStartNoSpace = func(left string) string {
return fmt.Sprintf("%sdefine", left)
}
defineContentStart = func(left, right string) string {
return fmt.Sprintf(`%sdefine "content"%s`, left, right)
}
defineContentEnd = func(left, right string) string {
return fmt.Sprintf("%send%s", left, right)
}
)
// Delims sets the action delimiters to the specified strings, to be used in
// Load. Nested template
// definitions will inherit the settings. An empty delimiter stands for the
// corresponding default: {{ or }}.
// The return value is the engine, so calls can be chained.
func (v *Blocks) Delims(left, right string) *Blocks {
v.left = left
v.right = right
v.Root.Delims(left, right)
return v
}
// Option sets options for the templates. Options are described by
// strings, either a simple string or "key=value". There can be at
// most one equals sign in an option string. If the option string
// is unrecognized or otherwise invalid, Option panics.
//
// Known options:
//
// missingkey: Control the behavior during execution if a map is
// indexed with a key that is not present in the map.
//
// "missingkey=default" or "missingkey=invalid"
// The default behavior: Do nothing and continue execution.
// If printed, the result of the index operation is the string
// "".
// "missingkey=zero"
// The operation returns the zero value for the map type's element.
// "missingkey=error"
// Execution stops immediately with an error.
func (v *Blocks) Option(opt ...string) *Blocks {
v.Root.Option(opt...)
return v
}
// Funcs adds the elements of the argument map to the root template's function map.
// It must be called before the engine is loaded.
// It panics if a value in the map is not a function with appropriate return
// type. However, it is legal to overwrite elements of the map. The return
// value is the engine, so calls can be chained.
//
// The default function map contains a single element of "partial" which
// can be used to render templates directly.
func (v *Blocks) Funcs(funcMap template.FuncMap) *Blocks {
if v.tmplFuncs == nil {
v.tmplFuncs = funcMap
return v
}
for name, fn := range funcMap {
v.tmplFuncs[name] = fn
}
v.Root.Funcs(funcMap)
return v
}
// LayoutFuncs same as `Funcs` but this map's elements will be added
// only to the layout templates. It's legal to override elements of the root `Funcs`.
func (v *Blocks) LayoutFuncs(funcMap template.FuncMap) *Blocks {
if v.layoutFuncs == nil {
v.layoutFuncs = funcMap
return v
}
for name, fn := range funcMap {
v.layoutFuncs[name] = fn
}
return v
}
// RootDir sets the directory to use as the root one inside the provided File System.
func (v *Blocks) RootDir(root string) *Blocks {
if v.fs != nil && root != "" && root != "/" && root != "." {
sub, err := fs.Sub(v.fs, root)
if err != nil {
panic(err)
}
v.fs = sub
}
// v.rootDir = filepath.ToSlash(root)
// v.layoutDir = path.Join(root, v.layoutDir)
return v
}
// LayoutDir sets a custom layouts directory,
// always relative to the "rootDir" one.
// Layouts are recognised by their prefix names.
// Defaults to "layouts".
func (v *Blocks) LayoutDir(relToDirLayoutDir string) *Blocks {
v.layoutDir = filepath.ToSlash(relToDirLayoutDir)
return v
}
// DefaultLayout sets the "layoutName" to be used
// when the `ExecuteTemplate`'s one is empty.
func (v *Blocks) DefaultLayout(layoutName string) *Blocks {
v.defaultLayoutName = layoutName
return v
}
// Extension sets the template file extension (with dot).
// Defaults to ".html".
func (v *Blocks) Extension(ext string) *Blocks {
v.extension = ext
return v
}
// Extensions registers a parser that will be called right before
// a file's contents parsed as a template.
// The "ext" should start with dot (.), e.g. ".md".
// The "parser" is a function which accepts the original file's contents
// and should return the parsed ones, e.g. return markdown.Run(contents), nil.
//
// The default underline map contains a single element of ".md": markdown.Run,
// which is responsible to convert markdown files to html right before its contents
// are given to the template's parser.
//
// To override an extension handler pass a nil "parser".
func (v *Blocks) Extensions(ext string, parser ExtensionParser) *Blocks {
v.extensionHandler[ext] = parser
return v
}
// Load parses the templates, including layouts,
// through the html/template standard package into the Blocks engine.
func (v *Blocks) Load() error {
return v.LoadWithContext(context.Background())
}
// LoadWithContext accepts a context that can be used for load cancelation, deadline/timeout.
// It parses the templates, including layouts,
// through the html/template standard package into the Blocks engine.
func (v *Blocks) LoadWithContext(ctx context.Context) error {
v.mu.Lock()
defer v.mu.Unlock()
maps.Clear(v.templatesContents)
maps.Clear(v.Templates)
maps.Clear(v.Layouts)
return v.load(ctx)
}
func (v *Blocks) load(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var (
layouts []string
mu sync.RWMutex
)
var assetNames []string // all assets names.
err := walk(v.fs, "", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() || !info.Mode().IsRegular() {
return nil
}
assetNames = append(assetNames, path)
return nil
})
if err != nil {
return err
}
if len(assetNames) == 0 {
return fmt.Errorf("no templates found")
}
// +---------------------+
// | Template Assets |
// +---------------------+
loadAsset := func(assetName string) error {
if dir := relDir(v.rootDir); dir != "" && !strings.HasPrefix(assetName, dir) {
// If contains a not empty directory and the asset name does not belong there
// then skip it, useful on bindata assets when they
// may contain other files that are not templates.
return nil
}
if layoutDir := relDir(v.layoutDir); layoutDir != "" &&
strings.HasPrefix(assetName, layoutDir) {
// it's a layout template file, add it to layouts and skip,
// in order to add them to each template file.
mu.Lock()
layouts = append(layouts, assetName)
mu.Unlock()
return nil
}
tmplName := trimDir(assetName, v.rootDir)
ext := path.Ext(assetName)
tmplName = strings.TrimSuffix(tmplName, ext)
tmplName = strings.TrimPrefix(tmplName, "/")
extParser := v.extensionHandler[ext]
hasHandler := extParser != nil // it may exists but if it's nil then we can't use it.
if v.extension != "" {
if ext != v.extension && !hasHandler {
return nil
}
}
contents, err := asset(v.fs, assetName)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
default:
break
}
if hasHandler {
contents, err = extParser(contents)
if err != nil {
// custom parsers may return a non-nil error,
// e.g. less or scss files
// and, yes, they can be used as templates too,
// because they are wrapped by a template block if necessary.
return err
}
}
mu.Lock()
v.Templates[tmplName], err = v.Root.Clone() // template.New(tmplName)
mu.Unlock()
if err != nil {
return err
}
str := string(contents)
// should have any kind of template or the whole as content template,
// if not we will make it as a single template definition.
if !strings.Contains(str, defineStart(v.left)) && !strings.Contains(str, defineStartNoSpace(v.left)) {
str = defineContentStart(v.left, v.right) + str + defineContentEnd(v.left, v.right)
}
mu.Lock()
_, err = v.Templates[tmplName].Funcs(v.tmplFuncs).Parse(str)
if err != nil {
err = fmt.Errorf("%w: %s: %s", err, tmplName, str)
} else {
v.templatesContents[tmplName] = str
}
mu.Unlock()
return err
}
var (
wg sync.WaitGroup
errOnce sync.Once
)
for _, assetName := range assetNames {
wg.Add(1)
go func(assetName string) {
defer wg.Done()
if loadErr := loadAsset(assetName); loadErr != nil {
errOnce.Do(func() {
err = loadErr
cancel()
})
}
}(assetName)
}
wg.Wait()
if err != nil {
return err
}
// +---------------------+
// | Layouts |
// +---------------------+
loadLayout := func(layout string) error {
contents, err := asset(v.fs, layout)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
default:
break
}
name := trimDir(layout, v.layoutDir) // if we want rel-to-the-dir instead we just replace with v.rootDir.
name = strings.TrimSuffix(name, v.extension)
str := string(contents)
builtins := translateFuncs(v, builtins)
for tmplName, tmplContents := range v.templatesContents {
// Make new layout template for each of the templates,
// the key of the layout in map will be the layoutName+tmplName.
// So each template owns all layouts. This fixes the issue with the new {{ block }} and the usual {{ define }} directives.
layoutTmpl, err := template.New(name).Funcs(builtins).Funcs(v.layoutFuncs).Parse(str)
if err != nil {
return fmt.Errorf("%w: for layout: %s", err, name)
}
_, err = layoutTmpl.Funcs(v.tmplFuncs).Parse(tmplContents)
if err != nil {
return fmt.Errorf("%w: layout: %s: for template: %s", err, name, tmplName)
}
key := makeLayoutTemplateName(tmplName, name)
mu.Lock()
v.Layouts[key] = layoutTmpl
mu.Unlock()
}
return nil
}
for _, layout := range layouts {
wg.Add(1)
go func(layout string) {
defer wg.Done()
if loadErr := loadLayout(layout); loadErr != nil {
errOnce.Do(func() {
err = loadErr
cancel()
})
}
}(layout)
}
wg.Wait()
// Clear the cached contents, we don't need them from now on.
maps.Clear(v.templatesContents)
return err
}
// ExecuteTemplate applies the template associated with "tmplName"
// to the specified "data" object and writes the output to "w".
// If an error occurs executing the template or writing its output,
// execution stops, but partial results may already have been written to
// the output writer.
//
// If "layoutName" and "v.defaultLayoutName" are both empty then
// the template is executed without a layout.
//
// A template may be executed safely in parallel, although if parallel
// executions share a Writer the output may be interleaved.
func (v *Blocks) ExecuteTemplate(w io.Writer, tmplName, layoutName string, data interface{}) error {
if v.reload {
if err := v.Load(); err != nil {
return err
}
}
if layoutName == "" {
layoutName = v.defaultLayoutName
}
return v.executeTemplate(w, tmplName, layoutName, data)
}
func (v *Blocks) executeTemplate(w io.Writer, tmplName, layoutName string, data interface{}) error {
if layoutName != "" {
tmpl := v.getTemplateWithLayout(tmplName, layoutName)
if tmpl == nil {
return ErrNotExist{layoutName}
}
// Full Template Name:
// fmt.Printf("executing %s.%s\n", layoutName, tmplName)
// Source:
// fmt.Println(tmpl.Tree.Root.String())
return tmpl.Execute(w, data)
}
tmpl, ok := v.Templates[tmplName]
if !ok {
return ErrNotExist{tmplName}
}
// if httpResponseWriter, ok := w.(http.ResponseWriter); ok {
// check if content-type exists, and if it's not:
// httpResponseWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
// } ^ No, leave it for the caller.
// if layoutName != "" {
// return tmpl.ExecuteTemplate(w, layoutName, data)
// }
return tmpl.Execute(w, data)
}
// ParseTemplate parses a template based on its "tmplName" name and returns the result.
// Note that, this does not reload the templates on each call if Reload was set to true.
// To refresh the templates you have to manually call the `Load` upfront.
func (v *Blocks) ParseTemplate(tmplName, layoutName string, data interface{}) (string, error) {
b := v.bufferPool.Get()
// use the unexported method so it does not re-reload the templates on each partial one
// when Reload was set to true.
err := v.executeTemplate(b, tmplName, layoutName, data)
contents := b.String()
v.bufferPool.Put(b)
return contents, err
}
// PartialFunc returns the parsed result of the "partialName" template's "content" block.
func (v *Blocks) PartialFunc(partialName string, data interface{}) (template.HTML, error) {
// contents, err := v.ParseTemplate(partialName, "content", data)
// if err != nil {
// return "", err
// }
contents, err := v.ParseTemplate(partialName, "", data)
if err != nil {
return "", err
}
return template.HTML(contents), nil
}
// ContextKeyType is the type which `Set`
// request's context value is using to store
// the current Blocks engine.
//
// See `Set` and `Get`.
type ContextKeyType struct{}
// ContextKey is the request's context value for a blocks engine.
//
// See `Set` and `Get`.
var ContextKey ContextKeyType
// Set returns a handler wrapper which sets the current
// view engine to this "v" Blocks.
// Useful when the caller needs multiple Blocks engine instances per group of routes.
// Note that this is entirely optional, the caller could just wrap a function of func(v *Blocks)
// and return a handler which will directly use it.
// See `Get` too.
func Set(v *Blocks) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(context.WithValue(r.Context(), ContextKey, v))
next.ServeHTTP(w, r)
})
}
}
// Get retrieves the associated Blocks view engine retrieved from the request's context.
// See `Set` too.
func Get(r *http.Request) *Blocks {
value := r.Context().Value(ContextKey)
if value == nil {
return nil
}
v, ok := value.(*Blocks)
if !ok {
return nil
}
return v
}
func withSuffix(s string, suf string) string {
if len(s) == 0 {
return ""
}
if !strings.HasSuffix(s, suf) {
s += suf
}
return s
}
func relDir(dir string) string {
if dir == "." {
return ""
}
if dir == "" || dir == "/" {
return ""
}
return strings.TrimPrefix(strings.TrimPrefix(dir, "."), "/")
}
func trimDir(s string, dir string) string {
dir = withSuffix(relDir(dir), "/")
return strings.TrimPrefix(s, dir)
}
func (v *Blocks) getTemplateWithLayout(tmplName, layoutName string) *template.Template {
key := makeLayoutTemplateName(tmplName, layoutName)
return v.Layouts[key]
}
func makeLayoutTemplateName(tmplName, layoutName string) string {
return layoutName + tmplName
}