123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652 |
- 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
- // "<no value>".
- // "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
- }
|