7
7
"io"
8
8
"io/fs"
9
9
"net/http"
10
- "os"
11
10
"path"
12
11
"path/filepath"
13
12
"regexp"
@@ -41,7 +40,7 @@ type Blocks struct {
41
40
fs fs.FS
42
41
43
42
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.
45
44
layoutFuncs template.FuncMap
46
45
tmplFuncs template.FuncMap
47
46
defaultLayoutName string // the default layout if it's missing from the `ExecuteTemplate`.
@@ -59,14 +58,11 @@ type Blocks struct {
59
58
60
59
// Root, Templates and Layouts can be accessed after `Load`.
61
60
Root * template.Template
62
- templatesContents map [string ]string
63
61
Templates , Layouts map [string ]* template.Template
64
62
}
65
63
66
64
// New returns a fresh Blocks engine instance.
67
65
// 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`.
70
66
// To set a default layout name for an empty layout definition on `ExecuteTemplate/ParseTemplate`
71
67
// use the `DefaultLayout` method.
72
68
//
@@ -101,11 +97,10 @@ func New(fs any) *Blocks {
101
97
// Note that, this is parsed, the delims can be configured later on.
102
98
Root : template .Must (template .New ("root" ).
103
99
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 ),
109
104
}
110
105
111
106
v .Root .Funcs (translateFuncs (v , builtins ))
@@ -250,7 +245,8 @@ func (v *Blocks) RootDir(root string) *Blocks {
250
245
251
246
// LayoutDir sets a custom layouts directory,
252
247
// 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.
254
250
// Defaults to "layouts".
255
251
func (v * Blocks ) LayoutDir (relToDirLayoutDir string ) * Blocks {
256
252
v .layoutDir = filepath .ToSlash (relToDirLayoutDir )
@@ -300,7 +296,6 @@ func (v *Blocks) LoadWithContext(ctx context.Context) error {
300
296
v .mu .Lock ()
301
297
defer v .mu .Unlock ()
302
298
303
- clearMap (v .templatesContents )
304
299
clearMap (v .Templates )
305
300
clearMap (v .Layouts )
306
301
@@ -311,213 +306,101 @@ func (v *Blocks) load(ctx context.Context) error {
311
306
ctx , cancel := context .WithCancel (ctx )
312
307
defer cancel ()
313
308
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 )
332
310
if err != nil {
333
311
return err
334
312
}
335
313
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" )
338
316
}
339
317
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.
391
330
if err != nil {
392
331
// custom parsers may return a non-nil error,
393
332
// e.g. less or scss files
394
333
// and, yes, they can be used as templates too,
395
334
// because they are wrapped by a template block if necessary.
396
335
return err
397
336
}
337
+ } else if ext != v .extension {
338
+ continue // extension not match with the given template extension and the extension handler is nil.
398
339
}
399
340
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 )
408
342
// Remove HTML comments.
409
- str = removeComments (str )
343
+ contents = removeComments (contents )
410
344
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
415
356
}
416
357
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 )
423
361
}
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 ()
438
362
439
- if loadErr := loadAsset (assetName ); loadErr != nil {
440
- errOnce .Do (func () {
441
- err = loadErr
442
- cancel ()
443
- })
444
- }
445
- }(assetName )
363
+ contentTemplates [tmplName ] = contents
446
364
}
447
365
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 ()
458
369
if err != nil {
459
370
return err
460
371
}
461
372
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 )
467
376
}
468
377
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,
480
386
// the key of the layout in map will be the layoutName+tmplName.
481
387
// 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 )
483
389
if err != nil {
484
- return fmt .Errorf ("%w: for layout: %s" , err , name )
390
+ return fmt .Errorf ("%w: for layout: %s" , err , tmplName )
485
391
}
486
392
487
- _ , err = layoutTmpl .Funcs (v .tmplFuncs ).Parse (tmplContents )
393
+ _ , err = layoutTmpl .Funcs (v .tmplFuncs ).Parse (contentTmplContents )
488
394
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 )
490
396
}
491
397
492
- key := makeLayoutTemplateName (tmplName , name )
493
- mu .Lock ()
398
+ key := makeLayoutTemplateName (contentTmplName , tmplName )
494
399
v .Layouts [key ] = layoutTmpl
495
- mu .Unlock ()
496
400
}
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 )
513
401
}
514
402
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
521
404
}
522
405
523
406
// ExecuteTemplate applies the template associated with "tmplName"
@@ -547,19 +430,15 @@ func (v *Blocks) ExecuteTemplate(w io.Writer, tmplName, layoutName string, data
547
430
548
431
func (v * Blocks ) executeTemplate (w io.Writer , tmplName , layoutName string , data any ) error {
549
432
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 )
551
433
552
434
if layoutName != "" {
435
+ layoutName = strings .TrimSuffix (layoutName , v .extension )
436
+ layoutName = strings .TrimPrefix (layoutName , v .layoutDir )
553
437
tmpl := v .getTemplateWithLayout (tmplName , layoutName )
554
438
if tmpl == nil {
555
439
return ErrNotExist {layoutName }
556
440
}
557
441
558
- // Full Template Name:
559
- // fmt.Printf("executing %s.%s\n", layoutName, tmplName)
560
- // Source:
561
- // fmt.Println(tmpl.Tree.Root.String())
562
-
563
442
return tmpl .Execute (w , data )
564
443
}
565
444
@@ -673,6 +552,9 @@ func relDir(dir string) string {
673
552
}
674
553
675
554
func trimDir (s string , dir string ) string {
555
+ if dir == "" {
556
+ return s
557
+ }
676
558
dir = withSuffix (relDir (dir ), "/" )
677
559
return strings .TrimPrefix (s , dir )
678
560
}
@@ -700,6 +582,14 @@ func replaceYieldWithTemplateContent(input string) string {
700
582
return yieldMatchRegex .ReplaceAllString (input , `{{ template "content" $1 }}` )
701
583
}
702
584
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
+
703
593
func clearMap [M ~ map [K ]V , K comparable , V any ](m M ) {
704
594
for k := range m {
705
595
delete (m , k )
0 commit comments