Add configurable ref/relref error handling and notFoundURL
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 18 Jul 2018 17:58:39 +0000 (19:58 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 19 Jul 2018 12:32:43 +0000 (14:32 +0200)
Two new settings:

* refLinksErrorLevel: ERROR (default) or WARNING. ERROR will fail the build.
* refLinksNotFoundURL: Used as a placeholder when page references cannot be found.

Fixes #4964

hugolib/config_test.go
hugolib/hugo_sites.go
hugolib/page.go
hugolib/shortcode_test.go
hugolib/site.go
hugolib/site_test.go
hugolib/testhelpers_test.go

index 0fe692805c4b0deab8c046732157b1a6f0bb2dca..16d07d1aff3252e3b115ed201dafb7dadc3536ad 100644 (file)
@@ -392,6 +392,6 @@ privacyEnhanced = true
        b.WithConfigFile("toml", tomlConfig)
        b.Build(BuildCfg{SkipRender: true})
 
-       assert.True(b.H.Sites[0].Info.Config.Privacy.YouTube.PrivacyEnhanced)
+       assert.True(b.H.Sites[0].Info.Config().Privacy.YouTube.PrivacyEnhanced)
 
 }
index 8cb3cf2fd8caccffc32b50b21c24c9a112d1d942..859e0da7c6ae680e1b8638303d957f95e30a1684 100644 (file)
@@ -129,7 +129,7 @@ func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) {
                s.owner = h
        }
 
-       if err := applyDepsIfNeeded(cfg, sites...); err != nil {
+       if err := applyDeps(cfg, sites...); err != nil {
                return nil, err
        }
 
@@ -161,7 +161,7 @@ func (h *HugoSites) initGitInfo() error {
        return nil
 }
 
-func applyDepsIfNeeded(cfg deps.DepsCfg, sites ...*Site) error {
+func applyDeps(cfg deps.DepsCfg, sites ...*Site) error {
        if cfg.TemplateProvider == nil {
                cfg.TemplateProvider = tplimpl.DefaultTemplateProvider
        }
@@ -208,6 +208,19 @@ func applyDepsIfNeeded(cfg deps.DepsCfg, sites ...*Site) error {
                        s.Deps = d
                }
 
+               if err := s.initializeSiteInfo(); err != nil {
+                       return err
+               }
+
+               siteConfig, err := loadSiteConfig(s.Language)
+               if err != nil {
+                       return err
+               }
+               s.siteConfig = siteConfig
+               s.siteRefLinker, err = newSiteRefLinker(s.Language, s)
+               if err != nil {
+                       return err
+               }
        }
 
        return nil
@@ -308,7 +321,7 @@ func (h *HugoSites) createSitesFromConfig() error {
                s.owner = h
        }
 
-       if err := applyDepsIfNeeded(depsCfg, sites...); err != nil {
+       if err := applyDeps(depsCfg, sites...); err != nil {
                return err
        }
 
index 353e546d34fb3d8d719a6a0158e93ad364ead70b..838791ab8342d3324f3e9285ebd5fa5114f38fa2 100644 (file)
@@ -2063,7 +2063,8 @@ func (p *Page) decodeRefArgs(args map[string]interface{}) (refArgs, *SiteInfo, e
                }
 
                if !found {
-                       return ra, nil, fmt.Errorf("no site found with lang %q", ra.Lang)
+                       p.s.siteRefLinker.logNotFound(ra.Path, fmt.Sprintf("no site found with lang %q", ra.Lang), p)
+                       return ra, nil, nil
                }
        }
 
@@ -2076,6 +2077,10 @@ func (p *Page) Ref(argsm map[string]interface{}) (string, error) {
                return "", fmt.Errorf("invalid arguments to Ref: %s", err)
        }
 
+       if s == nil {
+               return p.s.siteRefLinker.notFoundURL, nil
+       }
+
        if args.Path == "" {
                return "", nil
        }
@@ -2092,6 +2097,10 @@ func (p *Page) RelRef(argsm map[string]interface{}) (string, error) {
                return "", fmt.Errorf("invalid arguments to Ref: %s", err)
        }
 
+       if s == nil {
+               return p.s.siteRefLinker.notFoundURL, nil
+       }
+
        if args.Path == "" {
                return "", nil
        }
@@ -2178,7 +2187,7 @@ func (p *Page) shouldAddLanguagePrefix() bool {
                return false
        }
 
-       if !p.Site.defaultContentLanguageInSubdir && p.Lang() == p.Site.multilingual.DefaultLang.Lang {
+       if !p.Site.defaultContentLanguageInSubdir && p.Lang() == p.s.multilingual().DefaultLang.Lang {
                return false
        }
 
@@ -2191,7 +2200,7 @@ func (p *Page) initLanguage() {
                        return
                }
 
-               ml := p.Site.multilingual
+               ml := p.s.multilingual()
                if ml == nil {
                        panic("Multilanguage not set")
                }
index 1437ae0cf9c0f2db6802d32bae63defeba76a641..f4935c6e9c7bd4d26bd284015c2384ffca7c4373 100644 (file)
@@ -34,7 +34,6 @@ import (
 
        "github.com/gohugoio/hugo/deps"
        "github.com/gohugoio/hugo/helpers"
-       "github.com/gohugoio/hugo/langs"
        "github.com/gohugoio/hugo/tpl"
 
        "github.com/stretchr/testify/require"
@@ -42,19 +41,16 @@ import (
 
 // TODO(bep) remove
 func pageFromString(in, filename string, withTemplate ...func(templ tpl.TemplateHandler) error) (*Page, error) {
-       s := newTestSite(nil)
-       if len(withTemplate) > 0 {
-               // Have to create a new site
-               var err error
-               cfg, fs := newTestCfg()
+       var err error
+       cfg, fs := newTestCfg()
 
-               d := deps.DepsCfg{Language: langs.NewLanguage("en", cfg), Cfg: cfg, Fs: fs, WithTemplate: withTemplate[0]}
+       d := deps.DepsCfg{Cfg: cfg, Fs: fs, WithTemplate: withTemplate[0]}
 
-               s, err = NewSiteForCfg(d)
-               if err != nil {
-                       return nil, err
-               }
+       s, err := NewSiteForCfg(d)
+       if err != nil {
+               return nil, err
        }
+
        return s.NewPageFrom(strings.NewReader(in), filename)
 }
 
index 08cdd265218863f706d266bc8495bff845fc404b..b460a0f8105b0c094e689e54bf5e3821834ca9a6 100644 (file)
@@ -18,6 +18,7 @@ import (
        "fmt"
        "html/template"
        "io"
+       "log"
        "mime"
        "net/url"
        "os"
@@ -127,6 +128,8 @@ type Site struct {
        outputFormatsConfig output.Formats
        mediaTypesConfig    media.Types
 
+       siteConfig SiteConfig
+
        // How to handle page front matter.
        frontmatterHandler pagemeta.FrontMatterHandler
 
@@ -147,6 +150,7 @@ type Site struct {
        titleFunc func(s string) string
 
        relatedDocsHandler *relatedDocsHandler
+       siteRefLinker
 }
 
 type siteRenderingContext struct {
@@ -183,6 +187,7 @@ func (s *Site) reset() *Site {
                disabledKinds:       s.disabledKinds,
                titleFunc:           s.titleFunc,
                relatedDocsHandler:  newSearchIndexHandler(s.relatedDocsHandler.cfg),
+               siteRefLinker:       s.siteRefLinker,
                outputFormats:       s.outputFormats,
                rc:                  s.rc,
                outputFormatsConfig: s.outputFormatsConfig,
@@ -190,7 +195,9 @@ func (s *Site) reset() *Site {
                mediaTypesConfig:    s.mediaTypesConfig,
                Language:            s.Language,
                owner:               s.owner,
+               siteConfig:          s.siteConfig,
                PageCollections:     newPageCollections()}
+
 }
 
 // newSite creates a new site with the given configuration.
@@ -276,8 +283,6 @@ func newSite(cfg deps.DepsCfg) (*Site, error) {
                frontmatterHandler:  frontMatterHandler,
        }
 
-       s.Info = newSiteInfo(siteBuilderCfg{s: s, pageCollections: c, language: s.Language})
-
        return s, nil
 
 }
@@ -291,7 +296,7 @@ func NewSite(cfg deps.DepsCfg) (*Site, error) {
                return nil, err
        }
 
-       if err = applyDepsIfNeeded(cfg, s); err != nil {
+       if err = applyDeps(cfg, s); err != nil {
                return nil, err
        }
 
@@ -333,7 +338,7 @@ func newSiteForLang(lang *langs.Language, withTemplate ...func(templ tpl.Templat
                return nil
        }
 
-       cfg := deps.DepsCfg{WithTemplate: withTemplates, Language: lang, Cfg: lang}
+       cfg := deps.DepsCfg{WithTemplate: withTemplates, Cfg: lang}
 
        return NewSiteForCfg(cfg)
 
@@ -343,16 +348,12 @@ func newSiteForLang(lang *langs.Language, withTemplate ...func(templ tpl.Templat
 // The site will have a template system loaded and ready to use.
 // Note: This is mainly used in single site tests.
 func NewSiteForCfg(cfg deps.DepsCfg) (*Site, error) {
-       s, err := newSite(cfg)
-
+       h, err := NewHugoSites(cfg)
        if err != nil {
                return nil, err
        }
+       return h.Sites[0], nil
 
-       if err := applyDepsIfNeeded(cfg, s); err != nil {
-               return nil, err
-       }
-       return s, nil
 }
 
 type SiteInfos []*SiteInfo
@@ -370,28 +371,24 @@ type SiteInfo struct {
        Authors    AuthorList
        Social     SiteSocial
        *PageCollections
-       Menus                 *Menus
-       Hugo                  *HugoInfo
-       Title                 string
-       RSSLink               string
-       Author                map[string]interface{}
-       LanguageCode          string
-       Copyright             string
-       LastChange            time.Time
-       Permalinks            PermalinkOverrides
-       Params                map[string]interface{}
-       BuildDrafts           bool
-       canonifyURLs          bool
-       relativeURLs          bool
-       uglyURLs              func(p *Page) bool
-       preserveTaxonomyNames bool
-       Data                  *map[string]interface{}
-
-       Config SiteConfig
-
+       Menus                          *Menus
+       Hugo                           *HugoInfo
+       Title                          string
+       RSSLink                        string
+       Author                         map[string]interface{}
+       LanguageCode                   string
+       Copyright                      string
+       LastChange                     time.Time
+       Permalinks                     PermalinkOverrides
+       Params                         map[string]interface{}
+       BuildDrafts                    bool
+       canonifyURLs                   bool
+       relativeURLs                   bool
+       uglyURLs                       func(p *Page) bool
+       preserveTaxonomyNames          bool
+       Data                           *map[string]interface{}
        owner                          *HugoSites
        s                              *Site
-       multilingual                   *Multilingual
        Language                       *langs.Language
        LanguagePrefix                 string
        Languages                      langs.Languages
@@ -399,6 +396,10 @@ type SiteInfo struct {
        sectionPagesMenu               string
 }
 
+func (s *SiteInfo) Config() SiteConfig {
+       return s.s.siteConfig
+}
+
 func (s *SiteInfo) String() string {
        return fmt.Sprintf("Site(%q)", s.Title)
 }
@@ -422,34 +423,13 @@ func (s *SiteInfo) ServerPort() int {
 
 // GoogleAnalytics is kept here for historic reasons.
 func (s *SiteInfo) GoogleAnalytics() string {
-       return s.Config.Services.GoogleAnalytics.ID
+       return s.Config().Services.GoogleAnalytics.ID
 
 }
 
 // DisqusShortname is kept here for historic reasons.
 func (s *SiteInfo) DisqusShortname() string {
-       return s.Config.Services.Disqus.Shortname
-}
-
-// Used in tests.
-
-type siteBuilderCfg struct {
-       language        *langs.Language
-       s               *Site
-       pageCollections *PageCollections
-}
-
-// TODO(bep) get rid of this
-func newSiteInfo(cfg siteBuilderCfg) SiteInfo {
-       return SiteInfo{
-               s:               cfg.s,
-               multilingual:    newMultiLingualForLanguage(cfg.language),
-               PageCollections: cfg.pageCollections,
-               Params:          make(map[string]interface{}),
-               uglyURLs: func(p *Page) bool {
-                       return false
-               },
-       }
+       return s.Config().Services.Disqus.Shortname
 }
 
 // SiteSocial is a place to put social details on a site level. These are the
@@ -487,7 +467,35 @@ func (s *SiteInfo) IsServer() bool {
        return s.owner.running
 }
 
-func (s *SiteInfo) refLink(ref string, page *Page, relative bool, outputFormat string) (string, error) {
+type siteRefLinker struct {
+       s *Site
+
+       errorLogger *log.Logger
+       notFoundURL string
+}
+
+func newSiteRefLinker(cfg config.Provider, s *Site) (siteRefLinker, error) {
+       logger := s.Log.ERROR
+
+       notFoundURL := cfg.GetString("refLinksNotFoundURL")
+       errLevel := cfg.GetString("refLinksErrorLevel")
+       if strings.EqualFold(errLevel, "warning") {
+               logger = s.Log.WARN
+       }
+       return siteRefLinker{s: s, errorLogger: logger, notFoundURL: notFoundURL}, nil
+}
+
+func (s siteRefLinker) logNotFound(ref, what string, p *Page) {
+       if p != nil {
+               s.errorLogger.Printf("REF_NOT_FOUND: Ref %q: %s", ref, what)
+       } else {
+               s.errorLogger.Printf("REF_NOT_FOUND: Ref %q from page %q: %s", ref, p.absoluteSourceRef(), what)
+       }
+
+}
+
+func (s *siteRefLinker) refLink(ref string, page *Page, relative bool, outputFormat string) (string, error) {
+
        var refURL *url.URL
        var err error
 
@@ -496,21 +504,22 @@ func (s *SiteInfo) refLink(ref string, page *Page, relative bool, outputFormat s
        refURL, err = url.Parse(ref)
 
        if err != nil {
-               return "", err
+               return s.notFoundURL, err
        }
 
        var target *Page
        var link string
 
        if refURL.Path != "" {
-               target, err := s.getPageNew(page, refURL.Path)
+               target, err := s.s.getPageNew(page, refURL.Path)
 
                if err != nil {
                        return "", err
                }
 
                if target == nil {
-                       return "", fmt.Errorf("No page found with path or logical name \"%s\".\n", refURL.Path)
+                       s.logNotFound(refURL.Path, "page not found", page)
+                       return s.notFoundURL, nil
                }
 
                var permalinker Permalinker = target
@@ -519,7 +528,8 @@ func (s *SiteInfo) refLink(ref string, page *Page, relative bool, outputFormat s
                        o := target.OutputFormats().Get(outputFormat)
 
                        if o == nil {
-                               return "", fmt.Errorf("Output format %q not found for page %q", outputFormat, refURL.Path)
+                               s.logNotFound(refURL.Path, fmt.Sprintf("output format %q", outputFormat), page)
+                               return s.notFoundURL, nil
                        }
                        permalinker = o
                }
@@ -551,7 +561,7 @@ func (s *SiteInfo) Ref(ref string, page *Page, options ...string) (string, error
                outputFormat = options[0]
        }
 
-       return s.refLink(ref, page, false, outputFormat)
+       return s.s.refLink(ref, page, false, outputFormat)
 }
 
 // RelRef will give an relative URL to ref in the given Page.
@@ -561,13 +571,17 @@ func (s *SiteInfo) RelRef(ref string, page *Page, options ...string) (string, er
                outputFormat = options[0]
        }
 
-       return s.refLink(ref, page, true, outputFormat)
+       return s.s.refLink(ref, page, true, outputFormat)
 }
 
 func (s *Site) running() bool {
        return s.owner != nil && s.owner.running
 }
 
+func (s *Site) multilingual() *Multilingual {
+       return s.owner.multilingual
+}
+
 func init() {
        defaultTimer = nitro.Initalize()
 }
@@ -1102,11 +1116,6 @@ func (s *Site) initializeSiteInfo() error {
                languagePrefix = "/" + lang.Lang
        }
 
-       var multilingual *Multilingual
-       if s.owner != nil {
-               multilingual = s.owner.multilingual
-       }
-
        var uglyURLs = func(p *Page) bool {
                return false
        }
@@ -1132,18 +1141,12 @@ func (s *Site) initializeSiteInfo() error {
                }
        }
 
-       siteConfig, err := loadSiteConfig(lang)
-       if err != nil {
-               return err
-       }
-
        s.Info = SiteInfo{
                Title:                          lang.GetString("title"),
                Author:                         lang.GetStringMap("author"),
                Social:                         lang.GetStringMapString("social"),
                LanguageCode:                   lang.GetString("languageCode"),
                Copyright:                      lang.GetString("copyright"),
-               multilingual:                   multilingual,
                Language:                       lang,
                LanguagePrefix:                 languagePrefix,
                Languages:                      languages,
@@ -1161,7 +1164,6 @@ func (s *Site) initializeSiteInfo() error {
                Data:                           &s.Data,
                owner:                          s.owner,
                s:                              s,
-               Config:                         siteConfig,
                // TODO(bep) make this Menu and similar into delegate methods on SiteInfo
                Taxonomies: s.Taxonomies,
        }
index 202c019861a5d47b9cba3b1f48f1fb01c129099b..7787ea2a4bef80bf1763b278a5d2c86e8f15355e 100644 (file)
@@ -925,7 +925,7 @@ func TestRefLinking(t *testing.T) {
                {"level2/common.md", "", true, "/level2/common/"},
                {"3-root.md", "", true, "/level2/level3/3-root/"},
        } {
-               if out, err := site.Info.refLink(test.link, currentPage, test.relative, test.outputFormat); err != nil || out != test.expected {
+               if out, err := site.refLink(test.link, currentPage, test.relative, test.outputFormat); err != nil || out != test.expected {
                        t.Errorf("[%d] Expected %s to resolve to (%s), got (%s) - error: %s", i, test.link, test.expected, out, err)
                }
        }
index 9fe60c434667ca2b4d2d64a1aef63d16c4b8dda8..9a72bcbb89029d185d6a20df804c71ff9df27bbe 100644 (file)
@@ -566,7 +566,7 @@ func newTestSite(t testing.TB, configKeyValues ...interface{}) *Site {
                cfg.Set(configKeyValues[i].(string), configKeyValues[i+1])
        }
 
-       d := deps.DepsCfg{Language: langs.NewLanguage("en", cfg), Fs: fs, Cfg: cfg}
+       d := deps.DepsCfg{Fs: fs, Cfg: cfg}
 
        s, err := NewSiteForCfg(d)