blocks.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. package blocks
  2. import (
  3. "context"
  4. "fmt"
  5. "html/template"
  6. "io"
  7. "io/fs"
  8. "net/http"
  9. "os"
  10. "path"
  11. "path/filepath"
  12. "strings"
  13. "sync"
  14. "github.com/russross/blackfriday/v2"
  15. "github.com/valyala/bytebufferpool"
  16. "golang.org/x/exp/maps"
  17. )
  18. // ExtensionParser type declaration to customize other extension's parsers before passed to the template's one.
  19. type ExtensionParser func([]byte) ([]byte, error)
  20. // ErrNotExist reports whether a template was not found in the parsed templates tree.
  21. type ErrNotExist struct {
  22. Name string
  23. }
  24. // Error implements the `error` interface.
  25. func (e ErrNotExist) Error() string {
  26. return fmt.Sprintf("template '%s' does not exist", e.Name)
  27. }
  28. // Blocks is the main structure which
  29. // holds the necessary information and options
  30. // to parse and render templates.
  31. // See `New` to initialize a new one.
  32. type Blocks struct {
  33. // the file system to load templates from.
  34. // The "rootDir" field can be used to select a specific directory from this file system.
  35. fs fs.FS
  36. rootDir string // it always set to "/" as the RootDir method changes the filesystem to sub one.
  37. layoutDir string // /layouts
  38. layoutFuncs template.FuncMap
  39. tmplFuncs template.FuncMap
  40. defaultLayoutName string // the default layout if it's missing from the `ExecuteTemplate`.
  41. extension string // .html
  42. left, right string // delims.
  43. // extensionHandler can handle other file extensions rathen than the main one,
  44. // The default contains an entry of ".md" for `blackfriday.Run`.
  45. extensionHandler map[string]ExtensionParser // key = extension with dot, value = parser.
  46. // parse the templates on each request.
  47. reload bool
  48. mu sync.RWMutex
  49. bufferPool *bytebufferpool.Pool
  50. // Root, Templates and Layouts can be accessed after `Load`.
  51. Root *template.Template
  52. templatesContents map[string]string
  53. Templates, Layouts map[string]*template.Template
  54. }
  55. // New returns a fresh Blocks engine instance.
  56. // It loads the templates based on the given fs FileSystem (or string).
  57. // By default the layout files should be located at "$rootDir/layouts" sub-directory (see `RootDir` method),
  58. // change this behavior can be achieved through `LayoutDir` method before `Load/LoadContext`.
  59. // To set a default layout name for an empty layout definition on `ExecuteTemplate/ParseTemplate`
  60. // use the `DefaultLayout` method.
  61. //
  62. // The user can customize various options through the Blocks methods.
  63. // The user of this engine MUST call its `Load/LoadWithContext` method once
  64. // before any call of `ExecuteTemplate` and `ParseTemplate`.
  65. //
  66. // Global functions registered through `Register` package-level function
  67. // will be inherited from this engine. To add a function map to this engine
  68. // use its `Funcs` method.
  69. //
  70. // The default extension can be changed through the `Extension` method.
  71. // More extension parsers can be added through the `Extensions` method.
  72. // The left and right delimeters can be customized through its `Delims` method.
  73. // To reload templates on each request (useful for development stage) call its `Reload(true)` method.
  74. //
  75. // Usage:
  76. // New("./views") or
  77. // New(http.Dir("./views")) or
  78. // New(embeddedFS) or New(AssetFile()) for embedded data.
  79. func New(fs interface{}) *Blocks {
  80. v := &Blocks{
  81. fs: getFS(fs),
  82. layoutDir: "/layouts",
  83. extension: ".html",
  84. extensionHandler: map[string]ExtensionParser{
  85. ".md": func(b []byte) ([]byte, error) { return blackfriday.Run(b), nil },
  86. },
  87. left: "{{",
  88. right: "}}",
  89. // Root "content" for the default one, so templates without layout can still be rendered.
  90. // Note that, this is parsed, the delims can be configured later on.
  91. Root: template.Must(template.New("root").
  92. Parse(`{{ define "root" }} {{ template "content" . }} {{ end }}`)),
  93. templatesContents: make(map[string]string),
  94. Templates: make(map[string]*template.Template),
  95. Layouts: make(map[string]*template.Template),
  96. reload: false,
  97. bufferPool: new(bytebufferpool.Pool),
  98. }
  99. v.Root.Funcs(translateFuncs(v, builtins))
  100. return v
  101. }
  102. // Reload will turn on the `Reload` setting, for development use.
  103. // It forces the `ExecuteTemplate` to re-parse the templates on each incoming request.
  104. func (v *Blocks) Reload(b bool) *Blocks {
  105. v.reload = b
  106. return v
  107. }
  108. var (
  109. defineStart = func(left string) string {
  110. return fmt.Sprintf("%s define", left)
  111. }
  112. defineStartNoSpace = func(left string) string {
  113. return fmt.Sprintf("%sdefine", left)
  114. }
  115. defineContentStart = func(left, right string) string {
  116. return fmt.Sprintf(`%sdefine "content"%s`, left, right)
  117. }
  118. defineContentEnd = func(left, right string) string {
  119. return fmt.Sprintf("%send%s", left, right)
  120. }
  121. )
  122. // Delims sets the action delimiters to the specified strings, to be used in
  123. // Load. Nested template
  124. // definitions will inherit the settings. An empty delimiter stands for the
  125. // corresponding default: {{ or }}.
  126. // The return value is the engine, so calls can be chained.
  127. func (v *Blocks) Delims(left, right string) *Blocks {
  128. v.left = left
  129. v.right = right
  130. v.Root.Delims(left, right)
  131. return v
  132. }
  133. // Option sets options for the templates. Options are described by
  134. // strings, either a simple string or "key=value". There can be at
  135. // most one equals sign in an option string. If the option string
  136. // is unrecognized or otherwise invalid, Option panics.
  137. //
  138. // Known options:
  139. //
  140. // missingkey: Control the behavior during execution if a map is
  141. // indexed with a key that is not present in the map.
  142. //
  143. // "missingkey=default" or "missingkey=invalid"
  144. // The default behavior: Do nothing and continue execution.
  145. // If printed, the result of the index operation is the string
  146. // "<no value>".
  147. // "missingkey=zero"
  148. // The operation returns the zero value for the map type's element.
  149. // "missingkey=error"
  150. // Execution stops immediately with an error.
  151. func (v *Blocks) Option(opt ...string) *Blocks {
  152. v.Root.Option(opt...)
  153. return v
  154. }
  155. // Funcs adds the elements of the argument map to the root template's function map.
  156. // It must be called before the engine is loaded.
  157. // It panics if a value in the map is not a function with appropriate return
  158. // type. However, it is legal to overwrite elements of the map. The return
  159. // value is the engine, so calls can be chained.
  160. //
  161. // The default function map contains a single element of "partial" which
  162. // can be used to render templates directly.
  163. func (v *Blocks) Funcs(funcMap template.FuncMap) *Blocks {
  164. if v.tmplFuncs == nil {
  165. v.tmplFuncs = funcMap
  166. return v
  167. }
  168. for name, fn := range funcMap {
  169. v.tmplFuncs[name] = fn
  170. }
  171. v.Root.Funcs(funcMap)
  172. return v
  173. }
  174. // LayoutFuncs same as `Funcs` but this map's elements will be added
  175. // only to the layout templates. It's legal to override elements of the root `Funcs`.
  176. func (v *Blocks) LayoutFuncs(funcMap template.FuncMap) *Blocks {
  177. if v.layoutFuncs == nil {
  178. v.layoutFuncs = funcMap
  179. return v
  180. }
  181. for name, fn := range funcMap {
  182. v.layoutFuncs[name] = fn
  183. }
  184. return v
  185. }
  186. // RootDir sets the directory to use as the root one inside the provided File System.
  187. func (v *Blocks) RootDir(root string) *Blocks {
  188. if v.fs != nil && root != "" && root != "/" && root != "." {
  189. sub, err := fs.Sub(v.fs, root)
  190. if err != nil {
  191. panic(err)
  192. }
  193. v.fs = sub
  194. }
  195. // v.rootDir = filepath.ToSlash(root)
  196. // v.layoutDir = path.Join(root, v.layoutDir)
  197. return v
  198. }
  199. // LayoutDir sets a custom layouts directory,
  200. // always relative to the "rootDir" one.
  201. // Layouts are recognised by their prefix names.
  202. // Defaults to "layouts".
  203. func (v *Blocks) LayoutDir(relToDirLayoutDir string) *Blocks {
  204. v.layoutDir = filepath.ToSlash(relToDirLayoutDir)
  205. return v
  206. }
  207. // DefaultLayout sets the "layoutName" to be used
  208. // when the `ExecuteTemplate`'s one is empty.
  209. func (v *Blocks) DefaultLayout(layoutName string) *Blocks {
  210. v.defaultLayoutName = layoutName
  211. return v
  212. }
  213. // Extension sets the template file extension (with dot).
  214. // Defaults to ".html".
  215. func (v *Blocks) Extension(ext string) *Blocks {
  216. v.extension = ext
  217. return v
  218. }
  219. // Extensions registers a parser that will be called right before
  220. // a file's contents parsed as a template.
  221. // The "ext" should start with dot (.), e.g. ".md".
  222. // The "parser" is a function which accepts the original file's contents
  223. // and should return the parsed ones, e.g. return markdown.Run(contents), nil.
  224. //
  225. // The default underline map contains a single element of ".md": markdown.Run,
  226. // which is responsible to convert markdown files to html right before its contents
  227. // are given to the template's parser.
  228. //
  229. // To override an extension handler pass a nil "parser".
  230. func (v *Blocks) Extensions(ext string, parser ExtensionParser) *Blocks {
  231. v.extensionHandler[ext] = parser
  232. return v
  233. }
  234. // Load parses the templates, including layouts,
  235. // through the html/template standard package into the Blocks engine.
  236. func (v *Blocks) Load() error {
  237. return v.LoadWithContext(context.Background())
  238. }
  239. // LoadWithContext accepts a context that can be used for load cancelation, deadline/timeout.
  240. // It parses the templates, including layouts,
  241. // through the html/template standard package into the Blocks engine.
  242. func (v *Blocks) LoadWithContext(ctx context.Context) error {
  243. v.mu.Lock()
  244. defer v.mu.Unlock()
  245. maps.Clear(v.templatesContents)
  246. maps.Clear(v.Templates)
  247. maps.Clear(v.Layouts)
  248. return v.load(ctx)
  249. }
  250. func (v *Blocks) load(ctx context.Context) error {
  251. ctx, cancel := context.WithCancel(ctx)
  252. defer cancel()
  253. var (
  254. layouts []string
  255. mu sync.RWMutex
  256. )
  257. var assetNames []string // all assets names.
  258. err := walk(v.fs, "", func(path string, info os.FileInfo, err error) error {
  259. if err != nil {
  260. return err
  261. }
  262. if info.IsDir() || !info.Mode().IsRegular() {
  263. return nil
  264. }
  265. assetNames = append(assetNames, path)
  266. return nil
  267. })
  268. if err != nil {
  269. return err
  270. }
  271. if len(assetNames) == 0 {
  272. return fmt.Errorf("no templates found")
  273. }
  274. // +---------------------+
  275. // | Template Assets |
  276. // +---------------------+
  277. loadAsset := func(assetName string) error {
  278. if dir := relDir(v.rootDir); dir != "" && !strings.HasPrefix(assetName, dir) {
  279. // If contains a not empty directory and the asset name does not belong there
  280. // then skip it, useful on bindata assets when they
  281. // may contain other files that are not templates.
  282. return nil
  283. }
  284. if layoutDir := relDir(v.layoutDir); layoutDir != "" &&
  285. strings.HasPrefix(assetName, layoutDir) {
  286. // it's a layout template file, add it to layouts and skip,
  287. // in order to add them to each template file.
  288. mu.Lock()
  289. layouts = append(layouts, assetName)
  290. mu.Unlock()
  291. return nil
  292. }
  293. tmplName := trimDir(assetName, v.rootDir)
  294. ext := path.Ext(assetName)
  295. tmplName = strings.TrimSuffix(tmplName, ext)
  296. tmplName = strings.TrimPrefix(tmplName, "/")
  297. extParser := v.extensionHandler[ext]
  298. hasHandler := extParser != nil // it may exists but if it's nil then we can't use it.
  299. if v.extension != "" {
  300. if ext != v.extension && !hasHandler {
  301. return nil
  302. }
  303. }
  304. contents, err := asset(v.fs, assetName)
  305. if err != nil {
  306. return err
  307. }
  308. select {
  309. case <-ctx.Done():
  310. return ctx.Err()
  311. default:
  312. break
  313. }
  314. if hasHandler {
  315. contents, err = extParser(contents)
  316. if err != nil {
  317. // custom parsers may return a non-nil error,
  318. // e.g. less or scss files
  319. // and, yes, they can be used as templates too,
  320. // because they are wrapped by a template block if necessary.
  321. return err
  322. }
  323. }
  324. mu.Lock()
  325. v.Templates[tmplName], err = v.Root.Clone() // template.New(tmplName)
  326. mu.Unlock()
  327. if err != nil {
  328. return err
  329. }
  330. str := string(contents)
  331. // should have any kind of template or the whole as content template,
  332. // if not we will make it as a single template definition.
  333. if !strings.Contains(str, defineStart(v.left)) && !strings.Contains(str, defineStartNoSpace(v.left)) {
  334. str = defineContentStart(v.left, v.right) + str + defineContentEnd(v.left, v.right)
  335. }
  336. mu.Lock()
  337. _, err = v.Templates[tmplName].Funcs(v.tmplFuncs).Parse(str)
  338. if err != nil {
  339. err = fmt.Errorf("%w: %s: %s", err, tmplName, str)
  340. } else {
  341. v.templatesContents[tmplName] = str
  342. }
  343. mu.Unlock()
  344. return err
  345. }
  346. var (
  347. wg sync.WaitGroup
  348. errOnce sync.Once
  349. )
  350. for _, assetName := range assetNames {
  351. wg.Add(1)
  352. go func(assetName string) {
  353. defer wg.Done()
  354. if loadErr := loadAsset(assetName); loadErr != nil {
  355. errOnce.Do(func() {
  356. err = loadErr
  357. cancel()
  358. })
  359. }
  360. }(assetName)
  361. }
  362. wg.Wait()
  363. if err != nil {
  364. return err
  365. }
  366. // +---------------------+
  367. // | Layouts |
  368. // +---------------------+
  369. loadLayout := func(layout string) error {
  370. contents, err := asset(v.fs, layout)
  371. if err != nil {
  372. return err
  373. }
  374. select {
  375. case <-ctx.Done():
  376. return ctx.Err()
  377. default:
  378. break
  379. }
  380. name := trimDir(layout, v.layoutDir) // if we want rel-to-the-dir instead we just replace with v.rootDir.
  381. name = strings.TrimSuffix(name, v.extension)
  382. str := string(contents)
  383. builtins := translateFuncs(v, builtins)
  384. for tmplName, tmplContents := range v.templatesContents {
  385. // Make new layout template for each of the templates,
  386. // the key of the layout in map will be the layoutName+tmplName.
  387. // So each template owns all layouts. This fixes the issue with the new {{ block }} and the usual {{ define }} directives.
  388. layoutTmpl, err := template.New(name).Funcs(builtins).Funcs(v.layoutFuncs).Parse(str)
  389. if err != nil {
  390. return fmt.Errorf("%w: for layout: %s", err, name)
  391. }
  392. _, err = layoutTmpl.Funcs(v.tmplFuncs).Parse(tmplContents)
  393. if err != nil {
  394. return fmt.Errorf("%w: layout: %s: for template: %s", err, name, tmplName)
  395. }
  396. key := makeLayoutTemplateName(tmplName, name)
  397. mu.Lock()
  398. v.Layouts[key] = layoutTmpl
  399. mu.Unlock()
  400. }
  401. return nil
  402. }
  403. for _, layout := range layouts {
  404. wg.Add(1)
  405. go func(layout string) {
  406. defer wg.Done()
  407. if loadErr := loadLayout(layout); loadErr != nil {
  408. errOnce.Do(func() {
  409. err = loadErr
  410. cancel()
  411. })
  412. }
  413. }(layout)
  414. }
  415. wg.Wait()
  416. // Clear the cached contents, we don't need them from now on.
  417. maps.Clear(v.templatesContents)
  418. return err
  419. }
  420. // ExecuteTemplate applies the template associated with "tmplName"
  421. // to the specified "data" object and writes the output to "w".
  422. // If an error occurs executing the template or writing its output,
  423. // execution stops, but partial results may already have been written to
  424. // the output writer.
  425. //
  426. // If "layoutName" and "v.defaultLayoutName" are both empty then
  427. // the template is executed without a layout.
  428. //
  429. // A template may be executed safely in parallel, although if parallel
  430. // executions share a Writer the output may be interleaved.
  431. func (v *Blocks) ExecuteTemplate(w io.Writer, tmplName, layoutName string, data interface{}) error {
  432. if v.reload {
  433. if err := v.Load(); err != nil {
  434. return err
  435. }
  436. }
  437. if layoutName == "" {
  438. layoutName = v.defaultLayoutName
  439. }
  440. return v.executeTemplate(w, tmplName, layoutName, data)
  441. }
  442. func (v *Blocks) executeTemplate(w io.Writer, tmplName, layoutName string, data interface{}) error {
  443. if layoutName != "" {
  444. tmpl := v.getTemplateWithLayout(tmplName, layoutName)
  445. if tmpl == nil {
  446. return ErrNotExist{layoutName}
  447. }
  448. // Full Template Name:
  449. // fmt.Printf("executing %s.%s\n", layoutName, tmplName)
  450. // Source:
  451. // fmt.Println(tmpl.Tree.Root.String())
  452. return tmpl.Execute(w, data)
  453. }
  454. tmpl, ok := v.Templates[tmplName]
  455. if !ok {
  456. return ErrNotExist{tmplName}
  457. }
  458. // if httpResponseWriter, ok := w.(http.ResponseWriter); ok {
  459. // check if content-type exists, and if it's not:
  460. // httpResponseWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
  461. // } ^ No, leave it for the caller.
  462. // if layoutName != "" {
  463. // return tmpl.ExecuteTemplate(w, layoutName, data)
  464. // }
  465. return tmpl.Execute(w, data)
  466. }
  467. // ParseTemplate parses a template based on its "tmplName" name and returns the result.
  468. // Note that, this does not reload the templates on each call if Reload was set to true.
  469. // To refresh the templates you have to manually call the `Load` upfront.
  470. func (v *Blocks) ParseTemplate(tmplName, layoutName string, data interface{}) (string, error) {
  471. b := v.bufferPool.Get()
  472. // use the unexported method so it does not re-reload the templates on each partial one
  473. // when Reload was set to true.
  474. err := v.executeTemplate(b, tmplName, layoutName, data)
  475. contents := b.String()
  476. v.bufferPool.Put(b)
  477. return contents, err
  478. }
  479. // PartialFunc returns the parsed result of the "partialName" template's "content" block.
  480. func (v *Blocks) PartialFunc(partialName string, data interface{}) (template.HTML, error) {
  481. // contents, err := v.ParseTemplate(partialName, "content", data)
  482. // if err != nil {
  483. // return "", err
  484. // }
  485. contents, err := v.ParseTemplate(partialName, "", data)
  486. if err != nil {
  487. return "", err
  488. }
  489. return template.HTML(contents), nil
  490. }
  491. // ContextKeyType is the type which `Set`
  492. // request's context value is using to store
  493. // the current Blocks engine.
  494. //
  495. // See `Set` and `Get`.
  496. type ContextKeyType struct{}
  497. // ContextKey is the request's context value for a blocks engine.
  498. //
  499. // See `Set` and `Get`.
  500. var ContextKey ContextKeyType
  501. // Set returns a handler wrapper which sets the current
  502. // view engine to this "v" Blocks.
  503. // Useful when the caller needs multiple Blocks engine instances per group of routes.
  504. // Note that this is entirely optional, the caller could just wrap a function of func(v *Blocks)
  505. // and return a handler which will directly use it.
  506. // See `Get` too.
  507. func Set(v *Blocks) func(http.Handler) http.Handler {
  508. return func(next http.Handler) http.Handler {
  509. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  510. r = r.WithContext(context.WithValue(r.Context(), ContextKey, v))
  511. next.ServeHTTP(w, r)
  512. })
  513. }
  514. }
  515. // Get retrieves the associated Blocks view engine retrieved from the request's context.
  516. // See `Set` too.
  517. func Get(r *http.Request) *Blocks {
  518. value := r.Context().Value(ContextKey)
  519. if value == nil {
  520. return nil
  521. }
  522. v, ok := value.(*Blocks)
  523. if !ok {
  524. return nil
  525. }
  526. return v
  527. }
  528. func withSuffix(s string, suf string) string {
  529. if len(s) == 0 {
  530. return ""
  531. }
  532. if !strings.HasSuffix(s, suf) {
  533. s += suf
  534. }
  535. return s
  536. }
  537. func relDir(dir string) string {
  538. if dir == "." {
  539. return ""
  540. }
  541. if dir == "" || dir == "/" {
  542. return ""
  543. }
  544. return strings.TrimPrefix(strings.TrimPrefix(dir, "."), "/")
  545. }
  546. func trimDir(s string, dir string) string {
  547. dir = withSuffix(relDir(dir), "/")
  548. return strings.TrimPrefix(s, dir)
  549. }
  550. func (v *Blocks) getTemplateWithLayout(tmplName, layoutName string) *template.Template {
  551. key := makeLayoutTemplateName(tmplName, layoutName)
  552. return v.Layouts[key]
  553. }
  554. func makeLayoutTemplateName(tmplName, layoutName string) string {
  555. return layoutName + tmplName
  556. }