template.go (27465B)
1 // Copyright 2019 The Hugo Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13
14 package tplimpl
15
16 import (
17 "bytes"
18 "context"
19 "embed"
20 "fmt"
21 "io"
22 "io/fs"
23 "os"
24 "path/filepath"
25 "reflect"
26 "regexp"
27 "sort"
28 "strings"
29 "sync"
30 "time"
31 "unicode"
32 "unicode/utf8"
33
34 "github.com/gohugoio/hugo/common/types"
35
36 "github.com/gohugoio/hugo/helpers"
37
38 "github.com/gohugoio/hugo/output"
39
40 "github.com/gohugoio/hugo/deps"
41 "github.com/spf13/afero"
42
43 "github.com/gohugoio/hugo/common/herrors"
44 "github.com/gohugoio/hugo/hugofs"
45 "github.com/gohugoio/hugo/hugofs/files"
46
47 htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
48 texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
49
50 "github.com/gohugoio/hugo/identity"
51 "github.com/gohugoio/hugo/tpl"
52 )
53
54 const (
55 textTmplNamePrefix = "_text/"
56
57 shortcodesPathPrefix = "shortcodes/"
58 internalPathPrefix = "_internal/"
59 baseFileBase = "baseof"
60 )
61
62 // The identifiers may be truncated in the log, e.g.
63 // "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image"
64 // We need this to identify position in templates with base templates applied.
65 var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`)
66
67 var embeddedTemplatesAliases = map[string][]string{
68 "shortcodes/twitter.html": {"shortcodes/tweet.html"},
69 }
70
71 var (
72 _ tpl.TemplateManager = (*templateExec)(nil)
73 _ tpl.TemplateHandler = (*templateExec)(nil)
74 _ tpl.TemplateFuncGetter = (*templateExec)(nil)
75 _ tpl.TemplateFinder = (*templateExec)(nil)
76 _ tpl.UnusedTemplatesProvider = (*templateExec)(nil)
77
78 _ tpl.Template = (*templateState)(nil)
79 _ tpl.Info = (*templateState)(nil)
80 )
81
82 var baseTemplateDefineRe = regexp.MustCompile(`^{{-?\s*define`)
83
84 // needsBaseTemplate returns true if the first non-comment template block is a
85 // define block.
86 // If a base template does not exist, we will handle that when it's used.
87 func needsBaseTemplate(templ string) bool {
88 idx := -1
89 inComment := false
90 for i := 0; i < len(templ); {
91 if !inComment && strings.HasPrefix(templ[i:], "{{/*") {
92 inComment = true
93 i += 4
94 } else if inComment && strings.HasPrefix(templ[i:], "*/}}") {
95 inComment = false
96 i += 4
97 } else {
98 r, size := utf8.DecodeRuneInString(templ[i:])
99 if !inComment {
100 if strings.HasPrefix(templ[i:], "{{") {
101 idx = i
102 break
103 } else if !unicode.IsSpace(r) {
104 break
105 }
106 }
107 i += size
108 }
109 }
110
111 if idx == -1 {
112 return false
113 }
114
115 return baseTemplateDefineRe.MatchString(templ[idx:])
116 }
117
118 func newIdentity(name string) identity.Manager {
119 return identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name))
120 }
121
122 func newStandaloneTextTemplate(funcs map[string]any) tpl.TemplateParseFinder {
123 return &textTemplateWrapperWithLock{
124 RWMutex: &sync.RWMutex{},
125 Template: texttemplate.New("").Funcs(funcs),
126 }
127 }
128
129 func newTemplateExec(d *deps.Deps) (*templateExec, error) {
130 exec, funcs := newTemplateExecuter(d)
131 funcMap := make(map[string]any)
132 for k, v := range funcs {
133 funcMap[k] = v.Interface()
134 }
135
136 var templateUsageTracker map[string]templateInfo
137 if d.Cfg.GetBool("printUnusedTemplates") {
138 templateUsageTracker = make(map[string]templateInfo)
139 }
140
141 h := &templateHandler{
142 nameBaseTemplateName: make(map[string]string),
143 transformNotFound: make(map[string]*templateState),
144 identityNotFound: make(map[string][]identity.Manager),
145
146 shortcodes: make(map[string]*shortcodeTemplates),
147 templateInfo: make(map[string]tpl.Info),
148 baseof: make(map[string]templateInfo),
149 needsBaseof: make(map[string]templateInfo),
150
151 main: newTemplateNamespace(funcMap),
152
153 Deps: d,
154 layoutHandler: output.NewLayoutHandler(),
155 layoutsFs: d.BaseFs.Layouts.Fs,
156 layoutTemplateCache: make(map[layoutCacheKey]tpl.Template),
157
158 templateUsageTracker: templateUsageTracker,
159 }
160
161 if err := h.loadEmbedded(); err != nil {
162 return nil, err
163 }
164
165 if err := h.loadTemplates(); err != nil {
166 return nil, err
167 }
168
169 e := &templateExec{
170 d: d,
171 executor: exec,
172 funcs: funcs,
173 templateHandler: h,
174 }
175
176 d.SetTmpl(e)
177 d.SetTextTmpl(newStandaloneTextTemplate(funcMap))
178
179 if d.WithTemplate != nil {
180 if err := d.WithTemplate(e); err != nil {
181 return nil, err
182 }
183 }
184
185 return e, nil
186 }
187
188 func newTemplateNamespace(funcs map[string]any) *templateNamespace {
189 return &templateNamespace{
190 prototypeHTML: htmltemplate.New("").Funcs(funcs),
191 prototypeText: texttemplate.New("").Funcs(funcs),
192 templateStateMap: &templateStateMap{
193 templates: make(map[string]*templateState),
194 },
195 }
196 }
197
198 func newTemplateState(templ tpl.Template, info templateInfo) *templateState {
199 return &templateState{
200 info: info,
201 typ: info.resolveType(),
202 Template: templ,
203 Manager: newIdentity(info.name),
204 parseInfo: tpl.DefaultParseInfo,
205 }
206 }
207
208 type layoutCacheKey struct {
209 d output.LayoutDescriptor
210 f string
211 }
212
213 type templateExec struct {
214 d *deps.Deps
215 executor texttemplate.Executer
216 funcs map[string]reflect.Value
217
218 *templateHandler
219 }
220
221 func (t templateExec) Clone(d *deps.Deps) *templateExec {
222 exec, funcs := newTemplateExecuter(d)
223 t.executor = exec
224 t.funcs = funcs
225 t.d = d
226 return &t
227 }
228
229 func (t *templateExec) Execute(templ tpl.Template, wr io.Writer, data any) error {
230 return t.ExecuteWithContext(context.Background(), templ, wr, data)
231 }
232
233 func (t *templateExec) ExecuteWithContext(ctx context.Context, templ tpl.Template, wr io.Writer, data any) error {
234 if rlocker, ok := templ.(types.RLocker); ok {
235 rlocker.RLock()
236 defer rlocker.RUnlock()
237 }
238 if t.Metrics != nil {
239 defer t.Metrics.MeasureSince(templ.Name(), time.Now())
240 }
241
242 if t.templateUsageTracker != nil {
243 if ts, ok := templ.(*templateState); ok {
244 t.templateUsageTrackerMu.Lock()
245 if _, found := t.templateUsageTracker[ts.Name()]; !found {
246 t.templateUsageTracker[ts.Name()] = ts.info
247 }
248
249 if !ts.baseInfo.IsZero() {
250 if _, found := t.templateUsageTracker[ts.baseInfo.name]; !found {
251 t.templateUsageTracker[ts.baseInfo.name] = ts.baseInfo
252 }
253 }
254 t.templateUsageTrackerMu.Unlock()
255 }
256 }
257
258 execErr := t.executor.ExecuteWithContext(ctx, templ, wr, data)
259 if execErr != nil {
260 execErr = t.addFileContext(templ, execErr)
261 }
262 return execErr
263 }
264
265 func (t *templateExec) UnusedTemplates() []tpl.FileInfo {
266 if t.templateUsageTracker == nil {
267 return nil
268 }
269 var unused []tpl.FileInfo
270
271 for _, ti := range t.needsBaseof {
272 if _, found := t.templateUsageTracker[ti.name]; !found {
273 unused = append(unused, ti)
274 }
275 }
276
277 for _, ti := range t.baseof {
278 if _, found := t.templateUsageTracker[ti.name]; !found {
279 unused = append(unused, ti)
280 }
281 }
282
283 for _, ts := range t.main.templates {
284 ti := ts.info
285 if strings.HasPrefix(ti.name, "_internal/") || ti.realFilename == "" {
286 continue
287 }
288
289 if _, found := t.templateUsageTracker[ti.name]; !found {
290 unused = append(unused, ti)
291 }
292 }
293
294 sort.Slice(unused, func(i, j int) bool {
295 return unused[i].Name() < unused[j].Name()
296 })
297
298 return unused
299 }
300
301 func (t *templateExec) GetFunc(name string) (reflect.Value, bool) {
302 v, found := t.funcs[name]
303 return v, found
304 }
305
306 func (t *templateExec) MarkReady() error {
307 var err error
308 t.readyInit.Do(func() {
309 // We only need the clones if base templates are in use.
310 if len(t.needsBaseof) > 0 {
311 err = t.main.createPrototypes()
312 }
313 })
314
315 return err
316 }
317
318 type templateHandler struct {
319 main *templateNamespace
320 needsBaseof map[string]templateInfo
321 baseof map[string]templateInfo
322
323 readyInit sync.Once
324
325 // This is the filesystem to load the templates from. All the templates are
326 // stored in the root of this filesystem.
327 layoutsFs afero.Fs
328
329 layoutHandler *output.LayoutHandler
330
331 layoutTemplateCache map[layoutCacheKey]tpl.Template
332 layoutTemplateCacheMu sync.RWMutex
333
334 *deps.Deps
335
336 // Used to get proper filenames in errors
337 nameBaseTemplateName map[string]string
338
339 // Holds name and source of template definitions not found during the first
340 // AST transformation pass.
341 transformNotFound map[string]*templateState
342
343 // Holds identities of templates not found during first pass.
344 identityNotFound map[string][]identity.Manager
345
346 // shortcodes maps shortcode name to template variants
347 // (language, output format etc.) of that shortcode.
348 shortcodes map[string]*shortcodeTemplates
349
350 // templateInfo maps template name to some additional information about that template.
351 // Note that for shortcodes that same information is embedded in the
352 // shortcodeTemplates type.
353 templateInfo map[string]tpl.Info
354
355 // May be nil.
356 templateUsageTracker map[string]templateInfo
357 templateUsageTrackerMu sync.Mutex
358 }
359
360 // AddTemplate parses and adds a template to the collection.
361 // Templates with name prefixed with "_text" will be handled as plain
362 // text templates.
363 func (t *templateHandler) AddTemplate(name, tpl string) error {
364 templ, err := t.addTemplateTo(t.newTemplateInfo(name, tpl), t.main)
365 if err == nil {
366 t.applyTemplateTransformers(t.main, templ)
367 }
368 return err
369 }
370
371 func (t *templateHandler) Lookup(name string) (tpl.Template, bool) {
372 templ, found := t.main.Lookup(name)
373 if found {
374 return templ, true
375 }
376
377 return nil, false
378 }
379
380 func (t *templateHandler) LookupLayout(d output.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
381 key := layoutCacheKey{d, f.Name}
382 t.layoutTemplateCacheMu.RLock()
383 if cacheVal, found := t.layoutTemplateCache[key]; found {
384 t.layoutTemplateCacheMu.RUnlock()
385 return cacheVal, true, nil
386 }
387 t.layoutTemplateCacheMu.RUnlock()
388
389 t.layoutTemplateCacheMu.Lock()
390 defer t.layoutTemplateCacheMu.Unlock()
391
392 templ, found, err := t.findLayout(d, f)
393 if err == nil && found {
394 t.layoutTemplateCache[key] = templ
395 return templ, true, nil
396 }
397
398 return nil, false, err
399 }
400
401 // This currently only applies to shortcodes and what we get here is the
402 // shortcode name.
403 func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
404 name = templateBaseName(templateShortcode, name)
405 s, found := t.shortcodes[name]
406 if !found {
407 return nil, false, false
408 }
409
410 sv, found := s.fromVariants(variants)
411 if !found {
412 return nil, false, false
413 }
414
415 more := len(s.variants) > 1
416
417 return sv.ts, true, more
418 }
419
420 // LookupVariants returns all variants of name, nil if none found.
421 func (t *templateHandler) LookupVariants(name string) []tpl.Template {
422 name = templateBaseName(templateShortcode, name)
423 s, found := t.shortcodes[name]
424 if !found {
425 return nil
426 }
427
428 variants := make([]tpl.Template, len(s.variants))
429 for i := 0; i < len(variants); i++ {
430 variants[i] = s.variants[i].ts
431 }
432
433 return variants
434 }
435
436 func (t *templateHandler) HasTemplate(name string) bool {
437 if _, found := t.baseof[name]; found {
438 return true
439 }
440
441 if _, found := t.needsBaseof[name]; found {
442 return true
443 }
444
445 _, found := t.Lookup(name)
446 return found
447 }
448
449 func (t *templateHandler) findLayout(d output.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
450 layouts, _ := t.layoutHandler.For(d, f)
451 for _, name := range layouts {
452 templ, found := t.main.Lookup(name)
453 if found {
454 return templ, true, nil
455 }
456
457 overlay, found := t.needsBaseof[name]
458
459 if !found {
460 continue
461 }
462
463 d.Baseof = true
464 baseLayouts, _ := t.layoutHandler.For(d, f)
465 var base templateInfo
466 found = false
467 for _, l := range baseLayouts {
468 base, found = t.baseof[l]
469 if found {
470 break
471 }
472 }
473
474 templ, err := t.applyBaseTemplate(overlay, base)
475 if err != nil {
476 return nil, false, err
477 }
478
479 ts := newTemplateState(templ, overlay)
480
481 if found {
482 ts.baseInfo = base
483
484 // Add the base identity to detect changes
485 ts.Add(identity.NewPathIdentity(files.ComponentFolderLayouts, base.name))
486 }
487
488 t.applyTemplateTransformers(t.main, ts)
489
490 if err := t.extractPartials(ts.Template); err != nil {
491 return nil, false, err
492 }
493
494 return ts, true, nil
495
496 }
497
498 return nil, false, nil
499 }
500
501 func (t *templateHandler) findTemplate(name string) *templateState {
502 if templ, found := t.Lookup(name); found {
503 return templ.(*templateState)
504 }
505 return nil
506 }
507
508 func (t *templateHandler) newTemplateInfo(name, tpl string) templateInfo {
509 var isText bool
510 name, isText = t.nameIsText(name)
511 return templateInfo{
512 name: name,
513 isText: isText,
514 template: tpl,
515 }
516 }
517
518 func (t *templateHandler) addFileContext(templ tpl.Template, inerr error) error {
519 if strings.HasPrefix(templ.Name(), "_internal") {
520 return inerr
521 }
522
523 ts, ok := templ.(*templateState)
524 if !ok {
525 return inerr
526 }
527
528 identifiers := t.extractIdentifiers(inerr.Error())
529
530 //lint:ignore ST1008 the error is the main result
531 checkFilename := func(info templateInfo, inErr error) (error, bool) {
532 if info.filename == "" {
533 return inErr, false
534 }
535
536 lineMatcher := func(m herrors.LineMatcher) int {
537 if m.Position.LineNumber != m.LineNumber {
538 return -1
539 }
540
541 for _, id := range identifiers {
542 if strings.Contains(m.Line, id) {
543 // We found the line, but return a 0 to signal to
544 // use the column from the error message.
545 return 0
546 }
547 }
548 return -1
549 }
550
551 f, err := t.layoutsFs.Open(info.filename)
552 if err != nil {
553 return inErr, false
554 }
555 defer f.Close()
556
557 fe := herrors.NewFileErrorFromName(inErr, info.realFilename)
558 fe.UpdateContent(f, lineMatcher)
559
560 if !fe.ErrorContext().Position.IsValid() {
561 return inErr, false
562 }
563 return fe, true
564 }
565
566 inerr = fmt.Errorf("execute of template failed: %w", inerr)
567
568 if err, ok := checkFilename(ts.info, inerr); ok {
569 return err
570 }
571
572 err, _ := checkFilename(ts.baseInfo, inerr)
573
574 return err
575 }
576
577 func (t *templateHandler) extractIdentifiers(line string) []string {
578 m := identifiersRe.FindAllStringSubmatch(line, -1)
579 identifiers := make([]string, len(m))
580 for i := 0; i < len(m); i++ {
581 identifiers[i] = m[i][1]
582 }
583 return identifiers
584 }
585
586 func (t *templateHandler) addShortcodeVariant(ts *templateState) {
587 name := ts.Name()
588 base := templateBaseName(templateShortcode, name)
589
590 shortcodename, variants := templateNameAndVariants(base)
591
592 templs, found := t.shortcodes[shortcodename]
593 if !found {
594 templs = &shortcodeTemplates{}
595 t.shortcodes[shortcodename] = templs
596 }
597
598 sv := shortcodeVariant{variants: variants, ts: ts}
599
600 i := templs.indexOf(variants)
601
602 if i != -1 {
603 // Only replace if it's an override of an internal template.
604 if !isInternal(name) {
605 templs.variants[i] = sv
606 }
607 } else {
608 templs.variants = append(templs.variants, sv)
609 }
610 }
611
612 func (t *templateHandler) addTemplateFile(name, path string) error {
613 getTemplate := func(filename string) (templateInfo, error) {
614 fs := t.Layouts.Fs
615 b, err := afero.ReadFile(fs, filename)
616 if err != nil {
617 return templateInfo{filename: filename, fs: fs}, err
618 }
619
620 s := removeLeadingBOM(string(b))
621
622 realFilename := filename
623 if fi, err := fs.Stat(filename); err == nil {
624 if fim, ok := fi.(hugofs.FileMetaInfo); ok {
625 realFilename = fim.Meta().Filename
626 }
627 }
628
629 var isText bool
630 name, isText = t.nameIsText(name)
631
632 return templateInfo{
633 name: name,
634 isText: isText,
635 template: s,
636 filename: filename,
637 realFilename: realFilename,
638 fs: fs,
639 }, nil
640 }
641
642 tinfo, err := getTemplate(path)
643 if err != nil {
644 return err
645 }
646
647 if isBaseTemplatePath(name) {
648 // Store it for later.
649 t.baseof[name] = tinfo
650 return nil
651 }
652
653 needsBaseof := !t.noBaseNeeded(name) && needsBaseTemplate(tinfo.template)
654 if needsBaseof {
655 t.needsBaseof[name] = tinfo
656 return nil
657 }
658
659 templ, err := t.addTemplateTo(tinfo, t.main)
660 if err != nil {
661 return tinfo.errWithFileContext("parse failed", err)
662 }
663 t.applyTemplateTransformers(t.main, templ)
664
665 return nil
666 }
667
668 func (t *templateHandler) addTemplateTo(info templateInfo, to *templateNamespace) (*templateState, error) {
669 return to.parse(info)
670 }
671
672 func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Template, error) {
673 if overlay.isText {
674 var (
675 templ = t.main.prototypeTextClone.New(overlay.name)
676 err error
677 )
678
679 if !base.IsZero() {
680 templ, err = templ.Parse(base.template)
681 if err != nil {
682 return nil, base.errWithFileContext("parse failed", err)
683 }
684 }
685
686 templ, err = texttemplate.Must(templ.Clone()).Parse(overlay.template)
687 if err != nil {
688 return nil, overlay.errWithFileContext("parse failed", err)
689 }
690
691 // The extra lookup is a workaround, see
692 // * https://github.com/golang/go/issues/16101
693 // * https://github.com/gohugoio/hugo/issues/2549
694 // templ = templ.Lookup(templ.Name())
695
696 return templ, nil
697 }
698
699 var (
700 templ = t.main.prototypeHTMLClone.New(overlay.name)
701 err error
702 )
703
704 if !base.IsZero() {
705 templ, err = templ.Parse(base.template)
706 if err != nil {
707 return nil, base.errWithFileContext("parse failed", err)
708 }
709 }
710
711 templ, err = htmltemplate.Must(templ.Clone()).Parse(overlay.template)
712 if err != nil {
713 return nil, overlay.errWithFileContext("parse failed", err)
714 }
715
716 // The extra lookup is a workaround, see
717 // * https://github.com/golang/go/issues/16101
718 // * https://github.com/gohugoio/hugo/issues/2549
719 templ = templ.Lookup(templ.Name())
720
721 return templ, err
722 }
723
724 func (t *templateHandler) applyTemplateTransformers(ns *templateNamespace, ts *templateState) (*templateContext, error) {
725 c, err := applyTemplateTransformers(ts, ns.newTemplateLookup(ts))
726 if err != nil {
727 return nil, err
728 }
729
730 for k := range c.templateNotFound {
731 t.transformNotFound[k] = ts
732 t.identityNotFound[k] = append(t.identityNotFound[k], c.t)
733 }
734
735 for k := range c.identityNotFound {
736 t.identityNotFound[k] = append(t.identityNotFound[k], c.t)
737 }
738
739 return c, err
740 }
741
742 //go:embed embedded/templates/*
743 //go:embed embedded/templates/_default/*
744 //go:embed embedded/templates/_server/*
745 var embededTemplatesFs embed.FS
746
747 func (t *templateHandler) loadEmbedded() error {
748 return fs.WalkDir(embededTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error {
749 if d == nil || d.IsDir() {
750 return nil
751 }
752
753 templb, err := embededTemplatesFs.ReadFile(path)
754 if err != nil {
755 return err
756 }
757
758 // Get the newlines on Windows in line with how we had it back when we used Go Generate
759 // to write the templates to Go files.
760 templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n")))
761 name := strings.TrimPrefix(filepath.ToSlash(path), "embedded/templates/")
762 templateName := name
763
764 // For the render hooks and the server templates it does not make sense to preseve the
765 // double _indternal double book-keeping,
766 // just add it if its now provided by the user.
767 if !strings.Contains(path, "_default/_markup") && !strings.HasPrefix(name, "_server/") {
768 templateName = internalPathPrefix + name
769 }
770
771 if _, found := t.Lookup(templateName); !found {
772 if err := t.AddTemplate(templateName, templ); err != nil {
773 return err
774 }
775 }
776
777 if aliases, found := embeddedTemplatesAliases[name]; found {
778 // TODO(bep) avoid reparsing these aliases
779 for _, alias := range aliases {
780 alias = internalPathPrefix + alias
781 if err := t.AddTemplate(alias, templ); err != nil {
782 return err
783 }
784 }
785 }
786
787 return nil
788 })
789 }
790
791 func (t *templateHandler) loadTemplates() error {
792 walker := func(path string, fi hugofs.FileMetaInfo, err error) error {
793 if err != nil || fi.IsDir() {
794 return err
795 }
796
797 if isDotFile(path) || isBackupFile(path) {
798 return nil
799 }
800
801 name := strings.TrimPrefix(filepath.ToSlash(path), "/")
802 filename := filepath.Base(path)
803 outputFormat, found := t.OutputFormatsConfig.FromFilename(filename)
804
805 if found && outputFormat.IsPlainText {
806 name = textTmplNamePrefix + name
807 }
808
809 if err := t.addTemplateFile(name, path); err != nil {
810 return err
811 }
812
813 return nil
814 }
815
816 if err := helpers.SymbolicWalk(t.Layouts.Fs, "", walker); err != nil {
817 if !os.IsNotExist(err) {
818 return err
819 }
820 return nil
821 }
822
823 return nil
824 }
825
826 func (t *templateHandler) nameIsText(name string) (string, bool) {
827 isText := strings.HasPrefix(name, textTmplNamePrefix)
828 if isText {
829 name = strings.TrimPrefix(name, textTmplNamePrefix)
830 }
831 return name, isText
832 }
833
834 func (t *templateHandler) noBaseNeeded(name string) bool {
835 if strings.HasPrefix(name, "shortcodes/") || strings.HasPrefix(name, "partials/") {
836 return true
837 }
838 return strings.Contains(name, "_markup/")
839 }
840
841 func (t *templateHandler) extractPartials(templ tpl.Template) error {
842 templs := templates(templ)
843 for _, templ := range templs {
844 if templ.Name() == "" || !strings.HasPrefix(templ.Name(), "partials/") {
845 continue
846 }
847
848 ts := newTemplateState(templ, templateInfo{name: templ.Name()})
849 ts.typ = templatePartial
850
851 t.main.mu.RLock()
852 _, found := t.main.templates[templ.Name()]
853 t.main.mu.RUnlock()
854
855 if !found {
856 t.main.mu.Lock()
857 // This is a template defined inline.
858 _, err := applyTemplateTransformers(ts, t.main.newTemplateLookup(ts))
859 if err != nil {
860 t.main.mu.Unlock()
861 return err
862 }
863 t.main.templates[templ.Name()] = ts
864 t.main.mu.Unlock()
865
866 }
867 }
868
869 return nil
870 }
871
872 func (t *templateHandler) postTransform() error {
873 defineCheckedHTML := false
874 defineCheckedText := false
875
876 for _, v := range t.main.templates {
877 if v.typ == templateShortcode {
878 t.addShortcodeVariant(v)
879 }
880
881 if defineCheckedHTML && defineCheckedText {
882 continue
883 }
884
885 isText := isText(v.Template)
886 if isText {
887 if defineCheckedText {
888 continue
889 }
890 defineCheckedText = true
891 } else {
892 if defineCheckedHTML {
893 continue
894 }
895 defineCheckedHTML = true
896 }
897
898 if err := t.extractPartials(v.Template); err != nil {
899 return err
900 }
901 }
902
903 for name, source := range t.transformNotFound {
904 lookup := t.main.newTemplateLookup(source)
905 templ := lookup(name)
906 if templ != nil {
907 _, err := applyTemplateTransformers(templ, lookup)
908 if err != nil {
909 return err
910 }
911 }
912 }
913
914 for k, v := range t.identityNotFound {
915 ts := t.findTemplate(k)
916 if ts != nil {
917 for _, im := range v {
918 im.Add(ts)
919 }
920 }
921 }
922
923 for _, v := range t.shortcodes {
924 sort.Slice(v.variants, func(i, j int) bool {
925 v1, v2 := v.variants[i], v.variants[j]
926 name1, name2 := v1.ts.Name(), v2.ts.Name()
927 isHTMl1, isHTML2 := strings.HasSuffix(name1, "html"), strings.HasSuffix(name2, "html")
928
929 // There will be a weighted selection later, but make
930 // sure these are sorted to get a stable selection for
931 // output formats missing specific templates.
932 // Prefer HTML.
933 if isHTMl1 || isHTML2 && !(isHTMl1 && isHTML2) {
934 return isHTMl1
935 }
936
937 return name1 < name2
938 })
939 }
940
941 return nil
942 }
943
944 type templateNamespace struct {
945 prototypeText *texttemplate.Template
946 prototypeHTML *htmltemplate.Template
947 prototypeTextClone *texttemplate.Template
948 prototypeHTMLClone *htmltemplate.Template
949
950 *templateStateMap
951 }
952
953 func (t templateNamespace) Clone() *templateNamespace {
954 t.mu.Lock()
955 defer t.mu.Unlock()
956
957 t.templateStateMap = &templateStateMap{
958 templates: make(map[string]*templateState),
959 }
960
961 t.prototypeText = texttemplate.Must(t.prototypeText.Clone())
962 t.prototypeHTML = htmltemplate.Must(t.prototypeHTML.Clone())
963
964 return &t
965 }
966
967 func (t *templateNamespace) Lookup(name string) (tpl.Template, bool) {
968 t.mu.RLock()
969 defer t.mu.RUnlock()
970
971 templ, found := t.templates[name]
972 if !found {
973 return nil, false
974 }
975
976 return templ, found
977 }
978
979 func (t *templateNamespace) createPrototypes() error {
980 t.prototypeTextClone = texttemplate.Must(t.prototypeText.Clone())
981 t.prototypeHTMLClone = htmltemplate.Must(t.prototypeHTML.Clone())
982
983 return nil
984 }
985
986 func (t *templateNamespace) newTemplateLookup(in *templateState) func(name string) *templateState {
987 return func(name string) *templateState {
988 if templ, found := t.templates[name]; found {
989 if templ.isText() != in.isText() {
990 return nil
991 }
992 return templ
993 }
994 if templ, found := findTemplateIn(name, in); found {
995 return newTemplateState(templ, templateInfo{name: templ.Name()})
996 }
997 return nil
998 }
999 }
1000
1001 func (t *templateNamespace) parse(info templateInfo) (*templateState, error) {
1002 t.mu.Lock()
1003 defer t.mu.Unlock()
1004
1005 if info.isText {
1006 prototype := t.prototypeText
1007
1008 templ, err := prototype.New(info.name).Parse(info.template)
1009 if err != nil {
1010 return nil, err
1011 }
1012
1013 ts := newTemplateState(templ, info)
1014
1015 t.templates[info.name] = ts
1016
1017 return ts, nil
1018 }
1019
1020 prototype := t.prototypeHTML
1021
1022 templ, err := prototype.New(info.name).Parse(info.template)
1023 if err != nil {
1024 return nil, err
1025 }
1026
1027 ts := newTemplateState(templ, info)
1028
1029 t.templates[info.name] = ts
1030
1031 return ts, nil
1032 }
1033
1034 type templateState struct {
1035 tpl.Template
1036
1037 typ templateType
1038 parseInfo tpl.ParseInfo
1039 identity.Manager
1040
1041 info templateInfo
1042 baseInfo templateInfo // Set when a base template is used.
1043 }
1044
1045 func (t *templateState) ParseInfo() tpl.ParseInfo {
1046 return t.parseInfo
1047 }
1048
1049 func (t *templateState) isText() bool {
1050 return isText(t.Template)
1051 }
1052
1053 func isText(templ tpl.Template) bool {
1054 _, isText := templ.(*texttemplate.Template)
1055 return isText
1056 }
1057
1058 type templateStateMap struct {
1059 mu sync.RWMutex
1060 templates map[string]*templateState
1061 }
1062
1063 type templateWrapperWithLock struct {
1064 *sync.RWMutex
1065 tpl.Template
1066 }
1067
1068 type textTemplateWrapperWithLock struct {
1069 *sync.RWMutex
1070 *texttemplate.Template
1071 }
1072
1073 func (t *textTemplateWrapperWithLock) Lookup(name string) (tpl.Template, bool) {
1074 t.RLock()
1075 templ := t.Template.Lookup(name)
1076 t.RUnlock()
1077 if templ == nil {
1078 return nil, false
1079 }
1080 return &textTemplateWrapperWithLock{
1081 RWMutex: t.RWMutex,
1082 Template: templ,
1083 }, true
1084 }
1085
1086 func (t *textTemplateWrapperWithLock) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
1087 panic("not supported")
1088 }
1089
1090 func (t *textTemplateWrapperWithLock) LookupVariants(name string) []tpl.Template {
1091 panic("not supported")
1092 }
1093
1094 func (t *textTemplateWrapperWithLock) Parse(name, tpl string) (tpl.Template, error) {
1095 t.Lock()
1096 defer t.Unlock()
1097 return t.Template.New(name).Parse(tpl)
1098 }
1099
1100 func isBackupFile(path string) bool {
1101 return path[len(path)-1] == '~'
1102 }
1103
1104 func isBaseTemplatePath(path string) bool {
1105 return strings.Contains(filepath.Base(path), baseFileBase)
1106 }
1107
1108 func isDotFile(path string) bool {
1109 return filepath.Base(path)[0] == '.'
1110 }
1111
1112 func removeLeadingBOM(s string) string {
1113 const bom = '\ufeff'
1114
1115 for i, r := range s {
1116 if i == 0 && r != bom {
1117 return s
1118 }
1119 if i > 0 {
1120 return s[i:]
1121 }
1122 }
1123
1124 return s
1125 }
1126
1127 // resolves _internal/shortcodes/param.html => param.html etc.
1128 func templateBaseName(typ templateType, name string) string {
1129 name = strings.TrimPrefix(name, internalPathPrefix)
1130 switch typ {
1131 case templateShortcode:
1132 return strings.TrimPrefix(name, shortcodesPathPrefix)
1133 default:
1134 panic("not implemented")
1135 }
1136 }
1137
1138 func unwrap(templ tpl.Template) tpl.Template {
1139 if ts, ok := templ.(*templateState); ok {
1140 return ts.Template
1141 }
1142 return templ
1143 }
1144
1145 func templates(in tpl.Template) []tpl.Template {
1146 var templs []tpl.Template
1147 in = unwrap(in)
1148 if textt, ok := in.(*texttemplate.Template); ok {
1149 for _, t := range textt.Templates() {
1150 templs = append(templs, t)
1151 }
1152 }
1153
1154 if htmlt, ok := in.(*htmltemplate.Template); ok {
1155 for _, t := range htmlt.Templates() {
1156 templs = append(templs, t)
1157 }
1158 }
1159
1160 return templs
1161 }