mirror of
https://github.com/gohugoio/hugo.git
synced 2025-09-01 22:42:45 +02:00
commands: Show server error info in browser
The main item in this commit is showing of errors with a file context when running `hugo server`. This can be turned off: `hugo server --disableBrowserError` (can also be set in `config.toml`). But to get there, the error handling in Hugo needed a revision. There are some items left TODO for commits soon to follow, most notable errors in content and config files. Fixes #5284 Fixes #5290 See #5325 See #5324
This commit is contained in:
@@ -17,20 +17,17 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -856,7 +853,7 @@ func newDeps(cfg config.Provider) *deps.Deps {
|
||||
Cfg: cfg,
|
||||
Fs: hugofs.NewMem(l),
|
||||
ContentSpec: cs,
|
||||
Log: jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime),
|
||||
Log: loggers.NewErrorLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -18,12 +18,12 @@ import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
_errors "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// New returns a new instance of the data-namespaced template functions.
|
||||
@@ -59,7 +59,7 @@ func (ns *Namespace) GetCSV(sep string, urlParts ...string) (d [][]string, err e
|
||||
var req *http.Request
|
||||
req, err = http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to create request for getCSV for resource %s: %s", url, err)
|
||||
return nil, _errors.Wrapf(err, "Failed to create request for getCSV for resource %s:", url)
|
||||
}
|
||||
|
||||
req.Header.Add("Accept", "text/csv")
|
||||
@@ -103,7 +103,7 @@ func (ns *Namespace) GetJSON(urlParts ...string) (v interface{}, err error) {
|
||||
var req *http.Request
|
||||
req, err = http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to create request for getJSON resource %s: %s", url, err)
|
||||
return nil, _errors.Wrapf(err, "Failed to create request for getJSON resource %s:", url)
|
||||
}
|
||||
|
||||
req.Header.Add("Accept", "application/json")
|
||||
|
@@ -113,11 +113,11 @@ func TestGetCSV(t *testing.T) {
|
||||
require.NoError(t, err, msg)
|
||||
|
||||
if _, ok := test.expect.(bool); ok {
|
||||
require.Equal(t, 1, int(ns.deps.Log.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError)))
|
||||
require.Equal(t, 1, int(ns.deps.Log.ErrorCounter.Count()))
|
||||
require.Nil(t, got)
|
||||
continue
|
||||
}
|
||||
require.Equal(t, 0, int(ns.deps.Log.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError)))
|
||||
require.Equal(t, 0, int(ns.deps.Log.ErrorCounter.Count()))
|
||||
require.NotNil(t, got, msg)
|
||||
|
||||
assert.EqualValues(t, test.expect, got, msg)
|
||||
@@ -198,14 +198,14 @@ func TestGetJSON(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
if errLevel, ok := test.expect.(jww.Threshold); ok {
|
||||
logCount := ns.deps.Log.LogCountForLevelsGreaterThanorEqualTo(errLevel)
|
||||
if errLevel, ok := test.expect.(jww.Threshold); ok && errLevel >= jww.LevelError {
|
||||
logCount := ns.deps.Log.ErrorCounter.Count()
|
||||
require.True(t, logCount >= 1, fmt.Sprintf("got log count %d", logCount))
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err, msg)
|
||||
|
||||
require.Equal(t, 0, int(ns.deps.Log.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError)), msg)
|
||||
require.Equal(t, 0, int(ns.deps.Log.ErrorCounter.Count()), msg)
|
||||
require.NotNil(t, got, msg)
|
||||
|
||||
assert.EqualValues(t, test.expect, got, msg)
|
||||
|
@@ -16,12 +16,13 @@ package fmt
|
||||
import (
|
||||
_fmt "fmt"
|
||||
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
)
|
||||
|
||||
// New returns a new instance of the fmt-namespaced template functions.
|
||||
func New() *Namespace {
|
||||
return &Namespace{helpers.NewDistinctErrorLogger()}
|
||||
func New(d *deps.Deps) *Namespace {
|
||||
return &Namespace{helpers.NewDistinctLogger(d.Log.ERROR)}
|
||||
}
|
||||
|
||||
// Namespace provides template functions for the "fmt" namespace.
|
||||
|
@@ -22,7 +22,7 @@ const name = "fmt"
|
||||
|
||||
func init() {
|
||||
f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
|
||||
ctx := New()
|
||||
ctx := New(d)
|
||||
|
||||
ns := &internal.TemplateFuncsNamespace{
|
||||
Name: name,
|
||||
|
@@ -16,6 +16,7 @@ package fmt
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/tpl/internal"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -26,7 +27,7 @@ func TestInit(t *testing.T) {
|
||||
var ns *internal.TemplateFuncsNamespace
|
||||
|
||||
for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
|
||||
ns = nsf(&deps.Deps{})
|
||||
ns = nsf(&deps.Deps{Log: loggers.NewErrorLogger()})
|
||||
if ns.Name == name {
|
||||
found = true
|
||||
break
|
||||
|
@@ -16,6 +16,7 @@ package partials
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/tpl/internal"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -28,6 +29,7 @@ func TestInit(t *testing.T) {
|
||||
for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
|
||||
ns = nsf(&deps.Deps{
|
||||
BuildStartListeners: &deps.Listeners{},
|
||||
Log: loggers.NewErrorLogger(),
|
||||
})
|
||||
if ns.Name == name {
|
||||
found = true
|
||||
|
@@ -18,6 +18,8 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
_errors "github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/resource"
|
||||
"github.com/gohugoio/hugo/resource/bundler"
|
||||
@@ -256,7 +258,7 @@ func (ns *Namespace) resolveArgs(args []interface{}) (resource.Resource, map[str
|
||||
|
||||
m, err := cast.ToStringMapE(args[0])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid options type: %s", err)
|
||||
return nil, nil, _errors.Wrap(err, "invalid options type")
|
||||
}
|
||||
|
||||
return r, m, nil
|
||||
|
@@ -20,6 +20,8 @@ import (
|
||||
_strings "strings"
|
||||
"unicode/utf8"
|
||||
|
||||
_errors "github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/spf13/cast"
|
||||
@@ -44,7 +46,7 @@ type Namespace struct {
|
||||
func (ns *Namespace) CountRunes(s interface{}) (int, error) {
|
||||
ss, err := cast.ToStringE(s)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Failed to convert content to string: %s", err)
|
||||
return 0, _errors.Wrap(err, "Failed to convert content to string")
|
||||
}
|
||||
|
||||
counter := 0
|
||||
@@ -61,7 +63,7 @@ func (ns *Namespace) CountRunes(s interface{}) (int, error) {
|
||||
func (ns *Namespace) RuneCount(s interface{}) (int, error) {
|
||||
ss, err := cast.ToStringE(s)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Failed to convert content to string: %s", err)
|
||||
return 0, _errors.Wrap(err, "Failed to convert content to string")
|
||||
}
|
||||
return utf8.RuneCountInString(ss), nil
|
||||
}
|
||||
@@ -70,7 +72,7 @@ func (ns *Namespace) RuneCount(s interface{}) (int, error) {
|
||||
func (ns *Namespace) CountWords(s interface{}) (int, error) {
|
||||
ss, err := cast.ToStringE(s)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Failed to convert content to string: %s", err)
|
||||
return 0, _errors.Wrap(err, "Failed to convert content to string")
|
||||
}
|
||||
|
||||
counter := 0
|
||||
|
127
tpl/template.go
127
tpl/template.go
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017-present The Hugo Authors. All rights reserved.
|
||||
// Copyright 2018 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.
|
||||
@@ -14,16 +14,26 @@
|
||||
package tpl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"text/template/parse"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"html/template"
|
||||
texttemplate "text/template"
|
||||
"text/template/parse"
|
||||
|
||||
bp "github.com/gohugoio/hugo/bufferpool"
|
||||
"github.com/gohugoio/hugo/metrics"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -35,8 +45,7 @@ type TemplateHandler interface {
|
||||
TemplateFinder
|
||||
AddTemplate(name, tpl string) error
|
||||
AddLateTemplate(name, tpl string) error
|
||||
LoadTemplates(prefix string)
|
||||
PrintErrors()
|
||||
LoadTemplates(prefix string) error
|
||||
|
||||
NewTextTemplate() TemplateParseFinder
|
||||
|
||||
@@ -82,16 +91,122 @@ type TemplateDebugger interface {
|
||||
type TemplateAdapter struct {
|
||||
Template
|
||||
Metrics metrics.Provider
|
||||
|
||||
// The filesystem where the templates are stored.
|
||||
Fs afero.Fs
|
||||
|
||||
// Maps to base template if relevant.
|
||||
NameBaseTemplateName map[string]string
|
||||
}
|
||||
|
||||
var baseOfRe = regexp.MustCompile("template: (.*?):")
|
||||
|
||||
func extractBaseOf(err string) string {
|
||||
m := baseOfRe.FindStringSubmatch(err)
|
||||
if len(m) == 2 {
|
||||
return m[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Execute executes the current template. The actual execution is performed
|
||||
// by the embedded text or html template, but we add an implementation here so
|
||||
// we can add a timer for some metrics.
|
||||
func (t *TemplateAdapter) Execute(w io.Writer, data interface{}) error {
|
||||
func (t *TemplateAdapter) Execute(w io.Writer, data interface{}) (execErr error) {
|
||||
defer func() {
|
||||
// Panics in templates are a little bit too common (nil pointers etc.)
|
||||
if r := recover(); r != nil {
|
||||
execErr = t.addFileContext(t.Name(), fmt.Errorf("panic in Execute: %s", r))
|
||||
}
|
||||
}()
|
||||
|
||||
if t.Metrics != nil {
|
||||
defer t.Metrics.MeasureSince(t.Name(), time.Now())
|
||||
}
|
||||
return t.Template.Execute(w, data)
|
||||
|
||||
execErr = t.Template.Execute(w, data)
|
||||
if execErr != nil {
|
||||
execErr = t.addFileContext(t.Name(), execErr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var identifiersRe = regexp.MustCompile("at \\<(.*?)\\>:")
|
||||
|
||||
func (t *TemplateAdapter) 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 *TemplateAdapter) addFileContext(name string, inerr error) error {
|
||||
f, realFilename, err := t.fileAndFilename(t.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
master, hasMaster := t.NameBaseTemplateName[name]
|
||||
|
||||
ferr := errors.Wrapf(inerr, "execute of template %q failed", realFilename)
|
||||
|
||||
// Since this can be a composite of multiple template files (single.html + baseof.html etc.)
|
||||
// we potentially need to look in both -- and cannot rely on line number alone.
|
||||
lineMatcher := func(le herrors.FileError, lineNumber int, line string) bool {
|
||||
if le.LineNumber() != lineNumber {
|
||||
return false
|
||||
}
|
||||
if !hasMaster {
|
||||
return true
|
||||
}
|
||||
|
||||
identifiers := t.extractIdentifiers(le.Error())
|
||||
|
||||
for _, id := range identifiers {
|
||||
if strings.Contains(line, id) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO(bep) 2errors text vs HTML
|
||||
fe, ok := herrors.WithFileContext(ferr, f, "go-html-template", lineMatcher)
|
||||
if ok || !hasMaster {
|
||||
return fe
|
||||
}
|
||||
|
||||
// Try the base template if relevant
|
||||
f, realFilename, err = t.fileAndFilename(master)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
ferr = errors.Wrapf(inerr, "execute of template %q failed", realFilename)
|
||||
fe, _ = herrors.WithFileContext(ferr, f, "go-html-template", lineMatcher)
|
||||
return fe
|
||||
|
||||
}
|
||||
|
||||
func (t *TemplateAdapter) fileAndFilename(name string) (afero.File, string, error) {
|
||||
fs := t.Fs
|
||||
filename := filepath.FromSlash(name)
|
||||
|
||||
fi, err := fs.Stat(filename)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrapf(err, "failed to Stat %q", filename)
|
||||
}
|
||||
f, err := fs.Open(filename)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrapf(err, "failed to open template file %q:", filename)
|
||||
}
|
||||
|
||||
return f, fi.(hugofs.RealFilenameInfo).RealFilename(), nil
|
||||
}
|
||||
|
||||
// ExecuteToString executes the current template and returns the result as a
|
||||
|
31
tpl/template_test.go
Normal file
31
tpl/template_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2018 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 tpl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractBaseof(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
replaced := extractBaseOf(`failed: template: _default/baseof.html:37:11: executing "_default/baseof.html" at <.Parents>: can't evaluate field Parents in type *hugolib.PageOutput`)
|
||||
|
||||
assert.Equal("_default/baseof.html", replaced)
|
||||
assert.Equal("", extractBaseOf("not baseof for you"))
|
||||
assert.Equal("blog/baseof.html", extractBaseOf("template: blog/baseof.html:23:11:"))
|
||||
assert.Equal("blog/baseof.ace", extractBaseOf("template: blog/baseof.ace:23:11:"))
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017-present The Hugo Authors. All rights reserved.
|
||||
// Copyright 2018 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.
|
||||
@@ -20,7 +20,9 @@ import (
|
||||
"strings"
|
||||
texttemplate "text/template"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl/embedded"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/eknkc/amber"
|
||||
|
||||
@@ -64,7 +66,7 @@ type templateErr struct {
|
||||
}
|
||||
|
||||
type templateLoader interface {
|
||||
handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error
|
||||
handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error
|
||||
addTemplate(name, tpl string) error
|
||||
addLateTemplate(name, tpl string) error
|
||||
}
|
||||
@@ -114,22 +116,11 @@ func (t *templateHandler) NewTextTemplate() tpl.TemplateParseFinder {
|
||||
|
||||
}
|
||||
|
||||
func (t *templateHandler) addError(name string, err error) {
|
||||
t.errors = append(t.errors, &templateErr{name, err})
|
||||
}
|
||||
|
||||
func (t *templateHandler) Debug() {
|
||||
fmt.Println("HTML templates:\n", t.html.t.DefinedTemplates())
|
||||
fmt.Println("\n\nText templates:\n", t.text.t.DefinedTemplates())
|
||||
}
|
||||
|
||||
// PrintErrors prints the accumulated errors as ERROR to the log.
|
||||
func (t *templateHandler) PrintErrors() {
|
||||
for _, e := range t.errors {
|
||||
t.Log.ERROR.Println(e.name, ":", e.err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -156,8 +147,8 @@ func (t *templateHandler) clone(d *deps.Deps) *templateHandler {
|
||||
c := &templateHandler{
|
||||
Deps: d,
|
||||
layoutsFs: d.BaseFs.Layouts.Fs,
|
||||
html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template)},
|
||||
text: &textTemplates{textTemplate: &textTemplate{t: texttemplate.Must(t.text.t.Clone())}, overlays: make(map[string]*texttemplate.Template)},
|
||||
html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template), templatesCommon: t.html.templatesCommon},
|
||||
text: &textTemplates{textTemplate: &textTemplate{t: texttemplate.Must(t.text.t.Clone())}, overlays: make(map[string]*texttemplate.Template), templatesCommon: t.text.templatesCommon},
|
||||
errors: make([]*templateErr, 0),
|
||||
}
|
||||
|
||||
@@ -187,15 +178,21 @@ func (t *templateHandler) clone(d *deps.Deps) *templateHandler {
|
||||
}
|
||||
|
||||
func newTemplateAdapter(deps *deps.Deps) *templateHandler {
|
||||
common := &templatesCommon{
|
||||
nameBaseTemplateName: make(map[string]string),
|
||||
}
|
||||
|
||||
htmlT := &htmlTemplates{
|
||||
t: template.New(""),
|
||||
overlays: make(map[string]*template.Template),
|
||||
t: template.New(""),
|
||||
overlays: make(map[string]*template.Template),
|
||||
templatesCommon: common,
|
||||
}
|
||||
textT := &textTemplates{
|
||||
textTemplate: &textTemplate{t: texttemplate.New("")},
|
||||
overlays: make(map[string]*texttemplate.Template),
|
||||
textTemplate: &textTemplate{t: texttemplate.New("")},
|
||||
overlays: make(map[string]*texttemplate.Template),
|
||||
templatesCommon: common,
|
||||
}
|
||||
return &templateHandler{
|
||||
h := &templateHandler{
|
||||
Deps: deps,
|
||||
layoutsFs: deps.BaseFs.Layouts.Fs,
|
||||
html: htmlT,
|
||||
@@ -203,11 +200,23 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler {
|
||||
errors: make([]*templateErr, 0),
|
||||
}
|
||||
|
||||
common.handler = h
|
||||
|
||||
return h
|
||||
|
||||
}
|
||||
|
||||
type htmlTemplates struct {
|
||||
// Shared by both HTML and text templates.
|
||||
type templatesCommon struct {
|
||||
handler *templateHandler
|
||||
funcster *templateFuncster
|
||||
|
||||
// Used to get proper filenames in errors
|
||||
nameBaseTemplateName map[string]string
|
||||
}
|
||||
type htmlTemplates struct {
|
||||
*templatesCommon
|
||||
|
||||
t *template.Template
|
||||
|
||||
// This looks, and is, strange.
|
||||
@@ -231,7 +240,8 @@ func (t *htmlTemplates) Lookup(name string) (tpl.Template, bool) {
|
||||
if templ == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics}, true
|
||||
|
||||
return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics, Fs: t.handler.layoutsFs, NameBaseTemplateName: t.nameBaseTemplateName}, true
|
||||
}
|
||||
|
||||
func (t *htmlTemplates) lookup(name string) *template.Template {
|
||||
@@ -259,8 +269,8 @@ func (t *textTemplates) setTemplateFuncster(f *templateFuncster) {
|
||||
}
|
||||
|
||||
type textTemplates struct {
|
||||
*templatesCommon
|
||||
*textTemplate
|
||||
funcster *templateFuncster
|
||||
clone *texttemplate.Template
|
||||
cloneClone *texttemplate.Template
|
||||
|
||||
@@ -272,7 +282,7 @@ func (t *textTemplates) Lookup(name string) (tpl.Template, bool) {
|
||||
if templ == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics}, true
|
||||
return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics, Fs: t.handler.layoutsFs, NameBaseTemplateName: t.nameBaseTemplateName}, true
|
||||
}
|
||||
|
||||
func (t *textTemplates) lookup(name string) *texttemplate.Template {
|
||||
@@ -321,8 +331,8 @@ func (t *textTemplates) setFuncs(funcMap map[string]interface{}) {
|
||||
// LoadTemplates loads the templates from the layouts filesystem.
|
||||
// A prefix can be given to indicate a template namespace to load the templates
|
||||
// into, i.e. "_internal" etc.
|
||||
func (t *templateHandler) LoadTemplates(prefix string) {
|
||||
t.loadTemplates(prefix)
|
||||
func (t *templateHandler) LoadTemplates(prefix string) error {
|
||||
return t.loadTemplates(prefix)
|
||||
|
||||
}
|
||||
|
||||
@@ -423,7 +433,6 @@ func (t *templateHandler) addLateTemplate(name, tpl string) error {
|
||||
func (t *templateHandler) AddLateTemplate(name, tpl string) error {
|
||||
h := t.getTemplateHandler(name)
|
||||
if err := h.addLateTemplate(name, tpl); err != nil {
|
||||
t.addError(name, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -435,7 +444,6 @@ func (t *templateHandler) AddLateTemplate(name, tpl string) error {
|
||||
func (t *templateHandler) AddTemplate(name, tpl string) error {
|
||||
h := t.getTemplateHandler(name)
|
||||
if err := h.addTemplate(name, tpl); err != nil {
|
||||
t.addError(name, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -458,14 +466,19 @@ func (t *templateHandler) MarkReady() {
|
||||
|
||||
// RebuildClone rebuilds the cloned templates. Used for live-reloads.
|
||||
func (t *templateHandler) RebuildClone() {
|
||||
t.html.clone = template.Must(t.html.cloneClone.Clone())
|
||||
t.text.clone = texttemplate.Must(t.text.cloneClone.Clone())
|
||||
if t.html != nil && t.html.cloneClone != nil {
|
||||
t.html.clone = template.Must(t.html.cloneClone.Clone())
|
||||
}
|
||||
if t.text != nil && t.text.cloneClone != nil {
|
||||
t.text.clone = texttemplate.Must(t.text.cloneClone.Clone())
|
||||
}
|
||||
}
|
||||
|
||||
func (t *templateHandler) loadTemplates(prefix string) {
|
||||
func (t *templateHandler) loadTemplates(prefix string) error {
|
||||
|
||||
walker := func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil || fi.IsDir() {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) {
|
||||
@@ -490,21 +503,25 @@ func (t *templateHandler) loadTemplates(prefix string) {
|
||||
tplID, err := output.CreateTemplateNames(descriptor)
|
||||
if err != nil {
|
||||
t.Log.ERROR.Printf("Failed to resolve template in path %q: %s", path, err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := t.addTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != nil {
|
||||
t.Log.ERROR.Printf("Failed to add template %q in path %q: %s", tplID.Name, path, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := helpers.SymbolicWalk(t.Layouts.Fs, "", walker); err != nil {
|
||||
t.Log.ERROR.Printf("Failed to load templates: %s", err)
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (t *templateHandler) initFuncs() {
|
||||
@@ -553,12 +570,12 @@ func (t *templateHandler) getTemplateHandler(name string) templateLoader {
|
||||
return t.html
|
||||
}
|
||||
|
||||
func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error {
|
||||
func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error {
|
||||
h := t.getTemplateHandler(name)
|
||||
return h.handleMaster(name, overlayFilename, masterFilename, onMissing)
|
||||
}
|
||||
|
||||
func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error {
|
||||
func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error {
|
||||
|
||||
masterTpl := t.lookup(masterFilename)
|
||||
|
||||
@@ -568,9 +585,9 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin
|
||||
return err
|
||||
}
|
||||
|
||||
masterTpl, err = t.t.New(overlayFilename).Parse(templ)
|
||||
masterTpl, err = t.t.New(overlayFilename).Parse(templ.template)
|
||||
if err != nil {
|
||||
return err
|
||||
return templ.errWithFileContext("parse master failed", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,9 +596,9 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin
|
||||
return err
|
||||
}
|
||||
|
||||
overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ)
|
||||
overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ.template)
|
||||
if err != nil {
|
||||
return err
|
||||
return templ.errWithFileContext("parse failed", err)
|
||||
}
|
||||
|
||||
// The extra lookup is a workaround, see
|
||||
@@ -593,12 +610,13 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin
|
||||
}
|
||||
|
||||
t.overlays[name] = overlayTpl
|
||||
t.nameBaseTemplateName[name] = masterFilename
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error {
|
||||
func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error {
|
||||
|
||||
name = strings.TrimPrefix(name, textTmplNamePrefix)
|
||||
masterTpl := t.lookup(masterFilename)
|
||||
@@ -609,10 +627,11 @@ func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename strin
|
||||
return err
|
||||
}
|
||||
|
||||
masterTpl, err = t.t.New(overlayFilename).Parse(templ)
|
||||
masterTpl, err = t.t.New(masterFilename).Parse(templ.template)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrapf(err, "failed to parse %q:", templ.filename)
|
||||
}
|
||||
t.nameBaseTemplateName[masterFilename] = templ.filename
|
||||
}
|
||||
|
||||
templ, err := onMissing(overlayFilename)
|
||||
@@ -620,9 +639,9 @@ func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename strin
|
||||
return err
|
||||
}
|
||||
|
||||
overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ)
|
||||
overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ.template)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrapf(err, "failed to parse %q:", templ.filename)
|
||||
}
|
||||
|
||||
overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
|
||||
@@ -630,6 +649,7 @@ func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename strin
|
||||
return err
|
||||
}
|
||||
t.overlays[name] = overlayTpl
|
||||
t.nameBaseTemplateName[name] = templ.filename
|
||||
|
||||
return err
|
||||
|
||||
@@ -640,14 +660,22 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e
|
||||
|
||||
t.Log.DEBUG.Printf("Add template file: name %q, baseTemplatePath %q, path %q", name, baseTemplatePath, path)
|
||||
|
||||
getTemplate := func(filename string) (string, error) {
|
||||
b, err := afero.ReadFile(t.Layouts.Fs, filename)
|
||||
getTemplate := func(filename string) (templateInfo, error) {
|
||||
fs := t.Layouts.Fs
|
||||
b, err := afero.ReadFile(fs, filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return templateInfo{filename: filename, fs: fs}, err
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
return s, nil
|
||||
realFilename := filename
|
||||
if fi, err := fs.Stat(filename); err == nil {
|
||||
if fir, ok := fi.(hugofs.RealFilenameInfo); ok {
|
||||
realFilename = fir.RealFilename()
|
||||
}
|
||||
}
|
||||
|
||||
return templateInfo{template: s, filename: filename, realFilename: realFilename, fs: fs}, nil
|
||||
}
|
||||
|
||||
// get the suffix and switch on that
|
||||
@@ -712,7 +740,11 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e
|
||||
return err
|
||||
}
|
||||
|
||||
return t.AddTemplate(name, templ)
|
||||
err = t.AddTemplate(name, templ.template)
|
||||
if err != nil {
|
||||
return templ.errWithFileContext("parse failed", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,19 +752,24 @@ var embeddedTemplatesAliases = map[string][]string{
|
||||
"shortcodes/twitter.html": []string{"shortcodes/tweet.html"},
|
||||
}
|
||||
|
||||
func (t *templateHandler) loadEmbedded() {
|
||||
func (t *templateHandler) loadEmbedded() error {
|
||||
for _, kv := range embedded.EmbeddedTemplates {
|
||||
// TODO(bep) error handling
|
||||
name, templ := kv[0], kv[1]
|
||||
t.addInternalTemplate(name, templ)
|
||||
if err := t.addInternalTemplate(name, templ); err != nil {
|
||||
return err
|
||||
}
|
||||
if aliases, found := embeddedTemplatesAliases[name]; found {
|
||||
for _, alias := range aliases {
|
||||
t.addInternalTemplate(alias, templ)
|
||||
if err := t.addInternalTemplate(alias, templ); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (t *templateHandler) addInternalTemplate(name, tpl string) error {
|
||||
|
@@ -33,12 +33,15 @@ func (*TemplateProvider) Update(deps *deps.Deps) error {
|
||||
deps.TextTmpl = newTmpl.NewTextTemplate()
|
||||
|
||||
newTmpl.initFuncs()
|
||||
newTmpl.loadEmbedded()
|
||||
|
||||
if err := newTmpl.loadEmbedded(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if deps.WithTemplate != nil {
|
||||
err := deps.WithTemplate(newTmpl)
|
||||
if err != nil {
|
||||
newTmpl.addError("init", err)
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
46
tpl/tplimpl/template_errors.go
Normal file
46
tpl/tplimpl/template_errors.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2018 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 (
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
type templateInfo struct {
|
||||
template string
|
||||
|
||||
// Used to create some error context in error situations
|
||||
fs afero.Fs
|
||||
|
||||
// The filename relative to the fs above.
|
||||
filename string
|
||||
|
||||
// The real filename (if possible). Used for logging.
|
||||
realFilename string
|
||||
}
|
||||
|
||||
func (info templateInfo) errWithFileContext(what string, err error) error {
|
||||
err = errors.Wrapf(err, "file %q: %s:", info.realFilename, what)
|
||||
|
||||
err, _ = herrors.WithFileContextForFile(
|
||||
err,
|
||||
info.filename,
|
||||
info.fs,
|
||||
"go-html-template",
|
||||
herrors.SimpleLineMatcher)
|
||||
|
||||
return err
|
||||
}
|
@@ -21,10 +21,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
@@ -35,13 +32,12 @@ import (
|
||||
"github.com/gohugoio/hugo/tpl/internal"
|
||||
"github.com/gohugoio/hugo/tpl/partials"
|
||||
"github.com/spf13/afero"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
logger = jww.NewNotepad(jww.LevelFatal, jww.LevelFatal, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime)
|
||||
logger = loggers.NewErrorLogger()
|
||||
)
|
||||
|
||||
func newTestConfig() config.Provider {
|
||||
|
@@ -17,11 +17,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/russross/blackfriday"
|
||||
|
||||
"html/template"
|
||||
"net/url"
|
||||
|
||||
_errors "github.com/pkg/errors"
|
||||
"github.com/russross/blackfriday"
|
||||
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
@@ -55,7 +56,7 @@ func (ns *Namespace) AbsURL(a interface{}) (template.HTML, error) {
|
||||
func (ns *Namespace) Parse(rawurl interface{}) (*url.URL, error) {
|
||||
s, err := cast.ToStringE(rawurl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error in Parse: %s", err)
|
||||
return nil, _errors.Wrap(err, "Error in Parse")
|
||||
}
|
||||
|
||||
return url.Parse(s)
|
||||
|
Reference in New Issue
Block a user