mirror of
https://github.com/gohugoio/hugo.git
synced 2024-12-24 12:41:06 +00:00
6cd0784e44
Closes #8086 Closes #12589
1232 lines
29 KiB
Go
1232 lines
29 KiB
Go
// Copyright 2019 The Hugo Authors. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package tplimpl
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"embed"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/gohugoio/hugo/common/types"
|
|
"github.com/gohugoio/hugo/output/layouts"
|
|
|
|
"github.com/gohugoio/hugo/helpers"
|
|
|
|
"github.com/gohugoio/hugo/output"
|
|
|
|
"github.com/gohugoio/hugo/deps"
|
|
"github.com/spf13/afero"
|
|
|
|
"github.com/gohugoio/hugo/common/herrors"
|
|
"github.com/gohugoio/hugo/hugofs"
|
|
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
|
|
|
htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
|
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
|
|
|
"github.com/gohugoio/hugo/identity"
|
|
"github.com/gohugoio/hugo/tpl"
|
|
)
|
|
|
|
const (
|
|
textTmplNamePrefix = "_text/"
|
|
|
|
shortcodesPathPrefix = "shortcodes/"
|
|
internalPathPrefix = "_internal/"
|
|
embeddedPathPrefix = "_embedded/"
|
|
baseFileBase = "baseof"
|
|
)
|
|
|
|
// The identifiers may be truncated in the log, e.g.
|
|
// "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image"
|
|
// We need this to identify position in templates with base templates applied.
|
|
var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`)
|
|
|
|
var embeddedTemplatesAliases = map[string][]string{
|
|
"shortcodes/twitter.html": {"shortcodes/tweet.html"},
|
|
}
|
|
|
|
var (
|
|
_ tpl.TemplateManager = (*templateExec)(nil)
|
|
_ tpl.TemplateHandler = (*templateExec)(nil)
|
|
_ tpl.TemplateFuncGetter = (*templateExec)(nil)
|
|
_ tpl.TemplateFinder = (*templateExec)(nil)
|
|
_ tpl.UnusedTemplatesProvider = (*templateExec)(nil)
|
|
|
|
_ tpl.Template = (*templateState)(nil)
|
|
_ tpl.Info = (*templateState)(nil)
|
|
)
|
|
|
|
var baseTemplateDefineRe = regexp.MustCompile(`^{{-?\s*define`)
|
|
|
|
// needsBaseTemplate returns true if the first non-comment template block is a
|
|
// define block.
|
|
// If a base template does not exist, we will handle that when it's used.
|
|
func needsBaseTemplate(templ string) bool {
|
|
idx := -1
|
|
inComment := false
|
|
for i := 0; i < len(templ); {
|
|
if !inComment && strings.HasPrefix(templ[i:], "{{/*") {
|
|
inComment = true
|
|
i += 4
|
|
} else if !inComment && strings.HasPrefix(templ[i:], "{{- /*") {
|
|
inComment = true
|
|
i += 6
|
|
} else if inComment && strings.HasPrefix(templ[i:], "*/}}") {
|
|
inComment = false
|
|
i += 4
|
|
} else if inComment && strings.HasPrefix(templ[i:], "*/ -}}") {
|
|
inComment = false
|
|
i += 6
|
|
} else {
|
|
r, size := utf8.DecodeRuneInString(templ[i:])
|
|
if !inComment {
|
|
if strings.HasPrefix(templ[i:], "{{") {
|
|
idx = i
|
|
break
|
|
} else if !unicode.IsSpace(r) {
|
|
break
|
|
}
|
|
}
|
|
i += size
|
|
}
|
|
}
|
|
|
|
if idx == -1 {
|
|
return false
|
|
}
|
|
|
|
return baseTemplateDefineRe.MatchString(templ[idx:])
|
|
}
|
|
|
|
func newStandaloneTextTemplate(funcs map[string]any) tpl.TemplateParseFinder {
|
|
return &textTemplateWrapperWithLock{
|
|
RWMutex: &sync.RWMutex{},
|
|
Template: texttemplate.New("").Funcs(funcs),
|
|
}
|
|
}
|
|
|
|
func newTemplateHandlers(d *deps.Deps) (*tpl.TemplateHandlers, error) {
|
|
exec, funcs := newTemplateExecuter(d)
|
|
funcMap := make(map[string]any)
|
|
for k, v := range funcs {
|
|
funcMap[k] = v.Interface()
|
|
}
|
|
|
|
var templateUsageTracker map[string]templateInfo
|
|
if d.Conf.PrintUnusedTemplates() {
|
|
templateUsageTracker = make(map[string]templateInfo)
|
|
}
|
|
|
|
h := &templateHandler{
|
|
nameBaseTemplateName: make(map[string]string),
|
|
transformNotFound: make(map[string]*templateState),
|
|
|
|
shortcodes: make(map[string]*shortcodeTemplates),
|
|
templateInfo: make(map[string]tpl.Info),
|
|
baseof: make(map[string]templateInfo),
|
|
needsBaseof: make(map[string]templateInfo),
|
|
|
|
main: newTemplateNamespace(funcMap),
|
|
|
|
Deps: d,
|
|
layoutHandler: layouts.NewLayoutHandler(),
|
|
layoutsFs: d.BaseFs.Layouts.Fs,
|
|
layoutTemplateCache: make(map[layoutCacheKey]layoutCacheEntry),
|
|
|
|
templateUsageTracker: templateUsageTracker,
|
|
}
|
|
|
|
if err := h.loadEmbedded(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := h.loadTemplates(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
e := &templateExec{
|
|
d: d,
|
|
executor: exec,
|
|
funcs: funcs,
|
|
templateHandler: h,
|
|
}
|
|
|
|
if err := e.postTransform(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &tpl.TemplateHandlers{
|
|
Tmpl: e,
|
|
TxtTmpl: newStandaloneTextTemplate(funcMap),
|
|
}, nil
|
|
}
|
|
|
|
func newTemplateNamespace(funcs map[string]any) *templateNamespace {
|
|
return &templateNamespace{
|
|
prototypeHTML: htmltemplate.New("").Funcs(funcs),
|
|
prototypeText: texttemplate.New("").Funcs(funcs),
|
|
templateStateMap: &templateStateMap{
|
|
templates: make(map[string]*templateState),
|
|
},
|
|
}
|
|
}
|
|
|
|
func newTemplateState(owner *templateState, templ tpl.Template, info templateInfo, id identity.Identity) *templateState {
|
|
if id == nil {
|
|
id = info
|
|
}
|
|
return &templateState{
|
|
owner: owner,
|
|
info: info,
|
|
typ: info.resolveType(),
|
|
Template: templ,
|
|
parseInfo: tpl.DefaultParseInfo,
|
|
id: id,
|
|
}
|
|
}
|
|
|
|
type layoutCacheKey struct {
|
|
d layouts.LayoutDescriptor
|
|
f string
|
|
}
|
|
|
|
type templateExec struct {
|
|
d *deps.Deps
|
|
executor texttemplate.Executer
|
|
funcs map[string]reflect.Value
|
|
|
|
*templateHandler
|
|
}
|
|
|
|
func (t templateExec) Clone(d *deps.Deps) *templateExec {
|
|
exec, funcs := newTemplateExecuter(d)
|
|
t.executor = exec
|
|
t.funcs = funcs
|
|
t.d = d
|
|
return &t
|
|
}
|
|
|
|
func (t *templateExec) Execute(templ tpl.Template, wr io.Writer, data any) error {
|
|
return t.ExecuteWithContext(context.Background(), templ, wr, data)
|
|
}
|
|
|
|
func (t *templateExec) ExecuteWithContext(ctx context.Context, templ tpl.Template, wr io.Writer, data any) error {
|
|
if rlocker, ok := templ.(types.RLocker); ok {
|
|
rlocker.RLock()
|
|
defer rlocker.RUnlock()
|
|
}
|
|
if t.Metrics != nil {
|
|
defer t.Metrics.MeasureSince(templ.Name(), time.Now())
|
|
}
|
|
|
|
if t.templateUsageTracker != nil {
|
|
if ts, ok := templ.(*templateState); ok {
|
|
|
|
t.templateUsageTrackerMu.Lock()
|
|
if _, found := t.templateUsageTracker[ts.Name()]; !found {
|
|
t.templateUsageTracker[ts.Name()] = ts.info
|
|
}
|
|
|
|
if !ts.baseInfo.IsZero() {
|
|
if _, found := t.templateUsageTracker[ts.baseInfo.name]; !found {
|
|
t.templateUsageTracker[ts.baseInfo.name] = ts.baseInfo
|
|
}
|
|
}
|
|
t.templateUsageTrackerMu.Unlock()
|
|
}
|
|
}
|
|
|
|
execErr := t.executor.ExecuteWithContext(ctx, templ, wr, data)
|
|
if execErr != nil {
|
|
owner := templ
|
|
if ts, ok := templ.(*templateState); ok && ts.owner != nil {
|
|
owner = ts.owner
|
|
}
|
|
execErr = t.addFileContext(owner, execErr)
|
|
}
|
|
return execErr
|
|
}
|
|
|
|
func (t *templateExec) UnusedTemplates() []tpl.FileInfo {
|
|
if t.templateUsageTracker == nil {
|
|
return nil
|
|
}
|
|
var unused []tpl.FileInfo
|
|
|
|
for _, ti := range t.needsBaseof {
|
|
if _, found := t.templateUsageTracker[ti.name]; !found {
|
|
unused = append(unused, ti)
|
|
}
|
|
}
|
|
|
|
for _, ti := range t.baseof {
|
|
if _, found := t.templateUsageTracker[ti.name]; !found {
|
|
unused = append(unused, ti)
|
|
}
|
|
}
|
|
|
|
for _, ts := range t.main.templates {
|
|
ti := ts.info
|
|
if strings.HasPrefix(ti.name, "_internal/") || ti.meta == nil {
|
|
continue
|
|
}
|
|
|
|
if _, found := t.templateUsageTracker[ti.name]; !found {
|
|
unused = append(unused, ti)
|
|
}
|
|
}
|
|
|
|
sort.Slice(unused, func(i, j int) bool {
|
|
return unused[i].Name() < unused[j].Name()
|
|
})
|
|
|
|
return unused
|
|
}
|
|
|
|
func (t *templateExec) GetFunc(name string) (reflect.Value, bool) {
|
|
v, found := t.funcs[name]
|
|
return v, found
|
|
}
|
|
|
|
func (t *templateExec) MarkReady() error {
|
|
var err error
|
|
t.readyInit.Do(func() {
|
|
// We only need the clones if base templates are in use.
|
|
if len(t.needsBaseof) > 0 {
|
|
err = t.main.createPrototypes()
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
type templateHandler struct {
|
|
main *templateNamespace
|
|
needsBaseof map[string]templateInfo
|
|
baseof map[string]templateInfo
|
|
|
|
readyInit sync.Once
|
|
|
|
// This is the filesystem to load the templates from. All the templates are
|
|
// stored in the root of this filesystem.
|
|
layoutsFs afero.Fs
|
|
|
|
layoutHandler *layouts.LayoutHandler
|
|
|
|
layoutTemplateCache map[layoutCacheKey]layoutCacheEntry
|
|
layoutTemplateCacheMu sync.RWMutex
|
|
|
|
*deps.Deps
|
|
|
|
// Used to get proper filenames in errors
|
|
nameBaseTemplateName map[string]string
|
|
|
|
// Holds name and source of template definitions not found during the first
|
|
// AST transformation pass.
|
|
transformNotFound map[string]*templateState
|
|
|
|
// shortcodes maps shortcode name to template variants
|
|
// (language, output format etc.) of that shortcode.
|
|
shortcodes map[string]*shortcodeTemplates
|
|
|
|
// templateInfo maps template name to some additional information about that template.
|
|
// Note that for shortcodes that same information is embedded in the
|
|
// shortcodeTemplates type.
|
|
templateInfo map[string]tpl.Info
|
|
|
|
// May be nil.
|
|
templateUsageTracker map[string]templateInfo
|
|
templateUsageTrackerMu sync.Mutex
|
|
}
|
|
|
|
type layoutCacheEntry struct {
|
|
found bool
|
|
templ tpl.Template
|
|
err error
|
|
}
|
|
|
|
// AddTemplate parses and adds a template to the collection.
|
|
// Templates with name prefixed with "_text" will be handled as plain
|
|
// text templates.
|
|
func (t *templateHandler) AddTemplate(name, tpl string) error {
|
|
templ, err := t.addTemplateTo(t.newTemplateInfo(name, tpl), t.main)
|
|
if err == nil {
|
|
_, err = t.applyTemplateTransformers(t.main, templ)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (t *templateHandler) Lookup(name string) (tpl.Template, bool) {
|
|
templ, found := t.main.Lookup(name)
|
|
if found {
|
|
return templ, true
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
func (t *templateHandler) LookupLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
|
|
key := layoutCacheKey{d, f.Name}
|
|
t.layoutTemplateCacheMu.RLock()
|
|
if cacheVal, found := t.layoutTemplateCache[key]; found {
|
|
t.layoutTemplateCacheMu.RUnlock()
|
|
return cacheVal.templ, cacheVal.found, cacheVal.err
|
|
}
|
|
|
|
t.layoutTemplateCacheMu.RUnlock()
|
|
|
|
t.layoutTemplateCacheMu.Lock()
|
|
defer t.layoutTemplateCacheMu.Unlock()
|
|
|
|
templ, found, err := t.findLayout(d, f)
|
|
cacheVal := layoutCacheEntry{found: found, templ: templ, err: err}
|
|
t.layoutTemplateCache[key] = cacheVal
|
|
return cacheVal.templ, cacheVal.found, cacheVal.err
|
|
}
|
|
|
|
// This currently only applies to shortcodes and what we get here is the
|
|
// shortcode name.
|
|
func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
|
|
name = templateBaseName(templateShortcode, name)
|
|
s, found := t.shortcodes[name]
|
|
if !found {
|
|
return nil, false, false
|
|
}
|
|
|
|
sv, found := s.fromVariants(variants)
|
|
if !found {
|
|
return nil, false, false
|
|
}
|
|
|
|
more := len(s.variants) > 1
|
|
|
|
return sv.ts, true, more
|
|
}
|
|
|
|
// LookupVariants returns all variants of name, nil if none found.
|
|
func (t *templateHandler) LookupVariants(name string) []tpl.Template {
|
|
name = templateBaseName(templateShortcode, name)
|
|
s, found := t.shortcodes[name]
|
|
if !found {
|
|
return nil
|
|
}
|
|
|
|
variants := make([]tpl.Template, len(s.variants))
|
|
for i := 0; i < len(variants); i++ {
|
|
variants[i] = s.variants[i].ts
|
|
}
|
|
|
|
return variants
|
|
}
|
|
|
|
func (t *templateHandler) HasTemplate(name string) bool {
|
|
if _, found := t.baseof[name]; found {
|
|
return true
|
|
}
|
|
|
|
if _, found := t.needsBaseof[name]; found {
|
|
return true
|
|
}
|
|
|
|
_, found := t.Lookup(name)
|
|
return found
|
|
}
|
|
|
|
func (t *templateHandler) GetIdentity(name string) (identity.Identity, bool) {
|
|
if _, found := t.needsBaseof[name]; found {
|
|
return identity.StringIdentity(name), true
|
|
}
|
|
|
|
if _, found := t.baseof[name]; found {
|
|
return identity.StringIdentity(name), true
|
|
}
|
|
|
|
tt, found := t.Lookup(name)
|
|
if !found {
|
|
return nil, false
|
|
}
|
|
return tt.(identity.IdentityProvider).GetIdentity(), found
|
|
}
|
|
|
|
func (t *templateHandler) findLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
|
|
d.OutputFormatName = f.Name
|
|
d.Suffix = f.MediaType.FirstSuffix.Suffix
|
|
layouts, _ := t.layoutHandler.For(d)
|
|
for _, name := range layouts {
|
|
templ, found := t.main.Lookup(name)
|
|
if found {
|
|
return templ, true, nil
|
|
}
|
|
|
|
overlay, found := t.needsBaseof[name]
|
|
|
|
if !found {
|
|
continue
|
|
}
|
|
|
|
d.Baseof = true
|
|
baseLayouts, _ := t.layoutHandler.For(d)
|
|
var base templateInfo
|
|
found = false
|
|
for _, l := range baseLayouts {
|
|
base, found = t.baseof[l]
|
|
if found {
|
|
break
|
|
}
|
|
}
|
|
|
|
templ, err := t.applyBaseTemplate(overlay, base)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
ts := newTemplateState(nil, templ, overlay, identity.Or(base, overlay))
|
|
|
|
if found {
|
|
ts.baseInfo = base
|
|
}
|
|
|
|
if _, err := t.applyTemplateTransformers(t.main, ts); err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
if err := t.extractPartials(ts.Template); err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
return ts, true, nil
|
|
|
|
}
|
|
|
|
return nil, false, nil
|
|
}
|
|
|
|
func (t *templateHandler) newTemplateInfo(name, tpl string) templateInfo {
|
|
var isText bool
|
|
var isEmbedded bool
|
|
|
|
if strings.HasPrefix(name, embeddedPathPrefix) {
|
|
isEmbedded = true
|
|
name = strings.TrimPrefix(name, embeddedPathPrefix)
|
|
}
|
|
|
|
name, isText = t.nameIsText(name)
|
|
return templateInfo{
|
|
name: name,
|
|
isText: isText,
|
|
isEmbedded: isEmbedded,
|
|
template: tpl,
|
|
}
|
|
}
|
|
|
|
func (t *templateHandler) addFileContext(templ tpl.Template, inerr error) error {
|
|
if strings.HasPrefix(templ.Name(), "_internal") {
|
|
return inerr
|
|
}
|
|
|
|
ts, ok := templ.(*templateState)
|
|
if !ok {
|
|
return inerr
|
|
}
|
|
|
|
identifiers := t.extractIdentifiers(inerr.Error())
|
|
|
|
checkFilename := func(info templateInfo, inErr error) (error, bool) {
|
|
if info.meta == nil {
|
|
return inErr, false
|
|
}
|
|
|
|
lineMatcher := func(m herrors.LineMatcher) int {
|
|
if m.Position.LineNumber != m.LineNumber {
|
|
return -1
|
|
}
|
|
|
|
for _, id := range identifiers {
|
|
if strings.Contains(m.Line, id) {
|
|
// We found the line, but return a 0 to signal to
|
|
// use the column from the error message.
|
|
return 0
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
f, err := info.meta.Open()
|
|
if err != nil {
|
|
return inErr, false
|
|
}
|
|
defer f.Close()
|
|
|
|
fe := herrors.NewFileErrorFromName(inErr, info.meta.Filename)
|
|
fe.UpdateContent(f, lineMatcher)
|
|
|
|
if !fe.ErrorContext().Position.IsValid() {
|
|
return inErr, false
|
|
}
|
|
return fe, true
|
|
}
|
|
|
|
inerr = fmt.Errorf("execute of template failed: %w", inerr)
|
|
|
|
if err, ok := checkFilename(ts.info, inerr); ok {
|
|
return err
|
|
}
|
|
|
|
err, _ := checkFilename(ts.baseInfo, inerr)
|
|
|
|
return err
|
|
}
|
|
|
|
func (t *templateHandler) extractIdentifiers(line string) []string {
|
|
m := identifiersRe.FindAllStringSubmatch(line, -1)
|
|
identifiers := make([]string, len(m))
|
|
for i := 0; i < len(m); i++ {
|
|
identifiers[i] = m[i][1]
|
|
}
|
|
return identifiers
|
|
}
|
|
|
|
func (t *templateHandler) addShortcodeVariant(ts *templateState) {
|
|
name := ts.Name()
|
|
base := templateBaseName(templateShortcode, name)
|
|
|
|
shortcodename, variants := templateNameAndVariants(base)
|
|
|
|
templs, found := t.shortcodes[shortcodename]
|
|
if !found {
|
|
templs = &shortcodeTemplates{}
|
|
t.shortcodes[shortcodename] = templs
|
|
}
|
|
|
|
sv := shortcodeVariant{variants: variants, ts: ts}
|
|
|
|
i := templs.indexOf(variants)
|
|
|
|
if i != -1 {
|
|
// Only replace if it's an override of an internal template.
|
|
if !isInternal(name) {
|
|
templs.variants[i] = sv
|
|
}
|
|
} else {
|
|
templs.variants = append(templs.variants, sv)
|
|
}
|
|
}
|
|
|
|
func (t *templateHandler) addTemplateFile(name string, fim hugofs.FileMetaInfo) error {
|
|
getTemplate := func(fim hugofs.FileMetaInfo) (templateInfo, error) {
|
|
meta := fim.Meta()
|
|
f, err := meta.Open()
|
|
if err != nil {
|
|
return templateInfo{meta: meta}, err
|
|
}
|
|
defer f.Close()
|
|
b, err := io.ReadAll(f)
|
|
if err != nil {
|
|
return templateInfo{meta: meta}, err
|
|
}
|
|
|
|
s := removeLeadingBOM(string(b))
|
|
|
|
var isText bool
|
|
name, isText = t.nameIsText(name)
|
|
|
|
return templateInfo{
|
|
name: name,
|
|
isText: isText,
|
|
template: s,
|
|
meta: meta,
|
|
}, nil
|
|
}
|
|
|
|
tinfo, err := getTemplate(fim)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if isBaseTemplatePath(name) {
|
|
// Store it for later.
|
|
t.baseof[name] = tinfo
|
|
return nil
|
|
}
|
|
|
|
needsBaseof := !t.noBaseNeeded(name) && needsBaseTemplate(tinfo.template)
|
|
if needsBaseof {
|
|
t.needsBaseof[name] = tinfo
|
|
return nil
|
|
}
|
|
|
|
templ, err := t.addTemplateTo(tinfo, t.main)
|
|
if err != nil {
|
|
return tinfo.errWithFileContext("parse failed", err)
|
|
}
|
|
|
|
if _, err = t.applyTemplateTransformers(t.main, templ); err != nil {
|
|
return tinfo.errWithFileContext("transform failed", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *templateHandler) addTemplateTo(info templateInfo, to *templateNamespace) (*templateState, error) {
|
|
return to.parse(info)
|
|
}
|
|
|
|
func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Template, error) {
|
|
if overlay.isText {
|
|
var (
|
|
templ = t.main.prototypeTextClone.New(overlay.name)
|
|
err error
|
|
)
|
|
|
|
if !base.IsZero() {
|
|
templ, err = templ.Parse(base.template)
|
|
if err != nil {
|
|
return nil, base.errWithFileContext("parse failed", err)
|
|
}
|
|
}
|
|
|
|
templ, err = texttemplate.Must(templ.Clone()).Parse(overlay.template)
|
|
if err != nil {
|
|
return nil, overlay.errWithFileContext("parse failed", err)
|
|
}
|
|
|
|
// The extra lookup is a workaround, see
|
|
// * https://github.com/golang/go/issues/16101
|
|
// * https://github.com/gohugoio/hugo/issues/2549
|
|
// templ = templ.Lookup(templ.Name())
|
|
|
|
return templ, nil
|
|
}
|
|
|
|
var (
|
|
templ = t.main.prototypeHTMLClone.New(overlay.name)
|
|
err error
|
|
)
|
|
|
|
if !base.IsZero() {
|
|
templ, err = templ.Parse(base.template)
|
|
if err != nil {
|
|
return nil, base.errWithFileContext("parse failed", err)
|
|
}
|
|
}
|
|
|
|
templ, err = htmltemplate.Must(templ.Clone()).Parse(overlay.template)
|
|
if err != nil {
|
|
return nil, overlay.errWithFileContext("parse failed", err)
|
|
}
|
|
|
|
// The extra lookup is a workaround, see
|
|
// * https://github.com/golang/go/issues/16101
|
|
// * https://github.com/gohugoio/hugo/issues/2549
|
|
templ = templ.Lookup(templ.Name())
|
|
|
|
return templ, err
|
|
}
|
|
|
|
func (t *templateHandler) applyTemplateTransformers(ns *templateNamespace, ts *templateState) (*templateContext, error) {
|
|
c, err := applyTemplateTransformers(ts, ns.newTemplateLookup(ts))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for k := range c.templateNotFound {
|
|
t.transformNotFound[k] = ts
|
|
}
|
|
|
|
for k, v := range c.deferNodes {
|
|
if err = t.main.addDeferredTemplate(ts, k, v); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return c, err
|
|
}
|
|
|
|
//go:embed all:embedded/templates/*
|
|
//go:embed embedded/templates/_default/*
|
|
//go:embed embedded/templates/_server/*
|
|
var embeddedTemplatesFs embed.FS
|
|
|
|
func (t *templateHandler) loadEmbedded() error {
|
|
return fs.WalkDir(embeddedTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d == nil || d.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
templb, err := embeddedTemplatesFs.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the newlines on Windows in line with how we had it back when we used Go Generate
|
|
// to write the templates to Go files.
|
|
templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n")))
|
|
name := strings.TrimPrefix(filepath.ToSlash(path), "embedded/templates/")
|
|
templateName := name
|
|
|
|
// For the render hooks and the server templates it does not make sense to preserve the
|
|
// double _internal double book-keeping,
|
|
// just add it if its now provided by the user.
|
|
if !strings.Contains(path, "_default/_markup") && !strings.HasPrefix(name, "_server/") && !strings.HasPrefix(name, "partials/_funcs/") {
|
|
templateName = internalPathPrefix + name
|
|
}
|
|
|
|
if _, found := t.Lookup(templateName); !found {
|
|
if err := t.AddTemplate(embeddedPathPrefix+templateName, templ); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if aliases, found := embeddedTemplatesAliases[name]; found {
|
|
// TODO(bep) avoid reparsing these aliases
|
|
for _, alias := range aliases {
|
|
alias = internalPathPrefix + alias
|
|
if err := t.AddTemplate(embeddedPathPrefix+alias, templ); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (t *templateHandler) loadTemplates() error {
|
|
walker := func(path string, fi hugofs.FileMetaInfo) error {
|
|
if fi.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
if isDotFile(path) || isBackupFile(path) {
|
|
return nil
|
|
}
|
|
|
|
name := strings.TrimPrefix(filepath.ToSlash(path), "/")
|
|
filename := filepath.Base(path)
|
|
outputFormats := t.Conf.GetConfigSection("outputFormats").(output.Formats)
|
|
outputFormat, found := outputFormats.FromFilename(filename)
|
|
|
|
if found && outputFormat.IsPlainText {
|
|
name = textTmplNamePrefix + name
|
|
}
|
|
|
|
if err := t.addTemplateFile(name, fi); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if err := helpers.Walk(t.Layouts.Fs, "", walker); err != nil {
|
|
if !herrors.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *templateHandler) nameIsText(name string) (string, bool) {
|
|
isText := strings.HasPrefix(name, textTmplNamePrefix)
|
|
if isText {
|
|
name = strings.TrimPrefix(name, textTmplNamePrefix)
|
|
}
|
|
return name, isText
|
|
}
|
|
|
|
func (t *templateHandler) noBaseNeeded(name string) bool {
|
|
if strings.HasPrefix(name, "shortcodes/") || strings.HasPrefix(name, "partials/") {
|
|
return true
|
|
}
|
|
return strings.Contains(name, "_markup/")
|
|
}
|
|
|
|
func (t *templateHandler) extractPartials(templ tpl.Template) error {
|
|
templs := templates(templ)
|
|
for _, templ := range templs {
|
|
if templ.Name() == "" || !strings.HasPrefix(templ.Name(), "partials/") {
|
|
continue
|
|
}
|
|
|
|
ts := newTemplateState(nil, templ, templateInfo{name: templ.Name()}, nil)
|
|
ts.typ = templatePartial
|
|
|
|
t.main.mu.RLock()
|
|
_, found := t.main.templates[templ.Name()]
|
|
t.main.mu.RUnlock()
|
|
|
|
if !found {
|
|
t.main.mu.Lock()
|
|
// This is a template defined inline.
|
|
_, err := applyTemplateTransformers(ts, t.main.newTemplateLookup(ts))
|
|
if err != nil {
|
|
t.main.mu.Unlock()
|
|
return err
|
|
}
|
|
t.main.templates[templ.Name()] = ts
|
|
t.main.mu.Unlock()
|
|
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *templateHandler) postTransform() error {
|
|
defineCheckedHTML := false
|
|
defineCheckedText := false
|
|
|
|
for _, v := range t.main.templates {
|
|
if v.typ == templateShortcode {
|
|
t.addShortcodeVariant(v)
|
|
}
|
|
|
|
if defineCheckedHTML && defineCheckedText {
|
|
continue
|
|
}
|
|
|
|
isText := isText(v.Template)
|
|
if isText {
|
|
if defineCheckedText {
|
|
continue
|
|
}
|
|
defineCheckedText = true
|
|
} else {
|
|
if defineCheckedHTML {
|
|
continue
|
|
}
|
|
defineCheckedHTML = true
|
|
}
|
|
|
|
if err := t.extractPartials(v.Template); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for name, source := range t.transformNotFound {
|
|
lookup := t.main.newTemplateLookup(source)
|
|
templ := lookup(name)
|
|
if templ != nil {
|
|
_, err := applyTemplateTransformers(templ, lookup)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, v := range t.shortcodes {
|
|
sort.Slice(v.variants, func(i, j int) bool {
|
|
v1, v2 := v.variants[i], v.variants[j]
|
|
name1, name2 := v1.ts.Name(), v2.ts.Name()
|
|
isHTMl1, isHTML2 := strings.HasSuffix(name1, "html"), strings.HasSuffix(name2, "html")
|
|
|
|
// There will be a weighted selection later, but make
|
|
// sure these are sorted to get a stable selection for
|
|
// output formats missing specific templates.
|
|
// Prefer HTML.
|
|
if isHTMl1 || isHTML2 && !(isHTMl1 && isHTML2) {
|
|
return isHTMl1
|
|
}
|
|
|
|
return name1 < name2
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type templateNamespace struct {
|
|
prototypeText *texttemplate.Template
|
|
prototypeHTML *htmltemplate.Template
|
|
prototypeTextClone *texttemplate.Template
|
|
prototypeHTMLClone *htmltemplate.Template
|
|
|
|
*templateStateMap
|
|
}
|
|
|
|
func (t *templateNamespace) getPrototypeText() *texttemplate.Template {
|
|
if t.prototypeTextClone != nil {
|
|
return t.prototypeTextClone
|
|
}
|
|
return t.prototypeText
|
|
}
|
|
|
|
func (t *templateNamespace) getPrototypeHTML() *htmltemplate.Template {
|
|
if t.prototypeHTMLClone != nil {
|
|
return t.prototypeHTMLClone
|
|
}
|
|
return t.prototypeHTML
|
|
}
|
|
|
|
func (t *templateNamespace) Lookup(name string) (tpl.Template, bool) {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
|
|
templ, found := t.templates[name]
|
|
if !found {
|
|
return nil, false
|
|
}
|
|
|
|
return templ, found
|
|
}
|
|
|
|
func (t *templateNamespace) createPrototypes() error {
|
|
t.prototypeTextClone = texttemplate.Must(t.prototypeText.Clone())
|
|
t.prototypeHTMLClone = htmltemplate.Must(t.prototypeHTML.Clone())
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *templateNamespace) newTemplateLookup(in *templateState) func(name string) *templateState {
|
|
return func(name string) *templateState {
|
|
if templ, found := t.templates[name]; found {
|
|
if templ.isText() != in.isText() {
|
|
return nil
|
|
}
|
|
return templ
|
|
}
|
|
if templ, found := findTemplateIn(name, in); found {
|
|
return newTemplateState(nil, templ, templateInfo{name: templ.Name()}, nil)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (t *templateNamespace) addDeferredTemplate(owner *templateState, name string, n *parse.ListNode) error {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
if _, found := t.templates[name]; found {
|
|
return nil
|
|
}
|
|
|
|
var templ tpl.Template
|
|
|
|
if owner.isText() {
|
|
prototype := t.getPrototypeText()
|
|
tt, err := prototype.New(name).Parse("")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse empty text template %q: %w", name, err)
|
|
}
|
|
tt.Tree.Root = n
|
|
templ = tt
|
|
} else {
|
|
prototype := t.getPrototypeHTML()
|
|
tt, err := prototype.New(name).Parse("")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse empty HTML template %q: %w", name, err)
|
|
}
|
|
tt.Tree.Root = n
|
|
templ = tt
|
|
}
|
|
|
|
dts := newTemplateState(owner, templ, templateInfo{name: name}, nil)
|
|
t.templates[name] = dts
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *templateNamespace) parse(info templateInfo) (*templateState, error) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
if info.isText {
|
|
prototype := t.prototypeText
|
|
|
|
templ, err := prototype.New(info.name).Parse(info.template)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ts := newTemplateState(nil, templ, info, nil)
|
|
|
|
t.templates[info.name] = ts
|
|
|
|
return ts, nil
|
|
}
|
|
|
|
prototype := t.prototypeHTML
|
|
|
|
templ, err := prototype.New(info.name).Parse(info.template)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ts := newTemplateState(nil, templ, info, nil)
|
|
|
|
t.templates[info.name] = ts
|
|
|
|
return ts, nil
|
|
}
|
|
|
|
var _ tpl.IsInternalTemplateProvider = (*templateState)(nil)
|
|
|
|
type templateState struct {
|
|
tpl.Template
|
|
|
|
// Set for deferred templates.
|
|
owner *templateState
|
|
|
|
typ templateType
|
|
parseInfo tpl.ParseInfo
|
|
id identity.Identity
|
|
|
|
info templateInfo
|
|
baseInfo templateInfo // Set when a base template is used.
|
|
}
|
|
|
|
func (t *templateState) IsInternalTemplate() bool {
|
|
return t.info.isEmbedded
|
|
}
|
|
|
|
func (t *templateState) GetIdentity() identity.Identity {
|
|
return t.id
|
|
}
|
|
|
|
func (t *templateState) ParseInfo() tpl.ParseInfo {
|
|
return t.parseInfo
|
|
}
|
|
|
|
func (t *templateState) isText() bool {
|
|
return isText(t.Template)
|
|
}
|
|
|
|
func (t *templateState) String() string {
|
|
return t.Name()
|
|
}
|
|
|
|
func isText(templ tpl.Template) bool {
|
|
_, isText := templ.(*texttemplate.Template)
|
|
return isText
|
|
}
|
|
|
|
type templateStateMap struct {
|
|
mu sync.RWMutex
|
|
templates map[string]*templateState
|
|
}
|
|
|
|
type textTemplateWrapperWithLock struct {
|
|
*sync.RWMutex
|
|
*texttemplate.Template
|
|
}
|
|
|
|
func (t *textTemplateWrapperWithLock) Lookup(name string) (tpl.Template, bool) {
|
|
t.RLock()
|
|
templ := t.Template.Lookup(name)
|
|
t.RUnlock()
|
|
if templ == nil {
|
|
return nil, false
|
|
}
|
|
return &textTemplateWrapperWithLock{
|
|
RWMutex: t.RWMutex,
|
|
Template: templ,
|
|
}, true
|
|
}
|
|
|
|
func (t *textTemplateWrapperWithLock) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
|
|
panic("not supported")
|
|
}
|
|
|
|
func (t *textTemplateWrapperWithLock) LookupVariants(name string) []tpl.Template {
|
|
panic("not supported")
|
|
}
|
|
|
|
func (t *textTemplateWrapperWithLock) Parse(name, tpl string) (tpl.Template, error) {
|
|
t.Lock()
|
|
defer t.Unlock()
|
|
return t.Template.New(name).Parse(tpl)
|
|
}
|
|
|
|
func isBackupFile(path string) bool {
|
|
return path[len(path)-1] == '~'
|
|
}
|
|
|
|
func isBaseTemplatePath(path string) bool {
|
|
return strings.Contains(filepath.Base(path), baseFileBase)
|
|
}
|
|
|
|
func isDotFile(path string) bool {
|
|
return filepath.Base(path)[0] == '.'
|
|
}
|
|
|
|
func removeLeadingBOM(s string) string {
|
|
const bom = '\ufeff'
|
|
|
|
for i, r := range s {
|
|
if i == 0 && r != bom {
|
|
return s
|
|
}
|
|
if i > 0 {
|
|
return s[i:]
|
|
}
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// resolves _internal/shortcodes/param.html => param.html etc.
|
|
func templateBaseName(typ templateType, name string) string {
|
|
name = strings.TrimPrefix(name, internalPathPrefix)
|
|
switch typ {
|
|
case templateShortcode:
|
|
return strings.TrimPrefix(name, shortcodesPathPrefix)
|
|
default:
|
|
panic("not implemented")
|
|
}
|
|
}
|
|
|
|
func unwrap(templ tpl.Template) tpl.Template {
|
|
if ts, ok := templ.(*templateState); ok {
|
|
return ts.Template
|
|
}
|
|
return templ
|
|
}
|
|
|
|
func templates(in tpl.Template) []tpl.Template {
|
|
var templs []tpl.Template
|
|
in = unwrap(in)
|
|
if textt, ok := in.(*texttemplate.Template); ok {
|
|
for _, t := range textt.Templates() {
|
|
templs = append(templs, t)
|
|
}
|
|
}
|
|
|
|
if htmlt, ok := in.(*htmltemplate.Template); ok {
|
|
for _, t := range htmlt.Templates() {
|
|
templs = append(templs, t)
|
|
}
|
|
}
|
|
|
|
return templs
|
|
}
|