Add render template hooks for links and images

This commit also

* revises the change detection for templates used by content files in server mode.
* Adds a Page.RenderString method

Fixes #6545
Fixes #4663
Closes #6043
This commit is contained in:
Bjørn Erik Pedersen
2019-11-27 13:42:36 +01:00
parent 67f3aa72cf
commit e625088ef5
59 changed files with 2234 additions and 542 deletions

View File

@@ -658,7 +658,7 @@ var (
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
// it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0]
// as the function itself.
func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value {
func (s *state) evalCallOld(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value {
if args != nil {
args = args[1:] // Zeroth arg is function name/node; not passed to function.
}

View File

@@ -34,8 +34,9 @@ type Preparer interface {
// ExecHelper allows some custom eval hooks.
type ExecHelper interface {
GetFunc(name string) (reflect.Value, bool)
GetMapValue(receiver, key reflect.Value) (reflect.Value, bool)
GetFunc(tmpl Preparer, name string) (reflect.Value, bool)
GetMethod(tmpl Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value)
GetMapValue(tmpl Preparer, receiver, key reflect.Value) (reflect.Value, bool)
}
// Executer executes a given template.
@@ -64,6 +65,7 @@ func (t *executer) Execute(p Preparer, wr io.Writer, data interface{}) error {
state := &state{
helper: t.helper,
prep: p,
tmpl: tmpl,
wr: wr,
vars: []variable{{"$", value}},
@@ -75,7 +77,6 @@ func (t *executer) Execute(p Preparer, wr io.Writer, data interface{}) error {
// Prepare returns a template ready for execution.
func (t *Template) Prepare() (*Template, error) {
return t, nil
}
@@ -95,6 +96,7 @@ func (t *Template) executeWithState(state *state, value reflect.Value) (err erro
// can execute in parallel.
type state struct {
tmpl *Template
prep Preparer // Added for Hugo.
helper ExecHelper // Added for Hugo.
wr io.Writer
node parse.Node // current node, for errors
@@ -110,7 +112,7 @@ func (s *state) evalFunction(dot reflect.Value, node *parse.IdentifierNode, cmd
var ok bool
if s.helper != nil {
// Added for Hugo.
function, ok = s.helper.GetFunc(name)
function, ok = s.helper.GetFunc(s.prep, name)
}
if !ok {
@@ -148,9 +150,23 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node,
if ptr.Kind() != reflect.Interface && ptr.Kind() != reflect.Ptr && ptr.CanAddr() {
ptr = ptr.Addr()
}
if method := ptr.MethodByName(fieldName); method.IsValid() {
// Added for Hugo.
var first reflect.Value
var method reflect.Value
if s.helper != nil {
method, first = s.helper.GetMethod(s.prep, ptr, fieldName)
} else {
method = ptr.MethodByName(fieldName)
}
if method.IsValid() {
if first != zero {
return s.evalCall(dot, method, node, fieldName, args, final, first)
}
return s.evalCall(dot, method, node, fieldName, args, final)
}
hasArgs := len(args) > 1 || final != missingVal
// It's not a method; must be a field of a struct or an element of a map.
switch receiver.Kind() {
@@ -177,7 +193,7 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node,
var result reflect.Value
if s.helper != nil {
// Added for Hugo.
result, _ = s.helper.GetMapValue(receiver, nameVal)
result, _ = s.helper.GetMapValue(s.prep, receiver, nameVal)
} else {
result = receiver.MapIndex(nameVal)
}
@@ -209,3 +225,79 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node,
s.errorf("can't evaluate field %s in type %s", fieldName, typ)
panic("not reached")
}
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
// it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0]
// as the function itself.
func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value, first ...reflect.Value) reflect.Value {
if args != nil {
args = args[1:] // Zeroth arg is function name/node; not passed to function.
}
typ := fun.Type()
numFirst := len(first)
numIn := len(args) + numFirst // // Added for Hugo
if final != missingVal {
numIn++
}
numFixed := len(args) + len(first)
if typ.IsVariadic() {
numFixed = typ.NumIn() - 1 // last arg is the variadic one.
if numIn < numFixed {
s.errorf("wrong number of args for %s: want at least %d got %d", name, typ.NumIn()-1, len(args))
}
} else if numIn != typ.NumIn() {
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
}
if !goodFunc(typ) {
// TODO: This could still be a confusing error; maybe goodFunc should provide info.
s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
}
// Build the arg list.
argv := make([]reflect.Value, numIn)
// Args must be evaluated. Fixed args first.
i := len(first)
for ; i < numFixed && i < len(args)+numFirst; i++ {
argv[i] = s.evalArg(dot, typ.In(i), args[i-numFirst])
}
// Now the ... args.
if typ.IsVariadic() {
argType := typ.In(typ.NumIn() - 1).Elem() // Argument is a slice.
for ; i < len(args)+numFirst; i++ {
argv[i] = s.evalArg(dot, argType, args[i-numFirst])
}
}
// Add final value if necessary.
if final != missingVal {
t := typ.In(typ.NumIn() - 1)
if typ.IsVariadic() {
if numIn-1 < numFixed {
// The added final argument corresponds to a fixed parameter of the function.
// Validate against the type of the actual parameter.
t = typ.In(numIn - 1)
} else {
// The added final argument corresponds to the variadic part.
// Validate against the type of the elements of the variadic slice.
t = t.Elem()
}
}
argv[i] = s.validateType(final, t)
}
// Added for Hugo
for i := 0; i < len(first); i++ {
argv[i] = s.validateType(first[i], typ.In(i))
}
v, err := safeCall(fun, argv)
// If we have an error that is not nil, stop execution and return that
// error to the caller.
if err != nil {
s.at(node)
s.errorf("error calling %s: %v", name, err)
}
if v.Type() == reflectValueType {
v = v.Interface().(reflect.Value)
}
return v
}

View File

@@ -27,10 +27,18 @@ type TestStruct struct {
M map[string]string
}
func (t TestStruct) Hello1(arg string) string {
return arg
}
func (t TestStruct) Hello2(arg1, arg2 string) string {
return arg1 + " " + arg2
}
type execHelper struct {
}
func (e *execHelper) GetFunc(name string) (reflect.Value, bool) {
func (e *execHelper) GetFunc(tmpl Preparer, name string) (reflect.Value, bool) {
if name == "print" {
return zero, false
}
@@ -39,11 +47,19 @@ func (e *execHelper) GetFunc(name string) (reflect.Value, bool) {
}), true
}
func (e *execHelper) GetMapValue(m, key reflect.Value) (reflect.Value, bool) {
func (e *execHelper) GetMapValue(tmpl Preparer, m, key reflect.Value) (reflect.Value, bool) {
key = reflect.ValueOf(strings.ToLower(key.String()))
return m.MapIndex(key), true
}
func (e *execHelper) GetMethod(tmpl Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value) {
if name != "Hello1" {
return zero, zero
}
m := receiver.MethodByName("Hello2")
return m, reflect.ValueOf("v2")
}
func TestTemplateExecutor(t *testing.T) {
c := qt.New(t)
@@ -51,6 +67,7 @@ func TestTemplateExecutor(t *testing.T) {
{{ print "foo" }}
{{ printf "hugo" }}
Map: {{ .M.A }}
Method: {{ .Hello1 "v1" }}
`)
@@ -67,5 +84,6 @@ Map: {{ .M.A }}
c.Assert(got, qt.Contains, "foo")
c.Assert(got, qt.Contains, "hello hugo")
c.Assert(got, qt.Contains, "Map: av")
c.Assert(got, qt.Contains, "Method: v2 v1")
}

View File

@@ -116,9 +116,9 @@ func (ns *Namespace) Include(name string, contextList ...interface{}) (interface
return "", fmt.Errorf("partial %q not found", name)
}
var info tpl.Info
if ip, ok := templ.(tpl.TemplateInfoProvider); ok {
info = ip.TemplateInfo()
var info tpl.ParseInfo
if ip, ok := templ.(tpl.Info); ok {
info = ip.ParseInfo()
}
var w io.Writer

View File

@@ -24,8 +24,6 @@ import (
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
)
var _ TemplateInfoProvider = (*TemplateInfo)(nil)
// TemplateManager manages the collection of templates.
type TemplateManager interface {
TemplateHandler
@@ -34,7 +32,6 @@ type TemplateManager interface {
AddLateTemplate(name, tpl string) error
LoadTemplates(prefix string) error
MarkReady() error
RebuildClone()
}
@@ -80,11 +77,6 @@ type Template interface {
Prepare() (*texttemplate.Template, error)
}
// TemplateInfoProvider provides some contextual information about a template.
type TemplateInfoProvider interface {
TemplateInfo() Info
}
// TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain.
type TemplateParser interface {
Parse(name, tpl string) (Template, error)
@@ -101,10 +93,31 @@ type TemplateDebugger interface {
Debug()
}
// TemplateInfo wraps a Template with some additional information.
type TemplateInfo struct {
// templateInfo wraps a Template with some additional information.
type templateInfo struct {
Template
Info Info
Info
}
// templateInfo wraps a Template with some additional information.
type templateInfoManager struct {
Template
InfoManager
}
// WithInfo wraps the info in a template.
func WithInfo(templ Template, info Info) Template {
if manager, ok := info.(InfoManager); ok {
return &templateInfoManager{
Template: templ,
InfoManager: manager,
}
}
return &templateInfo{
Template: templ,
Info: info,
}
}
var baseOfRe = regexp.MustCompile("template: (.*?):")
@@ -117,10 +130,6 @@ func extractBaseOf(err string) string {
return ""
}
func (t *TemplateInfo) TemplateInfo() Info {
return t.Info
}
// TemplateFuncGetter allows to find a template func by name.
type TemplateFuncGetter interface {
GetFunc(name string) (reflect.Value, bool)

View File

@@ -13,12 +13,44 @@
package tpl
import (
"github.com/gohugoio/hugo/identity"
)
// Increments on breaking changes.
const TemplateVersion = 2
// Info holds some info extracted from a parsed template.
type Info struct {
type Info interface {
ParseInfo() ParseInfo
// Identifies this template and its dependencies.
identity.Provider
}
type InfoManager interface {
ParseInfo() ParseInfo
// Identifies and manages this template and its dependencies.
identity.Manager
}
type defaultInfo struct {
identity.Manager
parseInfo ParseInfo
}
func NewInfo(id identity.Manager, parseInfo ParseInfo) Info {
return &defaultInfo{
Manager: id,
parseInfo: parseInfo,
}
}
func (info *defaultInfo) ParseInfo() ParseInfo {
return info.parseInfo
}
type ParseInfo struct {
// Set for shortcode templates with any {{ .Inner }}
IsInner bool
@@ -26,17 +58,25 @@ type Info struct {
HasReturn bool
// Config extracted from template.
Config Config
Config ParseConfig
}
func (info Info) IsZero() bool {
func (info ParseInfo) IsZero() bool {
return info.Config.Version == 0
}
type Config struct {
// Info holds some info extracted from a parsed template.
type Info1 struct {
}
type ParseConfig struct {
Version int
}
var DefaultConfig = Config{
var DefaultParseConfig = ParseConfig{
Version: TemplateVersion,
}
var DefaultParseInfo = ParseInfo{
Config: DefaultParseConfig,
}

View File

@@ -83,10 +83,12 @@ func (s *shortcodeTemplates) fromVariantsSlice(variants []string) (shortcodeVari
func (s *shortcodeTemplates) compareVariants(a, b []string) int {
weight := 0
k := len(a)
for i, av := range a {
bv := b[i]
if av == bv {
weight++
// Add more weight to the left side (language...).
weight = weight + k - i
} else {
weight--
}

View File

@@ -53,10 +53,10 @@ func TestShortcodesTemplate(t *testing.T) {
name2 string
expected int
}{
{"Same suffix", "figure.html", "figure.html", 3},
{"Same suffix and output format", "figure.html.html", "figure.html.html", 3},
{"Same suffix, output format and language", "figure.no.html.html", "figure.no.html.html", 3},
{"No suffix", "figure", "figure", 3},
{"Same suffix", "figure.html", "figure.html", 6},
{"Same suffix and output format", "figure.html.html", "figure.html.html", 6},
{"Same suffix, output format and language", "figure.no.html.html", "figure.no.html.html", 6},
{"No suffix", "figure", "figure", 6},
{"Different output format", "figure.amp.html", "figure.html.html", -1},
{"One with output format, one without", "figure.amp.html", "figure.html", -1},
}

View File

@@ -20,6 +20,10 @@ import (
"regexp"
"time"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/common/herrors"
"strings"
@@ -27,7 +31,6 @@ import (
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/tpl/tplimpl/embedded"
@@ -81,6 +84,7 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler {
common := &templatesCommon{
nameBaseTemplateName: make(map[string]string),
transformNotFound: make(map[string]bool),
identityNotFound: make(map[string][]identity.Manager),
}
htmlT := &htmlTemplates{
@@ -100,13 +104,16 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler {
Deps: deps,
layoutsFs: deps.BaseFs.Layouts.Fs,
templateHandlerCommon: &templateHandlerCommon{
shortcodes: make(map[string]*shortcodeTemplates),
templateInfo: make(map[string]tpl.Info),
html: htmlT,
text: textT,
shortcodes: make(map[string]*shortcodeTemplates),
templateInfo: make(map[string]tpl.Info),
templateInfoTree: make(map[string]*templateInfoTree),
html: htmlT,
text: textT,
},
}
textT.textTemplate.templates = textT
textT.standalone.templates = textT
common.handler = h
return h
@@ -152,27 +159,26 @@ func (t *htmlTemplates) addTemplate(name, tpl string) (*templateContext, error)
return t.addTemplateIn(t.t, name, tpl)
}
func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) (*templateContext, error) {
templ, err := tt.New(name).Parse(tpl)
func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, templstr string) (*templateContext, error) {
templ, err := tt.New(name).Parse(templstr)
if err != nil {
return nil, err
}
typ := resolveTemplateType(name)
c, err := applyTemplateTransformersToHMLTTemplate(typ, templ)
c, err := t.handler.applyTemplateTransformersToHMLTTemplate(typ, templ)
if err != nil {
return nil, err
}
for k := range c.notFound {
for k := range c.templateNotFound {
t.transformNotFound[k] = true
t.identityNotFound[k] = append(t.identityNotFound[k], c.id)
}
if typ == templateShortcode {
t.handler.addShortcodeVariant(name, c.Info, templ)
} else {
t.handler.templateInfo[name] = c.Info
for k := range c.identityNotFound {
t.identityNotFound[k] = append(t.identityNotFound[k], c.id)
}
return c, nil
@@ -208,7 +214,7 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin
// * https://github.com/golang/go/issues/16101
// * https://github.com/gohugoio/hugo/issues/2549
overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
if _, err := applyTemplateTransformersToHMLTTemplate(templateUndefined, overlayTpl); err != nil {
if _, err := t.handler.applyTemplateTransformersToHMLTTemplate(templateUndefined, overlayTpl); err != nil {
return err
}
@@ -253,6 +259,8 @@ func (l nopLookupVariant) LookupVariant(name string, variants tpl.TemplateVarian
// It implements the templateLoader and tpl.TemplateHandler interfaces.
// There is one templateHandler created per Site.
type templateHandler struct {
ready bool
executor texttemplate.Executer
funcs map[string]reflect.Value
@@ -324,6 +332,7 @@ func (t *templateHandler) LoadTemplates(prefix string) error {
// Lookup tries to find a template with the given name in both template
// collections: First HTML, then the plain text template collection.
func (t *templateHandler) Lookup(name string) (tpl.Template, bool) {
if strings.HasPrefix(name, textTmplNamePrefix) {
// The caller has explicitly asked for a text template, so only look
// in the text template collection.
@@ -345,6 +354,9 @@ func (t *templateHandler) Lookup(name string) (tpl.Template, bool) {
// 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) {
if !t.ready {
panic("handler not ready")
}
name = templateBaseName(templateShortcode, name)
s, found := t.shortcodes[name]
if !found {
@@ -358,18 +370,17 @@ func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVarian
more := len(s.variants) > 1
return &tpl.TemplateInfo{
Template: sv.templ,
Info: sv.info,
}, true, more
return tpl.WithInfo(sv.templ, sv.info), true, more
}
// MarkReady marks the templates as "ready for execution". No changes allowed
// markReady marks the templates as "ready for execution". No changes allowed
// after this is set.
// TODO(bep) if this proves to be resource heavy, we could detect
// earlier if we really need this, or make it lazy.
func (t *templateHandler) MarkReady() error {
func (t *templateHandler) markReady() error {
defer func() {
t.ready = true
}()
if err := t.postTransform(); err != nil {
return err
}
@@ -483,6 +494,7 @@ func (t *templateHandler) addInternalTemplate(name, tpl string) error {
}
func (t *templateHandler) addShortcodeVariant(name string, info tpl.Info, templ tpl.Template) {
base := templateBaseName(templateShortcode, name)
shortcodename, variants := templateNameAndVariants(base)
@@ -561,18 +573,9 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e
}
func (t *templateHandler) applyTemplateInfo(templ tpl.Template, found bool) (tpl.Template, bool) {
if adapter, ok := templ.(*tpl.TemplateInfo); ok {
if adapter.Info.IsZero() {
if info, found := t.templateInfo[templ.Name()]; found {
adapter.Info = info
}
}
} else if templ != nil {
if templ != nil {
if info, found := t.templateInfo[templ.Name()]; found {
return &tpl.TemplateInfo{
Template: templ,
Info: info,
}, true
return tpl.WithInfo(templ, info), true
}
}
@@ -586,7 +589,11 @@ func (t *templateHandler) checkState() {
}
func (t *templateHandler) clone(d *deps.Deps) *templateHandler {
if !t.ready {
panic("invalid state")
}
c := &templateHandler{
ready: true,
Deps: d,
layoutsFs: d.BaseFs.Layouts.Fs,
}
@@ -703,36 +710,69 @@ func (t *templateHandler) loadTemplates(prefix string) error {
}
func (t *templateHandler) postTransform() error {
if len(t.html.transformNotFound) == 0 && len(t.text.transformNotFound) == 0 {
return nil
func (t *templateHandler) getOrCreateTemplateInfo(name string) (identity.Manager, tpl.ParseInfo) {
info, found := t.templateInfo[name]
if found {
return info.(identity.Manager), info.ParseInfo()
}
return identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)), tpl.DefaultParseInfo
}
func (t *templateHandler) createTemplateInfo(name string) (identity.Manager, tpl.ParseInfo) {
_, found := t.templateInfo[name]
if found {
panic("already created: " + name)
}
defer func() {
t.text.transformNotFound = make(map[string]bool)
t.html.transformNotFound = make(map[string]bool)
}()
return identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)), tpl.DefaultParseInfo
}
func (t *templateHandler) postTransform() error {
for k, v := range t.templateInfoTree {
if v.id != nil {
info := tpl.NewInfo(
v.id,
v.info,
)
t.templateInfo[k] = info
if v.typ == templateShortcode {
t.addShortcodeVariant(k, info, v.templ)
}
}
}
for _, s := range []struct {
lookup func(name string) *parse.Tree
lookup func(name string) *templateInfoTree
transformNotFound map[string]bool
identityNotFound map[string][]identity.Manager
}{
// html templates
{func(name string) *parse.Tree {
{func(name string) *templateInfoTree {
templ := t.html.lookup(name)
if templ == nil {
return nil
}
return templ.Tree
}, t.html.transformNotFound},
id, info := t.getOrCreateTemplateInfo(name)
return &templateInfoTree{
id: id,
info: info,
tree: templ.Tree,
}
}, t.html.transformNotFound, t.html.identityNotFound},
// text templates
{func(name string) *parse.Tree {
{func(name string) *templateInfoTree {
templT := t.text.lookup(name)
if templT == nil {
return nil
}
return templT.Tree
}, t.text.transformNotFound},
id, info := t.getOrCreateTemplateInfo(name)
return &templateInfoTree{
id: id,
info: info,
tree: templT.Tree,
}
}, t.text.transformNotFound, t.text.identityNotFound},
} {
for name := range s.transformNotFound {
templ := s.lookup(name)
@@ -743,6 +783,15 @@ func (t *templateHandler) postTransform() error {
}
}
}
for k, v := range s.identityNotFound {
tmpl := s.lookup(k)
if tmpl != nil {
for _, im := range v {
im.Add(tmpl.id)
}
}
}
}
return nil
@@ -758,7 +807,6 @@ func (t *templateHandler) wrapTextTemplate(tt *textTemplate) tpl.TemplateParseFi
tt,
new(nopLookupVariant),
}
}
type templateHandlerCommon struct {
@@ -771,6 +819,9 @@ type templateHandlerCommon struct {
// shortcodeTemplates type.
templateInfo map[string]tpl.Info
// Used to track templates during the AST transformations.
templateInfoTree map[string]*templateInfoTree
// text holds all the pure text templates.
text *textTemplates
html *htmlTemplates
@@ -795,9 +846,12 @@ type templatesCommon struct {
// Used to get proper filenames in errors
nameBaseTemplateName map[string]string
// Holds names of the templates not found during the first AST transformation
// Holds names of the template definitions not found during the first AST transformation
// pass.
transformNotFound map[string]bool
// Holds identities of templates not found during first pass.
identityNotFound map[string][]identity.Manager
}
func (t templatesCommon) withNewHandler(h *templateHandler) *templatesCommon {
@@ -806,8 +860,9 @@ func (t templatesCommon) withNewHandler(h *templateHandler) *templatesCommon {
}
type textTemplate struct {
mu sync.RWMutex
t *texttemplate.Template
mu sync.RWMutex
t *texttemplate.Template
templates *textTemplates
}
func (t *textTemplate) Lookup(name string) (tpl.Template, bool) {
@@ -831,7 +886,7 @@ func (t *textTemplate) parseIn(tt *texttemplate.Template, name, tpl string) (*te
return nil, err
}
if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, templ); err != nil {
if _, err := t.templates.handler.applyTemplateTransformersToTextTemplate(templateUndefined, templ); err != nil {
return nil, err
}
return templ, nil
@@ -868,30 +923,24 @@ func (t *textTemplates) addTemplate(name, tpl string) (*templateContext, error)
return t.addTemplateIn(t.t, name, tpl)
}
func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl string) (*templateContext, error) {
func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tplstr string) (*templateContext, error) {
name = strings.TrimPrefix(name, textTmplNamePrefix)
templ, err := t.parseIn(tt, name, tpl)
templ, err := t.parseIn(tt, name, tplstr)
if err != nil {
return nil, err
}
typ := resolveTemplateType(name)
c, err := applyTemplateTransformersToTextTemplate(typ, templ)
c, err := t.handler.applyTemplateTransformersToTextTemplate(typ, templ)
if err != nil {
return nil, err
}
for k := range c.notFound {
for k := range c.templateNotFound {
t.transformNotFound[k] = true
}
if typ == templateShortcode {
t.handler.addShortcodeVariant(name, c.Info, templ)
} else {
t.handler.templateInfo[name] = c.Info
}
return c, nil
}
@@ -924,7 +973,7 @@ func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename strin
}
overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, overlayTpl); err != nil {
if _, err := t.handler.applyTemplateTransformersToTextTemplate(templateUndefined, overlayTpl); err != nil {
return err
}
t.overlays[name] = overlayTpl

View File

@@ -44,16 +44,13 @@ func (*TemplateProvider) Update(deps *deps.Deps) error {
}
return newTmpl.MarkReady()
return newTmpl.markReady()
}
// Clone clones.
func (*TemplateProvider) Clone(d *deps.Deps) error {
t := d.Tmpl.(*templateHandler)
clone := t.clone(d)
return clone.MarkReady()
t.clone(d)
return nil
}

View File

@@ -14,8 +14,12 @@
package tplimpl
import (
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
"regexp"
"strings"
"github.com/gohugoio/hugo/identity"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
@@ -34,9 +38,10 @@ const (
)
type templateContext struct {
visited map[string]bool
notFound map[string]bool
lookupFn func(name string) *parse.Tree
visited map[string]bool
templateNotFound map[string]bool
identityNotFound map[string]bool
lookupFn func(name string) *templateInfoTree
// The last error encountered.
err error
@@ -47,13 +52,14 @@ type templateContext struct {
configChecked bool
// Contains some info about the template
tpl.Info
parseInfo *tpl.ParseInfo
id identity.Manager
// Store away the return node in partials.
returnNode *parse.CommandNode
}
func (c templateContext) getIfNotVisited(name string) *parse.Tree {
func (c templateContext) getIfNotVisited(name string) *templateInfoTree {
if c.visited[name] {
return nil
}
@@ -63,59 +69,95 @@ func (c templateContext) getIfNotVisited(name string) *parse.Tree {
// This may be a inline template defined outside of this file
// and not yet parsed. Unusual, but it happens.
// Store the name to try again later.
c.notFound[name] = true
c.templateNotFound[name] = true
}
return templ
}
func newTemplateContext(lookupFn func(name string) *parse.Tree) *templateContext {
return &templateContext{
Info: tpl.Info{Config: tpl.DefaultConfig},
lookupFn: lookupFn,
visited: make(map[string]bool),
notFound: make(map[string]bool)}
}
func newTemplateContext(
id identity.Manager,
info *tpl.ParseInfo,
lookupFn func(name string) *templateInfoTree) *templateContext {
func createParseTreeLookup(templ *template.Template) func(nn string) *parse.Tree {
return func(nn string) *parse.Tree {
tt := templ.Lookup(nn)
if tt != nil {
return tt.Tree
}
return nil
return &templateContext{
id: id,
parseInfo: info,
lookupFn: lookupFn,
visited: make(map[string]bool),
templateNotFound: make(map[string]bool),
identityNotFound: make(map[string]bool),
}
}
func applyTemplateTransformersToHMLTTemplate(typ templateType, templ *template.Template) (*templateContext, error) {
return applyTemplateTransformers(typ, templ.Tree, createParseTreeLookup(templ))
func createGetTemplateInfoTreeFor(getID func(name string) *templateInfoTree) func(nn string) *templateInfoTree {
return func(nn string) *templateInfoTree {
return getID(nn)
}
}
func applyTemplateTransformersToTextTemplate(typ templateType, templ *texttemplate.Template) (*templateContext, error) {
return applyTemplateTransformers(typ, templ.Tree,
func(nn string) *parse.Tree {
tt := templ.Lookup(nn)
if tt != nil {
return tt.Tree
}
return nil
})
func (t *templateHandler) applyTemplateTransformersToHMLTTemplate(typ templateType, templ *template.Template) (*templateContext, error) {
id, info := t.createTemplateInfo(templ.Name())
ti := &templateInfoTree{
tree: templ.Tree,
templ: templ,
typ: typ,
id: id,
info: info,
}
t.templateInfoTree[templ.Name()] = ti
getTemplateInfoTree := createGetTemplateInfoTreeFor(func(name string) *templateInfoTree {
return t.templateInfoTree[name]
})
return applyTemplateTransformers(typ, ti, getTemplateInfoTree)
}
func applyTemplateTransformers(typ templateType, templ *parse.Tree, lookupFn func(name string) *parse.Tree) (*templateContext, error) {
func (t *templateHandler) applyTemplateTransformersToTextTemplate(typ templateType, templ *texttemplate.Template) (*templateContext, error) {
id, info := t.createTemplateInfo(templ.Name())
ti := &templateInfoTree{
tree: templ.Tree,
templ: templ,
typ: typ,
id: id,
info: info,
}
t.templateInfoTree[templ.Name()] = ti
getTemplateInfoTree := createGetTemplateInfoTreeFor(func(name string) *templateInfoTree {
return t.templateInfoTree[name]
})
return applyTemplateTransformers(typ, ti, getTemplateInfoTree)
}
type templateInfoTree struct {
info tpl.ParseInfo
typ templateType
id identity.Manager
templ tpl.Template
tree *parse.Tree
}
func applyTemplateTransformers(
typ templateType,
templ *templateInfoTree,
lookupFn func(name string) *templateInfoTree) (*templateContext, error) {
if templ == nil {
return nil, errors.New("expected template, but none provided")
}
c := newTemplateContext(lookupFn)
c := newTemplateContext(templ.id, &templ.info, lookupFn)
c.typ = typ
_, err := c.applyTransformations(templ.Root)
_, err := c.applyTransformations(templ.tree.Root)
if err == nil && c.returnNode != nil {
// This is a partial with a return statement.
c.Info.HasReturn = true
templ.Root = c.wrapInPartialReturnWrapper(templ.Root)
c.parseInfo.HasReturn = true
templ.tree.Root = c.wrapInPartialReturnWrapper(templ.tree.Root)
}
return c, err
@@ -125,7 +167,9 @@ const (
partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ with .Arg }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
)
var partialReturnWrapper *parse.ListNode
var (
partialReturnWrapper *parse.ListNode
)
func init() {
templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl)
@@ -133,6 +177,7 @@ func init() {
panic(err)
}
partialReturnWrapper = templ.Tree.Root
}
func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
@@ -156,6 +201,7 @@ func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.L
// getif works slightly different than the Go built-in in that it also
// considers any IsZero methods on the values (as in time.Time).
// See https://github.com/gohugoio/hugo/issues/5738
// TODO(bep) get rid of this.
func (c *templateContext) wrapWithGetIf(p *parse.PipeNode) {
if len(p.Cmds) == 0 {
return
@@ -176,9 +222,9 @@ func (c *templateContext) wrapWithGetIf(p *parse.PipeNode) {
}
// applyTransformations do 3 things:
// 1) Make all .Params.CamelCase and similar into lowercase.
// 2) Wraps every with and if pipe in getif
// 3) Collects some information about the template content.
// 1) Wraps every with and if pipe in getif
// 2) Parses partial return statement.
// 3) Tracks template (partial) dependencies and some other info.
func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
switch x := n.(type) {
case *parse.ListNode:
@@ -198,7 +244,7 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
case *parse.TemplateNode:
subTempl := c.getIfNotVisited(x.Name)
if subTempl != nil {
c.applyTransformationsToNodes(subTempl.Root)
c.applyTransformationsToNodes(subTempl.tree.Root)
}
case *parse.PipeNode:
c.collectConfig(x)
@@ -210,6 +256,7 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
}
case *parse.CommandNode:
c.collectPartialInfo(x)
c.collectInner(x)
keep := c.collectReturnNode(x)
@@ -277,11 +324,10 @@ func (c *templateContext) collectConfig(n *parse.PipeNode) {
c.err = errors.Wrap(err, errMsg)
return
}
if err := mapstructure.WeakDecode(m, &c.Info.Config); err != nil {
if err := mapstructure.WeakDecode(m, &c.parseInfo.Config); err != nil {
c.err = errors.Wrap(err, errMsg)
}
}
}
// collectInner determines if the given CommandNode represents a
@@ -290,7 +336,7 @@ func (c *templateContext) collectInner(n *parse.CommandNode) {
if c.typ != templateShortcode {
return
}
if c.Info.IsInner || len(n.Args) == 0 {
if c.parseInfo.IsInner || len(n.Args) == 0 {
return
}
@@ -304,13 +350,45 @@ func (c *templateContext) collectInner(n *parse.CommandNode) {
}
if c.hasIdent(idents, "Inner") {
c.Info.IsInner = true
c.parseInfo.IsInner = true
break
}
}
}
var partialRe = regexp.MustCompile(`^partial(Cached)?$|^partials\.Include(Cached)?$`)
func (c *templateContext) collectPartialInfo(x *parse.CommandNode) {
if len(x.Args) < 2 {
return
}
first := x.Args[0]
var id string
switch v := first.(type) {
case *parse.IdentifierNode:
id = v.Ident
case *parse.ChainNode:
id = v.String()
}
if partialRe.MatchString(id) {
partialName := strings.Trim(x.Args[1].String(), "\"")
if !strings.Contains(partialName, ".") {
partialName += ".html"
}
partialName = "partials/" + partialName
info := c.lookupFn(partialName)
if info != nil {
c.id.Add(info.id)
} else {
// Delay for later
c.identityNotFound[partialName] = true
}
}
}
func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
if c.typ != templatePartial || c.returnNode != nil {
return true

View File

@@ -15,14 +15,17 @@ package tplimpl
import (
"strings"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
"github.com/gohugoio/hugo/hugofs/files"
"testing"
"time"
"github.com/gohugoio/hugo/tpl"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/tpl"
)
// Issue #2927
@@ -33,7 +36,7 @@ func TestTransformRecursiveTemplate(t *testing.T) {
{{ define "menu-nodes" }}
{{ template "menu-node" }}
{{ end }}
{{ define "menu-node" }}
{{ define "menu-nßode" }}
{{ template "menu-node" }}
{{ end }}
{{ template "menu-nodes" }}
@@ -41,12 +44,25 @@ func TestTransformRecursiveTemplate(t *testing.T) {
templ, err := template.New("foo").Parse(recursive)
c.Assert(err, qt.IsNil)
parseInfo := tpl.DefaultParseInfo
ctx := newTemplateContext(createParseTreeLookup(templ))
ctx := newTemplateContext(
newTemplateInfo("test").(identity.Manager),
&parseInfo,
createGetTemplateInfoTree(templ.Tree),
)
ctx.applyTransformations(templ.Tree.Root)
}
func createGetTemplateInfoTree(tree *parse.Tree) func(name string) *templateInfoTree {
return func(name string) *templateInfoTree {
return &templateInfoTree{
tree: tree,
}
}
}
type I interface {
Method0()
}
@@ -80,13 +96,10 @@ func TestInsertIsZeroFunc(t *testing.T) {
{{ with .TimeZero }}.TimeZero1 with: {{ . }}{{ else }}.TimeZero1 with: FALSE{{ end }}
{{ template "mytemplate" . }}
{{ if .T.NonEmptyInterfaceTypedNil }}.NonEmptyInterfaceTypedNil: TRUE{{ else }}.NonEmptyInterfaceTypedNil: FALSE{{ end }}
{{ template "other-file-template" . }}
{{ define "mytemplate" }}
{{ if .TimeZero }}.TimeZero1: mytemplate: TRUE{{ else }}.TimeZero1: mytemplate: FALSE{{ end }}
{{ end }}
`
// https://github.com/gohugoio/hugo/issues/5865
@@ -97,7 +110,7 @@ func TestInsertIsZeroFunc(t *testing.T) {
)
d := newD(c)
h := d.Tmpl.(tpl.TemplateManager)
h := d.Tmpl.(*templateHandler)
// HTML templates
c.Assert(h.AddTemplate("mytemplate.html", templ1), qt.IsNil)
@@ -107,15 +120,13 @@ func TestInsertIsZeroFunc(t *testing.T) {
c.Assert(h.AddTemplate("_text/mytexttemplate.txt", templ1), qt.IsNil)
c.Assert(h.AddTemplate("_text/myothertexttemplate.txt", templ2), qt.IsNil)
c.Assert(h.MarkReady(), qt.IsNil)
c.Assert(h.markReady(), qt.IsNil)
for _, name := range []string{"mytemplate.html", "mytexttemplate.txt"} {
var sb strings.Builder
tt, _ := d.Tmpl.Lookup(name)
sb := &strings.Builder{}
err := d.Tmpl.Execute(tt, sb, ctx)
err := h.Execute(tt, &sb, ctx)
c.Assert(err, qt.IsNil)
result := sb.String()
c.Assert(result, qt.Contains, ".True: TRUE")
@@ -138,14 +149,10 @@ func TestCollectInfo(t *testing.T) {
tests := []struct {
name string
tplString string
expected tpl.Info
expected tpl.ParseInfo
}{
{"Basic Inner", `{{ .Inner }}`, tpl.Info{IsInner: true, Config: tpl.DefaultConfig}},
{"Basic config map", "{{ $_hugo_config := `" + configStr + "` }}", tpl.Info{
Config: tpl.Config{
Version: 42,
},
}},
{"Basic Inner", `{{ .Inner }}`, tpl.ParseInfo{IsInner: true, Config: tpl.DefaultParseConfig}},
{"Basic config map", "{{ $_hugo_config := `" + configStr + "` }}", tpl.ParseInfo{Config: tpl.ParseConfig{Version: 42}}},
}
echo := func(in interface{}) interface{} {
@@ -162,12 +169,13 @@ func TestCollectInfo(t *testing.T) {
templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
c.Assert(err, qt.IsNil)
parseInfo := tpl.DefaultParseInfo
ctx := newTemplateContext(createParseTreeLookup(templ))
ctx := newTemplateContext(
newTemplateInfo("test").(identity.Manager), &parseInfo, createGetTemplateInfoTree(templ.Tree))
ctx.typ = templateShortcode
ctx.applyTransformations(templ.Tree.Root)
c.Assert(ctx.Info, qt.Equals, test.expected)
c.Assert(ctx.parseInfo, qt.DeepEquals, &test.expected)
})
}
@@ -205,7 +213,10 @@ func TestPartialReturn(t *testing.T) {
templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
c.Assert(err, qt.IsNil)
_, err = applyTemplateTransformers(templatePartial, templ.Tree, createParseTreeLookup(templ))
_, err = applyTemplateTransformers(
templatePartial,
&templateInfoTree{tree: templ.Tree, info: tpl.DefaultParseInfo},
createGetTemplateInfoTree(templ.Tree))
// Just check that it doesn't fail in this test. We have functional tests
// in hugoblib.
@@ -215,3 +226,10 @@ func TestPartialReturn(t *testing.T) {
}
}
func newTemplateInfo(name string) tpl.Info {
return tpl.NewInfo(
identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)),
tpl.DefaultParseInfo,
)
}

View File

@@ -19,6 +19,8 @@ import (
"reflect"
"strings"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/common/maps"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
@@ -62,14 +64,14 @@ type templateExecHelper struct {
funcs map[string]reflect.Value
}
func (t *templateExecHelper) GetFunc(name string) (reflect.Value, bool) {
func (t *templateExecHelper) GetFunc(tmpl texttemplate.Preparer, name string) (reflect.Value, bool) {
if fn, found := t.funcs[name]; found {
return fn, true
}
return zero, false
}
func (t *templateExecHelper) GetMapValue(receiver, key reflect.Value) (reflect.Value, bool) {
func (t *templateExecHelper) GetMapValue(tmpl texttemplate.Preparer, receiver, key reflect.Value) (reflect.Value, bool) {
if params, ok := receiver.Interface().(maps.Params); ok {
// Case insensitive.
keystr := strings.ToLower(key.String())
@@ -85,6 +87,22 @@ func (t *templateExecHelper) GetMapValue(receiver, key reflect.Value) (reflect.V
return v, v.IsValid()
}
func (t *templateExecHelper) GetMethod(tmpl texttemplate.Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value) {
// This is a hot path and receiver.MethodByName really shows up in the benchmarks.
// Page.Render is the only method with a WithTemplateInfo as of now, so let's just
// check that for now.
// TODO(bep) find a more flexible, but still fast, way.
if name == "Render" {
if info, ok := tmpl.(tpl.Info); ok {
if m := receiver.MethodByName(name + "WithTemplateInfo"); m.IsValid() {
return m, reflect.ValueOf(info)
}
}
}
return receiver.MethodByName(name), zero
}
func newTemplateExecuter(d *deps.Deps) (texttemplate.Executer, map[string]reflect.Value) {
funcs := createFuncMap(d)
funcsv := make(map[string]reflect.Value)
@@ -120,9 +138,7 @@ func createFuncMap(d *deps.Deps) map[string]interface{} {
}
funcMap[alias] = mm.Method
}
}
}
if d.OverloadedTemplateFuncs != nil {

View File

@@ -24,18 +24,19 @@ import (
func TestTemplateInfoShortcode(t *testing.T) {
c := qt.New(t)
d := newD(c)
h := d.Tmpl.(tpl.TemplateManager)
h := d.Tmpl.(*templateHandler)
c.Assert(h.AddTemplate("shortcodes/mytemplate.html", `
{{ .Inner }}
`), qt.IsNil)
c.Assert(h.markReady(), qt.IsNil)
tt, found, _ := d.Tmpl.LookupVariant("mytemplate", tpl.TemplateVariants{})
c.Assert(found, qt.Equals, true)
tti, ok := tt.(tpl.TemplateInfoProvider)
tti, ok := tt.(tpl.Info)
c.Assert(ok, qt.Equals, true)
c.Assert(tti.TemplateInfo().IsInner, qt.Equals, true)
c.Assert(tti.ParseInfo().IsInner, qt.Equals, true)
}