Skip to content

Commit 7231e86

Browse files
committed
major: change the way we understand the layout files, no need of a specific directory to be set
1 parent 78bb60c commit 7231e86

File tree

3 files changed

+174
-185
lines changed

3 files changed

+174
-185
lines changed

Diff for: _examples/static-generator/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module static-generator
33
go 1.23
44

55
require (
6-
github.com/kataras/blocks v0.0.9
6+
github.com/kataras/blocks v0.0.11
77
gopkg.in/yaml.v3 v3.0.1
88
)
99

Diff for: blocks.go

+74-184
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"io"
88
"io/fs"
99
"net/http"
10-
"os"
1110
"path"
1211
"path/filepath"
1312
"regexp"
@@ -41,7 +40,7 @@ type Blocks struct {
4140
fs fs.FS
4241

4342
rootDir string // it always set to "/" as the RootDir method changes the filesystem to sub one.
44-
layoutDir string // /layouts
43+
layoutDir string // the default is "/layouts". Empty, means the end-developer should provide the relative to root path of the layout template.
4544
layoutFuncs template.FuncMap
4645
tmplFuncs template.FuncMap
4746
defaultLayoutName string // the default layout if it's missing from the `ExecuteTemplate`.
@@ -59,14 +58,11 @@ type Blocks struct {
5958

6059
// Root, Templates and Layouts can be accessed after `Load`.
6160
Root *template.Template
62-
templatesContents map[string]string
6361
Templates, Layouts map[string]*template.Template
6462
}
6563

6664
// New returns a fresh Blocks engine instance.
6765
// It loads the templates based on the given fs FileSystem (or string).
68-
// By default the layout files should be located at "$rootDir/layouts" sub-directory (see `RootDir` method),
69-
// change this behavior can be achieved through `LayoutDir` method before `Load/LoadContext`.
7066
// To set a default layout name for an empty layout definition on `ExecuteTemplate/ParseTemplate`
7167
// use the `DefaultLayout` method.
7268
//
@@ -101,11 +97,10 @@ func New(fs any) *Blocks {
10197
// Note that, this is parsed, the delims can be configured later on.
10298
Root: template.Must(template.New("root").
10399
Parse(`{{ define "root" }} {{ template "content" . }} {{ end }}`)),
104-
templatesContents: make(map[string]string),
105-
Templates: make(map[string]*template.Template),
106-
Layouts: make(map[string]*template.Template),
107-
reload: false,
108-
bufferPool: new(bytebufferpool.Pool),
100+
Templates: make(map[string]*template.Template),
101+
Layouts: make(map[string]*template.Template),
102+
reload: false,
103+
bufferPool: new(bytebufferpool.Pool),
109104
}
110105

111106
v.Root.Funcs(translateFuncs(v, builtins))
@@ -250,7 +245,8 @@ func (v *Blocks) RootDir(root string) *Blocks {
250245

251246
// LayoutDir sets a custom layouts directory,
252247
// always relative to the "rootDir" one.
253-
// Layouts are recognised by their prefix names.
248+
// This can be used to trim the layout directory from the template's name,
249+
// for example: layouts/main -> main on ExecuteTemplate's "layoutName" argument.
254250
// Defaults to "layouts".
255251
func (v *Blocks) LayoutDir(relToDirLayoutDir string) *Blocks {
256252
v.layoutDir = filepath.ToSlash(relToDirLayoutDir)
@@ -300,7 +296,6 @@ func (v *Blocks) LoadWithContext(ctx context.Context) error {
300296
v.mu.Lock()
301297
defer v.mu.Unlock()
302298

303-
clearMap(v.templatesContents)
304299
clearMap(v.Templates)
305300
clearMap(v.Layouts)
306301

@@ -311,213 +306,101 @@ func (v *Blocks) load(ctx context.Context) error {
311306
ctx, cancel := context.WithCancel(ctx)
312307
defer cancel()
313308

314-
var (
315-
layouts []string
316-
mu sync.RWMutex
317-
)
318-
319-
var assetNames []string // all assets names.
320-
err := walk(v.fs, "", func(path string, info os.FileInfo, err error) error {
321-
if err != nil {
322-
return err
323-
}
324-
325-
if info.IsDir() || !info.Mode().IsRegular() {
326-
return nil
327-
}
328-
329-
assetNames = append(assetNames, path)
330-
return nil
331-
})
309+
filesMap, err := readFiles(ctx, v.fs, v.rootDir)
332310
if err != nil {
333311
return err
334312
}
335313

336-
if len(assetNames) == 0 {
337-
return fmt.Errorf("no templates found")
314+
if len(filesMap) == 0 {
315+
return fmt.Errorf("no template files found")
338316
}
339317

340-
// +---------------------+
341-
// | Template Assets |
342-
// +---------------------+
343-
344-
loadAsset := func(assetName string) error {
345-
if dir := relDir(v.rootDir); dir != "" && !strings.HasPrefix(assetName, dir) {
346-
// If contains a not empty directory and the asset name does not belong there
347-
// then skip it, useful on bindata assets when they
348-
// may contain other files that are not templates.
349-
return nil
350-
}
351-
352-
if layoutDir := relDir(v.layoutDir); layoutDir != "" &&
353-
strings.HasPrefix(assetName, layoutDir) {
354-
// it's a layout template file, add it to layouts and skip,
355-
// in order to add them to each template file.
356-
mu.Lock()
357-
358-
layouts = append(layouts, assetName)
359-
mu.Unlock()
360-
return nil
361-
}
362-
363-
tmplName := trimDir(assetName, v.rootDir)
364-
365-
ext := path.Ext(assetName)
366-
tmplName = strings.TrimSuffix(tmplName, ext)
367-
tmplName = strings.TrimPrefix(tmplName, "/")
368-
369-
extParser := v.extensionHandler[ext]
370-
hasHandler := extParser != nil // it may exists but if it's nil then we can't use it.
371-
if v.extension != "" {
372-
if ext != v.extension && !hasHandler {
373-
return nil
374-
}
375-
}
376-
377-
contents, err := asset(v.fs, assetName)
378-
if err != nil {
379-
return err
380-
}
381-
382-
select {
383-
case <-ctx.Done():
384-
return ctx.Err()
385-
default:
386-
break
387-
}
388-
389-
if hasHandler {
390-
contents, err = extParser(contents)
318+
// templatesContents is used to keep the contents of each content template in order
319+
// to be parsed on each layout, so all content templates have all layouts available,
320+
// and all layouts can inject all content templates.
321+
contentTemplates := make(map[string]string)
322+
// layoutTemplates is used to keep the contents of each layout template.
323+
layoutTemplates := make(map[string]string)
324+
325+
// collect all content and layout template contents.
326+
for filename, data := range filesMap {
327+
ext := path.Ext(filename)
328+
if extParser := v.extensionHandler[ext]; extParser != nil {
329+
data, err = extParser(data) // let the parser modify the contents.
391330
if err != nil {
392331
// custom parsers may return a non-nil error,
393332
// e.g. less or scss files
394333
// and, yes, they can be used as templates too,
395334
// because they are wrapped by a template block if necessary.
396335
return err
397336
}
337+
} else if ext != v.extension {
338+
continue // extension not match with the given template extension and the extension handler is nil.
398339
}
399340

400-
mu.Lock()
401-
v.Templates[tmplName], err = v.Root.Clone() // template.New(tmplName)
402-
mu.Unlock()
403-
if err != nil {
404-
return err
405-
}
406-
407-
str := string(contents)
341+
contents := string(data)
408342
// Remove HTML comments.
409-
str = removeComments(str)
343+
contents = removeComments(contents)
410344

411-
// Check if has custom define inside (skipping any html comments).
412-
// And if not, automatically inject the define content block.
413-
if !strings.Contains(str, defineStart(v.left)) && !strings.Contains(str, defineStartNoSpace(v.left)) {
414-
str = defineContentStart(v.left, v.right) + str + defineContentEnd(v.left, v.right)
345+
tmplName := trimDir(filename, v.rootDir)
346+
tmplName = strings.TrimPrefix(tmplName, "/")
347+
tmplName = strings.TrimSuffix(tmplName, v.extension)
348+
349+
if isLayoutTemplate(contents) {
350+
// Replace any {{ yield . }} with {{ template "content" . }}.
351+
contents = replaceYieldWithTemplateContent(contents)
352+
// Remove any given layout dir.
353+
tmplName = trimDir(tmplName, v.layoutDir)
354+
layoutTemplates[tmplName] = contents
355+
continue
415356
}
416357

417-
mu.Lock()
418-
_, err = v.Templates[tmplName].Funcs(v.tmplFuncs).Parse(str)
419-
if err != nil {
420-
err = fmt.Errorf("%w: %s: %s", err, tmplName, str)
421-
} else {
422-
v.templatesContents[tmplName] = str
358+
// Inject the define content block.
359+
if !strings.Contains(contents, defineStart(v.left)) && !strings.Contains(contents, defineStartNoSpace(v.left)) {
360+
contents = defineContentStart(v.left, v.right) + contents + defineContentEnd(v.left, v.right)
423361
}
424-
mu.Unlock()
425-
return err
426-
}
427-
428-
var (
429-
wg sync.WaitGroup
430-
errOnce sync.Once
431-
)
432-
433-
for _, assetName := range assetNames {
434-
wg.Add(1)
435-
436-
go func(assetName string) {
437-
defer wg.Done()
438362

439-
if loadErr := loadAsset(assetName); loadErr != nil {
440-
errOnce.Do(func() {
441-
err = loadErr
442-
cancel()
443-
})
444-
}
445-
}(assetName)
363+
contentTemplates[tmplName] = contents
446364
}
447365

448-
wg.Wait()
449-
if err != nil {
450-
return err
451-
}
452-
453-
// +---------------------+
454-
// | Layouts |
455-
// +---------------------+
456-
loadLayout := func(layout string) error {
457-
contents, err := asset(v.fs, layout)
366+
// Load the content templates first.
367+
for tmplName, contents := range contentTemplates {
368+
tmpl, err := v.Root.Clone()
458369
if err != nil {
459370
return err
460371
}
461372

462-
select {
463-
case <-ctx.Done():
464-
return ctx.Err()
465-
default:
466-
break
373+
_, err = tmpl.Funcs(v.tmplFuncs).Parse(contents)
374+
if err != nil {
375+
return fmt.Errorf("%w: %s: %s", err, tmplName, contents)
467376
}
468377

469-
name := trimDir(layout, v.layoutDir) // if we want rel-to-the-dir instead we just replace with v.rootDir.
470-
name = strings.TrimSuffix(name, v.extension)
471-
str := string(contents)
472-
// Strip HTML comments.
473-
str = removeComments(str)
474-
// Also replace any {{ yield . }} with {{ template "content" . }}.
475-
str = replaceYieldWithTemplateContent(str)
476-
477-
builtins := translateFuncs(v, builtins)
478-
for tmplName, tmplContents := range v.templatesContents {
479-
// Make new layout template for each of the templates,
378+
v.Templates[tmplName] = tmpl
379+
}
380+
381+
// Load the layout templates.
382+
layoutBuiltinFuncs := translateFuncs(v, builtins)
383+
for tmplName, contents := range layoutTemplates {
384+
for contentTmplName, contentTmplContents := range contentTemplates {
385+
// Make new layout template for each of the content templates,
480386
// the key of the layout in map will be the layoutName+tmplName.
481387
// So each template owns all layouts. This fixes the issue with the new {{ block }} and the usual {{ define }} directives.
482-
layoutTmpl, err := template.New(name).Funcs(builtins).Funcs(v.layoutFuncs).Parse(str)
388+
layoutTmpl, err := template.New(tmplName).Funcs(layoutBuiltinFuncs).Funcs(v.layoutFuncs).Parse(contents)
483389
if err != nil {
484-
return fmt.Errorf("%w: for layout: %s", err, name)
390+
return fmt.Errorf("%w: for layout: %s", err, tmplName)
485391
}
486392

487-
_, err = layoutTmpl.Funcs(v.tmplFuncs).Parse(tmplContents)
393+
_, err = layoutTmpl.Funcs(v.tmplFuncs).Parse(contentTmplContents)
488394
if err != nil {
489-
return fmt.Errorf("%w: layout: %s: for template: %s", err, name, tmplName)
395+
return fmt.Errorf("%w: layout: %s: for template: %s", err, tmplName, contentTmplName)
490396
}
491397

492-
key := makeLayoutTemplateName(tmplName, name)
493-
mu.Lock()
398+
key := makeLayoutTemplateName(contentTmplName, tmplName)
494399
v.Layouts[key] = layoutTmpl
495-
mu.Unlock()
496400
}
497-
498-
return nil
499-
}
500-
501-
for _, layout := range layouts {
502-
wg.Add(1)
503-
go func(layout string) {
504-
defer wg.Done()
505-
506-
if loadErr := loadLayout(layout); loadErr != nil {
507-
errOnce.Do(func() {
508-
err = loadErr
509-
cancel()
510-
})
511-
}
512-
}(layout)
513401
}
514402

515-
wg.Wait()
516-
517-
// Clear the cached contents, we don't need them from now on.
518-
clearMap(v.templatesContents)
519-
520-
return err
403+
return nil
521404
}
522405

523406
// ExecuteTemplate applies the template associated with "tmplName"
@@ -547,19 +430,15 @@ func (v *Blocks) ExecuteTemplate(w io.Writer, tmplName, layoutName string, data
547430

548431
func (v *Blocks) executeTemplate(w io.Writer, tmplName, layoutName string, data any) error {
549432
tmplName = strings.TrimSuffix(tmplName, v.extension) // trim any extension provided by mistake or by migrating from other engines.
550-
layoutName = strings.TrimSuffix(layoutName, v.extension)
551433

552434
if layoutName != "" {
435+
layoutName = strings.TrimSuffix(layoutName, v.extension)
436+
layoutName = strings.TrimPrefix(layoutName, v.layoutDir)
553437
tmpl := v.getTemplateWithLayout(tmplName, layoutName)
554438
if tmpl == nil {
555439
return ErrNotExist{layoutName}
556440
}
557441

558-
// Full Template Name:
559-
// fmt.Printf("executing %s.%s\n", layoutName, tmplName)
560-
// Source:
561-
// fmt.Println(tmpl.Tree.Root.String())
562-
563442
return tmpl.Execute(w, data)
564443
}
565444

@@ -673,6 +552,9 @@ func relDir(dir string) string {
673552
}
674553

675554
func trimDir(s string, dir string) string {
555+
if dir == "" {
556+
return s
557+
}
676558
dir = withSuffix(relDir(dir), "/")
677559
return strings.TrimPrefix(s, dir)
678560
}
@@ -700,6 +582,14 @@ func replaceYieldWithTemplateContent(input string) string {
700582
return yieldMatchRegex.ReplaceAllString(input, `{{ template "content" $1 }}`)
701583
}
702584

585+
// Regex pattern to match various forms of {{ template "content" ... }} and {{ yield ... }}
586+
var layoutPatternRegex = regexp.MustCompile(`{{-?\s*(template\s*"content"\s*[^}]*|yield\s*[^}]*)\s*-?}}`)
587+
588+
// isLayoutTemplate checks if the template contents indicate it is a layout template.
589+
func isLayoutTemplate(contents string) bool {
590+
return layoutPatternRegex.MatchString(contents)
591+
}
592+
703593
func clearMap[M ~map[K]V, K comparable, V any](m M) {
704594
for k := range m {
705595
delete(m, k)

0 commit comments

Comments
 (0)