tpl/fmt: Add erroridf template func
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 7 Jun 2021 14:36:48 +0000 (16:36 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 7 Jun 2021 17:11:03 +0000 (19:11 +0200)
Fixes #8613

19 files changed:
commands/static_syncer.go
common/loggers/ignorableLogger.go
common/loggers/loggers.go
deps/deps.go
docs/content/en/functions/errorf.md
helpers/general.go
hugolib/alias.go
hugolib/collections.go
hugolib/hugo_sites.go
hugolib/page__new.go
hugolib/site.go
hugolib/site_render.go
langs/i18n/i18n.go
resources/page/zero_file.autogen.go
tpl/collections/collections.go
tpl/data/resources_test.go
tpl/fmt/fmt.go
tpl/fmt/init.go
tpl/fmt/init_test.go

index 23bdbe2df315c5e34a12d079f8b0e58d84ab5ed6..5569d4de6882c435b1c22a78b811db60094746c6 100644 (file)
@@ -58,7 +58,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
                syncer.DestFs = c.Fs.Destination
 
                // prevent spamming the log on changes
-               logger := helpers.NewDistinctFeedbackLogger()
+               logger := helpers.NewDistinctErrorLogger()
 
                for _, ev := range staticEvents {
                        // Due to our approach of layering both directories and the content's rendered output
index 766aae07c9d8a8dbe2ed44a5963564bcc70ac752..0a130900db0f86b4a5eae18abb8f1a95df551bbe 100644 (file)
@@ -22,6 +22,7 @@ import (
 type IgnorableLogger interface {
        Logger
        Errorsf(statementID, format string, v ...interface{})
+       Apply(logger Logger) IgnorableLogger
 }
 
 type ignorableLogger struct {
@@ -55,3 +56,10 @@ ignoreErrors = [%q]`, statementID)
 
        l.Errorf(format, v...)
 }
+
+func (l ignorableLogger) Apply(logger Logger) IgnorableLogger {
+       return ignorableLogger{
+               Logger:     logger,
+               statements: l.statements,
+       }
+}
index c9b5d21be471dadc812e3562141714044f668b6f..4ed1880164e3ee8bdcb208557ea80fcc2522f967 100644 (file)
@@ -59,6 +59,8 @@ type Logger interface {
        Println(v ...interface{})
        PrintTimerIfDelayed(start time.Time, name string)
        Debug() *log.Logger
+       Debugf(format string, v ...interface{})
+       Debugln(v ...interface{})
        Info() *log.Logger
        Infof(format string, v ...interface{})
        Infoln(v ...interface{})
@@ -108,6 +110,14 @@ func (l *logger) Debug() *log.Logger {
        return l.DEBUG
 }
 
+func (l *logger) Debugf(format string, v ...interface{}) {
+       l.DEBUG.Printf(format, v...)
+}
+
+func (l *logger) Debugln(v ...interface{}) {
+       l.DEBUG.Println(v...)
+}
+
 func (l *logger) Infof(format string, v ...interface{}) {
        l.INFO.Printf(format, v...)
 }
index 36620c96bd6485179697106db29254b8da82cd09..c0546db7632eba343fb1e68563807b323ff3de62 100644 (file)
@@ -34,10 +34,7 @@ type Deps struct {
        Log loggers.Logger `json:"-"`
 
        // Used to log errors that may repeat itself many times.
-       DistinctErrorLog *helpers.DistinctLogger
-
-       // Used to log warnings that may repeat itself many times.
-       DistinctWarningLog *helpers.DistinctLogger
+       LogDistinct loggers.Logger
 
        // The templates to use. This will usually implement the full tpl.TemplateManager.
        tmpl tpl.TemplateHandler
@@ -266,14 +263,12 @@ func New(cfg DepsCfg) (*Deps, error) {
        ignoreErrors := cast.ToStringSlice(cfg.Cfg.Get("ignoreErrors"))
        ignorableLogger := loggers.NewIgnorableLogger(logger, ignoreErrors...)
 
-       distinctErrorLogger := helpers.NewDistinctLogger(logger.Error())
-       distinctWarnLogger := helpers.NewDistinctLogger(logger.Warn())
+       logDistinct := helpers.NewDistinctLogger(logger)
 
        d := &Deps{
                Fs:                      fs,
                Log:                     ignorableLogger,
-               DistinctErrorLog:        distinctErrorLogger,
-               DistinctWarningLog:      distinctWarnLogger,
+               LogDistinct:             logDistinct,
                templateProvider:        cfg.TemplateProvider,
                translationProvider:     cfg.TranslationProvider,
                WithTemplate:            cfg.WithTemplate,
index 450e92679ad68a90f4029a08ea32a855e30b4442..a20ad4f44fbba29083b7d2ce85495557a166a07b 100644 (file)
@@ -31,3 +31,22 @@ Both functions return an empty string, so the messages are only printed to the c
 ```
 
 Note that `errorf` and `warnf` support all the formatting verbs of the [fmt](https://golang.org/pkg/fmt/) package.
+
+## Suppress errors
+
+Some times it may make sense to let the user suppress an ERROR and make the build succeed.
+
+You can do this by using the `erroridf` function. This functions takes an error ID as the first arument.
+
+
+``
+{{ erroridf "my-custom-error" "You should consider fixing this."}}
+```  
+
+This will produce:
+
+```
+ERROR 2021/06/07 17:47:38 You should consider fixing this.
+If you feel that this should not be logged as an ERROR, you can ignore it by adding this to your site config:
+ignoreErrors = ["my-custom-error"]
+```
index 9589514e03fa36692f14f05ca4d060863bc74ed1..09bf311f94bfc60da9069bb9b40c803c0c80ff79 100644 (file)
@@ -29,6 +29,8 @@ import (
        "unicode"
        "unicode/utf8"
 
+       "github.com/gohugoio/hugo/common/loggers"
+
        "github.com/mitchellh/hashstructure"
 
        "github.com/gohugoio/hugo/hugofs"
@@ -40,7 +42,6 @@ import (
        "github.com/jdkato/prose/transform"
 
        bp "github.com/gohugoio/hugo/bufferpool"
-       jww "github.com/spf13/jwalterweatherman"
        "github.com/spf13/pflag"
 )
 
@@ -253,9 +254,9 @@ type LogPrinter interface {
 
 // DistinctLogger ignores duplicate log statements.
 type DistinctLogger struct {
+       loggers.Logger
        sync.RWMutex
-       getLogger func() LogPrinter
-       m         map[string]bool
+       m map[string]bool
 }
 
 func (l *DistinctLogger) Reset() {
@@ -270,52 +271,105 @@ func (l *DistinctLogger) Reset() {
 func (l *DistinctLogger) Println(v ...interface{}) {
        // fmt.Sprint doesn't add space between string arguments
        logStatement := strings.TrimSpace(fmt.Sprintln(v...))
-       l.print(logStatement)
+       l.printIfNotPrinted("println", logStatement, func() {
+               l.Logger.Println(logStatement)
+       })
 }
 
 // Printf will log the string returned from fmt.Sprintf given the arguments,
 // but not if it has been logged before.
-// Note: A newline is appended.
 func (l *DistinctLogger) Printf(format string, v ...interface{}) {
        logStatement := fmt.Sprintf(format, v...)
-       l.print(logStatement)
+       l.printIfNotPrinted("printf", logStatement, func() {
+               l.Logger.Printf(format, v...)
+       })
+}
+
+func (l *DistinctLogger) Debugf(format string, v ...interface{}) {
+       logStatement := fmt.Sprintf(format, v...)
+       l.printIfNotPrinted("debugf", logStatement, func() {
+               l.Logger.Debugf(format, v...)
+       })
+}
+
+func (l *DistinctLogger) Debugln(v ...interface{}) {
+       logStatement := fmt.Sprint(v...)
+       l.printIfNotPrinted("debugln", logStatement, func() {
+               l.Logger.Debugln(v...)
+       })
+}
+
+func (l *DistinctLogger) Infof(format string, v ...interface{}) {
+       logStatement := fmt.Sprintf(format, v...)
+       l.printIfNotPrinted("info", logStatement, func() {
+               l.Logger.Infof(format, v...)
+       })
+}
+
+func (l *DistinctLogger) Infoln(v ...interface{}) {
+       logStatement := fmt.Sprint(v...)
+       l.printIfNotPrinted("infoln", logStatement, func() {
+               l.Logger.Infoln(v...)
+       })
+}
+
+func (l *DistinctLogger) Warnf(format string, v ...interface{}) {
+       logStatement := fmt.Sprintf(format, v...)
+       l.printIfNotPrinted("warnf", logStatement, func() {
+               l.Logger.Warnf(format, v...)
+       })
+}
+func (l *DistinctLogger) Warnln(v ...interface{}) {
+       logStatement := fmt.Sprint(v...)
+       l.printIfNotPrinted("warnln", logStatement, func() {
+               l.Logger.Warnln(v...)
+       })
+}
+func (l *DistinctLogger) Errorf(format string, v ...interface{}) {
+       logStatement := fmt.Sprint(v...)
+       l.printIfNotPrinted("errorf", logStatement, func() {
+               l.Logger.Errorf(format, v...)
+       })
+}
+
+func (l *DistinctLogger) Errorln(v ...interface{}) {
+       logStatement := fmt.Sprint(v...)
+       l.printIfNotPrinted("errorln", logStatement, func() {
+               l.Logger.Errorln(v...)
+       })
 }
 
-func (l *DistinctLogger) print(logStatement string) {
+func (l *DistinctLogger) hasPrinted(key string) bool {
        l.RLock()
-       if l.m[logStatement] {
-               l.RUnlock()
+       defer l.RUnlock()
+       _, found := l.m[key]
+       return found
+}
+
+func (l *DistinctLogger) printIfNotPrinted(level, logStatement string, print func()) {
+       key := level + logStatement
+       if l.hasPrinted(key) {
                return
        }
-       l.RUnlock()
-
        l.Lock()
-       if !l.m[logStatement] {
-               l.getLogger().Println(logStatement)
-               l.m[logStatement] = true
-       }
+       print()
+       l.m[key] = true
        l.Unlock()
 }
 
 // NewDistinctErrorLogger creates a new DistinctLogger that logs ERRORs
-func NewDistinctErrorLogger() *DistinctLogger {
-       return &DistinctLogger{m: make(map[string]bool), getLogger: func() LogPrinter { return jww.ERROR }}
+func NewDistinctErrorLogger() loggers.Logger {
+       return &DistinctLogger{m: make(map[string]bool), Logger: loggers.NewErrorLogger()}
 }
 
 // NewDistinctLogger creates a new DistinctLogger that logs to the provided logger.
-func NewDistinctLogger(logger LogPrinter) *DistinctLogger {
-       return &DistinctLogger{m: make(map[string]bool), getLogger: func() LogPrinter { return logger }}
+func NewDistinctLogger(logger loggers.Logger) loggers.Logger {
+       return &DistinctLogger{m: make(map[string]bool), Logger: logger}
 }
 
 // NewDistinctWarnLogger creates a new DistinctLogger that logs WARNs
-func NewDistinctWarnLogger() *DistinctLogger {
-       return &DistinctLogger{m: make(map[string]bool), getLogger: func() LogPrinter { return jww.WARN }}
-}
-
-// NewDistinctFeedbackLogger creates a new DistinctLogger that can be used
-// to give feedback to the user while not spamming with duplicates.
-func NewDistinctFeedbackLogger() *DistinctLogger {
-       return &DistinctLogger{m: make(map[string]bool), getLogger: func() LogPrinter { return jww.FEEDBACK }}
+func NewDistinctWarnLogger() loggers.Logger {
+       return &DistinctLogger{m: make(map[string]bool), Logger: loggers.NewWarningLogger()}
 }
 
 var (
@@ -324,16 +378,13 @@ var (
 
        // DistinctWarnLog can be used to avoid spamming the logs with warnings.
        DistinctWarnLog = NewDistinctWarnLogger()
-
-       // DistinctFeedbackLog can be used to avoid spamming the logs with info messages.
-       DistinctFeedbackLog = NewDistinctFeedbackLogger()
 )
 
 // InitLoggers resets the global distinct loggers.
 func InitLoggers() {
        DistinctErrorLog.Reset()
        DistinctWarnLog.Reset()
-       DistinctFeedbackLog.Reset()
+
 }
 
 // Deprecated informs about a deprecation, but only once for a given set of arguments' values.
index 891098c9d51a2296917e25accb1c071e1d307492..2609cd6bb496325ef55243f2e53f113dec9277f9 100644 (file)
@@ -79,7 +79,7 @@ func (s *Site) writeDestAlias(path, permalink string, outputFormat output.Format
 func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p page.Page) (err error) {
        handler := newAliasHandler(s.Tmpl(), s.Log, allowRoot)
 
-       s.Log.Debug().Println("creating alias:", path, "redirecting to", permalink)
+       s.Log.Debugln("creating alias:", path, "redirecting to", permalink)
 
        targetPath, err := handler.targetPathAlias(path)
        if err != nil {
index a794a9866108cf74ec4e46fce9448af757624ecd..9b4f83cc69ee6671e2045ab9fb1c17cefa16ace4 100644 (file)
@@ -28,7 +28,6 @@ var (
 // implementations have no value on their own.
 
 // Slice is not meant to be used externally. It's a bridge function
-// for the template functions. See collections.Slice.
 func (p *pageState) Slice(items interface{}) (interface{}, error) {
        return page.ToPages(items)
 }
index a016cab99ebb59ba0c08d792b5ad3570d3afee2d..0607bde1cbf426a475dd554e7c0eea483e5d7f96 100644 (file)
@@ -579,7 +579,8 @@ func (h *HugoSites) resetLogs() {
        h.Log.Reset()
        loggers.GlobalErrorCounter.Reset()
        for _, s := range h.Sites {
-               s.Deps.DistinctErrorLog = helpers.NewDistinctLogger(h.Log.Error())
+               s.Deps.Log.Reset()
+               s.Deps.LogDistinct.Reset()
        }
 }
 
index b37631477ff9d838dcd71559cc15ecbfe7d8e61d..8c96d5014dd60ff1c1d771e945199fa3548110cc 100644 (file)
@@ -102,7 +102,7 @@ func newPageFromMeta(
        meta map[string]interface{},
        metaProvider *pageMeta) (*pageState, error) {
        if metaProvider.f == nil {
-               metaProvider.f = page.NewZeroFile(metaProvider.s.DistinctWarningLog)
+               metaProvider.f = page.NewZeroFile(metaProvider.s.LogDistinct)
        }
 
        ps, err := newPageBase(metaProvider)
index 3c7c03bd13e9cd2af17edafac831f57617db2810..12714892da9cc9d68adb66a73f77735dbc496550 100644 (file)
@@ -1007,7 +1007,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
 
        changeIdentities := make(identity.Identities)
 
-       s.Log.Debug().Printf("Rebuild for events %q", events)
+       s.Log.Debugf("Rebuild for events %q", events)
 
        h := s.h
 
@@ -1026,7 +1026,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
                sourceFilesChanged = make(map[string]bool)
 
                // prevent spamming the log on changes
-               logger = helpers.NewDistinctFeedbackLogger()
+               logger = helpers.NewDistinctErrorLogger()
        )
 
        var cachePartitions []string
@@ -1385,7 +1385,7 @@ func (s *Site) getMenusFromConfig() navigation.Menus {
                                s.Log.Errorln(err)
                        } else {
                                for _, entry := range m {
-                                       s.Log.Debug().Printf("found menu: %q, in site config\n", name)
+                                       s.Log.Debugf("found menu: %q, in site config\n", name)
 
                                        menuEntry := navigation.MenuEntry{Menu: name}
                                        ime, err := maps.ToStringMapE(entry)
@@ -1646,7 +1646,7 @@ func (s *Site) lookupLayouts(layouts ...string) tpl.Template {
 }
 
 func (s *Site) renderAndWriteXML(statCounter *uint64, name string, targetPath string, d interface{}, templ tpl.Template) error {
-       s.Log.Debug().Printf("Render XML for %q to %q", name, targetPath)
+       s.Log.Debugf("Render XML for %q to %q", name, targetPath)
        renderBuffer := bp.GetBuffer()
        defer bp.PutBuffer(renderBuffer)
 
@@ -1668,7 +1668,7 @@ func (s *Site) renderAndWriteXML(statCounter *uint64, name string, targetPath st
 }
 
 func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, templ tpl.Template) error {
-       s.Log.Debug().Printf("Render %s to %q", name, targetPath)
+       s.Log.Debugf("Render %s to %q", name, targetPath)
        renderBuffer := bp.GetBuffer()
        defer bp.PutBuffer(renderBuffer)
 
index 84293cfc0755c17b121c49e13feaf07c6c199340..77ece780bbed90e09356fb2def55d5b4698ee36d 100644 (file)
@@ -389,13 +389,13 @@ func (s *Site) renderMainLanguageRedirect() error {
                mainLang := s.h.multilingual.DefaultLang
                if s.Info.defaultContentLanguageInSubdir {
                        mainLangURL := s.PathSpec.AbsURL(mainLang.Lang+"/", false)
-                       s.Log.Debug().Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)
+                       s.Log.Debugf("Write redirect to main language %s: %s", mainLang, mainLangURL)
                        if err := s.publishDestAlias(true, "/", mainLangURL, html, nil); err != nil {
                                return err
                        }
                } else {
                        mainLangURL := s.PathSpec.AbsURL("", false)
-                       s.Log.Debug().Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)
+                       s.Log.Debugf("Write redirect to main language %s: %s", mainLang, mainLangURL)
                        if err := s.publishDestAlias(true, mainLang.Lang, mainLangURL, html, nil); err != nil {
                                return err
                        }
index 83d581a7415e68b9125c7b261f29ab17dc407d98..e45a16822b921ff9ae86b8c3d0b5e20bedf7388e 100644 (file)
@@ -30,7 +30,7 @@ import (
 
 type translateFunc func(translationID string, templateData interface{}) string
 
-var i18nWarningLogger = helpers.NewDistinctFeedbackLogger()
+var i18nWarningLogger = helpers.NewDistinctErrorLogger()
 
 // Translator handles i18n translations.
 type Translator struct {
index 23e36b76490cd19f035b62c702c9c927552456dc..e4718a700510e87ba80f43077a8bfa275250cb06 100644 (file)
 package page
 
 import (
-       "github.com/gohugoio/hugo/helpers"
+       "github.com/gohugoio/hugo/common/loggers"
        "github.com/gohugoio/hugo/hugofs"
        "github.com/gohugoio/hugo/source"
 )
 
 // ZeroFile represents a zero value of source.File with warnings if invoked.
 type zeroFile struct {
-       log *helpers.DistinctLogger
+       log loggers.Logger
 }
 
-func NewZeroFile(log *helpers.DistinctLogger) source.File {
+func NewZeroFile(log loggers.Logger) source.File {
        return zeroFile{log: log}
 }
 
index 669e386f89461a35730c35e1abb918c3067907f2..0a9774aa4fa9f7558f3b5206bf599abbdb06c406 100644 (file)
@@ -380,7 +380,7 @@ func (ns *Namespace) IsSet(a interface{}, key interface{}) (bool, error) {
                        return av.MapIndex(kv).IsValid(), nil
                }
        default:
-               helpers.DistinctFeedbackLog.Printf("WARNING: calling IsSet with unsupported type %q (%T) will always return false.\n", av.Kind(), a)
+               helpers.DistinctErrorLog.Printf("WARNING: calling IsSet with unsupported type %q (%T) will always return false.\n", av.Kind(), a)
        }
 
        return false, nil
index 5ad0f097056fd5fef0f61d04592472428c255c5e..18061c052889c0c9c9aa369fa17bc5704bd60b7c 100644 (file)
@@ -213,12 +213,12 @@ func newDeps(cfg config.Provider) *deps.Deps {
        }
 
        return &deps.Deps{
-               Cfg:              cfg,
-               Fs:               fs,
-               FileCaches:       fileCaches,
-               ContentSpec:      cs,
-               Log:              logger,
-               DistinctErrorLog: helpers.NewDistinctLogger(logger.Error()),
+               Cfg:         cfg,
+               Fs:          fs,
+               FileCaches:  fileCaches,
+               ContentSpec: cs,
+               Log:         logger,
+               LogDistinct: helpers.NewDistinctLogger(logger),
        }
 }
 
index 713088b5704fd0fabeb9354064a0a73536680bbc..9c16ca6564c8a0918c5d942ea157126d0bf2eb49 100644 (file)
@@ -17,20 +17,22 @@ package fmt
 import (
        _fmt "fmt"
 
+       "github.com/gohugoio/hugo/common/loggers"
+
        "github.com/gohugoio/hugo/deps"
        "github.com/gohugoio/hugo/helpers"
 )
 
 // New returns a new instance of the fmt-namespaced template functions.
 func New(d *deps.Deps) *Namespace {
+       ignorableLogger := d.Log.(loggers.IgnorableLogger)
+       distinctLogger := helpers.NewDistinctLogger(d.Log)
        ns := &Namespace{
-               errorLogger: helpers.NewDistinctLogger(d.Log.Error()),
-               warnLogger:  helpers.NewDistinctLogger(d.Log.Warn()),
+               distinctLogger: ignorableLogger.Apply(distinctLogger),
        }
 
        d.BuildStartListeners.Add(func() {
-               ns.errorLogger.Reset()
-               ns.warnLogger.Reset()
+               ns.distinctLogger.Reset()
        })
 
        return ns
@@ -38,8 +40,7 @@ func New(d *deps.Deps) *Namespace {
 
 // Namespace provides template functions for the "fmt" namespace.
 type Namespace struct {
-       errorLogger *helpers.DistinctLogger
-       warnLogger  *helpers.DistinctLogger
+       distinctLogger loggers.IgnorableLogger
 }
 
 // Print returns string representation of the passed arguments.
@@ -60,13 +61,21 @@ func (ns *Namespace) Println(a ...interface{}) string {
 // Errorf formats according to a format specifier and logs an ERROR.
 // It returns an empty string.
 func (ns *Namespace) Errorf(format string, a ...interface{}) string {
-       ns.errorLogger.Printf(format, a...)
+       ns.distinctLogger.Errorf(format, a...)
+       return ""
+}
+
+// Erroridf formats according to a format specifier and logs an ERROR and
+// an information text that the error with the given ID can be suppressed in config.
+// It returns an empty string.
+func (ns *Namespace) Erroridf(id, format string, a ...interface{}) string {
+       ns.distinctLogger.Errorsf(id, format, a...)
        return ""
 }
 
 // Warnf formats according to a format specifier and logs a WARNING.
 // It returns an empty string.
 func (ns *Namespace) Warnf(format string, a ...interface{}) string {
-       ns.warnLogger.Printf(format, a...)
+       ns.distinctLogger.Warnf(format, a...)
        return ""
 }
index 6a2c9a856ca397dedb1889b5accc5dd05c537005..f322f511773faf6ba16d40e28c68e51f9eaae1dd 100644 (file)
@@ -57,6 +57,13 @@ func init() {
                        },
                )
 
+               ns.AddMethodMapping(ctx.Erroridf,
+                       []string{"erroridf"},
+                       [][2]string{
+                               {`{{ erroridf "my-err-id" "%s." "failed" }}`, ``},
+                       },
+               )
+
                ns.AddMethodMapping(ctx.Warnf,
                        []string{"warnf"},
                        [][2]string{
index edc1dbb5ebd71c62df26bee7c9dbba3c56119f55..8fa3945b87f8ebc6323a3dfb83f6e7e0f59e7c95 100644 (file)
@@ -30,7 +30,7 @@ func TestInit(t *testing.T) {
        var ns *internal.TemplateFuncsNamespace
 
        for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-               ns = nsf(&deps.Deps{Log: loggers.NewErrorLogger()})
+               ns = nsf(&deps.Deps{Log: loggers.NewIgnorableLogger(loggers.NewErrorLogger())})
                if ns.Name == name {
                        found = true
                        break