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 }