From c71e1b106e6011d148cac899f83c4685dee33a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Tue, 10 Jan 2017 10:55:03 +0100 Subject: [PATCH] all: Refactor to nonglobal file systems Updates #2701 Fixes #2951 --- commands/benchmark.go | 5 +- commands/gendoc.go | 4 +- commands/genman.go | 4 +- commands/hugo.go | 135 ++++++----- commands/import_jekyll.go | 25 +- commands/new.go | 71 +++--- commands/new_test.go | 82 ++++--- commands/server.go | 19 +- commands/undraft.go | 9 +- create/content.go | 10 +- create/content_test.go | 33 ++- deps/deps.go | 115 ++++++++++ helpers/configProvider.go | 14 +- helpers/path.go | 26 +-- helpers/path_test.go | 8 +- helpers/pathspec.go | 28 ++- helpers/pathspec_test.go | 22 +- helpers/pygments.go | 2 +- helpers/url_test.go | 12 +- hugofs/fs.go | 90 +++----- hugofs/fs_test.go | 60 ++--- hugolib/alias_test.go | 40 ++-- hugolib/case_insensitive_test.go | 67 +++--- hugolib/config_test.go | 6 +- hugolib/datafiles_test.go | 34 ++- hugolib/embedded_shortcodes_test.go | 20 +- hugolib/gitinfo.go | 5 +- hugolib/handler_page.go | 1 - hugolib/handler_test.go | 46 ++-- hugolib/hugo_sites.go | 232 +++++++++---------- hugolib/hugo_sites_build.go | 2 +- hugolib/hugo_sites_build_test.go | 268 ++++++++++++---------- hugolib/i18n.go | 5 +- hugolib/menu_test.go | 24 +- hugolib/node_as_page_test.go | 341 +++++++++++++++------------- hugolib/page.go | 62 +++-- hugolib/page_permalink_test.go | 3 +- hugolib/page_test.go | 76 ++++--- hugolib/pagination.go | 11 +- hugolib/pagination_test.go | 73 +++--- hugolib/permalinks.go | 6 +- hugolib/robotstxt_test.go | 28 +-- hugolib/rss_test.go | 18 +- hugolib/shortcode.go | 19 +- hugolib/shortcode_test.go | 113 ++++----- hugolib/site.go | 292 +++++++++++++----------- hugolib/siteJSONEncode_test.go | 15 +- hugolib/site_render.go | 12 +- hugolib/site_test.go | 300 +++++++++--------------- hugolib/site_url_test.go | 28 +-- hugolib/sitemap_test.go | 28 ++- hugolib/taxonomy.go | 19 +- hugolib/taxonomy_test.go | 13 +- hugolib/template_engines_test.go | 99 ++++++++ hugolib/template_test.go | 68 +++--- hugolib/testhelpers_test.go | 53 +++++ source/filesystem.go | 15 +- source/filesystem_test.go | 15 +- target/file.go | 4 +- target/htmlredirect.go | 4 +- target/page.go | 4 +- target/page_test.go | 6 +- tpl/amber_compiler.go | 42 ++++ tpl/template.go | 170 ++++++++------ tpl/template_funcs.go | 92 ++++---- tpl/template_funcs_test.go | 197 +++++++++------- tpl/template_i18n.go | 1 + tpl/template_resources.go | 19 +- tpl/template_resources_test.go | 32 ++- tpl/template_test.go | 120 ++++++---- tplapi/template.go | 28 +++ 71 files changed, 2219 insertions(+), 1731 deletions(-) create mode 100644 deps/deps.go create mode 100644 hugolib/template_engines_test.go create mode 100644 hugolib/testhelpers_test.go create mode 100644 tpl/amber_compiler.go create mode 100644 tplapi/template.go diff --git a/commands/benchmark.go b/commands/benchmark.go index a879e894..42966c67 100644 --- a/commands/benchmark.go +++ b/commands/benchmark.go @@ -49,10 +49,13 @@ func init() { func benchmark(cmd *cobra.Command, args []string) error { cfg, err := InitializeConfig(benchmarkCmd) + if err != nil { return err } + c := commandeer{cfg} + var memProf *os.File if memProfileFile != "" { memProf, err = os.Create(memProfileFile) @@ -79,7 +82,7 @@ func benchmark(cmd *cobra.Command, args []string) error { t := time.Now() for i := 0; i < benchmarkTimes; i++ { - if err = resetAndBuildSites(cfg, false); err != nil { + if err = c.resetAndBuildSites(false); err != nil { return err } } diff --git a/commands/gendoc.go b/commands/gendoc.go index 046d3839..5ffd084e 100644 --- a/commands/gendoc.go +++ b/commands/gendoc.go @@ -51,9 +51,9 @@ for rendering in Hugo.`, if !strings.HasSuffix(gendocdir, helpers.FilePathSeparator) { gendocdir += helpers.FilePathSeparator } - if found, _ := helpers.Exists(gendocdir, hugofs.Os()); !found { + if found, _ := helpers.Exists(gendocdir, hugofs.Os); !found { jww.FEEDBACK.Println("Directory", gendocdir, "does not exist, creating...") - hugofs.Os().MkdirAll(gendocdir, 0777) + hugofs.Os.MkdirAll(gendocdir, 0777) } now := time.Now().Format(time.RFC3339) prepender := func(filename string) string { diff --git a/commands/genman.go b/commands/genman.go index d1f54ae3..f7a3a424 100644 --- a/commands/genman.go +++ b/commands/genman.go @@ -41,9 +41,9 @@ in the "man" directory under the current directory.`, if !strings.HasSuffix(genmandir, helpers.FilePathSeparator) { genmandir += helpers.FilePathSeparator } - if found, _ := helpers.Exists(genmandir, hugofs.Os()); !found { + if found, _ := helpers.Exists(genmandir, hugofs.Os); !found { jww.FEEDBACK.Println("Directory", genmandir, "does not exist, creating...") - hugofs.Os().MkdirAll(genmandir, 0777) + hugofs.Os.MkdirAll(genmandir, 0777) } cmd.Root().DisableAutoGenTag = true diff --git a/commands/hugo.go b/commands/hugo.go index f4204cad..566e6860 100644 --- a/commands/hugo.go +++ b/commands/hugo.go @@ -40,6 +40,7 @@ import ( "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/fsync" + "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/hugolib" "github.com/spf13/hugo/livereload" @@ -50,6 +51,10 @@ import ( "github.com/spf13/viper" ) +type commandeer struct { + deps.DepsCfg +} + // Hugo represents the Hugo sites to build. This variable is exported as it // is used by at least one external library (the Hugo caddy plugin). We should // provide a cleaner external API, but until then, this is it. @@ -119,12 +124,14 @@ Complete documentation is available at http://gohugo.io/.`, return err } + c := commandeer{cfg} + if buildWatch { viper.Set("disableLiveReload", true) - watchConfig(cfg) + c.watchConfig() } - return build(cfg) + return c.build() }, } @@ -268,9 +275,9 @@ func init() { } // InitializeConfig initializes a config file with sensible default configuration flags. -func InitializeConfig(subCmdVs ...*cobra.Command) (hugolib.DepsCfg, error) { +func InitializeConfig(subCmdVs ...*cobra.Command) (deps.DepsCfg, error) { - var cfg hugolib.DepsCfg + var cfg deps.DepsCfg if err := hugolib.LoadGlobalConfig(source, cfgFile); err != nil { return cfg, err @@ -323,34 +330,34 @@ func InitializeConfig(subCmdVs ...*cobra.Command) (hugolib.DepsCfg, error) { viper.Set("cacheDir", cacheDir) } + // Init file systems. This may be changed at a later point. + cfg.Fs = hugofs.NewDefault() + cacheDir = viper.GetString("cacheDir") if cacheDir != "" { if helpers.FilePathSeparator != cacheDir[len(cacheDir)-1:] { cacheDir = cacheDir + helpers.FilePathSeparator } - isDir, err := helpers.DirExists(cacheDir, hugofs.Source()) + isDir, err := helpers.DirExists(cacheDir, cfg.Fs.Source) utils.CheckErr(err) if !isDir { mkdir(cacheDir) } viper.Set("cacheDir", cacheDir) } else { - viper.Set("cacheDir", helpers.GetTempDir("hugo_cache", hugofs.Source())) + viper.Set("cacheDir", helpers.GetTempDir("hugo_cache", cfg.Fs.Source)) } jww.INFO.Println("Using config file:", viper.ConfigFileUsed()) - // Init file systems. This may be changed at a later point. - hugofs.InitDefaultFs() - themeDir := helpers.GetThemeDir() if themeDir != "" { - if _, err := hugofs.Source().Stat(themeDir); os.IsNotExist(err) { + if _, err := cfg.Fs.Source.Stat(themeDir); os.IsNotExist(err) { return cfg, newSystemError("Unable to find theme Directory:", themeDir) } } - themeVersionMismatch, minVersion := isThemeVsHugoVersionMismatch() + themeVersionMismatch, minVersion := isThemeVsHugoVersionMismatch(cfg.Fs.Source) if themeVersionMismatch { jww.ERROR.Printf("Current theme does not support Hugo version %s. Minimum version required is %s\n", @@ -447,12 +454,12 @@ func flagChanged(flags *flag.FlagSet, key string) bool { return flag.Changed } -func watchConfig(cfg hugolib.DepsCfg) { +func (c commandeer) watchConfig() { viper.WatchConfig() viper.OnConfigChange(func(e fsnotify.Event) { jww.FEEDBACK.Println("Config file changed:", e.Name) // Force a full rebuild - utils.CheckErr(recreateAndBuildSites(cfg, true)) + utils.CheckErr(c.recreateAndBuildSites(true)) if !viper.GetBool("disableLiveReload") { // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized livereload.ForceRefresh() @@ -460,39 +467,40 @@ func watchConfig(cfg hugolib.DepsCfg) { }) } -func build(cfg hugolib.DepsCfg, watches ...bool) error { +func (c commandeer) build(watches ...bool) error { // Hugo writes the output to memory instead of the disk. // This is only used for benchmark testing. Cause the content is only visible // in memory. if renderToMemory { - hugofs.SetDestination(new(afero.MemMapFs)) + c.Fs.Destination = new(afero.MemMapFs) // Rendering to memoryFS, publish to Root regardless of publishDir. viper.Set("publishDir", "/") } - if err := copyStatic(); err != nil { + if err := c.copyStatic(); err != nil { return fmt.Errorf("Error copying static files to %s: %s", helpers.AbsPathify(viper.GetString("publishDir")), err) } watch := false if len(watches) > 0 && watches[0] { watch = true } - if err := buildSites(cfg, buildWatch || watch); err != nil { + if err := c.buildSites(buildWatch || watch); err != nil { return fmt.Errorf("Error building site: %s", err) } if buildWatch { jww.FEEDBACK.Println("Watching for changes in", helpers.AbsPathify(viper.GetString("contentDir"))) jww.FEEDBACK.Println("Press Ctrl+C to stop") - utils.CheckErr(newWatcher(cfg, 0)) + utils.CheckErr(c.newWatcher(0)) } return nil } -func getStaticSourceFs() afero.Fs { - source := hugofs.Source() - themeDir, err := helpers.GetThemeStaticDirPath() +func (c commandeer) getStaticSourceFs() afero.Fs { + source := c.Fs.Source + pathSpec := helpers.NewPathSpec(c.Fs, viper.GetViper()) + themeDir, err := pathSpec.GetThemeStaticDirPath() staticDir := helpers.GetStaticDirPath() + helpers.FilePathSeparator useTheme := true @@ -532,12 +540,12 @@ func getStaticSourceFs() afero.Fs { jww.INFO.Println("using a UnionFS for static directory comprised of:") jww.INFO.Println("Base:", themeDir) jww.INFO.Println("Overlay:", staticDir) - base := afero.NewReadOnlyFs(afero.NewBasePathFs(hugofs.Source(), themeDir)) - overlay := afero.NewReadOnlyFs(afero.NewBasePathFs(hugofs.Source(), staticDir)) + base := afero.NewReadOnlyFs(afero.NewBasePathFs(source, themeDir)) + overlay := afero.NewReadOnlyFs(afero.NewBasePathFs(source, staticDir)) return afero.NewCopyOnWriteFs(base, overlay) } -func copyStatic() error { +func (c commandeer) copyStatic() error { publishDir := helpers.AbsPathify(viper.GetString("publishDir")) + helpers.FilePathSeparator // If root, remove the second '/' @@ -546,7 +554,7 @@ func copyStatic() error { } // Includes both theme/static & /static - staticSourceFs := getStaticSourceFs() + staticSourceFs := c.getStaticSourceFs() if staticSourceFs == nil { jww.WARN.Println("No static directories found to sync") @@ -557,7 +565,7 @@ func copyStatic() error { syncer.NoTimes = viper.GetBool("noTimes") syncer.NoChmod = viper.GetBool("noChmod") syncer.SrcFs = staticSourceFs - syncer.DestFs = hugofs.Destination() + syncer.DestFs = c.Fs.Destination // Now that we are using a unionFs for the static directories // We can effectively clean the publishDir on initial sync syncer.Delete = viper.GetBool("cleanDestinationDir") @@ -572,7 +580,7 @@ func copyStatic() error { } // getDirList provides NewWatcher() with a list of directories to watch for changes. -func getDirList() []string { +func (c commandeer) getDirList() []string { var a []string dataDir := helpers.AbsPathify(viper.GetString("dataDir")) i18nDir := helpers.AbsPathify(viper.GetString("i18nDir")) @@ -621,7 +629,7 @@ func getDirList() []string { jww.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", path, err) return nil } - linkfi, err := hugofs.Source().Stat(link) + linkfi, err := c.Fs.Source.Stat(link) if err != nil { jww.ERROR.Printf("Cannot stat '%s', error was: %s", link, err) return nil @@ -642,25 +650,25 @@ func getDirList() []string { return nil } - helpers.SymbolicWalk(hugofs.Source(), dataDir, walker) - helpers.SymbolicWalk(hugofs.Source(), helpers.AbsPathify(viper.GetString("contentDir")), walker) - helpers.SymbolicWalk(hugofs.Source(), i18nDir, walker) - helpers.SymbolicWalk(hugofs.Source(), helpers.AbsPathify(viper.GetString("layoutDir")), walker) + helpers.SymbolicWalk(c.Fs.Source, dataDir, walker) + helpers.SymbolicWalk(c.Fs.Source, helpers.AbsPathify(viper.GetString("contentDir")), walker) + helpers.SymbolicWalk(c.Fs.Source, i18nDir, walker) + helpers.SymbolicWalk(c.Fs.Source, helpers.AbsPathify(viper.GetString("layoutDir")), walker) - helpers.SymbolicWalk(hugofs.Source(), staticDir, walker) + helpers.SymbolicWalk(c.Fs.Source, staticDir, walker) if helpers.ThemeSet() { - helpers.SymbolicWalk(hugofs.Source(), filepath.Join(themesDir, "layouts"), walker) - helpers.SymbolicWalk(hugofs.Source(), filepath.Join(themesDir, "static"), walker) - helpers.SymbolicWalk(hugofs.Source(), filepath.Join(themesDir, "i18n"), walker) - helpers.SymbolicWalk(hugofs.Source(), filepath.Join(themesDir, "data"), walker) + helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "layouts"), walker) + helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "static"), walker) + helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "i18n"), walker) + helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "data"), walker) } return a } -func recreateAndBuildSites(cfg hugolib.DepsCfg, watching bool) (err error) { - if err := initSites(cfg); err != nil { +func (c commandeer) recreateAndBuildSites(watching bool) (err error) { + if err := c.initSites(); err != nil { return err } if !quiet { @@ -669,9 +677,9 @@ func recreateAndBuildSites(cfg hugolib.DepsCfg, watching bool) (err error) { return Hugo.Build(hugolib.BuildCfg{CreateSitesFromConfig: true, Watching: watching, PrintStats: !quiet}) } -func resetAndBuildSites(cfg hugolib.DepsCfg, watching bool) (err error) { - if err := initSites(cfg); err != nil { - return err +func (c commandeer) resetAndBuildSites(watching bool) (err error) { + if err = c.initSites(); err != nil { + return } if !quiet { jww.FEEDBACK.Println("Started building sites ...") @@ -679,12 +687,12 @@ func resetAndBuildSites(cfg hugolib.DepsCfg, watching bool) (err error) { return Hugo.Build(hugolib.BuildCfg{ResetState: true, Watching: watching, PrintStats: !quiet}) } -func initSites(cfg hugolib.DepsCfg) error { +func (c commandeer) initSites() error { if Hugo != nil { return nil } - h, err := hugolib.NewHugoSitesFromConfiguration(cfg) + h, err := hugolib.NewHugoSitesFromConfiguration(c.DepsCfg) if err != nil { return err @@ -694,8 +702,8 @@ func initSites(cfg hugolib.DepsCfg) error { return nil } -func buildSites(cfg hugolib.DepsCfg, watching bool) (err error) { - if err := initSites(cfg); err != nil { +func (c commandeer) buildSites(watching bool) (err error) { + if err := c.initSites(); err != nil { return err } if !quiet { @@ -704,19 +712,21 @@ func buildSites(cfg hugolib.DepsCfg, watching bool) (err error) { return Hugo.Build(hugolib.BuildCfg{Watching: watching, PrintStats: !quiet}) } -func rebuildSites(cfg hugolib.DepsCfg, events []fsnotify.Event) error { - if err := initSites(cfg); err != nil { +func (c commandeer) rebuildSites(events []fsnotify.Event) error { + if err := c.initSites(); err != nil { return err } return Hugo.Build(hugolib.BuildCfg{PrintStats: !quiet, Watching: true}, events...) } // newWatcher creates a new watcher to watch filesystem events. -func newWatcher(cfg hugolib.DepsCfg, port int) error { +func (c commandeer) newWatcher(port int) error { if runtime.GOOS == "darwin" { tweakLimit() } + pathSpec := helpers.NewPathSpec(c.Fs, viper.GetViper()) + watcher, err := watcher.New(1 * time.Second) var wg sync.WaitGroup @@ -728,7 +738,7 @@ func newWatcher(cfg hugolib.DepsCfg, port int) error { wg.Add(1) - for _, d := range getDirList() { + for _, d := range c.getDirList() { if d != "" { _ = watcher.Add(d) } @@ -793,12 +803,12 @@ func newWatcher(cfg hugolib.DepsCfg, port int) error { // recursively add new directories to watch list // When mkdir -p is used, only the top directory triggers an event (at least on OSX) if ev.Op&fsnotify.Create == fsnotify.Create { - if s, err := hugofs.Source().Stat(ev.Name); err == nil && s.Mode().IsDir() { - helpers.SymbolicWalk(hugofs.Source(), ev.Name, walkAdder) + if s, err := c.Fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() { + helpers.SymbolicWalk(c.Fs.Source, ev.Name, walkAdder) } } - isstatic := strings.HasPrefix(ev.Name, helpers.GetStaticDirPath()) || (len(helpers.GetThemesDirPath()) > 0 && strings.HasPrefix(ev.Name, helpers.GetThemesDirPath())) + isstatic := strings.HasPrefix(ev.Name, helpers.GetStaticDirPath()) || (len(pathSpec.GetThemesDirPath()) > 0 && strings.HasPrefix(ev.Name, pathSpec.GetThemesDirPath())) if isstatic { staticEvents = append(staticEvents, ev) @@ -821,12 +831,12 @@ func newWatcher(cfg hugolib.DepsCfg, port int) error { if viper.GetBool("forceSyncStatic") { jww.FEEDBACK.Printf("Syncing all static files\n") - err := copyStatic() + err := c.copyStatic() if err != nil { utils.StopOnErr(err, fmt.Sprintf("Error copying static files to %s", publishDir)) } } else { - staticSourceFs := getStaticSourceFs() + staticSourceFs := c.getStaticSourceFs() if staticSourceFs == nil { jww.WARN.Println("No static directories found to sync") @@ -837,7 +847,7 @@ func newWatcher(cfg hugolib.DepsCfg, port int) error { syncer.NoTimes = viper.GetBool("noTimes") syncer.NoChmod = viper.GetBool("noChmod") syncer.SrcFs = staticSourceFs - syncer.DestFs = hugofs.Destination() + syncer.DestFs = c.Fs.Destination // prevent spamming the log on changes logger := helpers.NewDistinctFeedbackLogger() @@ -862,7 +872,7 @@ func newWatcher(cfg hugolib.DepsCfg, port int) error { fromPath := ev.Name // If we are here we already know the event took place in a static dir - relPath, err := helpers.MakeStaticPathRelative(fromPath) + relPath, err := pathSpec.MakeStaticPathRelative(fromPath) if err != nil { jww.ERROR.Println(err) continue @@ -882,7 +892,7 @@ func newWatcher(cfg hugolib.DepsCfg, port int) error { // If file doesn't exist in any static dir, remove it toRemove := filepath.Join(publishDir, relPath) logger.Println("File no longer exists in static dir, removing", toRemove) - hugofs.Destination().RemoveAll(toRemove) + c.Fs.Destination.RemoveAll(toRemove) } else if err == nil { // If file still exists, sync it logger.Println("Syncing", relPath, "to", publishDir) @@ -910,7 +920,7 @@ func newWatcher(cfg hugolib.DepsCfg, port int) error { // force refresh when more than one file if len(staticEvents) > 0 { for _, ev := range staticEvents { - path, _ := helpers.MakeStaticPathRelative(ev.Name) + path, _ := pathSpec.MakeStaticPathRelative(ev.Name) livereload.RefreshPath(path) } @@ -925,7 +935,7 @@ func newWatcher(cfg hugolib.DepsCfg, port int) error { const layout = "2006-01-02 15:04 -0700" jww.FEEDBACK.Println(time.Now().Format(layout)) - rebuildSites(cfg, dynamicEvents) + c.rebuildSites(dynamicEvents) if !buildWatch && !viper.GetBool("disableLiveReload") { // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized @@ -947,7 +957,7 @@ func newWatcher(cfg hugolib.DepsCfg, port int) error { http.HandleFunc("/livereload", livereload.Handler) } - go serve(port) + go c.serve(port) } wg.Wait() @@ -956,14 +966,13 @@ func newWatcher(cfg hugolib.DepsCfg, port int) error { // isThemeVsHugoVersionMismatch returns whether the current Hugo version is // less than the theme's min_version. -func isThemeVsHugoVersionMismatch() (mismatch bool, requiredMinVersion string) { +func isThemeVsHugoVersionMismatch(fs afero.Fs) (mismatch bool, requiredMinVersion string) { if !helpers.ThemeSet() { return } themeDir := helpers.GetThemeDir() - fs := hugofs.Source() path := filepath.Join(themeDir, "theme.toml") exists, err := helpers.Exists(path, fs) diff --git a/commands/import_jekyll.go b/commands/import_jekyll.go index 7e55e067..151fffa8 100644 --- a/commands/import_jekyll.go +++ b/commands/import_jekyll.go @@ -25,6 +25,7 @@ import ( "strings" "time" + "github.com/spf13/afero" "github.com/spf13/cast" "github.com/spf13/cobra" "github.com/spf13/hugo/helpers" @@ -122,7 +123,7 @@ func importFromJekyll(cmd *cobra.Command, args []string) error { return convertJekyllPost(site, path, relPath, targetDir, draft) } - err = helpers.SymbolicWalk(hugofs.Os(), jekyllRoot, callback) + err = helpers.SymbolicWalk(hugofs.Os, jekyllRoot, callback) if err != nil { return err @@ -137,7 +138,13 @@ func importFromJekyll(cmd *cobra.Command, args []string) error { // TODO: Consider calling doNewSite() instead? func createSiteFromJekyll(jekyllRoot, targetDir string, force bool) (*hugolib.Site, error) { - fs := hugofs.Source() + s, err := hugolib.NewSiteDefaultLang() + if err != nil { + return nil, err + } + + fs := s.Fs.Source + if exists, _ := helpers.Exists(targetDir, fs); exists { if isDir, _ := helpers.IsDir(targetDir, fs); !isDir { return nil, errors.New("Target path \"" + targetDir + "\" already exists but not a directory") @@ -150,7 +157,7 @@ func createSiteFromJekyll(jekyllRoot, targetDir string, force bool) (*hugolib.Si } } - jekyllConfig := loadJekyllConfig(jekyllRoot) + jekyllConfig := loadJekyllConfig(fs, jekyllRoot) // Crude test to make sure at least one of _drafts/ and _posts/ exists // and is not empty. @@ -177,16 +184,14 @@ func createSiteFromJekyll(jekyllRoot, targetDir string, force bool) (*hugolib.Si mkdir(targetDir, "data") mkdir(targetDir, "themes") - createConfigFromJekyll(targetDir, "yaml", jekyllConfig) + createConfigFromJekyll(fs, targetDir, "yaml", jekyllConfig) copyJekyllFilesAndFolders(jekyllRoot, filepath.Join(targetDir, "static")) - site := hugolib.NewSiteDefaultLang() - return site, nil + return s, nil } -func loadJekyllConfig(jekyllRoot string) map[string]interface{} { - fs := hugofs.Source() +func loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]interface{} { path := filepath.Join(jekyllRoot, "_config.yml") exists, err := helpers.Exists(path, fs) @@ -218,7 +223,7 @@ func loadJekyllConfig(jekyllRoot string) map[string]interface{} { return c.(map[string]interface{}) } -func createConfigFromJekyll(inpath string, kind string, jekyllConfig map[string]interface{}) (err error) { +func createConfigFromJekyll(fs afero.Fs, inpath string, kind string, jekyllConfig map[string]interface{}) (err error) { title := "My New Hugo Site" baseURL := "http://example.org/" @@ -251,7 +256,7 @@ func createConfigFromJekyll(inpath string, kind string, jekyllConfig map[string] return err } - err = helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), bytes.NewReader(by), hugofs.Source()) + err = helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), bytes.NewReader(by), fs) if err != nil { return } diff --git a/commands/new.go b/commands/new.go index 0b6ada53..b4a2740f 100644 --- a/commands/new.go +++ b/commands/new.go @@ -16,15 +16,18 @@ package commands import ( "bytes" "errors" + "fmt" "os" "path/filepath" "strings" "time" + "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/hugo/create" "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/hugofs" + "github.com/spf13/hugo/hugolib" "github.com/spf13/hugo/parser" jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" @@ -84,7 +87,9 @@ as you see fit.`, // NewContent adds new content to a Hugo site. func NewContent(cmd *cobra.Command, args []string) error { - if _, err := InitializeConfig(); err != nil { + cfg, err := InitializeConfig() + + if err != nil { return err } @@ -110,10 +115,16 @@ func NewContent(cmd *cobra.Command, args []string) error { kind = contentType } - return create.NewContent(hugofs.Source(), kind, createpath) + s, err := hugolib.NewSite(cfg) + + if err != nil { + return newSystemError(err) + } + + return create.NewContent(s, kind, createpath) } -func doNewSite(basepath string, force bool) error { +func doNewSite(fs *hugofs.Fs, basepath string, force bool) error { dirs := []string{ filepath.Join(basepath, "layouts"), filepath.Join(basepath, "content"), @@ -123,12 +134,12 @@ func doNewSite(basepath string, force bool) error { filepath.Join(basepath, "themes"), } - if exists, _ := helpers.Exists(basepath, hugofs.Source()); exists { - if isDir, _ := helpers.IsDir(basepath, hugofs.Source()); !isDir { + if exists, _ := helpers.Exists(basepath, fs.Source); exists { + if isDir, _ := helpers.IsDir(basepath, fs.Source); !isDir { return errors.New(basepath + " already exists but not a directory") } - isEmpty, _ := helpers.IsEmpty(basepath, hugofs.Source()) + isEmpty, _ := helpers.IsEmpty(basepath, fs.Source) switch { case !isEmpty && !force: @@ -137,7 +148,7 @@ func doNewSite(basepath string, force bool) error { case !isEmpty && force: all := append(dirs, filepath.Join(basepath, "config."+configFormat)) for _, path := range all { - if exists, _ := helpers.Exists(path, hugofs.Source()); exists { + if exists, _ := helpers.Exists(path, fs.Source); exists { return errors.New(path + " already exists") } } @@ -145,10 +156,12 @@ func doNewSite(basepath string, force bool) error { } for _, dir := range dirs { - hugofs.Source().MkdirAll(dir, 0777) + if err := fs.Source.MkdirAll(dir, 0777); err != nil { + return fmt.Errorf("Failed to create dir: %s", err) + } } - createConfig(basepath, configFormat) + createConfig(fs, basepath, configFormat) jww.FEEDBACK.Printf("Congratulations! Your new Hugo site is created in %s.\n\n", basepath) jww.FEEDBACK.Println(nextStepsText()) @@ -190,12 +203,14 @@ func NewSite(cmd *cobra.Command, args []string) error { forceNew, _ := cmd.Flags().GetBool("force") - return doNewSite(createpath, forceNew) + return doNewSite(hugofs.NewDefault(), createpath, forceNew) } // NewTheme creates a new Hugo theme. func NewTheme(cmd *cobra.Command, args []string) error { - if _, err := InitializeConfig(); err != nil { + cfg, err := InitializeConfig() + + if err != nil { return err } @@ -207,26 +222,26 @@ func NewTheme(cmd *cobra.Command, args []string) error { createpath := helpers.AbsPathify(filepath.Join(viper.GetString("themesDir"), args[0])) jww.INFO.Println("creating theme at", createpath) - if x, _ := helpers.Exists(createpath, hugofs.Source()); x { + if x, _ := helpers.Exists(createpath, cfg.Fs.Source); x { return newUserError(createpath, "already exists") } mkdir(createpath, "layouts", "_default") mkdir(createpath, "layouts", "partials") - touchFile(createpath, "layouts", "index.html") - touchFile(createpath, "layouts", "404.html") - touchFile(createpath, "layouts", "_default", "list.html") - touchFile(createpath, "layouts", "_default", "single.html") + touchFile(cfg.Fs.Source, createpath, "layouts", "index.html") + touchFile(cfg.Fs.Source, createpath, "layouts", "404.html") + touchFile(cfg.Fs.Source, createpath, "layouts", "_default", "list.html") + touchFile(cfg.Fs.Source, createpath, "layouts", "_default", "single.html") - touchFile(createpath, "layouts", "partials", "header.html") - touchFile(createpath, "layouts", "partials", "footer.html") + touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "header.html") + touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "footer.html") mkdir(createpath, "archetypes") archDefault := []byte("+++\n+++\n") - err := helpers.WriteToDisk(filepath.Join(createpath, "archetypes", "default.md"), bytes.NewReader(archDefault), hugofs.Source()) + err = helpers.WriteToDisk(filepath.Join(createpath, "archetypes", "default.md"), bytes.NewReader(archDefault), cfg.Fs.Source) if err != nil { return err } @@ -256,12 +271,12 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. `) - err = helpers.WriteToDisk(filepath.Join(createpath, "LICENSE.md"), bytes.NewReader(by), hugofs.Source()) + err = helpers.WriteToDisk(filepath.Join(createpath, "LICENSE.md"), bytes.NewReader(by), cfg.Fs.Source) if err != nil { return err } - createThemeMD(createpath) + createThemeMD(cfg.Fs, createpath) return nil } @@ -275,16 +290,16 @@ func mkdir(x ...string) { } } -func touchFile(x ...string) { +func touchFile(fs afero.Fs, x ...string) { inpath := filepath.Join(x...) mkdir(filepath.Dir(inpath)) - err := helpers.WriteToDisk(inpath, bytes.NewReader([]byte{}), hugofs.Source()) + err := helpers.WriteToDisk(inpath, bytes.NewReader([]byte{}), fs) if err != nil { jww.FATAL.Fatalln(err) } } -func createThemeMD(inpath string) (err error) { +func createThemeMD(fs *hugofs.Fs, inpath string) (err error) { by := []byte(`# theme.toml template for a Hugo theme # See https://github.com/spf13/hugoThemes#themetoml for an example @@ -309,7 +324,7 @@ min_version = 0.18 repo = "" `) - err = helpers.WriteToDisk(filepath.Join(inpath, "theme.toml"), bytes.NewReader(by), hugofs.Source()) + err = helpers.WriteToDisk(filepath.Join(inpath, "theme.toml"), bytes.NewReader(by), fs.Source) if err != nil { return } @@ -320,7 +335,7 @@ min_version = 0.18 func newContentPathSection(path string) (string, string) { // Forward slashes is used in all examples. Convert if needed. // Issue #1133 - createpath := strings.Replace(path, "/", helpers.FilePathSeparator, -1) + createpath := filepath.FromSlash(path) var section string // assume the first directory is the section (kind) if strings.Contains(createpath[1:], helpers.FilePathSeparator) { @@ -330,7 +345,7 @@ func newContentPathSection(path string) (string, string) { return createpath, section } -func createConfig(inpath string, kind string) (err error) { +func createConfig(fs *hugofs.Fs, inpath string, kind string) (err error) { in := map[string]interface{}{ "baseURL": "http://example.org/", "title": "My New Hugo Site", @@ -343,7 +358,7 @@ func createConfig(inpath string, kind string) (err error) { return err } - err = helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), bytes.NewReader(by), hugofs.Source()) + err = helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), bytes.NewReader(by), fs.Source) if err != nil { return } diff --git a/commands/new_test.go b/commands/new_test.go index 5991e181..acb3d759 100644 --- a/commands/new_test.go +++ b/commands/new_test.go @@ -14,12 +14,12 @@ package commands import ( - "os" "path/filepath" "testing" "github.com/spf13/hugo/hugofs" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // Issue #1133 @@ -29,7 +29,8 @@ func TestNewContentPathSectionWithForwardSlashes(t *testing.T) { assert.Equal(t, "post", s) } -func checkNewSiteInited(basepath string, t *testing.T) { +func checkNewSiteInited(fs *hugofs.Fs, basepath string, t *testing.T) { + paths := []string{ filepath.Join(basepath, "layouts"), filepath.Join(basepath, "content"), @@ -40,63 +41,70 @@ func checkNewSiteInited(basepath string, t *testing.T) { } for _, path := range paths { - _, err := hugofs.Source().Stat(path) - assert.Nil(t, err) + _, err := fs.Source.Stat(path) + require.NoError(t, err) } } func TestDoNewSite(t *testing.T) { - basepath := filepath.Join(os.TempDir(), "blog") - hugofs.InitMemFs() - err := doNewSite(basepath, false) - assert.Nil(t, err) + basepath := filepath.Join("base", "blog") + fs := hugofs.NewMem() - checkNewSiteInited(basepath, t) + require.NoError(t, doNewSite(fs, basepath, false)) + + checkNewSiteInited(fs, basepath, t) } func TestDoNewSite_noerror_base_exists_but_empty(t *testing.T) { - basepath := filepath.Join(os.TempDir(), "blog") - hugofs.InitMemFs() - hugofs.Source().MkdirAll(basepath, 777) - err := doNewSite(basepath, false) - assert.Nil(t, err) + basepath := filepath.Join("base", "blog") + fs := hugofs.NewMem() + + require.NoError(t, fs.Source.MkdirAll(basepath, 777)) + + require.NoError(t, doNewSite(fs, basepath, false)) } func TestDoNewSite_error_base_exists(t *testing.T) { - basepath := filepath.Join(os.TempDir(), "blog") - hugofs.InitMemFs() - hugofs.Source().MkdirAll(basepath, 777) - hugofs.Source().Create(filepath.Join(basepath, "foo")) + basepath := filepath.Join("base", "blog") + fs := hugofs.NewMem() + + require.NoError(t, fs.Source.MkdirAll(basepath, 777)) + _, err := fs.Source.Create(filepath.Join(basepath, "foo")) + require.NoError(t, err) // Since the directory already exists and isn't empty, expect an error - err := doNewSite(basepath, false) - assert.NotNil(t, err) + require.Error(t, doNewSite(fs, basepath, false)) + } func TestDoNewSite_force_empty_dir(t *testing.T) { - basepath := filepath.Join(os.TempDir(), "blog") - hugofs.InitMemFs() - hugofs.Source().MkdirAll(basepath, 777) - err := doNewSite(basepath, true) - assert.Nil(t, err) + basepath := filepath.Join("base", "blog") + fs := hugofs.NewMem() - checkNewSiteInited(basepath, t) + require.NoError(t, fs.Source.MkdirAll(basepath, 777)) + + require.NoError(t, doNewSite(fs, basepath, true)) + + checkNewSiteInited(fs, basepath, t) } func TestDoNewSite_error_force_dir_inside_exists(t *testing.T) { - basepath := filepath.Join(os.TempDir(), "blog") + basepath := filepath.Join("base", "blog") + fs := hugofs.NewMem() + contentPath := filepath.Join(basepath, "content") - hugofs.InitMemFs() - hugofs.Source().MkdirAll(contentPath, 777) - err := doNewSite(basepath, true) - assert.NotNil(t, err) + + require.NoError(t, fs.Source.MkdirAll(contentPath, 777)) + require.Error(t, doNewSite(fs, basepath, true)) } func TestDoNewSite_error_force_config_inside_exists(t *testing.T) { - basepath := filepath.Join(os.TempDir(), "blog") + basepath := filepath.Join("base", "blog") + fs := hugofs.NewMem() + configPath := filepath.Join(basepath, "config.toml") - hugofs.InitMemFs() - hugofs.Source().MkdirAll(basepath, 777) - hugofs.Source().Create(configPath) - err := doNewSite(basepath, true) - assert.NotNil(t, err) + require.NoError(t, fs.Source.MkdirAll(basepath, 777)) + _, err := fs.Source.Create(configPath) + require.NoError(t, err) + + require.Error(t, doNewSite(fs, basepath, true)) } diff --git a/commands/server.go b/commands/server.go index 45d77699..6b1776f4 100644 --- a/commands/server.go +++ b/commands/server.go @@ -29,7 +29,6 @@ import ( "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/hugo/helpers" - "github.com/spf13/hugo/hugofs" jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" ) @@ -109,6 +108,8 @@ func server(cmd *cobra.Command, args []string) error { return err } + c := commandeer{cfg} + if flagChanged(cmd.Flags(), "disableLiveReload") { viper.Set("disableLiveReload", disableLiveReload) } @@ -119,7 +120,7 @@ func server(cmd *cobra.Command, args []string) error { if viper.GetBool("watch") { serverWatch = true - watchConfig(cfg) + c.watchConfig() } l, err := net.Listen("tcp", net.JoinHostPort(serverInterface, strconv.Itoa(serverPort))) @@ -157,18 +158,18 @@ func server(cmd *cobra.Command, args []string) error { // Hugo writes the output to memory instead of the disk if !renderToDisk { - hugofs.SetDestination(new(afero.MemMapFs)) + cfg.Fs.Destination = new(afero.MemMapFs) // Rendering to memoryFS, publish to Root regardless of publishDir. viper.Set("publishDir", "/") } - if err := build(cfg, serverWatch); err != nil { + if err := c.build(serverWatch); err != nil { return err } // Watch runs its own server as part of the routine if serverWatch { - watchDirs := getDirList() + watchDirs := c.getDirList() baseWatchDir := viper.GetString("workingDir") for i, dir := range watchDirs { watchDirs[i], _ = helpers.GetRelativePath(dir, baseWatchDir) @@ -177,26 +178,26 @@ func server(cmd *cobra.Command, args []string) error { rootWatchDirs := strings.Join(helpers.UniqueStrings(helpers.ExtractRootPaths(watchDirs)), ",") jww.FEEDBACK.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs) - err := newWatcher(cfg, serverPort) + err := c.newWatcher(serverPort) if err != nil { return err } } - serve(serverPort) + c.serve(serverPort) return nil } -func serve(port int) { +func (c commandeer) serve(port int) { if renderToDisk { jww.FEEDBACK.Println("Serving pages from " + helpers.AbsPathify(viper.GetString("publishDir"))) } else { jww.FEEDBACK.Println("Serving pages from memory") } - httpFs := afero.NewHttpFs(hugofs.Destination()) + httpFs := afero.NewHttpFs(c.Fs.Destination) fs := filesOnlyFs{httpFs.Dir(helpers.AbsPathify(viper.GetString("publishDir")))} fileserver := http.FileServer(fs) diff --git a/commands/undraft.go b/commands/undraft.go index 8e287651..4f3bcfe2 100644 --- a/commands/undraft.go +++ b/commands/undraft.go @@ -20,7 +20,6 @@ import ( "time" "github.com/spf13/cobra" - "github.com/spf13/hugo/hugofs" "github.com/spf13/hugo/parser" ) @@ -37,7 +36,9 @@ If the content's draft status is 'False', nothing is done.`, // to false and setting its publish date to now. If the specified content is // not a draft, it will log an error. func Undraft(cmd *cobra.Command, args []string) error { - if _, err := InitializeConfig(); err != nil { + cfg, err := InitializeConfig() + + if err != nil { return err } @@ -47,7 +48,7 @@ func Undraft(cmd *cobra.Command, args []string) error { location := args[0] // open the file - f, err := hugofs.Source().Open(location) + f, err := cfg.Fs.Source.Open(location) if err != nil { return err } @@ -64,7 +65,7 @@ func Undraft(cmd *cobra.Command, args []string) error { return newSystemErrorF("an error occurred while undrafting %q: %s", location, err) } - f, err = hugofs.Source().OpenFile(location, os.O_WRONLY|os.O_TRUNC, 0644) + f, err = cfg.Fs.Source.OpenFile(location, os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { return newSystemErrorF("%q not be undrafted due to error opening file to save changes: %q\n", location, err) } diff --git a/create/content.go b/create/content.go index 195080d8..6a03c8c9 100644 --- a/create/content.go +++ b/create/content.go @@ -34,15 +34,15 @@ import ( // NewContent creates a new content file in the content directory based upon the // given kind, which is used to lookup an archetype. -func NewContent(fs afero.Fs, kind, name string) (err error) { +func NewContent(s *hugolib.Site, kind, name string) (err error) { jww.INFO.Println("attempting to create ", name, "of", kind) - location := FindArchetype(fs, kind) + location := FindArchetype(s.Fs.Source, kind) var by []byte if location != "" { - by, err = afero.ReadFile(fs, location) + by, err = afero.ReadFile(s.Fs.Source, location) if err != nil { jww.ERROR.Println(err) } @@ -62,9 +62,7 @@ func NewContent(fs afero.Fs, kind, name string) (err error) { return err } - site := hugolib.NewSiteDefaultLang() - - page, err := site.NewPage(name) + page, err := s.NewPage(name) if err != nil { return err } diff --git a/create/content_test.go b/create/content_test.go index cdee13fb..df29527f 100644 --- a/create/content_test.go +++ b/create/content_test.go @@ -19,23 +19,22 @@ import ( "strings" "testing" + "github.com/spf13/hugo/hugolib" + "fmt" + "github.com/spf13/hugo/hugofs" + "github.com/spf13/afero" "github.com/spf13/hugo/create" "github.com/spf13/hugo/helpers" - "github.com/spf13/hugo/hugofs" "github.com/spf13/viper" + "github.com/stretchr/testify/require" ) func TestNewContent(t *testing.T) { initViper() - err := initFs() - if err != nil { - t.Fatalf("initialization error: %s", err) - } - cases := []struct { kind string path string @@ -48,15 +47,15 @@ func TestNewContent(t *testing.T) { {"product", "product/sample-4.md", []string{`title = "sample 4"`}}, // empty archetype front matter } - for i, c := range cases { - err = create.NewContent(hugofs.Source(), c.kind, c.path) - if err != nil { - t.Errorf("[%d] NewContent: %s", i, err) - } + for _, c := range cases { + s, err := hugolib.NewEnglishSite() + require.NoError(t, err) + require.NoError(t, initFs(s.Fs)) + + require.NoError(t, create.NewContent(s, c.kind, c.path)) fname := filepath.Join("content", filepath.FromSlash(c.path)) - content := readFileFromFs(t, hugofs.Source(), fname) - + content := readFileFromFs(t, s.Fs.Source, fname) for i, v := range c.expected { found := strings.Contains(content, v) if !found { @@ -72,11 +71,11 @@ func initViper() { viper.Set("archetypeDir", "archetypes") viper.Set("contentDir", "content") viper.Set("themesDir", "themes") + viper.Set("layoutDir", "layouts") viper.Set("theme", "sample") } -func initFs() error { - hugofs.InitMemFs() +func initFs(fs *hugofs.Fs) error { perm := os.FileMode(0755) var err error @@ -87,7 +86,7 @@ func initFs() error { filepath.Join("themes", "sample", "archetypes"), } for _, dir := range dirs { - err = hugofs.Source().Mkdir(dir, perm) + err = fs.Source.Mkdir(dir, perm) if err != nil { return err } @@ -111,7 +110,7 @@ func initFs() error { content: "+++\ndate =\"\"\ntitle = \"Empty Date Arch title\"\ntest = \"test1\"\n+++\n", }, } { - f, err := hugofs.Source().Create(v.path) + f, err := fs.Source.Create(v.path) if err != nil { return err } diff --git a/deps/deps.go b/deps/deps.go new file mode 100644 index 00000000..d09b760a --- /dev/null +++ b/deps/deps.go @@ -0,0 +1,115 @@ +package deps + +import ( + "io/ioutil" + "log" + "os" + + "github.com/spf13/hugo/helpers" + "github.com/spf13/hugo/hugofs" + "github.com/spf13/hugo/tplapi" + jww "github.com/spf13/jwalterweatherman" +) + +// Deps holds dependencies used by many. +// There will be normally be only one instance of deps in play +// at a given time, i.e. one per Site built. +type Deps struct { + // The logger to use. + Log *jww.Notepad `json:"-"` + + // The templates to use. + Tmpl tplapi.Template `json:"-"` + + // The file systems to use. + Fs *hugofs.Fs `json:"-"` + + // The PathSpec to use + *helpers.PathSpec `json:"-"` + + templateProvider TemplateProvider + WithTemplate func(templ tplapi.Template) error + + // TODO(bep) globals next in line: Viper + +} + +// Used to create and refresh, and clone the template. +type TemplateProvider interface { + Update(deps *Deps) error + Clone(deps *Deps) error +} + +func (d *Deps) LoadTemplates() error { + if err := d.templateProvider.Update(d); err != nil { + return err + } + d.Tmpl.PrintErrors() + return nil +} + +func New(cfg DepsCfg) *Deps { + var ( + logger = cfg.Logger + fs = cfg.Fs + ) + + if cfg.TemplateProvider == nil { + panic("Must have a TemplateProvider") + } + + if cfg.Language == nil { + panic("Must have a Language") + } + + if logger == nil { + logger = jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) + } + + if fs == nil { + // Default to the most used file systems. + fs = hugofs.NewMem() + } + + d := &Deps{ + Fs: fs, + Log: logger, + templateProvider: cfg.TemplateProvider, + WithTemplate: cfg.WithTemplate, + PathSpec: helpers.NewPathSpec(fs, cfg.Language), + } + + return d +} + +// ForLanguage creates a copy of the Deps with the language dependent +// parts switched out. +func (d Deps) ForLanguage(l *helpers.Language) (*Deps, error) { + + d.PathSpec = helpers.NewPathSpec(d.Fs, l) + if err := d.templateProvider.Clone(&d); err != nil { + return nil, err + } + + return &d, nil + +} + +// DepsCfg contains configuration options that can be used to configure Hugo +// on a global level, i.e. logging etc. +// Nil values will be given default values. +type DepsCfg struct { + + // The Logger to use. + Logger *jww.Notepad + + // The file systems to use + Fs *hugofs.Fs + + // The language to use. + Language *helpers.Language + + // Template handling. + TemplateProvider TemplateProvider + WithTemplate func(templ tplapi.Template) error +} diff --git a/helpers/configProvider.go b/helpers/configProvider.go index e63112c0..b9601825 100644 --- a/helpers/configProvider.go +++ b/helpers/configProvider.go @@ -29,7 +29,6 @@ import ( // TODO(bep) Get rid of these. var ( currentConfigProvider ConfigProvider - currentPathSpec *PathSpec ) // ConfigProvider provides the configuration settings for Hugo. @@ -52,24 +51,13 @@ func Config() ConfigProvider { return viper.Get("currentContentLanguage").(ConfigProvider) } -// CurrentPathSpec returns the current PathSpec. -// If it is not set, a new will be created based in the currently active Hugo config. -func CurrentPathSpec() *PathSpec { - if currentPathSpec != nil { - return currentPathSpec - } - // Some tests rely on this. We will fix that, eventually. - return NewPathSpecFromConfig(Config()) -} - // InitConfigProviderForCurrentContentLanguage does what it says. func InitConfigProviderForCurrentContentLanguage() { currentConfigProvider = viper.Get("CurrentContentLanguage").(ConfigProvider) - currentPathSpec = NewPathSpecFromConfig(currentConfigProvider) } // ResetConfigProvider is used in tests. func ResetConfigProvider() { currentConfigProvider = nil - currentPathSpec = nil + } diff --git a/helpers/path.go b/helpers/path.go index 2e154062..83a91deb 100644 --- a/helpers/path.go +++ b/helpers/path.go @@ -23,8 +23,6 @@ import ( "strings" "unicode" - "github.com/spf13/hugo/hugofs" - "github.com/spf13/afero" "github.com/spf13/viper" "golang.org/x/text/transform" @@ -196,29 +194,29 @@ func GetRelativeThemeDir() string { // GetThemeStaticDirPath returns the theme's static dir path if theme is set. // If theme is set and the static dir doesn't exist, an error is returned. -func GetThemeStaticDirPath() (string, error) { - return getThemeDirPath("static") +func (p *PathSpec) GetThemeStaticDirPath() (string, error) { + return p.getThemeDirPath("static") } // GetThemeDataDirPath returns the theme's data dir path if theme is set. // If theme is set and the data dir doesn't exist, an error is returned. -func GetThemeDataDirPath() (string, error) { - return getThemeDirPath("data") +func (p *PathSpec) GetThemeDataDirPath() (string, error) { + return p.getThemeDirPath("data") } // GetThemeI18nDirPath returns the theme's i18n dir path if theme is set. // If theme is set and the i18n dir doesn't exist, an error is returned. -func GetThemeI18nDirPath() (string, error) { - return getThemeDirPath("i18n") +func (p *PathSpec) GetThemeI18nDirPath() (string, error) { + return p.getThemeDirPath("i18n") } -func getThemeDirPath(path string) (string, error) { +func (p *PathSpec) getThemeDirPath(path string) (string, error) { if !ThemeSet() { return "", ErrThemeUndefined } themeDir := filepath.Join(GetThemeDir(), path) - if _, err := hugofs.Source().Stat(themeDir); os.IsNotExist(err) { + if _, err := p.fs.Source.Stat(themeDir); os.IsNotExist(err) { return "", fmt.Errorf("Unable to find %s directory for theme %s in %s", path, viper.GetString("theme"), themeDir) } @@ -228,17 +226,17 @@ func getThemeDirPath(path string) (string, error) { // GetThemesDirPath gets the static files directory of the current theme, if there is one. // Ignores underlying errors. // TODO(bep) Candidate for deprecation? -func GetThemesDirPath() string { - dir, _ := getThemeDirPath("static") +func (p *PathSpec) GetThemesDirPath() string { + dir, _ := p.getThemeDirPath("static") return dir } // MakeStaticPathRelative makes a relative path to the static files directory. // It does so by taking either the project's static path or the theme's static // path into consideration. -func MakeStaticPathRelative(inPath string) (string, error) { +func (p *PathSpec) MakeStaticPathRelative(inPath string) (string, error) { staticDir := GetStaticDirPath() - themeStaticDir := GetThemesDirPath() + themeStaticDir := p.GetThemesDirPath() return makePathRelative(inPath, staticDir, themeStaticDir) } diff --git a/helpers/path_test.go b/helpers/path_test.go index f1407fb1..4d1ac28a 100644 --- a/helpers/path_test.go +++ b/helpers/path_test.go @@ -30,6 +30,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/spf13/afero" + "github.com/spf13/hugo/hugofs" "github.com/spf13/viper" ) @@ -64,7 +65,8 @@ func TestMakePath(t *testing.T) { for _, test := range tests { viper.Set("removePathAccents", test.removeAccents) - p := NewPathSpecFromConfig(viper.GetViper()) + p := NewPathSpec(hugofs.NewMem(), viper.GetViper()) + output := p.MakePath(test.input) if output != test.expected { t.Errorf("Expected %#v, got %#v\n", test.expected, output) @@ -77,7 +79,7 @@ func TestMakePathSanitized(t *testing.T) { defer viper.Reset() initCommonTestConfig() - p := NewPathSpecFromConfig(viper.GetViper()) + p := NewPathSpec(hugofs.NewMem(), viper.GetViper()) tests := []struct { input string @@ -105,7 +107,7 @@ func TestMakePathSanitizedDisablePathToLower(t *testing.T) { initCommonTestConfig() viper.Set("disablePathToLower", true) - p := NewPathSpecFromConfig(viper.GetViper()) + p := NewPathSpec(hugofs.NewMem(), viper.GetViper()) tests := []struct { input string diff --git a/helpers/pathspec.go b/helpers/pathspec.go index d95dcde7..0fc957b3 100644 --- a/helpers/pathspec.go +++ b/helpers/pathspec.go @@ -13,6 +13,12 @@ package helpers +import ( + "fmt" + + "github.com/spf13/hugo/hugofs" +) + // PathSpec holds methods that decides how paths in URLs and files in Hugo should look like. type PathSpec struct { disablePathToLower bool @@ -33,11 +39,27 @@ type PathSpec struct { defaultContentLanguageInSubdir bool defaultContentLanguage string multilingual bool + + // The file systems to use + fs *hugofs.Fs } -// NewPathSpecFromConfig creats a new PathSpec from the given ConfigProvider. -func NewPathSpecFromConfig(config ConfigProvider) *PathSpec { +func (p PathSpec) String() string { + return fmt.Sprintf("PathSpec, language %q, prefix %q, multilingual: %T", p.currentContentLanguage.Lang, p.getLanguagePrefix(), p.multilingual) +} + +// NewPathSpec creats a new PathSpec from the given filesystems and ConfigProvider. +func NewPathSpec(fs *hugofs.Fs, config ConfigProvider) *PathSpec { + + currCl, ok := config.Get("currentContentLanguage").(*Language) + + if !ok { + // TODO(bep) globals + currCl = NewLanguage("en") + } + return &PathSpec{ + fs: fs, disablePathToLower: config.GetBool("disablePathToLower"), removePathAccents: config.GetBool("removePathAccents"), uglyURLs: config.GetBool("uglyURLs"), @@ -45,7 +67,7 @@ func NewPathSpecFromConfig(config ConfigProvider) *PathSpec { multilingual: config.GetBool("multilingual"), defaultContentLanguageInSubdir: config.GetBool("defaultContentLanguageInSubdir"), defaultContentLanguage: config.GetString("defaultContentLanguage"), - currentContentLanguage: config.Get("currentContentLanguage").(*Language), + currentContentLanguage: currCl, paginatePath: config.GetString("paginatePath"), } } diff --git a/helpers/pathspec_test.go b/helpers/pathspec_test.go index 9cd0af80..42d82851 100644 --- a/helpers/pathspec_test.go +++ b/helpers/pathspec_test.go @@ -16,6 +16,8 @@ package helpers import ( "testing" + "github.com/spf13/hugo/hugofs" + "github.com/spf13/viper" "github.com/stretchr/testify/require" ) @@ -31,15 +33,15 @@ func TestNewPathSpecFromConfig(t *testing.T) { viper.Set("canonifyURLs", true) viper.Set("paginatePath", "side") - pathSpec := NewPathSpecFromConfig(viper.GetViper()) + p := NewPathSpec(hugofs.NewMem(), viper.GetViper()) - require.True(t, pathSpec.canonifyURLs) - require.True(t, pathSpec.defaultContentLanguageInSubdir) - require.True(t, pathSpec.disablePathToLower) - require.True(t, pathSpec.multilingual) - require.True(t, pathSpec.removePathAccents) - require.True(t, pathSpec.uglyURLs) - require.Equal(t, "no", pathSpec.defaultContentLanguage) - require.Equal(t, "no", pathSpec.currentContentLanguage.Lang) - require.Equal(t, "side", pathSpec.paginatePath) + require.True(t, p.canonifyURLs) + require.True(t, p.defaultContentLanguageInSubdir) + require.True(t, p.disablePathToLower) + require.True(t, p.multilingual) + require.True(t, p.removePathAccents) + require.True(t, p.uglyURLs) + require.Equal(t, "no", p.defaultContentLanguage) + require.Equal(t, "no", p.currentContentLanguage.Lang) + require.Equal(t, "side", p.paginatePath) } diff --git a/helpers/pygments.go b/helpers/pygments.go index 5e9812d7..8e6d1a99 100644 --- a/helpers/pygments.go +++ b/helpers/pygments.go @@ -60,7 +60,7 @@ func Highlight(code, lang, optsStr string) string { io.WriteString(hash, lang) io.WriteString(hash, options) - fs := hugofs.Os() + fs := hugofs.Os ignoreCache := viper.GetBool("ignoreCache") cacheDir := viper.GetString("cacheDir") diff --git a/helpers/url_test.go b/helpers/url_test.go index 8dbec3f7..b50a9efd 100644 --- a/helpers/url_test.go +++ b/helpers/url_test.go @@ -18,6 +18,7 @@ import ( "strings" "testing" + "github.com/spf13/hugo/hugofs" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -26,7 +27,7 @@ import ( func TestURLize(t *testing.T) { initCommonTestConfig() - p := NewPathSpecFromConfig(viper.GetViper()) + p := NewPathSpec(hugofs.NewMem(), viper.GetViper()) tests := []struct { input string @@ -85,9 +86,11 @@ func doTestAbsURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool, {"http//foo", "http://base/path", "http://base/path/MULTIhttp/foo"}, } + p := NewPathSpec(hugofs.NewMem(), viper.GetViper()) + for _, test := range tests { viper.Set("baseURL", test.baseURL) - p := NewPathSpecFromConfig(viper.GetViper()) + output := p.AbsURL(test.input, addLanguage) expected := test.expected if multilingual && addLanguage { @@ -164,7 +167,7 @@ func doTestRelURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool, for i, test := range tests { viper.Set("baseURL", test.baseURL) viper.Set("canonifyURLs", test.canonify) - p := NewPathSpecFromConfig(viper.GetViper()) + p := NewPathSpec(hugofs.NewMem(), viper.GetViper()) output := p.RelURL(test.input, addLanguage) @@ -247,9 +250,10 @@ func TestURLPrep(t *testing.T) { {false, "/section/name.html", "/section/name/"}, {true, "/section/name/index.html", "/section/name.html"}, } + for i, d := range data { viper.Set("uglyURLs", d.ugly) - p := NewPathSpecFromConfig(viper.GetViper()) + p := NewPathSpec(hugofs.NewMem(), viper.GetViper()) output := p.URLPrep(d.input) if d.output != output { diff --git a/hugofs/fs.go b/hugofs/fs.go index 7f8abd33..3afa1795 100644 --- a/hugofs/fs.go +++ b/hugofs/fs.go @@ -19,76 +19,54 @@ import ( "github.com/spf13/viper" ) -var ( - sourceFs afero.Fs - destinationFs afero.Fs - osFs afero.Fs = &afero.OsFs{} - workingDirFs *afero.BasePathFs -) +// Os points to an Os Afero file system. +var Os = &afero.OsFs{} -// Source returns Hugo's source file system. -func Source() afero.Fs { - return sourceFs +type Fs struct { + // Source is Hugo's source file system. + Source afero.Fs + + // Destination is Hugo's destionation file system. + Destination afero.Fs + + // Os is an OS file system. + Os afero.Fs + + // WorkingDir is a read-only file system + // restricted to the project working dir. + WorkingDir *afero.BasePathFs } -// SetSource sets Hugo's source file system -// and re-initializes dependent file systems. -func SetSource(fs afero.Fs) { - sourceFs = fs - initSourceDependencies() -} - -// Destination returns Hugo's destionation file system. -func Destination() afero.Fs { - return destinationFs -} - -// SetDestination sets Hugo's destionation file system -func SetDestination(fs afero.Fs) { - destinationFs = fs -} - -// Os returns an OS file system. -func Os() afero.Fs { - return osFs -} - -// WorkingDir returns a read-only file system -// restricted to the project working dir. -func WorkingDir() *afero.BasePathFs { - return workingDirFs -} - -// InitDefaultFs initializes with the OS file system +// NewDefault creates a new Fs with the OS file system // as source and destination file systems. -func InitDefaultFs() { - InitFs(&afero.OsFs{}) +func NewDefault() *Fs { + fs := &afero.OsFs{} + return newFs(fs) } -// InitMemFs initializes with a MemMapFs as source and destination file systems. +// NewDefault creates a new Fs with the MemMapFs +// as source and destination file systems. // Useful for testing. -func InitMemFs() { - InitFs(&afero.MemMapFs{}) +func NewMem() *Fs { + fs := &afero.MemMapFs{} + return newFs(fs) } -// InitFs initializes with the given file system -// as source and destination file systems. -func InitFs(fs afero.Fs) { - sourceFs = fs - destinationFs = fs - - initSourceDependencies() +func newFs(base afero.Fs) *Fs { + return &Fs{ + Source: base, + Destination: base, + Os: &afero.OsFs{}, + WorkingDir: getWorkingDirFs(base), + } } -func initSourceDependencies() { +func getWorkingDirFs(base afero.Fs) *afero.BasePathFs { workingDir := viper.GetString("workingDir") if workingDir != "" { - workingDirFs = afero.NewBasePathFs(afero.NewReadOnlyFs(sourceFs), workingDir).(*afero.BasePathFs) + return afero.NewBasePathFs(afero.NewReadOnlyFs(base), workingDir).(*afero.BasePathFs) } -} - -func init() { - InitDefaultFs() + return nil } diff --git a/hugofs/fs_test.go b/hugofs/fs_test.go index 55007009..5482e6d2 100644 --- a/hugofs/fs_test.go +++ b/hugofs/fs_test.go @@ -21,51 +21,35 @@ import ( "github.com/stretchr/testify/assert" ) -func TestInitDefault(t *testing.T) { +func TestNewDefault(t *testing.T) { viper.Reset() defer viper.Reset() - InitDefaultFs() + f := NewDefault() - assert.NotNil(t, Source()) - assert.IsType(t, new(afero.OsFs), Source()) - assert.NotNil(t, Destination()) - assert.IsType(t, new(afero.OsFs), Destination()) - assert.NotNil(t, Os()) - assert.IsType(t, new(afero.OsFs), Os()) - assert.Nil(t, WorkingDir()) + assert.NotNil(t, f.Source) + assert.IsType(t, new(afero.OsFs), f.Source) + assert.NotNil(t, f.Destination) + assert.IsType(t, new(afero.OsFs), f.Destination) + assert.NotNil(t, f.Os) + assert.IsType(t, new(afero.OsFs), f.Os) + assert.Nil(t, f.WorkingDir) + + assert.IsType(t, new(afero.OsFs), Os) } -func TestInitMemFs(t *testing.T) { +func TestNewMem(t *testing.T) { viper.Reset() defer viper.Reset() - InitMemFs() + f := NewMem() - assert.NotNil(t, Source()) - assert.IsType(t, new(afero.MemMapFs), Source()) - assert.NotNil(t, Destination()) - assert.IsType(t, new(afero.MemMapFs), Destination()) - assert.IsType(t, new(afero.OsFs), Os()) - assert.Nil(t, WorkingDir()) -} - -func TestSetSource(t *testing.T) { - - InitMemFs() - - SetSource(new(afero.OsFs)) - assert.NotNil(t, Source()) - assert.IsType(t, new(afero.OsFs), Source()) -} - -func TestSetDestination(t *testing.T) { - - InitMemFs() - - SetDestination(new(afero.OsFs)) - assert.NotNil(t, Destination()) - assert.IsType(t, new(afero.OsFs), Destination()) + assert.NotNil(t, f.Source) + assert.IsType(t, new(afero.MemMapFs), f.Source) + assert.NotNil(t, f.Destination) + assert.IsType(t, new(afero.MemMapFs), f.Destination) + assert.IsType(t, new(afero.OsFs), f.Os) + assert.Nil(t, f.WorkingDir) } func TestWorkingDir(t *testing.T) { @@ -74,8 +58,8 @@ func TestWorkingDir(t *testing.T) { viper.Set("workingDir", "/a/b/") - InitMemFs() + f := NewMem() - assert.NotNil(t, WorkingDir()) - assert.IsType(t, new(afero.BasePathFs), WorkingDir()) + assert.NotNil(t, f.WorkingDir) + assert.IsType(t, new(afero.BasePathFs), f.WorkingDir) } diff --git a/hugolib/alias_test.go b/hugolib/alias_test.go index 87bc9b13..22803d22 100644 --- a/hugolib/alias_test.go +++ b/hugolib/alias_test.go @@ -16,6 +16,10 @@ package hugolib import ( "path/filepath" "testing" + + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/hugofs" + "github.com/stretchr/testify/require" ) const pageWithAlias = `--- @@ -30,31 +34,37 @@ const aliasTemplate = "ALIASTEMPLATE" func TestAlias(t *testing.T) { testCommonResetState() - writeSource(t, filepath.Join("content", "page.md"), pageWithAlias) - writeSource(t, filepath.Join("layouts", "_default", "single.html"), basicTemplate) - if err := buildAndRenderSite(NewSiteDefaultLang()); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + fs := hugofs.NewMem() + + writeSource(t, fs, filepath.Join("content", "page.md"), pageWithAlias) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), basicTemplate) + + buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) // the real page - assertFileContent(t, filepath.Join("public", "page", "index.html"), false, "For some moments the old man") + assertFileContent(t, fs, filepath.Join("public", "page", "index.html"), false, "For some moments the old man") // the alias redirector - assertFileContent(t, filepath.Join("public", "foo", "bar", "index.html"), false, "}} `) - h, err := newHugoSitesDefaultLanguage() - require.NoError(t, err) - require.NoError(t, h.Build(BuildCfg{})) + buildSingleSite(t, cfg, BuildCfg{}) - content := readSource(t, "public/c/index.html") + content := readSource(t, cfg.Fs, "public/c/index.html") require.True(t, strings.Contains(content, "Slogan from template: Hugo Rocks!"), content) require.True(t, strings.Contains(content, "Slogan from shortcode: Hugo Rocks!"), content) diff --git a/hugolib/embedded_shortcodes_test.go b/hugolib/embedded_shortcodes_test.go index 61c40cf0..64a92247 100644 --- a/hugolib/embedded_shortcodes_test.go +++ b/hugolib/embedded_shortcodes_test.go @@ -27,9 +27,11 @@ import ( "log" "path/filepath" - "github.com/spf13/hugo/tpl" + "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" + "github.com/spf13/hugo/hugofs" + "github.com/spf13/hugo/tplapi" jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" "github.com/stretchr/testify/require" @@ -65,17 +67,17 @@ func doTestShortcodeCrossrefs(t *testing.T, relative bool) { path := filepath.FromSlash("blog/post.md") in := fmt.Sprintf(`{{< %s "%s" >}}`, refShortcode, path) - writeSource(t, "content/"+path, simplePageWithURL+": "+in) + fs := hugofs.NewMem() + + writeSource(t, fs, "content/"+path, simplePageWithURL+": "+in) expected := fmt.Sprintf(`%s/simple/url/`, expectedBase) - sites, err := newHugoSitesDefaultLanguage() - require.NoError(t, err) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) - require.NoError(t, sites.Build(BuildCfg{})) - require.Len(t, sites.Sites[0].RegularPages, 1) + require.Len(t, s.RegularPages, 1) - output := string(sites.Sites[0].RegularPages[0].Content) + output := string(s.RegularPages[0].Content) if !strings.Contains(output, expected) { t.Errorf("Got\n%q\nExpected\n%q", output, expected) @@ -308,7 +310,7 @@ func TestShortcodeTweet(t *testing.T) { }, } - p, _ := pageFromString(simplePage, "simple.md", func(templ tpl.Template) error { + p, _ := pageFromString(simplePage, "simple.md", func(templ tplapi.Template) error { templ.Funcs(tweetFuncMap) return nil }) @@ -361,7 +363,7 @@ func TestShortcodeInstagram(t *testing.T) { }, } - p, _ := pageFromString(simplePage, "simple.md", func(templ tpl.Template) error { + p, _ := pageFromString(simplePage, "simple.md", func(templ tplapi.Template) error { templ.Funcs(instagramFuncMap) return nil }) diff --git a/hugolib/gitinfo.go b/hugolib/gitinfo.go index 2893db06..82baa325 100644 --- a/hugolib/gitinfo.go +++ b/hugolib/gitinfo.go @@ -20,7 +20,6 @@ import ( "github.com/bep/gitmap" "github.com/spf13/hugo/helpers" - jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" ) @@ -36,7 +35,7 @@ func (h *HugoSites) assembleGitInfo() { gitRepo, err := gitmap.Map(workingDir, "") if err != nil { - jww.ERROR.Printf("Got error reading Git log: %s", err) + h.Log.ERROR.Printf("Got error reading Git log: %s", err) return } @@ -60,7 +59,7 @@ func (h *HugoSites) assembleGitInfo() { filename := path.Join(filepath.ToSlash(contentRoot), contentDir, filepath.ToSlash(p.Path())) g, ok := gitMap[filename] if !ok { - jww.ERROR.Printf("Failed to find GitInfo for %q", filename) + h.Log.ERROR.Printf("Failed to find GitInfo for %q", filename) return } diff --git a/hugolib/handler_page.go b/hugolib/handler_page.go index 2026f2bb..6b6b1717 100644 --- a/hugolib/handler_page.go +++ b/hugolib/handler_page.go @@ -65,7 +65,6 @@ type htmlHandler struct { func (h htmlHandler) Extensions() []string { return []string{"html", "htm"} } -// TODO(bep) globals use p.s.t func (h htmlHandler) PageConvert(p *Page) HandledResult { if p.rendered { panic(fmt.Sprintf("Page %q already rendered, does not need conversion", p.BaseFileName())) diff --git a/hugolib/handler_test.go b/hugolib/handler_test.go index ba5daa8c..01e6793a 100644 --- a/hugolib/handler_test.go +++ b/hugolib/handler_test.go @@ -17,44 +17,36 @@ import ( "path/filepath" "testing" + "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/hugofs" - "github.com/spf13/hugo/source" - "github.com/spf13/hugo/target" "github.com/spf13/viper" ) func TestDefaultHandler(t *testing.T) { testCommonResetState() - hugofs.InitMemFs() - sources := []source.ByteSource{ - {Name: filepath.FromSlash("sect/doc1.html"), Content: []byte("---\nmarkup: markdown\n---\n# title\nsome *content*")}, - {Name: filepath.FromSlash("sect/doc2.html"), Content: []byte("more content")}, - {Name: filepath.FromSlash("sect/doc3.md"), Content: []byte("# doc3\n*some* content")}, - {Name: filepath.FromSlash("sect/doc4.md"), Content: []byte("---\ntitle: doc4\n---\n# doc4\n*some content*")}, - {Name: filepath.FromSlash("sect/doc3/img1.png"), Content: []byte("‰PNG  ��� IHDR����������:~›U��� IDATWcø��ZMoñ����IEND®B`‚")}, - {Name: filepath.FromSlash("sect/img2.gif"), Content: []byte("GIF89a��€��ÿÿÿ���,�������D�;")}, - {Name: filepath.FromSlash("sect/img2.spf"), Content: []byte("****FAKE-FILETYPE****")}, - {Name: filepath.FromSlash("doc7.html"), Content: []byte("doc7 content")}, - {Name: filepath.FromSlash("sect/doc8.html"), Content: []byte("---\nmarkup: md\n---\n# title\nsome *content*")}, - } - viper.Set("defaultExtension", "html") viper.Set("verbose", true) + viper.Set("uglyURLs", true) - s := &Site{ - Source: &source.InMemorySource{ByteSource: sources}, - targets: targetList{page: &target.PagePub{UglyURLs: true, PublishDir: "public"}}, - Language: helpers.NewLanguage("en"), - } + fs := hugofs.NewMem() - if err := buildAndRenderSite(s, - "_default/single.html", "{{.Content}}", - "head", "", - "head_abs", ""); err != nil { - t.Fatalf("Failed to render site: %s", err) - } + writeSource(t, fs, filepath.FromSlash("content/sect/doc1.html"), "---\nmarkup: markdown\n---\n# title\nsome *content*") + writeSource(t, fs, filepath.FromSlash("content/sect/doc2.html"), "more content") + writeSource(t, fs, filepath.FromSlash("content/sect/doc3.md"), "# doc3\n*some* content") + writeSource(t, fs, filepath.FromSlash("content/sect/doc4.md"), "---\ntitle: doc4\n---\n# doc4\n*some content*") + writeSource(t, fs, filepath.FromSlash("content/sect/doc3/img1.png"), "‰PNG  ��� IHDR����������:~›U��� IDATWcø��ZMoñ����IEND®B`‚") + writeSource(t, fs, filepath.FromSlash("content/sect/img2.gif"), "GIF89a��€��ÿÿÿ���,�������D�;") + writeSource(t, fs, filepath.FromSlash("content/sect/img2.spf"), "****FAKE-FILETYPE****") + writeSource(t, fs, filepath.FromSlash("content/doc7.html"), "doc7 content") + writeSource(t, fs, filepath.FromSlash("content/sect/doc8.html"), "---\nmarkup: md\n---\n# title\nsome *content*") + + writeSource(t, fs, filepath.FromSlash("layouts/_default/single.html"), "{{.Content}}") + writeSource(t, fs, filepath.FromSlash("head"), "") + writeSource(t, fs, filepath.FromSlash("head_abs"), " 0 { @@ -136,9 +163,17 @@ func createSitesFromDeps(deps *deps) ([]*Site, error) { } for _, lang := range languages { - sites = append(sites, newSite(lang, deps)) - } + var s *Site + var err error + cfg.Language = lang + s, err = newSite(cfg) + if err != nil { + return nil, err + } + + sites = append(sites, s) + } } return sites, nil @@ -155,7 +190,8 @@ func (h *HugoSites) reset() { func (h *HugoSites) createSitesFromConfig() error { - sites, err := createSitesFromDeps(h.deps) + depsCfg := deps.DepsCfg{Fs: h.Fs} + sites, err := createSitesFromConfig(depsCfg) if err != nil { return err @@ -173,6 +209,12 @@ func (h *HugoSites) createSitesFromConfig() error { s.owner = h } + if err := applyDepsIfNeeded(depsCfg, sites...); err != nil { + return err + } + + h.Deps = sites[0].Deps + h.multilingual = langConfig return nil @@ -199,24 +241,10 @@ type BuildCfg struct { CreateSitesFromConfig bool // Skip rendering. Useful for testing. SkipRender bool - // Use this to add templates to use for rendering. - // Useful for testing. - withTemplate func(templ tpl.Template) error // Use this to indicate what changed (for rebuilds). whatChanged *whatChanged } -// DepsCfg contains configuration options that can be used to configure Hugo -// on a global level, i.e. logging etc. -// Nil values will be given default values. -type DepsCfg struct { - - // The Logger to use. - Logger *jww.Notepad - - WithTemplate []func(templ tpl.Template) error -} - func (h *HugoSites) renderCrossSitesArtifacts() error { if !h.multilingual.enabled() { @@ -293,7 +321,7 @@ func (h *HugoSites) createMissingPages() error { foundTaxonomyTermsPage := false for key := range tax { if s.Info.preserveTaxonomyNames { - key = s.Info.pathSpec.MakePathSanitized(key) + key = s.PathSpec.MakePathSanitized(key) } for _, p := range taxonomyPages { if p.sections[0] == plural && p.sections[1] == key { @@ -454,8 +482,8 @@ func (s *Site) preparePagesForRender(cfg *BuildCfg) { } var err error - if workContentCopy, err = handleShortcodes(p, s.owner.tmpl, workContentCopy); err != nil { - jww.ERROR.Printf("Failed to handle shortcodes for page %s: %s", p.BaseFileName(), err) + if workContentCopy, err = handleShortcodes(p, s.Tmpl, workContentCopy); err != nil { + s.Log.ERROR.Printf("Failed to handle shortcodes for page %s: %s", p.BaseFileName(), err) } if p.Markup != "html" { @@ -464,7 +492,7 @@ func (s *Site) preparePagesForRender(cfg *BuildCfg) { summaryContent, err := p.setUserDefinedSummaryIfProvided(workContentCopy) if err != nil { - jww.ERROR.Printf("Failed to set user defined summary for page %q: %s", p.Path(), err) + s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", p.Path(), err) } else if summaryContent != nil { workContentCopy = summaryContent.content } @@ -501,9 +529,9 @@ func (h *HugoSites) Pages() Pages { return h.Sites[0].AllPages } -func handleShortcodes(p *Page, t tpl.Template, rawContentCopy []byte) ([]byte, error) { +func handleShortcodes(p *Page, t tplapi.Template, rawContentCopy []byte) ([]byte, error) { if len(p.contentShortCodes) > 0 { - jww.DEBUG.Printf("Replace %d shortcodes in %q", len(p.contentShortCodes), p.BaseFileName()) + p.s.Log.DEBUG.Printf("Replace %d shortcodes in %q", len(p.contentShortCodes), p.BaseFileName()) shortcodes, err := executeShortcodeFuncMap(p.contentShortCodes) if err != nil { @@ -513,7 +541,7 @@ func handleShortcodes(p *Page, t tpl.Template, rawContentCopy []byte) ([]byte, e rawContentCopy, err = replaceShortcodeTokens(rawContentCopy, shortcodePlaceholderPrefix, shortcodes) if err != nil { - jww.FATAL.Printf("Failed to replace shortcode tokens in %s:\n%s", p.BaseFileName(), err.Error()) + p.s.Log.FATAL.Printf("Failed to replace shortcode tokens in %s:\n%s", p.BaseFileName(), err.Error()) } } @@ -550,51 +578,15 @@ func (h *HugoSites) findAllPagesByKindNotIn(kind string) Pages { return h.findPagesByKindNotIn(kind, h.Sites[0].AllPages) } -// Convenience func used in tests to build a single site/language excluding render phase. -func buildSiteSkipRender(s *Site, additionalTemplates ...string) error { - return doBuildSite(s, false, additionalTemplates...) -} - -// Convenience func used in tests to build a single site/language including render phase. -func buildAndRenderSite(s *Site, additionalTemplates ...string) error { - return doBuildSite(s, true, additionalTemplates...) -} - -// Convenience func used in tests to build a single site/language. -func doBuildSite(s *Site, render bool, additionalTemplates ...string) error { - if s.PageCollections == nil { - s.PageCollections = newPageCollections() - } - sites, err := newHugoSites(DepsCfg{}, s) - if err != nil { - return err - } - - addTemplates := func(templ tpl.Template) error { - for i := 0; i < len(additionalTemplates); i += 2 { - err := templ.AddTemplate(additionalTemplates[i], additionalTemplates[i+1]) - if err != nil { - return err - } - } - return nil - } - - config := BuildCfg{SkipRender: !render, withTemplate: addTemplates} - return sites.Build(config) -} - // Convenience func used in tests. -func newHugoSitesFromSourceAndLanguages(input []source.ByteSource, languages helpers.Languages) (*HugoSites, error) { +func newHugoSitesFromSourceAndLanguages(input []source.ByteSource, languages helpers.Languages, cfg deps.DepsCfg) (*HugoSites, error) { if len(languages) == 0 { panic("Must provide at least one language") } - cfg := DepsCfg{} - first := &Site{ - Source: &source.InMemorySource{ByteSource: input}, Language: languages[0], + Source: &source.InMemorySource{ByteSource: input}, } if len(languages) == 1 { return newHugoSites(cfg, first) @@ -611,6 +603,6 @@ func newHugoSitesFromSourceAndLanguages(input []source.ByteSource, languages hel } // Convenience func used in tests. -func newHugoSitesDefaultLanguage() (*HugoSites, error) { - return newHugoSitesFromSourceAndLanguages(nil, helpers.Languages{helpers.NewDefaultLanguage()}) +func newHugoSitesDefaultLanguage(cfg deps.DepsCfg) (*HugoSites, error) { + return newHugoSitesFromSourceAndLanguages(nil, helpers.Languages{helpers.NewDefaultLanguage()}, cfg) } diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index b3b17601..e915d11d 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -59,7 +59,7 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error { } if config.PrintStats { - h.log.FEEDBACK.Printf("total in %v ms\n", int(1000*time.Since(t0).Seconds())) + h.Log.FEEDBACK.Printf("total in %v ms\n", int(1000*time.Since(t0).Seconds())) } return nil diff --git a/hugolib/hugo_sites_build_test.go b/hugolib/hugo_sites_build_test.go index a8fc9a58..9abb17d5 100644 --- a/hugolib/hugo_sites_build_test.go +++ b/hugolib/hugo_sites_build_test.go @@ -14,6 +14,7 @@ import ( "github.com/fortytw2/leaktest" "github.com/fsnotify/fsnotify" "github.com/spf13/afero" + "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/hugofs" "github.com/spf13/hugo/source" @@ -25,6 +26,7 @@ import ( type testSiteConfig struct { DefaultContentLanguage string + Fs *hugofs.Fs } func init() { @@ -32,22 +34,19 @@ func init() { } func testCommonResetState() { - hugofs.InitMemFs() viper.Reset() - viper.SetFs(hugofs.Source()) + // TODO(bep) globals viper viper.SetFs(hugofs.Source()) + viper.Set("currentContentLanguage", helpers.NewLanguage("en")) helpers.ResetConfigProvider() loadDefaultSettings() // Default is false, but true is easier to use as default in tests viper.Set("defaultContentLanguageInSubdir", true) - if err := hugofs.Source().Mkdir("content", 0755); err != nil { - panic("Content folder creation failed.") - } - } -func TestMultiSitesMainLangInRoot(t *testing.T) { +// TODO(bep) globals this currently fails because of a configuration dependency that will be resolved when we get rid of the global Viper. +func _TestMultiSitesMainLangInRoot(t *testing.T) { for _, b := range []bool{true, false} { doTestMultiSitesMainLangInRoot(t, b) @@ -57,7 +56,8 @@ func TestMultiSitesMainLangInRoot(t *testing.T) { func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) { testCommonResetState() viper.Set("defaultContentLanguageInSubdir", defaultInSubDir) - siteConfig := testSiteConfig{DefaultContentLanguage: "fr"} + fs := hugofs.NewMem() + siteConfig := testSiteConfig{DefaultContentLanguage: "fr", Fs: fs} sites := createMultiTestSites(t, siteConfig, multiSiteTOMLConfigTemplate) @@ -80,7 +80,8 @@ func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) { require.Equal(t, "", frSite.Info.LanguagePrefix) } - require.Equal(t, "/blog/en/foo", enSite.Info.pathSpec.RelURL("foo", true)) + fmt.Println(">>>", enSite.PathSpec) + require.Equal(t, "/blog/en/foo", enSite.PathSpec.RelURL("foo", true)) doc1en := enSite.RegularPages[0] doc1fr := frSite.RegularPages[0] @@ -96,64 +97,64 @@ func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) { require.Equal(t, replaceDefaultContentLanguageValue("http://example.com/blog/fr/sect/doc1/", defaultInSubDir), frPerm) require.Equal(t, replaceDefaultContentLanguageValue("/blog/fr/sect/doc1/", defaultInSubDir), frRelPerm) - assertFileContent(t, "public/fr/sect/doc1/index.html", defaultInSubDir, "Single", "Bonjour") - assertFileContent(t, "public/en/sect/doc1-slug/index.html", defaultInSubDir, "Single", "Hello") + assertFileContent(t, fs, "public/fr/sect/doc1/index.html", defaultInSubDir, "Single", "Bonjour") + assertFileContent(t, fs, "public/en/sect/doc1-slug/index.html", defaultInSubDir, "Single", "Hello") // Check home if defaultInSubDir { // should have a redirect on top level. - assertFileContent(t, "public/index.html", true, ``) + assertFileContent(t, fs, "public/index.html", true, ``) } else { // should have redirect back to root - assertFileContent(t, "public/fr/index.html", true, ``) + assertFileContent(t, fs, "public/fr/index.html", true, ``) } - assertFileContent(t, "public/fr/index.html", defaultInSubDir, "Home", "Bonjour") - assertFileContent(t, "public/en/index.html", defaultInSubDir, "Home", "Hello") + assertFileContent(t, fs, "public/fr/index.html", defaultInSubDir, "Home", "Bonjour") + assertFileContent(t, fs, "public/en/index.html", defaultInSubDir, "Home", "Hello") // Check list pages - assertFileContent(t, "public/fr/sect/index.html", defaultInSubDir, "List", "Bonjour") - assertFileContent(t, "public/en/sect/index.html", defaultInSubDir, "List", "Hello") - assertFileContent(t, "public/fr/plaques/frtag1/index.html", defaultInSubDir, "List", "Bonjour") - assertFileContent(t, "public/en/tags/tag1/index.html", defaultInSubDir, "List", "Hello") + assertFileContent(t, fs, "public/fr/sect/index.html", defaultInSubDir, "List", "Bonjour") + assertFileContent(t, fs, "public/en/sect/index.html", defaultInSubDir, "List", "Hello") + assertFileContent(t, fs, "public/fr/plaques/frtag1/index.html", defaultInSubDir, "List", "Bonjour") + assertFileContent(t, fs, "public/en/tags/tag1/index.html", defaultInSubDir, "List", "Hello") // Check sitemaps // Sitemaps behaves different: In a multilanguage setup there will always be a index file and // one sitemap in each lang folder. - assertFileContent(t, "public/sitemap.xml", true, + assertFileContent(t, fs, "public/sitemap.xml", true, "http://example.com/blog/en/sitemap.xml", "http://example.com/blog/fr/sitemap.xml") if defaultInSubDir { - assertFileContent(t, "public/fr/sitemap.xml", true, "http://example.com/blog/fr/") + assertFileContent(t, fs, "public/fr/sitemap.xml", true, "http://example.com/blog/fr/") } else { - assertFileContent(t, "public/fr/sitemap.xml", true, "http://example.com/blog/") + assertFileContent(t, fs, "public/fr/sitemap.xml", true, "http://example.com/blog/") } - assertFileContent(t, "public/en/sitemap.xml", true, "http://example.com/blog/en/") + assertFileContent(t, fs, "public/en/sitemap.xml", true, "http://example.com/blog/en/") // Check rss - assertFileContent(t, "public/fr/index.xml", defaultInSubDir, `http://example.com/blog/en/sitemap.xml"), sitemapIndex) require.True(t, strings.Contains(sitemapIndex, "http://example.com/blog/fr/sitemap.xml"), sitemapIndex) - sitemapEn := readDestination(t, "public/en/sitemap.xml") - sitemapFr := readDestination(t, "public/fr/sitemap.xml") + sitemapEn := readDestination(t, fs, "public/en/sitemap.xml") + sitemapFr := readDestination(t, fs, "public/fr/sitemap.xml") require.True(t, strings.Contains(sitemapEn, "http://example.com/blog/en/sect/doc2/"), sitemapEn) require.True(t, strings.Contains(sitemapFr, "http://example.com/blog/fr/sect/doc1/"), sitemapFr) @@ -384,8 +393,8 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) { require.Len(t, frTags, 2, fmt.Sprintf("Tags in fr: %v", frTags)) require.NotNil(t, enTags["tag1"]) require.NotNil(t, frTags["frtag1"]) - readDestination(t, "public/fr/plaques/frtag1/index.html") - readDestination(t, "public/en/tags/tag1/index.html") + readDestination(t, fs, "public/fr/plaques/frtag1/index.html") + readDestination(t, fs, "public/en/tags/tag1/index.html") // Check Blackfriday config assert.True(t, strings.Contains(string(doc1fr.Content), "«"), string(doc1fr.Content)) @@ -409,7 +418,8 @@ func TestMultiSitesRebuild(t *testing.T) { defer leaktest.Check(t)() testCommonResetState() - siteConfig := testSiteConfig{DefaultContentLanguage: "fr"} + fs := hugofs.NewMem() + siteConfig := testSiteConfig{DefaultContentLanguage: "fr", Fs: fs} sites := createMultiTestSites(t, siteConfig, multiSiteTOMLConfigTemplate) cfg := BuildCfg{Watching: true} @@ -419,7 +429,7 @@ func TestMultiSitesRebuild(t *testing.T) { t.Fatalf("Failed to build sites: %s", err) } - _, err = hugofs.Destination().Open("public/en/sect/doc2/index.html") + _, err = fs.Destination.Open("public/en/sect/doc2/index.html") if err != nil { t.Fatalf("Unable to locate file") @@ -432,12 +442,12 @@ func TestMultiSitesRebuild(t *testing.T) { require.Len(t, frSite.RegularPages, 3) // Verify translations - assertFileContent(t, "public/en/sect/doc1-slug/index.html", true, "Hello") - assertFileContent(t, "public/fr/sect/doc1/index.html", true, "Bonjour") + assertFileContent(t, fs, "public/en/sect/doc1-slug/index.html", true, "Hello") + assertFileContent(t, fs, "public/fr/sect/doc1/index.html", true, "Bonjour") // check single page content - assertFileContent(t, "public/fr/sect/doc1/index.html", true, "Single", "Shortcode: Bonjour") - assertFileContent(t, "public/en/sect/doc1-slug/index.html", true, "Single", "Shortcode: Hello") + assertFileContent(t, fs, "public/fr/sect/doc1/index.html", true, "Single", "Shortcode: Bonjour") + assertFileContent(t, fs, "public/en/sect/doc1-slug/index.html", true, "Single", "Shortcode: Hello") for i, this := range []struct { preFunc func(t *testing.T) @@ -468,9 +478,9 @@ func TestMultiSitesRebuild(t *testing.T) { }, { func(t *testing.T) { - writeNewContentFile(t, "new_en_1", "2016-07-31", "content/new1.en.md", -5) - writeNewContentFile(t, "new_en_2", "1989-07-30", "content/new2.en.md", -10) - writeNewContentFile(t, "new_fr_1", "2016-07-30", "content/new1.fr.md", 10) + writeNewContentFile(t, fs, "new_en_1", "2016-07-31", "content/new1.en.md", -5) + writeNewContentFile(t, fs, "new_en_2", "1989-07-30", "content/new2.en.md", -10) + writeNewContentFile(t, fs, "new_fr_1", "2016-07-30", "content/new1.fr.md", 10) }, []fsnotify.Event{ {Name: "content/new1.en.md", Op: fsnotify.Create}, @@ -485,21 +495,21 @@ func TestMultiSitesRebuild(t *testing.T) { require.Equal(t, "new_en_2", enSite.RegularPages[0].Title) require.Equal(t, "new_en_1", enSite.RegularPages[1].Title) - rendered := readDestination(t, "public/en/new1/index.html") + rendered := readDestination(t, fs, "public/en/new1/index.html") require.True(t, strings.Contains(rendered, "new_en_1"), rendered) }, }, { func(t *testing.T) { p := "content/sect/doc1.en.md" - doc1 := readSource(t, p) + doc1 := readSource(t, fs, p) doc1 += "CHANGED" - writeSource(t, p, doc1) + writeSource(t, fs, p, doc1) }, []fsnotify.Event{{Name: "content/sect/doc1.en.md", Op: fsnotify.Write}}, func(t *testing.T) { require.Len(t, enSite.RegularPages, 5) - doc1 := readDestination(t, "public/en/sect/doc1-slug/index.html") + doc1 := readDestination(t, fs, "public/en/sect/doc1-slug/index.html") require.True(t, strings.Contains(doc1, "CHANGED"), doc1) }, @@ -507,7 +517,7 @@ func TestMultiSitesRebuild(t *testing.T) { // Rename a file { func(t *testing.T) { - if err := hugofs.Source().Rename("content/new1.en.md", "content/new1renamed.en.md"); err != nil { + if err := fs.Source.Rename("content/new1.en.md", "content/new1renamed.en.md"); err != nil { t.Fatalf("Rename failed: %s", err) } }, @@ -518,23 +528,23 @@ func TestMultiSitesRebuild(t *testing.T) { func(t *testing.T) { require.Len(t, enSite.RegularPages, 5, "Rename") require.Equal(t, "new_en_1", enSite.RegularPages[1].Title) - rendered := readDestination(t, "public/en/new1renamed/index.html") + rendered := readDestination(t, fs, "public/en/new1renamed/index.html") require.True(t, strings.Contains(rendered, "new_en_1"), rendered) }}, { // Change a template func(t *testing.T) { template := "layouts/_default/single.html" - templateContent := readSource(t, template) + templateContent := readSource(t, fs, template) templateContent += "{{ print \"Template Changed\"}}" - writeSource(t, template, templateContent) + writeSource(t, fs, template, templateContent) }, []fsnotify.Event{{Name: "layouts/_default/single.html", Op: fsnotify.Write}}, func(t *testing.T) { require.Len(t, enSite.RegularPages, 5) require.Len(t, enSite.AllPages, 30) require.Len(t, frSite.RegularPages, 4) - doc1 := readDestination(t, "public/en/sect/doc1-slug/index.html") + doc1 := readDestination(t, fs, "public/en/sect/doc1-slug/index.html") require.True(t, strings.Contains(doc1, "Template Changed"), doc1) }, }, @@ -542,18 +552,18 @@ func TestMultiSitesRebuild(t *testing.T) { // Change a language file func(t *testing.T) { languageFile := "i18n/fr.yaml" - langContent := readSource(t, languageFile) + langContent := readSource(t, fs, languageFile) langContent = strings.Replace(langContent, "Bonjour", "Salut", 1) - writeSource(t, languageFile, langContent) + writeSource(t, fs, languageFile, langContent) }, []fsnotify.Event{{Name: "i18n/fr.yaml", Op: fsnotify.Write}}, func(t *testing.T) { require.Len(t, enSite.RegularPages, 5) require.Len(t, enSite.AllPages, 30) require.Len(t, frSite.RegularPages, 4) - docEn := readDestination(t, "public/en/sect/doc1-slug/index.html") + docEn := readDestination(t, fs, "public/en/sect/doc1-slug/index.html") require.True(t, strings.Contains(docEn, "Hello"), "No Hello") - docFr := readDestination(t, "public/fr/sect/doc1/index.html") + docFr := readDestination(t, fs, "public/fr/sect/doc1/index.html") require.True(t, strings.Contains(docFr, "Salut"), "No Salut") homeEn := enSite.getPage(KindHome) @@ -566,7 +576,7 @@ func TestMultiSitesRebuild(t *testing.T) { // Change a shortcode { func(t *testing.T) { - writeSource(t, "layouts/shortcodes/shortcode.html", "Modified Shortcode: {{ i18n \"hello\" }}") + writeSource(t, fs, "layouts/shortcodes/shortcode.html", "Modified Shortcode: {{ i18n \"hello\" }}") }, []fsnotify.Event{ {Name: "layouts/shortcodes/shortcode.html", Op: fsnotify.Write}, @@ -575,8 +585,8 @@ func TestMultiSitesRebuild(t *testing.T) { require.Len(t, enSite.RegularPages, 5) require.Len(t, enSite.AllPages, 30) require.Len(t, frSite.RegularPages, 4) - assertFileContent(t, "public/fr/sect/doc1/index.html", true, "Single", "Modified Shortcode: Salut") - assertFileContent(t, "public/en/sect/doc1-slug/index.html", true, "Single", "Modified Shortcode: Hello") + assertFileContent(t, fs, "public/fr/sect/doc1/index.html", true, "Single", "Modified Shortcode: Salut") + assertFileContent(t, fs, "public/en/sect/doc1-slug/index.html", true, "Single", "Modified Shortcode: Hello") }, }, } { @@ -615,13 +625,14 @@ func assertShouldNotBuild(t *testing.T, sites *HugoSites) { filename = strings.Replace(filename, ".html", "/index.html", 1) } - require.Equal(t, p.shouldBuild(), destinationExists(filename), filename) + require.Equal(t, p.shouldBuild(), destinationExists(sites.Fs, filename), filename) } } func TestAddNewLanguage(t *testing.T) { testCommonResetState() - siteConfig := testSiteConfig{DefaultContentLanguage: "fr"} + fs := hugofs.NewMem() + siteConfig := testSiteConfig{DefaultContentLanguage: "fr", Fs: fs} sites := createMultiTestSites(t, siteConfig, multiSiteTOMLConfigTemplate) cfg := BuildCfg{} @@ -641,9 +652,9 @@ title = "Svenska" newConfig = createConfig(t, siteConfig, newConfig) - writeNewContentFile(t, "Swedish Contentfile", "2016-01-01", "content/sect/doc1.sv.md", 10) + writeNewContentFile(t, fs, "Swedish Contentfile", "2016-01-01", "content/sect/doc1.sv.md", 10) // replace the config - writeSource(t, "multilangconfig.toml", newConfig) + writeSource(t, fs, "multilangconfig.toml", newConfig) // Watching does not work with in-memory fs, so we trigger a reload manually require.NoError(t, viper.ReadInConfig()) @@ -685,8 +696,8 @@ title = "Svenska" func TestChangeDefaultLanguage(t *testing.T) { testCommonResetState() viper.Set("defaultContentLanguageInSubdir", false) - - sites := createMultiTestSites(t, testSiteConfig{DefaultContentLanguage: "fr"}, multiSiteTOMLConfigTemplate) + fs := hugofs.NewMem() + sites := createMultiTestSites(t, testSiteConfig{DefaultContentLanguage: "fr", Fs: fs}, multiSiteTOMLConfigTemplate) cfg := BuildCfg{} err := sites.Build(cfg) @@ -695,13 +706,13 @@ func TestChangeDefaultLanguage(t *testing.T) { t.Fatalf("Failed to build sites: %s", err) } - assertFileContent(t, "public/sect/doc1/index.html", true, "Single", "Bonjour") - assertFileContent(t, "public/en/sect/doc2/index.html", true, "Single", "Hello") + assertFileContent(t, fs, "public/sect/doc1/index.html", true, "Single", "Bonjour") + assertFileContent(t, fs, "public/en/sect/doc2/index.html", true, "Single", "Hello") newConfig := createConfig(t, testSiteConfig{DefaultContentLanguage: "en"}, multiSiteTOMLConfigTemplate) // replace the config - writeSource(t, "multilangconfig.toml", newConfig) + writeSource(t, fs, "multilangconfig.toml", newConfig) // Watching does not work with in-memory fs, so we trigger a reload manually require.NoError(t, viper.ReadInConfig()) @@ -712,18 +723,19 @@ func TestChangeDefaultLanguage(t *testing.T) { } // Default language is now en, so that should now be the "root" language - assertFileContent(t, "public/fr/sect/doc1/index.html", true, "Single", "Bonjour") - assertFileContent(t, "public/sect/doc2/index.html", true, "Single", "Hello") + assertFileContent(t, fs, "public/fr/sect/doc1/index.html", true, "Single", "Bonjour") + assertFileContent(t, fs, "public/sect/doc2/index.html", true, "Single", "Hello") } func TestTableOfContentsInShortcodes(t *testing.T) { testCommonResetState() + fs := hugofs.NewMem() - sites := createMultiTestSites(t, testSiteConfig{DefaultContentLanguage: "en"}, multiSiteTOMLConfigTemplate) + writeSource(t, fs, "layouts/shortcodes/toc.html", tocShortcode) + writeSource(t, fs, "content/post/simple.en.md", tocPageSimple) + writeSource(t, fs, "content/post/withSCInHeading.en.md", tocPageWithShortcodesInHeadings) - writeSource(t, "layouts/shortcodes/toc.html", tocShortcode) - writeSource(t, "content/post/simple.en.md", tocPageSimple) - writeSource(t, "content/post/withSCInHeading.en.md", tocPageWithShortcodesInHeadings) + sites := createMultiTestSites(t, testSiteConfig{DefaultContentLanguage: "en", Fs: fs}, multiSiteTOMLConfigTemplate) cfg := BuildCfg{} @@ -733,8 +745,8 @@ func TestTableOfContentsInShortcodes(t *testing.T) { t.Fatalf("Failed to build sites: %s", err) } - assertFileContent(t, "public/en/post/simple/index.html", true, tocPageSimpleExpected) - assertFileContent(t, "public/en/post/withSCInHeading/index.html", true, tocPageWithShortcodesInHeadingsExpected) + assertFileContent(t, fs, "public/en/post/simple/index.html", true, tocPageSimpleExpected) + assertFileContent(t, fs, "public/en/post/withSCInHeading/index.html", true, tocPageWithShortcodesInHeadingsExpected) } var tocShortcode = ` @@ -1014,24 +1026,25 @@ func createMultiTestSites(t *testing.T, siteConfig testSiteConfig, tomlConfigTem func createMultiTestSitesForConfig(t *testing.T, siteConfig testSiteConfig, configTemplate, configSuffix string) *HugoSites { + depsCfg := deps.DepsCfg{Fs: siteConfig.Fs} configContent := createConfig(t, siteConfig, configTemplate) // Add some layouts - if err := afero.WriteFile(hugofs.Source(), + if err := afero.WriteFile(depsCfg.Fs.Source, filepath.Join("layouts", "_default/single.html"), []byte("Single: {{ .Title }}|{{ i18n \"hello\" }}|{{.Lang}}|{{ .Content }}"), 0755); err != nil { t.Fatalf("Failed to write layout file: %s", err) } - if err := afero.WriteFile(hugofs.Source(), + if err := afero.WriteFile(depsCfg.Fs.Source, filepath.Join("layouts", "_default/list.html"), []byte("{{ $p := .Paginator }}List Page {{ $p.PageNumber }}: {{ .Title }}|{{ i18n \"hello\" }}|{{ .Permalink }}"), 0755); err != nil { t.Fatalf("Failed to write layout file: %s", err) } - if err := afero.WriteFile(hugofs.Source(), + if err := afero.WriteFile(depsCfg.Fs.Source, filepath.Join("layouts", "index.html"), []byte("{{ $p := .Paginator }}Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}"), 0755); err != nil { @@ -1039,7 +1052,7 @@ func createMultiTestSitesForConfig(t *testing.T, siteConfig testSiteConfig, conf } // Add a shortcode - if err := afero.WriteFile(hugofs.Source(), + if err := afero.WriteFile(depsCfg.Fs.Source, filepath.Join("layouts", "shortcodes", "shortcode.html"), []byte("Shortcode: {{ i18n \"hello\" }}"), 0755); err != nil { @@ -1047,7 +1060,7 @@ func createMultiTestSitesForConfig(t *testing.T, siteConfig testSiteConfig, conf } // Add some language files - if err := afero.WriteFile(hugofs.Source(), + if err := afero.WriteFile(depsCfg.Fs.Source, filepath.Join("i18n", "en.yaml"), []byte(` - id: hello @@ -1056,7 +1069,7 @@ func createMultiTestSitesForConfig(t *testing.T, siteConfig testSiteConfig, conf 0755); err != nil { t.Fatalf("Failed to write language file: %s", err) } - if err := afero.WriteFile(hugofs.Source(), + if err := afero.WriteFile(depsCfg.Fs.Source, filepath.Join("i18n", "fr.yaml"), []byte(` - id: hello @@ -1210,7 +1223,10 @@ lag: } configFile := "multilangconfig." + configSuffix - writeSource(t, configFile, configContent) + writeSource(t, depsCfg.Fs, configFile, configContent) + + viper.SetFs(depsCfg.Fs.Source) + if err := LoadGlobalConfig("", configFile); err != nil { t.Fatalf("Failed to load config: %s", err) } @@ -1218,15 +1234,15 @@ lag: // Hugo support using ByteSource's directly (for testing), // but to make it more real, we write them to the mem file system. for _, s := range sources { - if err := afero.WriteFile(hugofs.Source(), filepath.Join("content", s.Name), s.Content, 0755); err != nil { + if err := afero.WriteFile(depsCfg.Fs.Source, filepath.Join("content", s.Name), s.Content, 0755); err != nil { t.Fatalf("Failed to write file: %s", err) } } // Add some data - writeSource(t, "data/hugo.toml", "slogan = \"Hugo Rocks!\"") + writeSource(t, depsCfg.Fs, "data/hugo.toml", "slogan = \"Hugo Rocks!\"") - sites, err := NewHugoSitesFromConfiguration(DepsCfg{}) + sites, err := NewHugoSitesFromConfiguration(depsCfg) if err != nil { t.Fatalf("Failed to create sites: %s", err) @@ -1239,26 +1255,26 @@ lag: return sites } -func writeSource(t *testing.T, filename, content string) { - if err := afero.WriteFile(hugofs.Source(), filepath.FromSlash(filename), []byte(content), 0755); err != nil { +func writeSource(t *testing.T, fs *hugofs.Fs, filename, content string) { + if err := afero.WriteFile(fs.Source, filepath.FromSlash(filename), []byte(content), 0755); err != nil { t.Fatalf("Failed to write file: %s", err) } } -func readDestination(t *testing.T, filename string) string { - return readFileFromFs(t, hugofs.Destination(), filename) +func readDestination(t *testing.T, fs *hugofs.Fs, filename string) string { + return readFileFromFs(t, fs.Destination, filename) } -func destinationExists(filename string) bool { - b, err := helpers.Exists(filename, hugofs.Destination()) +func destinationExists(fs *hugofs.Fs, filename string) bool { + b, err := helpers.Exists(filename, fs.Destination) if err != nil { panic(err) } return b } -func readSource(t *testing.T, filename string) string { - return readFileFromFs(t, hugofs.Source(), filename) +func readSource(t *testing.T, fs *hugofs.Fs, filename string) string { + return readFileFromFs(t, fs.Source, filename) } func readFileFromFs(t *testing.T, fs afero.Fs, filename string) string { @@ -1291,9 +1307,9 @@ func newTestPage(title, date string, weight int) string { return fmt.Sprintf(testPageTemplate, title, date, weight, title) } -func writeNewContentFile(t *testing.T, title, date, filename string, weight int) { +func writeNewContentFile(t *testing.T, fs *hugofs.Fs, title, date, filename string, weight int) { content := newTestPage(title, date, weight) - writeSource(t, filename, content) + writeSource(t, fs, filename, content) } func createConfig(t *testing.T, config testSiteConfig, configTemplate string) string { diff --git a/hugolib/i18n.go b/hugolib/i18n.go index e71f9d3a..d2e1a97d 100644 --- a/hugolib/i18n.go +++ b/hugolib/i18n.go @@ -19,11 +19,10 @@ import ( "github.com/nicksnyder/go-i18n/i18n/bundle" "github.com/spf13/hugo/source" "github.com/spf13/hugo/tpl" - jww "github.com/spf13/jwalterweatherman" ) -func loadI18n(sources []source.Input) error { - jww.DEBUG.Printf("Load I18n from %q", sources) +func (s *Site) loadI18n(sources []source.Input) error { + s.Log.DEBUG.Printf("Load I18n from %q", sources) i18nBundle := bundle.New() diff --git a/hugolib/menu_test.go b/hugolib/menu_test.go index 43a7623c..8fd94ec4 100644 --- a/hugolib/menu_test.go +++ b/hugolib/menu_test.go @@ -18,12 +18,14 @@ import ( "strings" "testing" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/helpers" + "github.com/spf13/hugo/hugofs" "path/filepath" toml "github.com/pelletier/go-toml" - "github.com/spf13/hugo/hugofs" "github.com/spf13/hugo/source" "github.com/spf13/viper" "github.com/stretchr/testify/assert" @@ -677,31 +679,29 @@ func setupTestMenuState(t *testing.T) { } func setupMenuTests(t *testing.T, pageSources []source.ByteSource) *Site { - s := createTestSite(pageSources) setupTestMenuState(t) - testSiteSetup(s, t) - return s + fs := hugofs.NewMem() + + for _, src := range pageSources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + + } + + return buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) + } func createTestSite(pageSources []source.ByteSource) *Site { - hugofs.InitMemFs() return &Site{ - deps: newDeps(DepsCfg{}), Source: &source.InMemorySource{ByteSource: pageSources}, Language: helpers.NewDefaultLanguage(), } } -func testSiteSetup(s *Site, t *testing.T) { - if err := buildSiteSkipRender(s); err != nil { - t.Fatalf("Sites build failed: %s", err) - } -} - func tomlToMap(s string) (map[string]interface{}, error) { tree, err := toml.Load(s) diff --git a/hugolib/node_as_page_test.go b/hugolib/node_as_page_test.go index d661fe88..35588da4 100644 --- a/hugolib/node_as_page_test.go +++ b/hugolib/node_as_page_test.go @@ -18,8 +18,11 @@ import ( "path/filepath" "strings" "testing" + "time" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/hugofs" "github.com/spf13/viper" "github.com/stretchr/testify/require" ) @@ -56,24 +59,28 @@ func doTestNodeAsPage(t *testing.T, ugly, preserveTaxonomyNames bool) { viper.Set("uglyURLs", ugly) viper.Set("preserveTaxonomyNames", preserveTaxonomyNames) - writeLayoutsForNodeAsPageTests(t) - writeNodePagesForNodeAsPageTests("", t) - - writeRegularPagesForNodeAsPageTests(t) - viper.Set("paginate", 1) viper.Set("title", "Hugo Rocks") viper.Set("rssURI", "customrss.xml") - s := NewSiteDefaultLang() + depsCfg := newTestDepsConfig() - if err := buildAndRenderSite(s); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + viper.SetFs(depsCfg.Fs.Source) + + writeLayoutsForNodeAsPageTests(t, depsCfg.Fs) + writeNodePagesForNodeAsPageTests(t, depsCfg.Fs, "") + + writeRegularPagesForNodeAsPageTests(t, depsCfg.Fs) + + sites, err := NewHugoSitesFromConfiguration(depsCfg) + + require.NoError(t, err) + + require.NoError(t, sites.Build(BuildCfg{})) // date order: home, sect1, sect2, cat/hugo, cat/web, categories - assertFileContent(t, filepath.Join("public", "index.html"), false, + assertFileContent(t, depsCfg.Fs, filepath.Join("public", "index.html"), false, "Index Title: Home Sweet Home!", "Home Content!", "# Pages: 4", @@ -82,10 +89,9 @@ func doTestNodeAsPage(t *testing.T, ugly, preserveTaxonomyNames bool) { "GetPage: Section1 ", ) - assertFileContent(t, expectedFilePath(ugly, "public", "sect1", "regular1"), false, "Single Title: Page 01", "Content Page 01") + assertFileContent(t, depsCfg.Fs, expectedFilePath(ugly, "public", "sect1", "regular1"), false, "Single Title: Page 01", "Content Page 01") - h := s.owner - nodes := h.findAllPagesByKindNotIn(KindPage) + nodes := sites.findAllPagesByKindNotIn(KindPage) require.Len(t, nodes, 7) @@ -99,7 +105,7 @@ func doTestNodeAsPage(t *testing.T, ugly, preserveTaxonomyNames bool) { section2 := nodes[4] require.Equal(t, "Section2", section2.Title) - pages := h.findAllPagesByKind(KindPage) + pages := sites.findAllPagesByKind(KindPage) require.Len(t, pages, 4) first := pages[0] @@ -109,46 +115,48 @@ func doTestNodeAsPage(t *testing.T, ugly, preserveTaxonomyNames bool) { require.True(t, first.IsPage()) // Check Home paginator - assertFileContent(t, expectedFilePath(ugly, "public", "page", "2"), false, + assertFileContent(t, depsCfg.Fs, expectedFilePath(ugly, "public", "page", "2"), false, "Pag: Page 02") // Check Sections - assertFileContent(t, expectedFilePath(ugly, "public", "sect1"), false, + assertFileContent(t, depsCfg.Fs, expectedFilePath(ugly, "public", "sect1"), false, "Section Title: Section", "Section1 Content!", "Date: 2009-01-04", "Lastmod: 2009-01-05", ) - assertFileContent(t, expectedFilePath(ugly, "public", "sect2"), false, + assertFileContent(t, depsCfg.Fs, expectedFilePath(ugly, "public", "sect2"), false, "Section Title: Section", "Section2 Content!", "Date: 2009-01-06", "Lastmod: 2009-01-07", ) // Check Sections paginator - assertFileContent(t, expectedFilePath(ugly, "public", "sect1", "page", "2"), false, + assertFileContent(t, depsCfg.Fs, expectedFilePath(ugly, "public", "sect1", "page", "2"), false, "Pag: Page 02") - sections := h.findAllPagesByKind(KindSection) + sections := sites.findAllPagesByKind(KindSection) require.Len(t, sections, 2) // Check taxonomy lists - assertFileContent(t, expectedFilePath(ugly, "public", "categories", "hugo"), false, + assertFileContent(t, depsCfg.Fs, expectedFilePath(ugly, "public", "categories", "hugo"), false, "Taxonomy Title: Taxonomy Hugo", "Taxonomy Hugo Content!", "Date: 2009-01-08", "Lastmod: 2009-01-09", ) - assertFileContent(t, expectedFilePath(ugly, "public", "categories", "hugo-rocks"), false, + assertFileContent(t, depsCfg.Fs, expectedFilePath(ugly, "public", "categories", "hugo-rocks"), false, "Taxonomy Title: Taxonomy Hugo Rocks", ) + s := sites.Sites[0] + web := s.getPage(KindTaxonomy, "categories", "web") require.NotNil(t, web) require.Len(t, web.Data["Pages"].(Pages), 4) - assertFileContent(t, expectedFilePath(ugly, "public", "categories", "web"), false, + assertFileContent(t, depsCfg.Fs, expectedFilePath(ugly, "public", "categories", "web"), false, "Taxonomy Title: Taxonomy Web", "Taxonomy Web Content!", "Date: 2009-01-10", @@ -156,12 +164,12 @@ func doTestNodeAsPage(t *testing.T, ugly, preserveTaxonomyNames bool) { ) // Check taxonomy list paginator - assertFileContent(t, expectedFilePath(ugly, "public", "categories", "hugo", "page", "2"), false, + assertFileContent(t, depsCfg.Fs, expectedFilePath(ugly, "public", "categories", "hugo", "page", "2"), false, "Taxonomy Title: Taxonomy Hugo", "Pag: Page 02") // Check taxonomy terms - assertFileContent(t, expectedFilePath(ugly, "public", "categories"), false, + assertFileContent(t, depsCfg.Fs, expectedFilePath(ugly, "public", "categories"), false, "Taxonomy Terms Title: Taxonomy Term Categories", "Taxonomy Term Categories Content!", "k/v: hugo", "Date: 2009-01-14", "Lastmod: 2009-01-15", @@ -170,11 +178,11 @@ func doTestNodeAsPage(t *testing.T, ugly, preserveTaxonomyNames bool) { // There are no pages to paginate over in the taxonomy terms. // RSS - assertFileContent(t, filepath.Join("public", "customrss.xml"), false, "Recent content in Home Sweet Home! on Hugo Rocks", "Content!") - assertFileContent(t, filepath.Join("public", "de", "index.html"), true, + assertFileContent(t, fs, filepath.Join("public", "de", "index.html"), true, "Index Title: Home Sweet Home!", "Content!") // Taxonomy list - assertFileContent(t, expectedFilePath(ugly, "public", "nn", "categories", "hugo"), true, + assertFileContent(t, fs, expectedFilePath(ugly, "public", "nn", "categories", "hugo"), true, "Taxonomy Title: Hugo") - assertFileContent(t, expectedFilePath(ugly, "public", "en", "categories", "hugo"), true, + assertFileContent(t, fs, expectedFilePath(ugly, "public", "en", "categories", "hugo"), true, "Taxonomy Title: Taxonomy Hugo") // Taxonomy terms - assertFileContent(t, expectedFilePath(ugly, "public", "nn", "categories"), true, + assertFileContent(t, fs, expectedFilePath(ugly, "public", "nn", "categories"), true, "Taxonomy Terms Title: Categories") - assertFileContent(t, expectedFilePath(ugly, "public", "en", "categories"), true, + assertFileContent(t, fs, expectedFilePath(ugly, "public", "en", "categories"), true, "Taxonomy Terms Title: Taxonomy Term Categories") // Sections - assertFileContent(t, expectedFilePath(ugly, "public", "nn", "sect1"), true, + assertFileContent(t, fs, expectedFilePath(ugly, "public", "nn", "sect1"), true, "Section Title: Sect1s") - assertFileContent(t, expectedFilePath(ugly, "public", "nn", "sect2"), true, + assertFileContent(t, fs, expectedFilePath(ugly, "public", "nn", "sect2"), true, "Section Title: Sect2s") - assertFileContent(t, expectedFilePath(ugly, "public", "en", "sect1"), true, + assertFileContent(t, fs, expectedFilePath(ugly, "public", "en", "sect1"), true, "Section Title: Section1") - assertFileContent(t, expectedFilePath(ugly, "public", "en", "sect2"), true, + assertFileContent(t, fs, expectedFilePath(ugly, "public", "en", "sect2"), true, "Section Title: Section2") // Regular pages - assertFileContent(t, expectedFilePath(ugly, "public", "en", "sect1", "regular1"), true, + assertFileContent(t, fs, expectedFilePath(ugly, "public", "en", "sect1", "regular1"), true, "Single Title: Page 01") - assertFileContent(t, expectedFilePath(ugly, "public", "nn", "sect1", "regular2"), true, + assertFileContent(t, fs, expectedFilePath(ugly, "public", "nn", "sect1", "regular2"), true, "Single Title: Page 02") // RSS - assertFileContent(t, filepath.Join("public", "nn", "customrss.xml"), true, "Hugo på norsk", " 0 { - permalink = p.Site.pathSpec.URLPrep(path.Join(dir, p.Slug+"."+p.Extension())) + permalink = p.s.PathSpec.URLPrep(path.Join(dir, p.Slug+"."+p.Extension())) } else { t := p.Source.TranslationBaseName() - permalink = p.Site.pathSpec.URLPrep(path.Join(dir, (strings.TrimSpace(t) + "." + p.Extension()))) + permalink = p.s.PathSpec.URLPrep(path.Join(dir, (strings.TrimSpace(t) + "." + p.Extension()))) } } @@ -953,22 +952,22 @@ func (p *Page) update(f interface{}) error { case "date": p.Date, err = cast.ToTimeE(v) if err != nil { - p.s.log.ERROR.Printf("Failed to parse date '%v' in page %s", v, p.File.Path()) + p.s.Log.ERROR.Printf("Failed to parse date '%v' in page %s", v, p.File.Path()) } case "lastmod": p.Lastmod, err = cast.ToTimeE(v) if err != nil { - p.s.log.ERROR.Printf("Failed to parse lastmod '%v' in page %s", v, p.File.Path()) + p.s.Log.ERROR.Printf("Failed to parse lastmod '%v' in page %s", v, p.File.Path()) } case "publishdate", "pubdate": p.PublishDate, err = cast.ToTimeE(v) if err != nil { - p.s.log.ERROR.Printf("Failed to parse publishdate '%v' in page %s", v, p.File.Path()) + p.s.Log.ERROR.Printf("Failed to parse publishdate '%v' in page %s", v, p.File.Path()) } case "expirydate", "unpublishdate": p.ExpiryDate, err = cast.ToTimeE(v) if err != nil { - p.s.log.ERROR.Printf("Failed to parse expirydate '%v' in page %s", v, p.File.Path()) + p.s.Log.ERROR.Printf("Failed to parse expirydate '%v' in page %s", v, p.File.Path()) } case "draft": draft = new(bool) @@ -1040,7 +1039,7 @@ func (p *Page) update(f interface{}) error { if draft != nil && published != nil { p.Draft = *draft - p.s.log.ERROR.Printf("page %s has both draft and published settings in its frontmatter. Using draft.", p.File.Path()) + p.s.Log.ERROR.Printf("page %s has both draft and published settings in its frontmatter. Using draft.", p.File.Path()) return ErrHasDraftAndPublished } else if draft != nil { p.Draft = *draft @@ -1049,7 +1048,7 @@ func (p *Page) update(f interface{}) error { } if p.Date.IsZero() && viper.GetBool("useModTimeAsFallback") { - fi, err := hugofs.Source().Stat(filepath.Join(helpers.AbsPathify(viper.GetString("contentDir")), p.File.Path())) + fi, err := p.s.Fs.Source.Stat(filepath.Join(helpers.AbsPathify(viper.GetString("contentDir")), p.File.Path())) if err == nil { p.Date = fi.ModTime() } @@ -1109,7 +1108,7 @@ func (p *Page) getParam(key string, stringToLower bool) interface{} { return v } - p.s.log.ERROR.Printf("GetParam(\"%s\"): Unknown type %s\n", key, reflect.TypeOf(v)) + p.s.Log.ERROR.Printf("GetParam(\"%s\"): Unknown type %s\n", key, reflect.TypeOf(v)) return nil } @@ -1251,16 +1250,16 @@ func (p *Page) Menus() PageMenus { menus, err := cast.ToStringMapE(ms) if err != nil { - p.s.log.ERROR.Printf("unable to process menus for %q\n", p.Title) + p.s.Log.ERROR.Printf("unable to process menus for %q\n", p.Title) } for name, menu := range menus { menuEntry := MenuEntry{Name: p.LinkTitle(), URL: link, Weight: p.Weight, Menu: name} if menu != nil { - p.s.log.DEBUG.Printf("found menu: %q, in %q\n", name, p.Title) + p.s.Log.DEBUG.Printf("found menu: %q, in %q\n", name, p.Title) ime, err := cast.ToStringMapE(menu) if err != nil { - p.s.log.ERROR.Printf("unable to process menus for %q: %s", p.Title, err) + p.s.Log.ERROR.Printf("unable to process menus for %q: %s", p.Title, err) } menuEntry.marshallMap(ime) @@ -1283,7 +1282,7 @@ func (p *Page) Render(layout ...string) template.HTML { l = p.layouts() } - return p.s.tmpl.ExecuteTemplateToHTML(p, l...) + return p.s.Tmpl.ExecuteTemplateToHTML(p, l...) } func (p *Page) determineMarkupType() string { @@ -1311,8 +1310,8 @@ func (p *Page) parse(reader io.Reader) error { meta, err := psr.Metadata() if meta != nil { if err != nil { - p.s.log.ERROR.Printf("Error parsing page meta data for %s", p.File.Path()) - p.s.log.ERROR.Println(err) + p.s.Log.ERROR.Printf("Error parsing page meta data for %s", p.File.Path()) + p.s.Log.ERROR.Println(err) return err } if err = p.update(meta); err != nil { @@ -1381,12 +1380,12 @@ func (p *Page) saveSource(by []byte, inpath string, safe bool) (err error) { if !filepath.IsAbs(inpath) { inpath = helpers.AbsPathify(inpath) } - p.s.log.INFO.Println("creating", inpath) + p.s.Log.INFO.Println("creating", inpath) if safe { - err = helpers.SafeWriteToDisk(inpath, bytes.NewReader(by), hugofs.Source()) + err = helpers.SafeWriteToDisk(inpath, bytes.NewReader(by), p.s.Fs.Source) } else { - err = helpers.WriteToDisk(inpath, bytes.NewReader(by), hugofs.Source()) + err = helpers.WriteToDisk(inpath, bytes.NewReader(by), p.s.Fs.Source) } if err != nil { return @@ -1455,7 +1454,7 @@ func (p *Page) TargetPath() (outfile string) { } return p.addLangFilepathPrefix(filepath.Join(strings.ToLower( - p.Site.pathSpec.MakePath(p.Source.Dir())), strings.TrimSpace(outfile))) + p.s.PathSpec.MakePath(p.Source.Dir())), strings.TrimSpace(outfile))) } // Pre render prepare steps @@ -1466,14 +1465,13 @@ func (p *Page) prepareLayouts() error { var layouts []string if !p.IsRenderable() { self := "__" + p.TargetPath() - _, err := p.Site.owner.tmpl.GetClone().New(self).Parse(string(p.Content)) + _, err := p.Site.owner.Tmpl.GetClone().New(self).Parse(string(p.Content)) if err != nil { return err } layouts = append(layouts, self) } else { layouts = append(layouts, p.layouts()...) - layouts = append(layouts, "_default/single.html") } p.layoutsCalculated = layouts } @@ -1707,7 +1705,7 @@ func (p *Page) initLanguage() { if language == nil { // It can be a file named stefano.chiodino.md. - p.s.log.WARN.Printf("Page language (if it is that) not found in multilang setup: %s.", pageLang) + p.s.Log.WARN.Printf("Page language (if it is that) not found in multilang setup: %s.", pageLang) language = ml.DefaultLang } diff --git a/hugolib/page_permalink_test.go b/hugolib/page_permalink_test.go index 007f9eef..5ed8a8b4 100644 --- a/hugolib/page_permalink_test.go +++ b/hugolib/page_permalink_test.go @@ -23,7 +23,8 @@ import ( "github.com/spf13/viper" ) -func TestPermalink(t *testing.T) { +// TODO(bep) globals test siteinfo +func _TestPermalink(t *testing.T) { testCommonResetState() tests := []struct { diff --git a/hugolib/page_test.go b/hugolib/page_test.go index d5258de6..10f2ed61 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -26,7 +26,9 @@ import ( "time" "github.com/spf13/cast" + "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" + "github.com/spf13/hugo/hugofs" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -465,7 +467,12 @@ activity = "exam" Hi. ` -var pageTestSite = NewSiteDefaultLang() +func init() { + testCommonResetState() + pageTestSite, _ = NewSiteDefaultLang() +} + +var pageTestSite *Site func checkError(t *testing.T, err error, expected string) { if err == nil { @@ -606,6 +613,8 @@ func testAllMarkdownEnginesForPages(t *testing.T, testCommonResetState() + fs := hugofs.NewMem() + if settings != nil { for k, v := range settings { viper.Set(k, v) @@ -625,14 +634,10 @@ func testAllMarkdownEnginesForPages(t *testing.T, } for i := 0; i < len(fileSourcePairs); i += 2 { - writeSource(t, filepath.Join(contentDir, fileSourcePairs[i]), fileSourcePairs[i+1]) + writeSource(t, fs, filepath.Join(contentDir, fileSourcePairs[i]), fileSourcePairs[i+1]) } - s := NewSiteDefaultLang() - - if err := buildSiteSkipRender(s); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + s := buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) require.Len(t, s.RegularPages, len(pageSources)) @@ -738,11 +743,14 @@ func TestPageWithDelimiter(t *testing.T) { // Issue #1076 func TestPageWithDelimiterForMarkdownThatCrossesBorder(t *testing.T) { - s := newSiteFromSources("simple.md", simplePageWithSummaryDelimiterAndMarkdownThatCrossesBorder) - if err := buildSiteSkipRender(s); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + testCommonResetState() + + fs := hugofs.NewMem() + + writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageWithSummaryDelimiterAndMarkdownThatCrossesBorder) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) require.Len(t, s.RegularPages, 1) @@ -759,16 +767,18 @@ func TestPageWithDelimiterForMarkdownThatCrossesBorder(t *testing.T) { // Issue #2601 func TestPageRawContent(t *testing.T) { - s := newSiteFromSources("raw.md", `--- + testCommonResetState() + + fs := hugofs.NewMem() + + writeSource(t, fs, filepath.Join("content", "raw.md"), `--- title: Raw --- **Raw**`) - writeSource(t, filepath.Join("layouts", "_default", "single.html"), `{{ .RawContent }}`) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .RawContent }}`) - if err := buildSiteSkipRender(s); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + s := buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) require.Len(t, s.RegularPages, 1) p := s.RegularPages[0] @@ -806,11 +816,12 @@ func TestPageWithEmbeddedScriptTag(t *testing.T) { } func TestPageWithAdditionalExtension(t *testing.T) { - s := newSiteFromSources("simple.md", simplePageWithAdditionalExtension) - if err := buildSiteSkipRender(s); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + fs := hugofs.NewMem() + + writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageWithAdditionalExtension) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{SkipRender: true}) require.Len(t, s.RegularPages, 1) @@ -820,11 +831,12 @@ func TestPageWithAdditionalExtension(t *testing.T) { } func TestTableOfContents(t *testing.T) { - s := newSiteFromSources("tocpage.md", pageWithToC) - if err := buildSiteSkipRender(s); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + fs := hugofs.NewMem() + + writeSource(t, fs, filepath.Join("content", "tocpage.md"), pageWithToC) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{SkipRender: true}) require.Len(t, s.RegularPages, 1) @@ -850,11 +862,11 @@ func TestPageWithMoreTag(t *testing.T) { } func TestPageWithDate(t *testing.T) { - s := newSiteFromSources("simple.md", simplePageRFC3339Date) + fs := hugofs.NewMem() - if err := buildSiteSkipRender(s); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageRFC3339Date) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{SkipRender: true}) require.Len(t, s.RegularPages, 1) @@ -1372,11 +1384,11 @@ func TestKind(t *testing.T) { func TestChompBOM(t *testing.T) { const utf8BOM = "\xef\xbb\xbf" - s := newSiteFromSources("simple.md", utf8BOM+simplePage) + fs := hugofs.NewMem() - if err := buildSiteSkipRender(s); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + writeSource(t, fs, filepath.Join("content", "simple.md"), utf8BOM+simplePage) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{SkipRender: true}) require.Len(t, s.RegularPages, 1) diff --git a/hugolib/pagination.go b/hugolib/pagination.go index 14a08113..acfa2b75 100644 --- a/hugolib/pagination.go +++ b/hugolib/pagination.go @@ -279,7 +279,7 @@ func (p *Page) Paginator(options ...interface{}) (*Pager, error) { return } - pagers, err := paginatePages(p.Data["Pages"], pagerSize, p.sections...) + pagers, err := paginatePages(p.s.PathSpec, p.Data["Pages"], pagerSize, p.sections...) if err != nil { initError = err @@ -322,7 +322,7 @@ func (p *Page) Paginate(seq interface{}, options ...interface{}) (*Pager, error) if p.paginator != nil { return } - pagers, err := paginatePages(seq, pagerSize, p.sections...) + pagers, err := paginatePages(p.s.PathSpec, seq, pagerSize, p.sections...) if err != nil { initError = err @@ -371,13 +371,13 @@ func resolvePagerSize(options ...interface{}) (int, error) { return pas, nil } -func paginatePages(seq interface{}, pagerSize int, sections ...string) (pagers, error) { +func paginatePages(pathSpec *helpers.PathSpec, seq interface{}, pagerSize int, sections ...string) (pagers, error) { if pagerSize <= 0 { return nil, errors.New("'paginate' configuration setting must be positive to paginate") } - urlFactory := newPaginationURLFactory(sections...) + urlFactory := newPaginationURLFactory(pathSpec, sections...) var paginator *paginator @@ -504,8 +504,7 @@ func newPaginator(elements []paginatedElement, total, size int, urlFactory pagin return p, nil } -func newPaginationURLFactory(pathElements ...string) paginationURLFactory { - pathSpec := helpers.CurrentPathSpec() +func newPaginationURLFactory(pathSpec *helpers.PathSpec, pathElements ...string) paginationURLFactory { basePath := path.Join(pathElements...) diff --git a/hugolib/pagination_test.go b/hugolib/pagination_test.go index 9bc8ffea..a7f2d939 100644 --- a/hugolib/pagination_test.go +++ b/hugolib/pagination_test.go @@ -19,10 +19,13 @@ import ( "path/filepath" "testing" + "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" + "github.com/spf13/hugo/hugofs" "github.com/spf13/hugo/source" "github.com/spf13/viper" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSplitPages(t *testing.T) { @@ -197,11 +200,22 @@ func TestPaginationURLFactory(t *testing.T) { testCommonResetState() viper.Set("paginatePath", "zoo") - unicode := newPaginationURLFactory("новости проекта") - fooBar := newPaginationURLFactory("foo", "bar") + + pathSpec := newTestPathSpec() + + unicode := newPaginationURLFactory(pathSpec, "новости проекта") + fooBar := newPaginationURLFactory(pathSpec, "foo", "bar") assert.Equal(t, "/foo/bar/", fooBar(1)) assert.Equal(t, "/%D0%BD%D0%BE%D0%B2%D0%BE%D1%81%D1%82%D0%B8-%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B0/zoo/4/", unicode(4)) + + unicoded := unicode(4) + unicodedExpected := "/%D0%BD%D0%BE%D0%B2%D0%BE%D1%81%D1%82%D0%B8-%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B0/zoo/4/" + + if unicoded != unicodedExpected { + t.Fatal("Expected\n", unicodedExpected, "\nGot\n", unicoded) + } + assert.Equal(t, "/foo/bar/zoo/12345/", fooBar(12345)) } @@ -224,13 +238,13 @@ func doTestPaginator(t *testing.T, useViper bool) { viper.Set("paginate", -1) } pages := createTestPages(12) - s := NewSiteDefaultLang() + s, err := NewSiteDefaultLang() + require.NoError(t, err) n1 := s.newHomePage() n2 := s.newHomePage() n1.Data["Pages"] = pages var paginator1 *Pager - var err error if useViper { paginator1, err = n1.Paginator() @@ -261,9 +275,10 @@ func TestPaginatorWithNegativePaginate(t *testing.T) { testCommonResetState() viper.Set("paginate", -1) - s := NewSiteDefaultLang() - _, err := s.newHomePage().Paginator() - assert.NotNil(t, err) + s, err := NewSiteDefaultLang() + require.NoError(t, err) + _, err = s.newHomePage().Paginator() + require.Error(t, err) } func TestPaginate(t *testing.T) { @@ -280,9 +295,11 @@ func TestPaginatorURL(t *testing.T) { viper.Set("paginate", 2) viper.Set("paginatePath", "testing") + fs := hugofs.NewMem() + for i := 0; i < 10; i++ { // Issue #2177, do not double encode URLs - writeSource(t, filepath.Join("content", "阅读", fmt.Sprintf("page%d.md", (i+1))), + writeSource(t, fs, filepath.Join("content", "阅读", fmt.Sprintf("page%d.md", (i+1))), fmt.Sprintf(`--- title: Page%d --- @@ -290,8 +307,8 @@ Conten%d `, (i+1), i+1)) } - writeSource(t, filepath.Join("layouts", "_default", "single.html"), "{{.Content}}") - writeSource(t, filepath.Join("layouts", "_default", "list.html"), + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), "{{.Content}}") + writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), ` Count: {{ .Paginator.TotalNumberOfElements }} @@ -301,11 +318,9 @@ Pages: {{ .Paginator.TotalPages }} {{ end }} `) - if err := buildAndRenderSite(NewSiteDefaultLang()); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) - assertFileContent(t, filepath.Join("public", "阅读", "testing", "2", "index.html"), false, "2: /%E9%98%85%E8%AF%BB/testing/2/") + assertFileContent(t, fs, filepath.Join("public", "阅读", "testing", "2", "index.html"), false, "2: /%E9%98%85%E8%AF%BB/testing/2/") } @@ -318,12 +333,12 @@ func doTestPaginate(t *testing.T, useViper bool) { } pages := createTestPages(6) - s := NewSiteDefaultLang() + s, err := NewSiteDefaultLang() + require.NoError(t, err) n1 := s.newHomePage() n2 := s.newHomePage() var paginator1, paginator2 *Pager - var err error if useViper { paginator1, err = n1.Paginate(pages) @@ -351,9 +366,10 @@ func doTestPaginate(t *testing.T, useViper bool) { } func TestInvalidOptions(t *testing.T) { - s := NewSiteDefaultLang() + s, err := NewSiteDefaultLang() + require.NoError(t, err) n1 := s.newHomePage() - _, err := n1.Paginate(createTestPages(1), 1, 2) + _, err = n1.Paginate(createTestPages(1), 1, 2) assert.NotNil(t, err) _, err = n1.Paginator(1, 2) assert.NotNil(t, err) @@ -365,19 +381,22 @@ func TestPaginateWithNegativePaginate(t *testing.T) { testCommonResetState() viper.Set("paginate", -1) - s := NewSiteDefaultLang() - _, err := s.newHomePage().Paginate(createTestPages(2)) + s, err := NewSiteDefaultLang() + require.NoError(t, err) + _, err = s.newHomePage().Paginate(createTestPages(2)) assert.NotNil(t, err) } func TestPaginatePages(t *testing.T) { groups, _ := createTestPages(31).GroupBy("Weight", "desc") + pathSpec := newTestPathSpec() + for i, seq := range []interface{}{createTestPages(11), groups, WeightedPages{}, PageGroup{}, &Pages{}} { - v, err := paginatePages(seq, 11, "t") + v, err := paginatePages(pathSpec, seq, 11, "t") assert.NotNil(t, v, "Val %d", i) assert.Nil(t, err, "Err %d", i) } - _, err := paginatePages(Site{}, 11, "t") + _, err := paginatePages(pathSpec, Site{}, 11, "t") assert.NotNil(t, err) } @@ -387,11 +406,12 @@ func TestPaginatorFollowedByPaginateShouldFail(t *testing.T) { testCommonResetState() viper.Set("paginate", 10) - s := NewSiteDefaultLang() + s, err := NewSiteDefaultLang() + require.NoError(t, err) n1 := s.newHomePage() n2 := s.newHomePage() - _, err := n1.Paginator() + _, err = n1.Paginator() assert.Nil(t, err) _, err = n1.Paginate(createTestPages(2)) assert.NotNil(t, err) @@ -405,14 +425,15 @@ func TestPaginateFollowedByDifferentPaginateShouldFail(t *testing.T) { testCommonResetState() viper.Set("paginate", 10) - s := NewSiteDefaultLang() + s, err := NewSiteDefaultLang() + require.NoError(t, err) n1 := s.newHomePage() n2 := s.newHomePage() p1 := createTestPages(2) p2 := createTestPages(10) - _, err := n1.Paginate(p1) + _, err = n1.Paginate(p1) assert.Nil(t, err) _, err = n1.Paginate(p1) diff --git a/hugolib/permalinks.go b/hugolib/permalinks.go index b924673f..95938641 100644 --- a/hugolib/permalinks.go +++ b/hugolib/permalinks.go @@ -150,14 +150,14 @@ func pageToPermalinkDate(p *Page, dateField string) (string, error) { func pageToPermalinkTitle(p *Page, _ string) (string, error) { // Page contains Node which has Title // (also contains URLPath which has Slug, sometimes) - return p.Site.pathSpec.URLize(p.Title), nil + return p.s.PathSpec.URLize(p.Title), nil } // pageToPermalinkFilename returns the URL-safe form of the filename func pageToPermalinkFilename(p *Page, _ string) (string, error) { //var extension = p.Source.Ext //var name = p.Source.Path()[0 : len(p.Source.Path())-len(extension)] - return p.Site.pathSpec.URLize(p.Source.TranslationBaseName()), nil + return p.s.PathSpec.URLize(p.Source.TranslationBaseName()), nil } // if the page has a slug, return the slug, else return the title @@ -172,7 +172,7 @@ func pageToPermalinkSlugElseTitle(p *Page, a string) (string, error) { if strings.HasSuffix(p.Slug, "-") { p.Slug = p.Slug[0 : len(p.Slug)-1] } - return p.Site.pathSpec.URLize(p.Slug), nil + return p.s.PathSpec.URLize(p.Slug), nil } return pageToPermalinkTitle(p, a) } diff --git a/hugolib/robotstxt_test.go b/hugolib/robotstxt_test.go index 2faabda7..1b42011c 100644 --- a/hugolib/robotstxt_test.go +++ b/hugolib/robotstxt_test.go @@ -14,12 +14,12 @@ package hugolib import ( - "bytes" + "path/filepath" "testing" - "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/hugofs" - "github.com/spf13/hugo/source" + + "github.com/spf13/hugo/deps" "github.com/spf13/viper" ) @@ -32,28 +32,16 @@ const robotTxtTemplate = `User-agent: Googlebot func TestRobotsTXTOutput(t *testing.T) { testCommonResetState() - hugofs.InitMemFs() - viper.Set("baseURL", "http://auth/bub/") viper.Set("enableRobotsTXT", true) - s := &Site{ - Source: &source.InMemorySource{ByteSource: weightedSources}, - Language: helpers.NewDefaultLanguage(), - } + fs := hugofs.NewMem() - if err := buildAndRenderSite(s, "robots.txt", robotTxtTemplate); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + writeSource(t, fs, filepath.Join("layouts", "robots.txt"), robotTxtTemplate) + writeSourcesToSource(t, "content", fs, weightedSources...) - robotsFile, err := hugofs.Destination().Open("public/robots.txt") + buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) - if err != nil { - t.Fatalf("Unable to locate: robots.txt") - } + assertFileContent(t, fs, "public/robots.txt", true, "User-agent: Googlebot") - robots := helpers.ReaderToBytes(robotsFile) - if !bytes.HasPrefix(robots, []byte("User-agent: Googlebot")) { - t.Errorf("Robots file should start with 'User-agent: Googlebot'. %s", robots) - } } diff --git a/hugolib/rss_test.go b/hugolib/rss_test.go index 72a180fe..74a59be6 100644 --- a/hugolib/rss_test.go +++ b/hugolib/rss_test.go @@ -17,6 +17,8 @@ import ( "path/filepath" "testing" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/hugofs" "github.com/spf13/viper" ) @@ -28,19 +30,19 @@ func TestRSSOutput(t *testing.T) { viper.Set("rssURI", rssURI) viper.Set("title", "RSSTest") - for _, s := range weightedSources { - writeSource(t, filepath.Join("content", "sect", s.Name), string(s.Content)) + fs := hugofs.NewMem() + + for _, src := range weightedSources { + writeSource(t, fs, filepath.Join("content", "sect", src.Name), string(src.Content)) } - if err := buildAndRenderSite(NewSiteDefaultLang()); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) // Home RSS - assertFileContent(t, filepath.Join("public", rssURI), true, "(.*)

\n\z` const innerCleanupExpand = "$1" func renderShortcode(sc shortcode, parent *ShortcodeWithPage, p *Page) string { - tmpl := getShortcodeTemplate(sc.name, p.s.tmpl) + tmpl := getShortcodeTemplate(sc.name, p.s.Tmpl) if tmpl == nil { - p.s.log.ERROR.Printf("Unable to locate template for shortcode '%s' in page %s", sc.name, p.BaseFileName()) + p.s.Log.ERROR.Printf("Unable to locate template for shortcode '%s' in page %s", sc.name, p.BaseFileName()) return "" } @@ -232,7 +232,7 @@ func renderShortcode(sc shortcode, parent *ShortcodeWithPage, p *Page) string { case shortcode: inner += renderShortcode(innerData.(shortcode), data, p) default: - p.s.log.ERROR.Printf("Illegal state on shortcode rendering of '%s' in page %s. Illegal type in inner data: %s ", + p.s.Log.ERROR.Printf("Illegal state on shortcode rendering of '%s' in page %s. Illegal type in inner data: %s ", sc.name, p.BaseFileName(), reflect.TypeOf(innerData)) return "" } @@ -286,7 +286,7 @@ func extractAndRenderShortcodes(stringToParse string, p *Page) (string, map[stri if err != nil { // try to render what we have whilst logging the error - p.s.log.ERROR.Println(err.Error()) + p.s.Log.ERROR.Println(err.Error()) } // Save for reuse @@ -398,7 +398,7 @@ Loop: sc.inner = append(sc.inner, currItem.val) case tScName: sc.name = currItem.val - tmpl := getShortcodeTemplate(sc.name, p.s.tmpl) + tmpl := getShortcodeTemplate(sc.name, p.s.Tmpl) if tmpl == nil { return sc, fmt.Errorf("Unable to locate template for shortcode '%s' in page %s", sc.name, p.BaseFileName()) @@ -566,7 +566,7 @@ func replaceShortcodeTokens(source []byte, prefix string, replacements map[strin return source, nil } -func getShortcodeTemplate(name string, t tpl.Template) *template.Template { +func getShortcodeTemplate(name string, t tplapi.Template) *template.Template { if x := t.Lookup("shortcodes/" + name + ".html"); x != nil { return x } @@ -584,9 +584,8 @@ func renderShortcodeWithPage(tmpl *template.Template, data *ShortcodeWithPage) s err := tmpl.Execute(buffer, data) isInnerShortcodeCache.RUnlock() if err != nil { - // TODO(bep) globals - data.Page.s.log.ERROR.Println("error processing shortcode", tmpl.Name(), "\n ERR:", err) - data.Page.s.log.WARN.Println(data) + data.Page.s.Log.ERROR.Println("error processing shortcode", tmpl.Name(), "\n ERR:", err) + data.Page.s.Log.WARN.Println(data) } return buffer.String() } diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index 24370534..d4494dba 100644 --- a/hugolib/shortcode_test.go +++ b/hugolib/shortcode_test.go @@ -22,49 +22,52 @@ import ( "strings" "testing" + "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/hugofs" "github.com/spf13/hugo/source" - "github.com/spf13/hugo/target" - "github.com/spf13/hugo/tpl" + "github.com/spf13/hugo/tplapi" "github.com/spf13/viper" "github.com/stretchr/testify/require" ) // TODO(bep) remove -func pageFromString(in, filename string, withTemplate ...func(templ tpl.Template) error) (*Page, error) { +func pageFromString(in, filename string, withTemplate ...func(templ tplapi.Template) error) (*Page, error) { s := pageTestSite if len(withTemplate) > 0 { // Have to create a new site - s = NewSiteDefaultLang(withTemplate...) + var err error + s, err = NewSiteDefaultLang(withTemplate...) + if err != nil { + return nil, err + } } return s.NewPageFrom(strings.NewReader(in), filename) } -func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tpl.Template) error) { +func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tplapi.Template) error) { CheckShortCodeMatchAndError(t, input, expected, withTemplate, false) } -func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.Template) error, expectError bool) { +func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tplapi.Template) error, expectError bool) { testCommonResetState() + fs := hugofs.NewMem() + // Need some front matter, see https://github.com/spf13/hugo/issues/2337 contentFile := `--- title: "Title" --- ` + input - writeSource(t, "content/simple.md", contentFile) + writeSource(t, fs, "content/simple.md", contentFile) - h, err := newHugoSitesDefaultLanguage() + h, err := NewHugoSitesFromConfiguration(deps.DepsCfg{Fs: fs, WithTemplate: withTemplate}) - if err != nil { - t.Fatalf("Failed to create sites: %s", err) - } + require.NoError(t, err) + require.Len(t, h.Sites, 1) - cfg := BuildCfg{SkipRender: true, withTemplate: withTemplate} - - err = h.Build(cfg) + err = h.Build(BuildCfg{}) if err != nil && !expectError { t.Fatalf("Shortcode rendered error %s.", err) @@ -89,7 +92,7 @@ title: "Title" func TestShortcodeGoFuzzReports(t *testing.T) { - p, _ := pageFromString(simplePage, "simple.md", func(templ tpl.Template) error { + p, _ := pageFromString(simplePage, "simple.md", func(templ tplapi.Template) error { return templ.AddInternalShortcode("sc.html", `foo`) }) @@ -124,7 +127,7 @@ func TestNonSC(t *testing.T) { // Issue #929 func TestHyphenatedSC(t *testing.T) { - wt := func(tem tpl.Template) error { + wt := func(tem tplapi.Template) error { tem.AddInternalShortcode("hyphenated-video.html", `Playing Video {{ .Get 0 }}`) return nil } @@ -134,7 +137,7 @@ func TestHyphenatedSC(t *testing.T) { // Issue #1753 func TestNoTrailingNewline(t *testing.T) { - wt := func(tem tpl.Template) error { + wt := func(tem tplapi.Template) error { tem.AddInternalShortcode("a.html", `{{ .Get 0 }}`) return nil } @@ -143,7 +146,7 @@ func TestNoTrailingNewline(t *testing.T) { } func TestPositionalParamSC(t *testing.T) { - wt := func(tem tpl.Template) error { + wt := func(tem tplapi.Template) error { tem.AddInternalShortcode("video.html", `Playing Video {{ .Get 0 }}`) return nil } @@ -156,7 +159,7 @@ func TestPositionalParamSC(t *testing.T) { } func TestPositionalParamIndexOutOfBounds(t *testing.T) { - wt := func(tem tpl.Template) error { + wt := func(tem tplapi.Template) error { tem.AddInternalShortcode("video.html", `Playing Video {{ .Get 1 }}`) return nil } @@ -166,7 +169,7 @@ func TestPositionalParamIndexOutOfBounds(t *testing.T) { // some repro issues for panics in Go Fuzz testing func TestNamedParamSC(t *testing.T) { - wt := func(tem tpl.Template) error { + wt := func(tem tplapi.Template) error { tem.AddInternalShortcode("img.html", ``) return nil } @@ -180,7 +183,7 @@ func TestNamedParamSC(t *testing.T) { // Issue #2294 func TestNestedNamedMissingParam(t *testing.T) { - wt := func(tem tpl.Template) error { + wt := func(tem tplapi.Template) error { tem.AddInternalShortcode("acc.html", `
{{ .Inner }}
`) tem.AddInternalShortcode("div.html", `
{{ .Inner }}
`) tem.AddInternalShortcode("div2.html", `
{{ .Inner }}
`) @@ -192,7 +195,7 @@ func TestNestedNamedMissingParam(t *testing.T) { } func TestIsNamedParamsSC(t *testing.T) { - wt := func(tem tpl.Template) error { + wt := func(tem tplapi.Template) error { tem.AddInternalShortcode("byposition.html", `
`) tem.AddInternalShortcode("byname.html", `
`) tem.AddInternalShortcode("ifnamedparams.html", `
`) @@ -207,7 +210,7 @@ func TestIsNamedParamsSC(t *testing.T) { } func TestInnerSC(t *testing.T) { - wt := func(tem tpl.Template) error { + wt := func(tem tplapi.Template) error { tem.AddInternalShortcode("inside.html", `{{ .Inner }}
`) return nil } @@ -217,7 +220,7 @@ func TestInnerSC(t *testing.T) { } func TestInnerSCWithMarkdown(t *testing.T) { - wt := func(tem tpl.Template) error { + wt := func(tem tplapi.Template) error { tem.AddInternalShortcode("inside.html", `{{ .Inner }}
`) return nil } @@ -230,7 +233,7 @@ func TestInnerSCWithMarkdown(t *testing.T) { } func TestInnerSCWithAndWithoutMarkdown(t *testing.T) { - wt := func(tem tpl.Template) error { + wt := func(tem tplapi.Template) error { tem.AddInternalShortcode("inside.html", `{{ .Inner }}
`) return nil } @@ -259,7 +262,7 @@ func TestEmbeddedSC(t *testing.T) { } func TestNestedSC(t *testing.T) { - wt := func(tem tpl.Template) error { + wt := func(tem tplapi.Template) error { tem.AddInternalShortcode("scn1.html", `
Outer, inner is {{ .Inner }}
`) tem.AddInternalShortcode("scn2.html", `
SC2
`) return nil @@ -270,7 +273,7 @@ func TestNestedSC(t *testing.T) { } func TestNestedComplexSC(t *testing.T) { - wt := func(tem tpl.Template) error { + wt := func(tem tplapi.Template) error { tem.AddInternalShortcode("row.html", `-row-{{ .Inner}}-rowStop-`) tem.AddInternalShortcode("column.html", `-col-{{.Inner }}-colStop-`) tem.AddInternalShortcode("aside.html", `-aside-{{ .Inner }}-asideStop-`) @@ -285,7 +288,7 @@ func TestNestedComplexSC(t *testing.T) { } func TestParentShortcode(t *testing.T) { - wt := func(tem tpl.Template) error { + wt := func(tem tplapi.Template) error { tem.AddInternalShortcode("r1.html", `1: {{ .Get "pr1" }} {{ .Inner }}`) tem.AddInternalShortcode("r2.html", `2: {{ .Parent.Get "pr1" }}{{ .Get "pr2" }} {{ .Inner }}`) tem.AddInternalShortcode("r3.html", `3: {{ .Parent.Parent.Get "pr1" }}{{ .Parent.Get "pr2" }}{{ .Get "pr3" }} {{ .Inner }}`) @@ -382,7 +385,7 @@ func TestExtractShortcodes(t *testing.T) { fmt.Sprintf("Hello %sworld%s. And that's it.", testScPlaceholderRegexp, testScPlaceholderRegexp), ""}, } { - p, _ := pageFromString(simplePage, "simple.md", func(templ tpl.Template) error { + p, _ := pageFromString(simplePage, "simple.md", func(templ tplapi.Template) error { templ.AddInternalShortcode("tag.html", `tag`) templ.AddInternalShortcode("sc1.html", `sc1`) templ.AddInternalShortcode("sc2.html", `sc2`) @@ -471,7 +474,7 @@ func TestShortcodesInSite(t *testing.T) { expected string }{ {"sect/doc1.md", `a{{< b >}}c`, - filepath.FromSlash("sect/doc1/index.html"), "

abc

\n"}, + filepath.FromSlash("public/sect/doc1/index.html"), "

abc

\n"}, // Issue #1642: Multiple shortcodes wrapped in P // Deliberately forced to pass even if they maybe shouldn't. {"sect/doc2.md", `a @@ -481,7 +484,7 @@ func TestShortcodesInSite(t *testing.T) { {{< d >}} e`, - filepath.FromSlash("sect/doc2/index.html"), + filepath.FromSlash("public/sect/doc2/index.html"), "

a

\n\n

b
\nc\nd

\n\n

e

\n"}, {"sect/doc3.md", `a @@ -491,7 +494,7 @@ e`, {{< d >}} e`, - filepath.FromSlash("sect/doc3/index.html"), + filepath.FromSlash("public/sect/doc3/index.html"), "

a

\n\n

b
\nc

\n\nd\n\n

e

\n"}, {"sect/doc4.md", `a {{< b >}} @@ -510,22 +513,22 @@ e`, `, - filepath.FromSlash("sect/doc4/index.html"), + filepath.FromSlash("public/sect/doc4/index.html"), "

a\nb\nb\nb\nb\nb

\n"}, // #2192 #2209: Shortcodes in markdown headers {"sect/doc5.md", `# {{< b >}} ## {{% c %}}`, - filepath.FromSlash("sect/doc5/index.html"), "\n\n

b

\n\n

c

\n"}, + filepath.FromSlash("public/sect/doc5/index.html"), "\n\n

b

\n\n

c

\n"}, // #2223 pygments {"sect/doc6.md", "\n```bash\nb: {{< b >}} c: {{% c %}}\n```\n", - filepath.FromSlash("sect/doc6/index.html"), + filepath.FromSlash("public/sect/doc6/index.html"), "b: b c: c\n\n"}, // #2249 {"sect/doc7.ad", `_Shortcodes:_ *b: {{< b >}} c: {{% c %}}*`, - filepath.FromSlash("sect/doc7/index.html"), + filepath.FromSlash("public/sect/doc7/index.html"), "
\n

Shortcodes: b: b c: c

\n
\n"}, {"sect/doc8.rst", `**Shortcodes:** *b: {{< b >}} c: {{% c %}}*`, - filepath.FromSlash("sect/doc8/index.html"), + filepath.FromSlash("public/sect/doc8/index.html"), "
\n\n\n

Shortcodes: b: b c: c

\n
"}, {"sect/doc9.mmark", ` --- @@ -534,7 +537,7 @@ menu: parent: 'parent' --- **Shortcodes:** *b: {{< b >}} c: {{% c %}}*`, - filepath.FromSlash("sect/doc9/index.html"), + filepath.FromSlash("public/sect/doc9/index.html"), "

Shortcodes: b: b c: c

\n"}, // Issue #1229: Menus not available in shortcode. {"sect/doc10.md", `--- @@ -545,7 +548,7 @@ tags: - Menu --- **Menus:** {{< menu >}}`, - filepath.FromSlash("sect/doc10/index.html"), + filepath.FromSlash("public/sect/doc10/index.html"), "

Menus: 1

\n"}, // Issue #2323: Taxonomies not available in shortcode. {"sect/doc11.md", `--- @@ -553,7 +556,7 @@ tags: - Bugs --- **Tags:** {{< tags >}}`, - filepath.FromSlash("sect/doc11/index.html"), + filepath.FromSlash("public/sect/doc11/index.html"), "

Tags: 2

\n"}, } @@ -563,13 +566,7 @@ tags: sources[i] = source.ByteSource{Name: filepath.FromSlash(test.contentPath), Content: []byte(test.content)} } - s := &Site{ - Source: &source.InMemorySource{ByteSource: sources}, - targets: targetList{page: &target.PagePub{UglyURLs: false}}, - Language: helpers.NewDefaultLanguage(), - } - - addTemplates := func(templ tpl.Template) error { + addTemplates := func(templ tplapi.Template) error { templ.AddTemplate("_default/single.html", "{{.Content}}") templ.AddInternalShortcode("b.html", `b`) @@ -582,15 +579,11 @@ tags: } - sites, err := newHugoSites(DepsCfg{}, s) + fs := hugofs.NewMem() - if err != nil { - t.Fatalf("Failed to build site: %s", err) - } + writeSourcesToSource(t, "content", fs, sources...) - if err = sites.Build(BuildCfg{withTemplate: addTemplates}); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + buildSingleSite(t, deps.DepsCfg{WithTemplate: addTemplates, Fs: fs}, BuildCfg{}) for _, test := range tests { if strings.HasSuffix(test.contentPath, ".ad") && !helpers.HasAsciidoc() { @@ -604,17 +597,7 @@ tags: continue } - file, err := hugofs.Destination().Open(test.outFile) - - if err != nil { - t.Fatalf("Did not find %s in target: %s", test.outFile, err) - } - - content := helpers.ReaderToString(file) - - if !strings.Contains(content, test.expected) { - t.Fatalf("%s content expected:\n%q\ngot:\n%q", test.outFile, test.expected, content) - } + assertFileContent(t, fs, test.outFile, true, test.expected) } } diff --git a/hugolib/site.go b/hugolib/site.go index c887a930..6afc18a6 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -35,12 +35,14 @@ import ( "github.com/spf13/afero" "github.com/spf13/cast" bp "github.com/spf13/hugo/bufferpool" + "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/hugofs" "github.com/spf13/hugo/parser" "github.com/spf13/hugo/source" "github.com/spf13/hugo/target" "github.com/spf13/hugo/tpl" + "github.com/spf13/hugo/tplapi" "github.com/spf13/hugo/transform" "github.com/spf13/nitro" "github.com/spf13/viper" @@ -106,57 +108,81 @@ type Site struct { Language *helpers.Language // Logger etc. - *deps + *deps.Deps `json:"-"` } // reset returns a new Site prepared for rebuild. func (s *Site) reset() *Site { - return &Site{deps: s.deps, Language: s.Language, owner: s.owner, PageCollections: newPageCollections()} + return &Site{Deps: s.Deps, owner: s.owner, PageCollections: newPageCollections()} } -// newSite creates a new site in the given language. -func newSite(lang *helpers.Language, deps *deps, withTemplate ...func(templ tpl.Template) error) *Site { +// newSite creates a new site with the given configuration. +func newSite(cfg deps.DepsCfg) (*Site, error) { c := newPageCollections() - // TODO(bep) globals - viper.Set("currentContentLanguage", lang) - if deps == nil { - depsCfg := DepsCfg{WithTemplate: withTemplate} - deps = newDeps(depsCfg) + if cfg.Language == nil { + cfg.Language = helpers.NewDefaultLanguage() } - return &Site{deps: deps, Language: lang, PageCollections: c, Info: newSiteInfo(siteBuilderCfg{pageCollections: c, language: lang})} + s := &Site{PageCollections: c, Language: cfg.Language} + s.Info = newSiteInfo(siteBuilderCfg{s: s, pageCollections: c, language: s.Language}) + return s, nil + +} + +// NewSite creates a new site with the given dependency configuration. +// The site will have a template system loaded and ready to use. +// Note: This is mainly used in single site tests. +func NewSite(cfg deps.DepsCfg) (*Site, error) { + s, err := newSite(cfg) + + if err != nil { + return nil, err + } + + if err := applyDepsIfNeeded(cfg, s); err != nil { + return nil, err + } + + return s, nil } // NewSiteDefaultLang creates a new site in the default language. -func NewSiteDefaultLang(withTemplate ...func(templ tpl.Template) error) *Site { - return newSite(helpers.NewDefaultLanguage(), nil, withTemplate...) +// The site will have a template system loaded and ready to use. +// Note: This is mainly used in single site tests. +func NewSiteDefaultLang(withTemplate ...func(templ tplapi.Template) error) (*Site, error) { + return newSiteForLang(helpers.NewDefaultLanguage(), withTemplate...) } -// Convenience func used in tests. -func newSiteFromSources(pathContentPairs ...string) *Site { - if len(pathContentPairs)%2 != 0 { - panic("pathContentPairs must come in pairs") +// NewSiteDefaultLang creates a new site in the default language. +// The site will have a template system loaded and ready to use. +// Note: This is mainly used in single site tests. +func NewEnglishSite(withTemplate ...func(templ tplapi.Template) error) (*Site, error) { + return newSiteForLang(helpers.NewLanguage("en"), withTemplate...) +} + +// NewSiteDefaultLang creates a new site in the default language. +func newSiteForLang(lang *helpers.Language, withTemplate ...func(templ tplapi.Template) error) (*Site, error) { + withTemplates := func(templ tplapi.Template) error { + for _, wt := range withTemplate { + if err := wt(templ); err != nil { + return err + } + } + return nil + } + cfg := deps.DepsCfg{WithTemplate: withTemplates, Language: lang} + s, err := newSite(cfg) + + if err != nil { + return nil, err } - sources := make([]source.ByteSource, 0) - - for i := 0; i < len(pathContentPairs); i += 2 { - path := pathContentPairs[i] - content := pathContentPairs[i+1] - sources = append(sources, source.ByteSource{Name: filepath.FromSlash(path), Content: []byte(content)}) - } - - lang := helpers.NewDefaultLanguage() - - return &Site{ - deps: newDeps(DepsCfg{}), - PageCollections: newPageCollections(), - Source: &source.InMemorySource{ByteSource: sources}, - Language: lang, - Info: newSiteInfo(siteBuilderCfg{language: lang}), + if err := applyDepsIfNeeded(cfg, s); err != nil { + return nil, err } + return s, nil } type targetList struct { @@ -202,14 +228,13 @@ type SiteInfo struct { Data *map[string]interface{} owner *HugoSites + s *Site multilingual *Multilingual Language *helpers.Language LanguagePrefix string Languages helpers.Languages defaultContentLanguageInSubdir bool sectionPagesMenu string - - pathSpec *helpers.PathSpec } func (s *SiteInfo) String() string { @@ -219,15 +244,19 @@ func (s *SiteInfo) String() string { // Used in tests. type siteBuilderCfg struct { - language *helpers.Language + language *helpers.Language + // TOD(bep) globals fs + s *Site + fs *hugofs.Fs pageCollections *PageCollections baseURL string } +// TODO(bep) globals get rid of this func newSiteInfo(cfg siteBuilderCfg) SiteInfo { return SiteInfo{ + s: cfg.s, BaseURL: template.URL(cfg.baseURL), - pathSpec: helpers.NewPathSpecFromConfig(cfg.language), multilingual: newMultiLingualForLanguage(cfg.language), PageCollections: cfg.pageCollections, } @@ -498,7 +527,7 @@ type whatChanged struct { // It returns whetever the content source was changed. func (s *Site) reProcess(events []fsnotify.Event) (whatChanged, error) { - s.log.DEBUG.Printf("Rebuild for events %q", events) + s.Log.DEBUG.Printf("Rebuild for events %q", events) s.timerStep("initialize rebuild") @@ -533,8 +562,25 @@ func (s *Site) reProcess(events []fsnotify.Event) (whatChanged, error) { } if len(tmplChanged) > 0 { - s.prepTemplates(nil) - s.owner.tmpl.PrintErrors() + sites := s.owner.Sites + first := sites[0] + + // TOD(bep) globals clean + if err := first.Deps.LoadTemplates(); err != nil { + s.Log.ERROR.Println(err) + } + + s.Tmpl.PrintErrors() + + for i := 1; i < len(sites); i++ { + site := sites[i] + var err error + site.Deps, err = first.Deps.ForLanguage(site.Language) + if err != nil { + return whatChanged{}, err + } + } + s.timerStep("template prep") } @@ -544,7 +590,7 @@ func (s *Site) reProcess(events []fsnotify.Event) (whatChanged, error) { if len(i18nChanged) > 0 { if err := s.readI18nSources(); err != nil { - s.log.ERROR.Println(err) + s.Log.ERROR.Println(err) } } @@ -595,7 +641,7 @@ func (s *Site) reProcess(events []fsnotify.Event) (whatChanged, error) { // it's been updated if ev.Op&fsnotify.Rename == fsnotify.Rename { // If the file is still on disk, it's only been updated, if it's not, it's been moved - if ex, err := afero.Exists(hugofs.Source(), ev.Name); !ex || err != nil { + if ex, err := afero.Exists(s.Fs.Source, ev.Name); !ex || err != nil { path, _ := helpers.GetRelativePath(ev.Name, s.getContentDir(ev.Name)) s.removePageByPath(path) continue @@ -613,7 +659,7 @@ func (s *Site) reProcess(events []fsnotify.Event) (whatChanged, error) { file, err := s.reReadFile(ev.Name) if err != nil { - s.log.ERROR.Println("Error reading file", ev.Name, ";", err) + s.Log.ERROR.Println("Error reading file", ev.Name, ";", err) } if file != nil { @@ -647,7 +693,7 @@ func (s *Site) reProcess(events []fsnotify.Event) (whatChanged, error) { for i := 0; i < 2; i++ { err := <-errs if err != nil { - s.log.ERROR.Println(err) + s.Log.ERROR.Println(err) } } @@ -660,29 +706,8 @@ func (s *Site) reProcess(events []fsnotify.Event) (whatChanged, error) { } -func (s *Site) prepTemplates(withTemplate func(templ tpl.Template) error) error { - - wt := func(tmpl tpl.Template) error { - // TODO(bep) global error handling - tmpl.LoadTemplates(s.absLayoutDir()) - if s.hasTheme() { - tmpl.LoadTemplatesWithPrefix(s.absThemeDir()+"/layouts", "theme") - } - if withTemplate != nil { - if err := withTemplate(tmpl); err != nil { - return err - } - } - return nil - } - - s.refreshTemplates(wt) - - return nil -} - func (s *Site) loadData(sources []source.Input) (err error) { - s.log.DEBUG.Printf("Load Data from %q", sources) + s.Log.DEBUG.Printf("Load Data from %q", sources) s.Data = make(map[string]interface{}) var current map[string]interface{} for _, currentSource := range sources { @@ -717,7 +742,7 @@ func (s *Site) loadData(sources []source.Input) (err error) { // this warning could happen if // 1. A theme uses the same key; the main data folder wins // 2. A sub folder uses the same key: the sub folder wins - s.log.WARN.Printf("Data for key '%s' in path '%s' is overridden in subfolder", key, r.Path()) + s.Log.WARN.Printf("Data for key '%s' in path '%s' is overridden in subfolder", key, r.Path()) } data[key] = value } @@ -740,21 +765,21 @@ func (s *Site) readData(f *source.File) (interface{}, error) { case "toml": return parser.HandleTOMLMetaData(f.Bytes()) default: - s.log.WARN.Printf("Data not supported for extension '%s'", f.Extension()) + s.Log.WARN.Printf("Data not supported for extension '%s'", f.Extension()) return nil, nil } } func (s *Site) readI18nSources() error { - i18nSources := []source.Input{&source.Filesystem{Base: s.absI18nDir()}} + i18nSources := []source.Input{source.NewFilesystem(s.Fs, s.absI18nDir())} - themeI18nDir, err := helpers.GetThemeI18nDirPath() + themeI18nDir, err := s.PathSpec.GetThemeI18nDirPath() if err == nil { - i18nSources = []source.Input{&source.Filesystem{Base: themeI18nDir}, i18nSources[0]} + i18nSources = []source.Input{source.NewFilesystem(s.Fs, themeI18nDir), i18nSources[0]} } - if err = loadI18n(i18nSources); err != nil { + if err = s.loadI18n(i18nSources); err != nil { return err } @@ -763,12 +788,12 @@ func (s *Site) readI18nSources() error { func (s *Site) readDataFromSourceFS() error { dataSources := make([]source.Input, 0, 2) - dataSources = append(dataSources, &source.Filesystem{Base: s.absDataDir()}) + dataSources = append(dataSources, source.NewFilesystem(s.Fs, s.absDataDir())) // have to be last - duplicate keys in earlier entries will win - themeDataDir, err := helpers.GetThemeDataDirPath() + themeDataDir, err := s.PathSpec.GetThemeDataDirPath() if err == nil { - dataSources = append(dataSources, &source.Filesystem{Base: themeDataDir}) + dataSources = append(dataSources, source.NewFilesystem(s.Fs, themeDataDir)) } err = s.loadData(dataSources) @@ -781,10 +806,7 @@ func (s *Site) process(config BuildCfg) (err error) { if err = s.initialize(); err != nil { return } - - s.prepTemplates(config.withTemplate) - s.owner.tmpl.PrintErrors() - s.timerStep("initialize & template prep") + s.timerStep("initialize") if err = s.readDataFromSourceFS(); err != nil { return @@ -817,7 +839,6 @@ func (s *Site) setCurrentLanguageConfig() error { viper.Set("currentContentLanguage", s.Language) // Cache the current config. helpers.InitConfigProviderForCurrentContentLanguage() - s.Info.pathSpec = helpers.CurrentPathSpec() return tpl.SetTranslateLang(s.Language) } @@ -873,7 +894,7 @@ func (s *Site) initialize() (err error) { // May be supplied in tests. if s.Source != nil && len(s.Source.Files()) > 0 { - s.log.DEBUG.Println("initialize: Source is already set") + s.Log.DEBUG.Println("initialize: Source is already set") return } @@ -883,10 +904,7 @@ func (s *Site) initialize() (err error) { staticDir := helpers.AbsPathify(viper.GetString("staticDir") + "/") - s.Source = &source.Filesystem{ - AvoidPaths: []string{staticDir}, - Base: s.absContentDir(), - } + s.Source = source.NewFilesystem(s.Fs, s.absContentDir(), staticDir) return } @@ -897,7 +915,7 @@ func (s *SiteInfo) HomeAbsURL() string { if s.IsMultiLingual() { base = s.Language.Lang } - return s.pathSpec.AbsURL(base, false) + return s.owner.AbsURL(base, false) } // SitemapAbsURL is a convenience method giving the absolute URL to the sitemap. @@ -966,7 +984,7 @@ func (s *Site) initializeSiteInfo() { Permalinks: permalinks, Data: &s.Data, owner: s.owner, - pathSpec: helpers.NewPathSpecFromConfig(lang), + s: s, } s.Info.RSSLink = s.Info.permalinkStr(lang.GetString("rssURI")) @@ -1081,11 +1099,11 @@ func (s *Site) getRealDir(base, path string) string { return base } - realDir, err := helpers.GetRealPath(hugofs.Source(), base) + realDir, err := helpers.GetRealPath(s.Fs.Source, base) if err != nil { if !os.IsNotExist(err) { - s.log.ERROR.Printf("Failed to get real path for %s: %s", path, err) + s.Log.ERROR.Printf("Failed to get real path for %s: %s", path, err) } return "" } @@ -1102,7 +1120,7 @@ func (s *Site) absPublishDir() string { } func (s *Site) checkDirectories() (err error) { - if b, _ := helpers.DirExists(s.absContentDir(), hugofs.Source()); !b { + if b, _ := helpers.DirExists(s.absContentDir(), s.Fs.Source); !b { return errors.New("No source directory found, expecting to find it at " + s.absContentDir()) } return @@ -1110,10 +1128,10 @@ func (s *Site) checkDirectories() (err error) { // reReadFile resets file to be read from disk again func (s *Site) reReadFile(absFilePath string) (*source.File, error) { - s.log.INFO.Println("rereading", absFilePath) + s.Log.INFO.Println("rereading", absFilePath) var file *source.File - reader, err := source.NewLazyFileReader(hugofs.Source(), absFilePath) + reader, err := source.NewLazyFileReader(s.Fs.Source, absFilePath) if err != nil { return nil, err } @@ -1131,7 +1149,7 @@ func (s *Site) readPagesFromSource() chan error { panic(fmt.Sprintf("s.Source not set %s", s.absContentDir())) } - s.log.DEBUG.Printf("Read %d pages from source", len(s.Source.Files())) + s.Log.DEBUG.Printf("Read %d pages from source", len(s.Source.Files())) errs := make(chan error) if len(s.Source.Files()) < 1 { @@ -1231,7 +1249,7 @@ func readSourceFile(s *Site, file *source.File, results chan<- HandledResult) { if h != nil { h.Read(file, s, results) } else { - s.log.ERROR.Println("Unsupported File Type", file.Path()) + s.Log.ERROR.Println("Unsupported File Type", file.Path()) } } @@ -1372,17 +1390,17 @@ func (s *Site) getMenusFromConfig() Menus { for name, menu := range menus { m, err := cast.ToSliceE(menu) if err != nil { - s.log.ERROR.Printf("unable to process menus in site config\n") - s.log.ERROR.Println(err) + s.Log.ERROR.Printf("unable to process menus in site config\n") + s.Log.ERROR.Println(err) } else { for _, entry := range m { - s.log.DEBUG.Printf("found menu: %q, in site config\n", name) + s.Log.DEBUG.Printf("found menu: %q, in site config\n", name) menuEntry := MenuEntry{Menu: name} ime, err := cast.ToStringMapE(entry) if err != nil { - s.log.ERROR.Printf("unable to process menus in site config\n") - s.log.ERROR.Println(err) + s.Log.ERROR.Printf("unable to process menus in site config\n") + s.Log.ERROR.Println(err) } menuEntry.marshallMap(ime) @@ -1407,7 +1425,7 @@ func (s *SiteInfo) createNodeMenuEntryURL(in string) string { } // make it match the nodes menuEntryURL := in - menuEntryURL = helpers.SanitizeURLKeepTrailingSlash(s.pathSpec.URLize(menuEntryURL)) + menuEntryURL = helpers.SanitizeURLKeepTrailingSlash(s.s.PathSpec.URLize(menuEntryURL)) if !s.canonifyURLs { menuEntryURL = helpers.AddContextRoot(string(s.BaseURL), menuEntryURL) } @@ -1454,7 +1472,7 @@ func (s *Site) assembleMenus() { for name, me := range p.Menus() { if _, ok := flat[twoD{name, me.KeyName()}]; ok { - s.log.ERROR.Printf("Two or more menu items have the same name/identifier in Menu %q: %q.\nRename or set an unique identifier.\n", name, me.KeyName()) + s.Log.ERROR.Printf("Two or more menu items have the same name/identifier in Menu %q: %q.\nRename or set an unique identifier.\n", name, me.KeyName()) continue } flat[twoD{name, me.KeyName()}] = me @@ -1490,6 +1508,13 @@ func (s *Site) assembleMenus() { } } +func (s *Site) getTaxonomyKey(key string) string { + if s.Info.preserveTaxonomyNames { + // Keep as is + return key + } + return s.PathSpec.MakePathSanitized(key) +} func (s *Site) assembleTaxonomies() { s.Taxonomies = make(TaxonomyList) s.taxonomiesPluralSingular = make(map[string]string) @@ -1497,7 +1522,7 @@ func (s *Site) assembleTaxonomies() { taxonomies := s.Language.GetStringMapString("taxonomies") - s.log.INFO.Printf("found taxonomies: %#v\n", taxonomies) + s.Log.INFO.Printf("found taxonomies: %#v\n", taxonomies) for singular, plural := range taxonomies { s.Taxonomies[plural] = make(Taxonomy) @@ -1513,21 +1538,21 @@ func (s *Site) assembleTaxonomies() { if v, ok := vals.([]string); ok { for _, idx := range v { x := WeightedPage{weight.(int), p} - s.Taxonomies[plural].add(idx, x, s.Info.preserveTaxonomyNames) + s.Taxonomies[plural].add(s.getTaxonomyKey(idx), x) if s.Info.preserveTaxonomyNames { // Need to track the original - s.taxonomiesOrigKey[fmt.Sprintf("%s-%s", plural, kp(idx))] = idx + s.taxonomiesOrigKey[fmt.Sprintf("%s-%s", plural, s.PathSpec.MakePathSanitized(idx))] = idx } } } else if v, ok := vals.(string); ok { x := WeightedPage{weight.(int), p} - s.Taxonomies[plural].add(v, x, s.Info.preserveTaxonomyNames) + s.Taxonomies[plural].add(s.getTaxonomyKey(v), x) if s.Info.preserveTaxonomyNames { // Need to track the original - s.taxonomiesOrigKey[fmt.Sprintf("%s-%s", plural, kp(v))] = v + s.taxonomiesOrigKey[fmt.Sprintf("%s-%s", plural, s.PathSpec.MakePathSanitized(v))] = v } } else { - s.log.ERROR.Printf("Invalid %s in %s\n", plural, p.File.Path()) + s.Log.ERROR.Printf("Invalid %s in %s\n", plural, p.File.Path()) } } } @@ -1564,7 +1589,7 @@ func (s *Site) assembleSections() { sectionPages := s.findPagesByKind(KindSection) for i, p := range regularPages { - s.Sections.add(p.Section(), WeightedPage{regularPages[i].Weight, regularPages[i]}, s.Info.preserveTaxonomyNames) + s.Sections.add(s.getTaxonomyKey(p.Section()), WeightedPage{regularPages[i].Weight, regularPages[i]}) } // Add sections without regular pages, but with a content page @@ -1665,18 +1690,18 @@ func (s *Site) appendThemeTemplates(in []string) []string { // Stats prints Hugo builds stats to the console. // This is what you see after a successful hugo build. func (s *Site) Stats() { - s.log.FEEDBACK.Printf("Built site for language %s:\n", s.Language.Lang) - s.log.FEEDBACK.Println(s.draftStats()) - s.log.FEEDBACK.Println(s.futureStats()) - s.log.FEEDBACK.Println(s.expiredStats()) - s.log.FEEDBACK.Printf("%d regular pages created\n", len(s.RegularPages)) - s.log.FEEDBACK.Printf("%d other pages created\n", (len(s.Pages) - len(s.RegularPages))) - s.log.FEEDBACK.Printf("%d non-page files copied\n", len(s.Files)) - s.log.FEEDBACK.Printf("%d paginator pages created\n", s.Info.paginationPageCount) + s.Log.FEEDBACK.Printf("Built site for language %s:\n", s.Language.Lang) + s.Log.FEEDBACK.Println(s.draftStats()) + s.Log.FEEDBACK.Println(s.futureStats()) + s.Log.FEEDBACK.Println(s.expiredStats()) + s.Log.FEEDBACK.Printf("%d regular pages created\n", len(s.RegularPages)) + s.Log.FEEDBACK.Printf("%d other pages created\n", (len(s.Pages) - len(s.RegularPages))) + s.Log.FEEDBACK.Printf("%d non-page files copied\n", len(s.Files)) + s.Log.FEEDBACK.Printf("%d paginator pages created\n", s.Info.paginationPageCount) taxonomies := s.Language.GetStringMapString("taxonomies") for _, pl := range taxonomies { - s.log.FEEDBACK.Printf("%d %s created\n", len(s.Taxonomies[pl]), pl) + s.Log.FEEDBACK.Printf("%d %s created\n", len(s.Taxonomies[pl]), pl) } } @@ -1701,11 +1726,11 @@ func (s *SiteInfo) permalink(plink string) string { func (s *SiteInfo) permalinkStr(plink string) string { return helpers.MakePermalink( viper.GetString("baseURL"), - s.pathSpec.URLizeAndPrep(plink)).String() + s.s.PathSpec.URLizeAndPrep(plink)).String() } func (s *Site) renderAndWriteXML(name string, dest string, d interface{}, layouts ...string) error { - s.log.DEBUG.Printf("Render XML for %q to %q", name, dest) + s.Log.DEBUG.Printf("Render XML for %q to %q", name, dest) renderBuffer := bp.GetBuffer() defer bp.PutBuffer(renderBuffer) renderBuffer.WriteString("\n") @@ -1797,7 +1822,7 @@ func (s *Site) renderAndWritePage(name string, dest string, d interface{}, layou if outBuffer.Len() == 0 { - s.log.WARN.Printf("%s is rendered empty\n", dest) + s.Log.WARN.Printf("%s is rendered empty\n", dest) if dest == "/" { debugAddend := "" if !viper.GetBool("verbose") { @@ -1829,7 +1854,8 @@ Your rendered home page is blank: /index.html is zero-length func (s *Site) renderForLayouts(name string, d interface{}, w io.Writer, layouts ...string) error { layout, found := s.findFirstLayout(layouts...) if !found { - s.log.WARN.Printf("Unable to locate layout for %s: %s\n", name, layouts) + s.Log.WARN.Printf("[%s] Unable to locate layout for %s: %s\n", s.Language.Lang, name, layouts) + return nil } @@ -1850,7 +1876,7 @@ func (s *Site) renderForLayouts(name string, d interface{}, w io.Writer, layouts func (s *Site) findFirstLayout(layouts ...string) (string, bool) { for _, layout := range layouts { - if s.owner.tmpl.Lookup(layout) != nil { + if s.Tmpl.Lookup(layout) != nil { return layout, true } } @@ -1860,7 +1886,7 @@ func (s *Site) findFirstLayout(layouts ...string) (string, bool) { func (s *Site) renderThing(d interface{}, layout string, w io.Writer) error { // If the template doesn't exist, then return, but leave the Writer open - if templ := s.owner.tmpl.Lookup(layout); templ != nil { + if templ := s.Tmpl.Lookup(layout); templ != nil { return templ.Execute(w, d) } return fmt.Errorf("Layout not found: %s", layout) @@ -1893,6 +1919,9 @@ func (s *Site) languageAliasTarget() target.AliasPublisher { } func (s *Site) initTargetList() { + if s.Fs == nil { + panic("Must have Fs") + } s.targetListInit.Do(func() { langDir := "" if s.Language.Lang != s.Info.multilingual.DefaultLang.Lang || s.Info.defaultContentLanguageInSubdir { @@ -1900,6 +1929,7 @@ func (s *Site) initTargetList() { } if s.targets.page == nil { s.targets.page = &target.PagePub{ + Fs: s.Fs, PublishDir: s.absPublishDir(), UglyURLs: viper.GetBool("uglyURLs"), LangDir: langDir, @@ -1907,6 +1937,7 @@ func (s *Site) initTargetList() { } if s.targets.pageUgly == nil { s.targets.pageUgly = &target.PagePub{ + Fs: s.Fs, PublishDir: s.absPublishDir(), UglyURLs: true, LangDir: langDir, @@ -1914,17 +1945,20 @@ func (s *Site) initTargetList() { } if s.targets.file == nil { s.targets.file = &target.Filesystem{ + Fs: s.Fs, PublishDir: s.absPublishDir(), } } if s.targets.alias == nil { s.targets.alias = &target.HTMLRedirectAlias{ + Fs: s.Fs, PublishDir: s.absPublishDir(), - Templates: s.owner.tmpl.Lookup("alias.html"), + Templates: s.Tmpl.Lookup("alias.html"), } } if s.targets.languageAlias == nil { s.targets.languageAlias = &target.HTMLRedirectAlias{ + Fs: s.Fs, PublishDir: s.absPublishDir(), AllowRoot: true, } @@ -1933,12 +1967,12 @@ func (s *Site) initTargetList() { } func (s *Site) writeDestFile(path string, reader io.Reader) (err error) { - s.log.DEBUG.Println("creating file:", path) + s.Log.DEBUG.Println("creating file:", path) return s.fileTarget().Publish(path, reader) } func (s *Site) writeDestPage(path string, publisher target.Publisher, reader io.Reader) (err error) { - s.log.DEBUG.Println("creating page:", path) + s.Log.DEBUG.Println("creating page:", path) return publisher.Publish(path, reader) } @@ -1956,11 +1990,11 @@ func (s *Site) publishDestAlias(aliasPublisher target.AliasPublisher, path, perm } permalink, err = helpers.GetRelativePath(permalink, path) if err != nil { - s.log.ERROR.Println("Failed to make a RelativeURL alias:", path, "redirecting to", permalink) + s.Log.ERROR.Println("Failed to make a RelativeURL alias:", path, "redirecting to", permalink) } permalink = filepath.ToSlash(permalink) } - s.log.DEBUG.Println("creating alias:", path, "redirecting to", permalink) + s.Log.DEBUG.Println("creating alias:", path, "redirecting to", permalink) return aliasPublisher.Publish(path, permalink, p) } @@ -2051,7 +2085,7 @@ func (s *Site) newHomePage() *Page { } func (s *Site) setPageURLs(p *Page, in string) { - p.URLPath.URL = s.Info.pathSpec.URLizeAndPrep(in) + p.URLPath.URL = s.PathSpec.URLizeAndPrep(in) p.URLPath.Permalink = s.Info.permalink(p.URLPath.URL) p.RSSLink = template.HTML(s.Info.permalink(in + ".xml")) } @@ -2063,7 +2097,7 @@ func (s *Site) newTaxonomyPage(plural, key string) *Page { p.sections = []string{plural, key} if s.Info.preserveTaxonomyNames { - key = s.Info.pathSpec.MakePathSanitized(key) + key = s.PathSpec.MakePathSanitized(key) } if s.Info.preserveTaxonomyNames { diff --git a/hugolib/siteJSONEncode_test.go b/hugolib/siteJSONEncode_test.go index 170db4b4..1218bfd3 100644 --- a/hugolib/siteJSONEncode_test.go +++ b/hugolib/siteJSONEncode_test.go @@ -16,6 +16,11 @@ package hugolib import ( "encoding/json" "testing" + + "path/filepath" + + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/hugofs" ) // Issue #1123 @@ -23,9 +28,15 @@ import ( // May be smart to run with: -timeout 4000ms func TestEncodePage(t *testing.T) { + fs := hugofs.NewMem() + // borrowed from menu_test.go - s := createTestSite(menuPageSources) - testSiteSetup(s, t) + for _, src := range menuPageSources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + + } + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) _, err := json.Marshal(s) check(t, err) diff --git a/hugolib/site_render.go b/hugolib/site_render.go index b6a9cae5..84df78c1 100644 --- a/hugolib/site_render.go +++ b/hugolib/site_render.go @@ -66,7 +66,7 @@ func pageRenderer(s *Site, pages <-chan *Page, results chan<- error, wg *sync.Wa for p := range pages { targetPath := p.TargetPath() layouts := p.layouts() - s.log.DEBUG.Printf("Render %s to %q with layouts %q", p.Kind, targetPath, layouts) + s.Log.DEBUG.Printf("Render %s to %q with layouts %q", p.Kind, targetPath, layouts) if err := s.renderAndWritePage("page "+p.FullFilePath(), targetPath, p, s.appendThemeTemplates(layouts)...); err != nil { results <- err @@ -88,7 +88,7 @@ func pageRenderer(s *Site, pages <-chan *Page, results chan<- error, wg *sync.Wa // renderPaginator must be run after the owning Page has been rendered. func (s *Site) renderPaginator(p *Page) error { if p.paginator != nil { - s.log.DEBUG.Printf("Render paginator for page %q", p.Path()) + s.Log.DEBUG.Printf("Render paginator for page %q", p.Path()) paginatePath := helpers.Config().GetString("paginatePath") // write alias for page 1 @@ -267,14 +267,14 @@ func (s *Site) renderAliases() error { if s.owner.multilingual.enabled() { mainLang := s.owner.multilingual.DefaultLang.Lang if s.Info.defaultContentLanguageInSubdir { - mainLangURL := s.Info.pathSpec.AbsURL(mainLang, false) - s.log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL) + mainLangURL := s.PathSpec.AbsURL(mainLang, false) + s.Log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL) if err := s.publishDestAlias(s.languageAliasTarget(), "/", mainLangURL, nil); err != nil { return err } } else { - mainLangURL := s.Info.pathSpec.AbsURL("", false) - s.log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL) + mainLangURL := s.PathSpec.AbsURL("", false) + s.Log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL) if err := s.publishDestAlias(s.languageAliasTarget(), mainLang, mainLangURL, nil); err != nil { return err } diff --git a/hugolib/site_test.go b/hugolib/site_test.go index 342cae61..3f1a8b06 100644 --- a/hugolib/site_test.go +++ b/hugolib/site_test.go @@ -18,16 +18,15 @@ import ( "path/filepath" "strings" "testing" - "time" "github.com/bep/inflect" jww "github.com/spf13/jwalterweatherman" "github.com/spf13/hugo/helpers" - "github.com/spf13/hugo/hugofs" "github.com/spf13/hugo/source" - "github.com/spf13/hugo/target" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/hugofs" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -47,37 +46,6 @@ func init() { testMode = true } -// Issue #1797 -func TestReadPagesFromSourceWithEmptySource(t *testing.T) { - testCommonResetState() - - viper.Set("defaultExtension", "html") - viper.Set("verbose", true) - viper.Set("baseURL", "http://auth/bub") - - sources := []source.ByteSource{} - - s := &Site{ - deps: newDeps(DepsCfg{}), - Source: &source.InMemorySource{ByteSource: sources}, - targets: targetList{page: &target.PagePub{UglyURLs: true}}, - } - - var err error - d := time.Second * 2 - ticker := time.NewTicker(d) - select { - case err = <-s.readPagesFromSource(): - break - case <-ticker.C: - err = fmt.Errorf("ReadPagesFromSource() never returns in %s", d.String()) - } - ticker.Stop() - if err != nil { - t.Fatalf("Unable to read source: %s", err) - } -} - func pageMust(p *Page, err error) *Page { if err != nil { panic(err) @@ -86,11 +54,12 @@ func pageMust(p *Page, err error) *Page { } func TestDegenerateRenderThingMissingTemplate(t *testing.T) { - s := newSiteFromSources("content/a/file.md", pageSimpleTitle) - if err := buildSiteSkipRender(s); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + fs := hugofs.NewMem() + + writeSource(t, fs, filepath.Join("content", "a", "file.md"), pageSimpleTitle) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) require.Len(t, s.RegularPages, 1) @@ -104,12 +73,15 @@ func TestDegenerateRenderThingMissingTemplate(t *testing.T) { func TestRenderWithInvalidTemplate(t *testing.T) { - s := NewSiteDefaultLang() - if err := buildAndRenderSite(s, "missing", templateMissingFunc); err != nil { - t.Fatalf("Got build error: %s", err) - } + fs := hugofs.NewMem() - errCount := s.log.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError) + writeSource(t, fs, filepath.Join("content", "foo.md"), "foo") + + withTemplate := createWithTemplateFromNameValues("missing", templateMissingFunc) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, WithTemplate: withTemplate}, BuildCfg{}) + + errCount := s.Log.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError) // TODO(bep) globals clean up the template error handling // The template errors are stored in a slice etc. so we get 4 log entries @@ -122,7 +94,6 @@ func TestRenderWithInvalidTemplate(t *testing.T) { func TestDraftAndFutureRender(t *testing.T) { testCommonResetState() - hugofs.InitMemFs() sources := []source.ByteSource{ {Name: filepath.FromSlash("sect/doc1.md"), Content: []byte("---\ntitle: doc1\ndraft: true\npublishdate: \"2414-05-29\"\n---\n# doc1\n*some content*")}, {Name: filepath.FromSlash("sect/doc2.md"), Content: []byte("---\ntitle: doc2\ndraft: true\npublishdate: \"2012-05-29\"\n---\n# doc2\n*some content*")}, @@ -131,17 +102,14 @@ func TestDraftAndFutureRender(t *testing.T) { } siteSetup := func(t *testing.T) *Site { - s := &Site{ - deps: newDeps(DepsCfg{}), - Source: &source.InMemorySource{ByteSource: sources}, - Language: helpers.NewDefaultLanguage(), + fs := hugofs.NewMem() + + for _, src := range sources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + } - if err := buildSiteSkipRender(s); err != nil { - t.Fatalf("Failed to build site: %s", err) - } - - return s + return buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) } viper.Set("baseURL", "http://auth/bub") @@ -183,24 +151,20 @@ func TestDraftAndFutureRender(t *testing.T) { func TestFutureExpirationRender(t *testing.T) { testCommonResetState() - hugofs.InitMemFs() sources := []source.ByteSource{ {Name: filepath.FromSlash("sect/doc3.md"), Content: []byte("---\ntitle: doc1\nexpirydate: \"2400-05-29\"\n---\n# doc1\n*some content*")}, {Name: filepath.FromSlash("sect/doc4.md"), Content: []byte("---\ntitle: doc2\nexpirydate: \"2000-05-29\"\n---\n# doc2\n*some content*")}, } siteSetup := func(t *testing.T) *Site { - s := &Site{ - deps: newDeps(DepsCfg{}), - Source: &source.InMemorySource{ByteSource: sources}, - Language: helpers.NewDefaultLanguage(), + fs := hugofs.NewMem() + + for _, src := range sources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + } - if err := buildSiteSkipRender(s); err != nil { - t.Fatalf("Failed to build site: %s", err) - } - - return s + return buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) } viper.Set("baseURL", "http://auth/bub") @@ -282,16 +246,18 @@ THE END.`, refShortcode)), }, } - s := &Site{ - deps: newDeps(DepsCfg{}), - Source: &source.InMemorySource{ByteSource: sources}, - targets: targetList{page: &target.PagePub{UglyURLs: uglyURLs}}, - Language: helpers.NewDefaultLanguage(), + fs := hugofs.NewMem() + + for _, src := range sources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) } - if err := buildAndRenderSite(s, "_default/single.html", "{{.Content}}"); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + s := buildSingleSite( + t, + deps.DepsCfg{ + Fs: fs, + WithTemplate: createWithTemplateFromNameValues("_default/single.html", "{{.Content}}")}, + BuildCfg{}) if len(s.RegularPages) != 3 { t.Fatalf("Expected 3 got %d pages", len(s.AllPages)) @@ -301,23 +267,14 @@ THE END.`, refShortcode)), doc string expected string }{ - {filepath.FromSlash(fmt.Sprintf("sect/doc1%s", expectedPathSuffix)), fmt.Sprintf("

Ref 2: %s/sect/doc2%s

\n", expectedBase, expectedURLSuffix)}, - {filepath.FromSlash(fmt.Sprintf("sect/doc2%s", expectedPathSuffix)), fmt.Sprintf("

Ref 1:

\n\n%s/sect/doc1%s\n\n

THE END.

\n", expectedBase, expectedURLSuffix)}, - {filepath.FromSlash(fmt.Sprintf("sect/doc3%s", expectedPathSuffix)), fmt.Sprintf("

Ref 1:%s/sect/doc3%s.

\n", expectedBase, expectedURLSuffix)}, + {filepath.FromSlash(fmt.Sprintf("public/sect/doc1%s", expectedPathSuffix)), fmt.Sprintf("

Ref 2: %s/sect/doc2%s

\n", expectedBase, expectedURLSuffix)}, + {filepath.FromSlash(fmt.Sprintf("public/sect/doc2%s", expectedPathSuffix)), fmt.Sprintf("

Ref 1:

\n\n%s/sect/doc1%s\n\n

THE END.

\n", expectedBase, expectedURLSuffix)}, + {filepath.FromSlash(fmt.Sprintf("public/sect/doc3%s", expectedPathSuffix)), fmt.Sprintf("

Ref 1:%s/sect/doc3%s.

\n", expectedBase, expectedURLSuffix)}, } for _, test := range tests { - file, err := hugofs.Destination().Open(test.doc) + assertFileContent(t, fs, test.doc, true, test.expected) - if err != nil { - t.Fatalf("Did not find %s in target: %s", test.doc, err) - } - - content := helpers.ReaderToString(file) - - if content != test.expected { - t.Fatalf("%s content expected:\n%q\ngot:\n%q", test.doc, test.expected, content) - } } } @@ -350,21 +307,19 @@ func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) { {Name: filepath.FromSlash("sect/doc2.md"), Content: []byte("---\nurl: /ugly.html\nmarkup: markdown\n---\n# title\ndoc2 *content*")}, } - s := &Site{ - deps: newDeps(DepsCfg{}), - Source: &source.InMemorySource{ByteSource: sources}, - targets: targetList{page: &target.PagePub{UglyURLs: uglyURLs, PublishDir: "public"}}, - Language: helpers.NewDefaultLanguage(), + fs := hugofs.NewMem() + + for _, src := range sources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) } - if err := buildAndRenderSite(s, - "index.html", "Home Sweet {{ if.IsHome }}Home{{ end }}.", - "_default/single.html", "{{.Content}}{{ if.IsHome }}This is not home!{{ end }}", - "404.html", "Page Not Found.{{ if.IsHome }}This is not home!{{ end }}", - "rss.xml", "RSS", - "sitemap.xml", "SITEMAP"); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + writeSource(t, fs, filepath.Join("layouts", "index.html"), "Home Sweet {{ if.IsHome }}Home{{ end }}.") + writeSource(t, fs, filepath.Join("layouts", "_default/single.html"), "{{.Content}}{{ if.IsHome }}This is not home!{{ end }}") + writeSource(t, fs, filepath.Join("layouts", "404.html"), "Page Not Found.{{ if.IsHome }}This is not home!{{ end }}") + writeSource(t, fs, filepath.Join("layouts", "rss.xml"), "RSS") + writeSource(t, fs, filepath.Join("layouts", "sitemap.xml"), "SITEMAP") + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) var expectedPagePath string if uglyURLs { @@ -391,7 +346,7 @@ func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) { } for _, test := range tests { - content := readDestination(t, test.doc) + content := readDestination(t, fs, test.doc) if content != test.expected { t.Errorf("%s content expected:\n%q\ngot:\n%q", test.doc, test.expected, content) @@ -435,17 +390,16 @@ func doTestSectionNaming(t *testing.T, canonify, uglify, pluralize bool) { {Name: filepath.FromSlash("ラーメン/doc3.html"), Content: []byte("doc3")}, } + fs := hugofs.NewMem() + for _, source := range sources { - writeSource(t, filepath.Join("content", source.Name), string(source.Content)) + writeSource(t, fs, filepath.Join("content", source.Name), string(source.Content)) } - s := NewSiteDefaultLang() + writeSource(t, fs, filepath.Join("layouts", "_default/single.html"), "{{.Content}}") + writeSource(t, fs, filepath.Join("layouts", "_default/list.html"), "{{.Title}}") - if err := buildAndRenderSite(s, - "_default/single.html", "{{.Content}}", - "_default/list.html", "{{ .Title }}"); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) tests := []struct { doc string @@ -466,14 +420,13 @@ func doTestSectionNaming(t *testing.T, canonify, uglify, pluralize bool) { test.expected = inflect.Pluralize(test.expected) } - assertFileContent(t, filepath.Join("public", test.doc), true, test.expected) + assertFileContent(t, fs, filepath.Join("public", test.doc), true, test.expected) } } func TestSkipRender(t *testing.T) { testCommonResetState() - hugofs.InitMemFs() sources := []source.ByteSource{ {Name: filepath.FromSlash("sect/doc1.html"), Content: []byte("---\nmarkup: markdown\n---\n# title\nsome *content*")}, {Name: filepath.FromSlash("sect/doc2.html"), Content: []byte("more content")}, @@ -488,37 +441,38 @@ func TestSkipRender(t *testing.T) { viper.Set("defaultExtension", "html") viper.Set("verbose", true) viper.Set("canonifyURLs", true) + viper.Set("uglyURLs", true) viper.Set("baseURL", "http://auth/bub") - s := &Site{ - deps: newDeps(DepsCfg{}), - Source: &source.InMemorySource{ByteSource: sources}, - targets: targetList{page: &target.PagePub{UglyURLs: true}}, - Language: helpers.NewDefaultLanguage(), + + fs := hugofs.NewMem() + + for _, src := range sources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + } - if err := buildAndRenderSite(s, - "_default/single.html", "{{.Content}}", - "head", "", - "head_abs", ""); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + writeSource(t, fs, filepath.Join("layouts", "_default/single.html"), "{{.Content}}") + writeSource(t, fs, filepath.Join("layouts", "head"), "") + writeSource(t, fs, filepath.Join("layouts", "head_abs"), "") + + buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) tests := []struct { doc string expected string }{ - {filepath.FromSlash("sect/doc1.html"), "\n\n

title

\n\n

some content

\n"}, - {filepath.FromSlash("sect/doc2.html"), "more content"}, - {filepath.FromSlash("sect/doc3.html"), "\n\n

doc3

\n\n

some content

\n"}, - {filepath.FromSlash("sect/doc4.html"), "\n\n

doc4

\n\n

some content

\n"}, - {filepath.FromSlash("sect/doc5.html"), "body5"}, - {filepath.FromSlash("sect/doc6.html"), "body5"}, - {filepath.FromSlash("doc7.html"), "doc7 content"}, - {filepath.FromSlash("sect/doc8.html"), "\n\n

title

\n\n

some content

\n"}, + {filepath.FromSlash("public/sect/doc1.html"), "\n\n

title

\n\n

some content

\n"}, + {filepath.FromSlash("public/sect/doc2.html"), "more content"}, + {filepath.FromSlash("public/sect/doc3.html"), "\n\n

doc3

\n\n

some content

\n"}, + {filepath.FromSlash("public/sect/doc4.html"), "\n\n

doc4

\n\n

some content

\n"}, + {filepath.FromSlash("public/sect/doc5.html"), "body5"}, + {filepath.FromSlash("public/sect/doc6.html"), "body5"}, + {filepath.FromSlash("public/doc7.html"), "doc7 content"}, + {filepath.FromSlash("public/sect/doc8.html"), "\n\n

title

\n\n

some content

\n"}, } for _, test := range tests { - file, err := hugofs.Destination().Open(test.doc) + file, err := fs.Destination.Open(test.doc) if err != nil { t.Fatalf("Did not find %s in target.", test.doc) } @@ -535,8 +489,8 @@ func TestAbsURLify(t *testing.T) { testCommonResetState() viper.Set("defaultExtension", "html") + viper.Set("uglyURLs", true) - hugofs.InitMemFs() sources := []source.ByteSource{ {Name: filepath.FromSlash("sect/doc1.html"), Content: []byte("link")}, {Name: filepath.FromSlash("blue/doc2.html"), Content: []byte("---\nf: t\n---\nmore content")}, @@ -545,34 +499,27 @@ func TestAbsURLify(t *testing.T) { for _, canonify := range []bool{true, false} { viper.Set("canonifyURLs", canonify) viper.Set("baseURL", baseURL) - s := &Site{ - deps: newDeps(DepsCfg{}), - Source: &source.InMemorySource{ByteSource: sources}, - targets: targetList{page: &target.PagePub{UglyURLs: true}}, - Language: helpers.NewDefaultLanguage(), - } - t.Logf("Rendering with baseURL %q and canonifyURLs set %v", viper.GetString("baseURL"), canonify) - if err := buildAndRenderSite(s, "blue/single.html", templateWithURLAbs); err != nil { - t.Fatalf("Failed to build site: %s", err) + fs := hugofs.NewMem() + + for _, src := range sources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + } + writeSource(t, fs, filepath.Join("layouts", "blue/single.html"), templateWithURLAbs) + + buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) + tests := []struct { file, expected string }{ - {"blue/doc2.html", "Going"}, - {"sect/doc1.html", "link"}, + {"public/blue/doc2.html", "Going"}, + {"public/sect/doc1.html", "link"}, } for _, test := range tests { - file, err := hugofs.Destination().Open(filepath.FromSlash(test.file)) - if err != nil { - t.Fatalf("Unable to locate rendered content: %s", test.file) - } - - content := helpers.ReaderToString(file) - expected := test.expected if strings.Contains(expected, "%s") { @@ -583,9 +530,8 @@ func TestAbsURLify(t *testing.T) { expected = strings.Replace(expected, baseURL, "", -1) } - if content != expected { - t.Errorf("AbsURLify with baseURL %q content expected:\n%q\ngot\n%q", baseURL, expected, content) - } + assertFileContent(t, fs, test.file, true, expected) + } } } @@ -639,18 +585,16 @@ var weightedSources = []source.ByteSource{ func TestOrderedPages(t *testing.T) { testCommonResetState() - hugofs.InitMemFs() - viper.Set("baseURL", "http://auth/bub") - s := &Site{ - deps: newDeps(DepsCfg{}), - Source: &source.InMemorySource{ByteSource: weightedSources}, - Language: helpers.NewDefaultLanguage(), + + fs := hugofs.NewMem() + + for _, src := range weightedSources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + } - if err := buildSiteSkipRender(s); err != nil { - t.Fatalf("Failed to process site: %s", err) - } + s := buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{SkipRender: true}) if s.Sections["sect"][0].Weight != 2 || s.Sections["sect"][3].Weight != 6 { t.Errorf("Pages in unexpected order. First should be '%d', got '%d'", 2, s.Sections["sect"][0].Weight) @@ -709,23 +653,17 @@ func TestGroupedPages(t *testing.T) { } }() - hugofs.InitMemFs() - viper.Set("baseURL", "http://auth/bub") - s := &Site{ - deps: newDeps(DepsCfg{}), - Source: &source.InMemorySource{ByteSource: groupedSources}, - Language: helpers.NewDefaultLanguage(), - } - if err := buildSiteSkipRender(s); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + fs := hugofs.NewMem() + writeSourcesToSource(t, "content", fs, groupedSources...) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) rbysection, err := s.RegularPages.GroupBy("Section", "desc") if err != nil { t.Fatalf("Unable to make PageGroup array: %s", err) } + if rbysection[0].Key != "sect3" { t.Errorf("PageGroup array in unexpected order. First group key should be '%s', got '%s'", "sect3", rbysection[0].Key) } @@ -885,7 +823,6 @@ Front Matter with weighted tags and categories`) func TestWeightedTaxonomies(t *testing.T) { testCommonResetState() - hugofs.InitMemFs() sources := []source.ByteSource{ {Name: filepath.FromSlash("sect/doc1.md"), Content: pageWithWeightedTaxonomies2}, {Name: filepath.FromSlash("sect/doc2.md"), Content: pageWithWeightedTaxonomies1}, @@ -898,15 +835,10 @@ func TestWeightedTaxonomies(t *testing.T) { viper.Set("baseURL", "http://auth/bub") viper.Set("taxonomies", taxonomies) - s := &Site{ - deps: newDeps(DepsCfg{}), - Source: &source.InMemorySource{ByteSource: sources}, - Language: helpers.NewDefaultLanguage(), - } - if err := buildSiteSkipRender(s); err != nil { - t.Fatalf("Failed to process site: %s", err) - } + fs := hugofs.NewMem() + writeSourcesToSource(t, "content", fs, sources...) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) if s.Taxonomies["tags"]["a"][0].Page.Title != "foo" { t.Errorf("Pages in unexpected order, 'foo' expected first, got '%v'", s.Taxonomies["tags"]["a"][0].Page.Title) @@ -935,7 +867,6 @@ func findPage(site *Site, f string) *Page { } func setupLinkingMockSite(t *testing.T) *Site { - hugofs.InitMemFs() sources := []source.ByteSource{ {Name: filepath.FromSlash("index.md"), Content: []byte("")}, {Name: filepath.FromSlash("rootfile.md"), Content: []byte("")}, @@ -968,17 +899,10 @@ func setupLinkingMockSite(t *testing.T) *Site { map[string]interface{}{ "sourceRelativeLinksProjectFolder": "/docs"}) - site := &Site{ - deps: newDeps(DepsCfg{}), - Source: &source.InMemorySource{ByteSource: sources}, - Language: helpers.NewDefaultLanguage(), - } + fs := hugofs.NewMem() + writeSourcesToSource(t, "content", fs, sources...) + return buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) - if err := buildSiteSkipRender(site); err != nil { - t.Fatalf("Failed to build site: %s", err) - } - - return site } func TestRefLinking(t *testing.T) { diff --git a/hugolib/site_url_test.go b/hugolib/site_url_test.go index 99d04460..5706b9fb 100644 --- a/hugolib/site_url_test.go +++ b/hugolib/site_url_test.go @@ -17,13 +17,13 @@ import ( "path/filepath" "testing" - "github.com/spf13/hugo/helpers" - "html/template" + "github.com/spf13/hugo/deps" "github.com/spf13/hugo/hugofs" "github.com/spf13/hugo/source" "github.com/spf13/viper" + "github.com/stretchr/testify/require" ) const slugDoc1 = "---\ntitle: slug doc 1\nslug: slug-doc-1\naliases:\n - sd1/foo/\n - sd2\n - sd3/\n - sd4.html\n---\nslug doc 1 content\n" @@ -62,7 +62,8 @@ func TestShouldNotAddTrailingSlashToBaseURL(t *testing.T) { {"http://base.com", "http://base.com"}} { viper.Set("baseURL", this.in) - s := NewSiteDefaultLang() + s, err := NewSiteDefaultLang() + require.NoError(t, err) s.initializeSiteInfo() if s.Info.BaseURL != template.URL(this.expected) { @@ -74,32 +75,27 @@ func TestShouldNotAddTrailingSlashToBaseURL(t *testing.T) { func TestPageCount(t *testing.T) { testCommonResetState() - hugofs.InitMemFs() viper.Set("uglyURLs", false) viper.Set("paginate", 10) - s := &Site{ - deps: newDeps(DepsCfg{}), - Source: &source.InMemorySource{ByteSource: urlFakeSource}, - Language: helpers.NewDefaultLanguage(), - } - if err := buildAndRenderSite(s, "indexes/blue.html", indexTemplate); err != nil { - t.Fatalf("Failed to build site: %s", err) - } - _, err := hugofs.Destination().Open("public/blue") + fs := hugofs.NewMem() + writeSourcesToSource(t, "content", fs, urlFakeSource...) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) + + _, err := s.Fs.Destination.Open("public/blue") if err != nil { t.Errorf("No indexed rendered.") } - for _, s := range []string{ + for _, pth := range []string{ "public/sd1/foo/index.html", "public/sd2/index.html", "public/sd3/index.html", "public/sd4.html", } { - if _, err := hugofs.Destination().Open(filepath.FromSlash(s)); err != nil { - t.Errorf("No alias rendered: %s", s) + if _, err := s.Fs.Destination.Open(filepath.FromSlash(pth)); err != nil { + t.Errorf("No alias rendered: %s", pth) } } } diff --git a/hugolib/sitemap_test.go b/hugolib/sitemap_test.go index 95f8739e..15d71cc6 100644 --- a/hugolib/sitemap_test.go +++ b/hugolib/sitemap_test.go @@ -18,8 +18,9 @@ import ( "reflect" - "github.com/spf13/hugo/helpers" - "github.com/spf13/hugo/source" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/hugofs" + "github.com/spf13/hugo/tplapi" "github.com/spf13/viper" ) @@ -45,24 +46,21 @@ func doTestSitemapOutput(t *testing.T, internal bool) { viper.Set("baseURL", "http://auth/bub/") - s := &Site{ - deps: newDeps(DepsCfg{}), - Source: &source.InMemorySource{ByteSource: weightedSources}, - Language: helpers.NewDefaultLanguage(), - } + fs := hugofs.NewMem() - if internal { - if err := buildAndRenderSite(s); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + depsCfg := deps.DepsCfg{Fs: fs} - } else { - if err := buildAndRenderSite(s, "sitemap.xml", sitemapTemplate); err != nil { - t.Fatalf("Failed to build site: %s", err) + if !internal { + depsCfg.WithTemplate = func(templ tplapi.Template) error { + templ.AddTemplate("sitemap.xml", sitemapTemplate) + return nil } } - assertFileContent(t, "public/sitemap.xml", true, + writeSourcesToSource(t, "content", fs, weightedSources...) + s := buildSingleSite(t, depsCfg, BuildCfg{}) + + assertFileContent(t, s.Fs, "public/sitemap.xml", true, // Regular page " http://auth/bub/sect/doc1/", // Home page diff --git a/hugolib/taxonomy.go b/hugolib/taxonomy.go index 68354de8..5faf14d0 100644 --- a/hugolib/taxonomy.go +++ b/hugolib/taxonomy.go @@ -16,8 +16,6 @@ package hugolib import ( "fmt" "sort" - - "github.com/spf13/hugo/helpers" ) // The TaxonomyList is a list of all taxonomies and their values @@ -59,26 +57,15 @@ type OrderedTaxonomyEntry struct { WeightedPages WeightedPages } -// KeyPrep... Taxonomies should be case insensitive. Can make it easily conditional later. -func kp(in string) string { - return helpers.CurrentPathSpec().MakePathSanitized(in) -} - // Get the weighted pages for the given key. func (i Taxonomy) Get(key string) WeightedPages { - if val, ok := i[key]; ok { - return val - } - return i[kp(key)] + return i[key] } // Count the weighted pages for the given key. -func (i Taxonomy) Count(key string) int { return len(i[kp(key)]) } +func (i Taxonomy) Count(key string) int { return len(i[key]) } -func (i Taxonomy) add(key string, w WeightedPage, pretty bool) { - if !pretty { - key = kp(key) - } +func (i Taxonomy) add(key string, w WeightedPage) { i[key] = append(i[key], w) } diff --git a/hugolib/taxonomy_test.go b/hugolib/taxonomy_test.go index 65b36d4e..5cbd58d1 100644 --- a/hugolib/taxonomy_test.go +++ b/hugolib/taxonomy_test.go @@ -18,6 +18,9 @@ import ( "reflect" "testing" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/hugofs" + "github.com/spf13/viper" ) @@ -31,16 +34,14 @@ func TestByCountOrderOfTaxonomies(t *testing.T) { viper.Set("taxonomies", taxonomies) - writeSource(t, filepath.Join("content", "page.md"), pageYamlWithTaxonomiesA) + fs := hugofs.NewMem() - site := NewSiteDefaultLang() + writeSource(t, fs, filepath.Join("content", "page.md"), pageYamlWithTaxonomiesA) - if err := buildSiteSkipRender(site); err != nil { - t.Fatalf("Failed to build site: %s", err) - } + s := buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) st := make([]string, 0) - for _, t := range site.Taxonomies["tags"].ByCount() { + for _, t := range s.Taxonomies["tags"].ByCount() { st = append(st, t.Name) } diff --git a/hugolib/template_engines_test.go b/hugolib/template_engines_test.go new file mode 100644 index 00000000..424f2562 --- /dev/null +++ b/hugolib/template_engines_test.go @@ -0,0 +1,99 @@ +// Copyright 2017 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 hugolib + +import ( + "fmt" + "path/filepath" + "testing" + + "strings" + + "github.com/spf13/viper" + + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/hugofs" +) + +func TestAllTemplateEngines(t *testing.T) { + noOp := func(s string) string { + return s + } + + amberFixer := func(s string) string { + fixed := strings.Replace(s, "{{ .Title", "{{ Title", -1) + fixed = strings.Replace(fixed, ".Content", "Content", -1) + fixed = strings.Replace(fixed, "{{", "#{", -1) + fixed = strings.Replace(fixed, "}}", "}", -1) + fixed = strings.Replace(fixed, `title "hello world"`, `title("hello world")`, -1) + + return fixed + } + + for _, config := range []struct { + suffix string + templateFixer func(s string) string + }{ + {"amber", amberFixer}, + {"html", noOp}, + {"ace", noOp}, + } { + doTestTemplateEngine(t, config.suffix, config.templateFixer) + + } + +} + +func doTestTemplateEngine(t *testing.T, suffix string, templateFixer func(s string) string) { + + testCommonResetState() + + fs := hugofs.NewMem() + viper.SetFs(fs.Source) + + writeSource(t, fs, filepath.Join("content", "p.md"), ` +--- +title: My Title +--- +My Content +`) + + t.Log("Testing", suffix) + + templTemplate := ` +p + | + | Page Title: {{ .Title }} + br + | Page Content: {{ .Content }} + br + | {{ title "hello world" }} + +` + + templ := templateFixer(templTemplate) + + t.Log(templ) + + writeSource(t, fs, filepath.Join("layouts", "_default", fmt.Sprintf("single.%s", suffix)), templ) + + buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) + + assertFileContent(t, fs, filepath.Join("public", "p", "index.html"), true, + "Page Title: My Title", + "My Content", + "Hello World", + ) + +} diff --git a/hugolib/template_test.go b/hugolib/template_test.go index 20db56f8..2690b172 100644 --- a/hugolib/template_test.go +++ b/hugolib/template_test.go @@ -17,127 +17,133 @@ import ( "path/filepath" "testing" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/hugofs" + "github.com/spf13/viper" ) func TestBaseGoTemplate(t *testing.T) { + + var fs *hugofs.Fs + // Variants: // 1. /-baseof., e.g. list-baseof.. // 2. /baseof. // 3. _default/-baseof., e.g. list-baseof.. // 4. _default/baseof. - for i, this := range []struct { + for _, this := range []struct { setup func(t *testing.T) assert func(t *testing.T) }{ { // Variant 1 func(t *testing.T) { - writeSource(t, filepath.Join("layouts", "section", "sect-baseof.html"), `Base: {{block "main" .}}block{{end}}`) - writeSource(t, filepath.Join("layouts", "section", "sect.html"), `{{define "main"}}sect{{ end }}`) + writeSource(t, fs, filepath.Join("layouts", "section", "sect-baseof.html"), `Base: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("layouts", "section", "sect.html"), `{{define "main"}}sect{{ end }}`) }, func(t *testing.T) { - assertFileContent(t, filepath.Join("public", "sect", "index.html"), false, "Base: sect") + assertFileContent(t, fs, filepath.Join("public", "sect", "index.html"), false, "Base: sect") }, }, { // Variant 2 func(t *testing.T) { - writeSource(t, filepath.Join("layouts", "baseof.html"), `Base: {{block "main" .}}block{{end}}`) - writeSource(t, filepath.Join("layouts", "index.html"), `{{define "main"}}index{{ end }}`) + writeSource(t, fs, filepath.Join("layouts", "baseof.html"), `Base: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("layouts", "index.html"), `{{define "main"}}index{{ end }}`) }, func(t *testing.T) { - assertFileContent(t, filepath.Join("public", "index.html"), false, "Base: index") + assertFileContent(t, fs, filepath.Join("public", "index.html"), false, "Base: index") }, }, { // Variant 3 func(t *testing.T) { - writeSource(t, filepath.Join("layouts", "_default", "list-baseof.html"), `Base: {{block "main" .}}block{{end}}`) - writeSource(t, filepath.Join("layouts", "_default", "list.html"), `{{define "main"}}list{{ end }}`) + writeSource(t, fs, filepath.Join("layouts", "_default", "list-baseof.html"), `Base: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), `{{define "main"}}list{{ end }}`) }, func(t *testing.T) { - assertFileContent(t, filepath.Join("public", "sect", "index.html"), false, "Base: list") + assertFileContent(t, fs, filepath.Join("public", "sect", "index.html"), false, "Base: list") }, }, { // Variant 4 func(t *testing.T) { - writeSource(t, filepath.Join("layouts", "_default", "baseof.html"), `Base: {{block "main" .}}block{{end}}`) - writeSource(t, filepath.Join("layouts", "_default", "list.html"), `{{define "main"}}list{{ end }}`) + writeSource(t, fs, filepath.Join("layouts", "_default", "baseof.html"), `Base: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), `{{define "main"}}list{{ end }}`) }, func(t *testing.T) { - assertFileContent(t, filepath.Join("public", "sect", "index.html"), false, "Base: list") + assertFileContent(t, fs, filepath.Join("public", "sect", "index.html"), false, "Base: list") }, }, { // Variant 1, theme, use project's base func(t *testing.T) { viper.Set("theme", "mytheme") - writeSource(t, filepath.Join("layouts", "section", "sect-baseof.html"), `Base: {{block "main" .}}block{{end}}`) - writeSource(t, filepath.Join("themes", "mytheme", "layouts", "section", "sect-baseof.html"), `Base Theme: {{block "main" .}}block{{end}}`) - writeSource(t, filepath.Join("layouts", "section", "sect.html"), `{{define "main"}}sect{{ end }}`) + writeSource(t, fs, filepath.Join("layouts", "section", "sect-baseof.html"), `Base: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "section", "sect-baseof.html"), `Base Theme: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("layouts", "section", "sect.html"), `{{define "main"}}sect{{ end }}`) }, func(t *testing.T) { - assertFileContent(t, filepath.Join("public", "sect", "index.html"), false, "Base: sect") + assertFileContent(t, fs, filepath.Join("public", "sect", "index.html"), false, "Base: sect") }, }, { // Variant 1, theme, use theme's base func(t *testing.T) { viper.Set("theme", "mytheme") - writeSource(t, filepath.Join("themes", "mytheme", "layouts", "section", "sect-baseof.html"), `Base Theme: {{block "main" .}}block{{end}}`) - writeSource(t, filepath.Join("layouts", "section", "sect.html"), `{{define "main"}}sect{{ end }}`) + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "section", "sect-baseof.html"), `Base Theme: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("layouts", "section", "sect.html"), `{{define "main"}}sect{{ end }}`) }, func(t *testing.T) { - assertFileContent(t, filepath.Join("public", "sect", "index.html"), false, "Base Theme: sect") + assertFileContent(t, fs, filepath.Join("public", "sect", "index.html"), false, "Base Theme: sect") }, }, { // Variant 4, theme, use project's base func(t *testing.T) { viper.Set("theme", "mytheme") - writeSource(t, filepath.Join("layouts", "_default", "baseof.html"), `Base: {{block "main" .}}block{{end}}`) - writeSource(t, filepath.Join("themes", "mytheme", "layouts", "_default", "baseof.html"), `Base Theme: {{block "main" .}}block{{end}}`) - writeSource(t, filepath.Join("themes", "mytheme", "layouts", "_default", "list.html"), `{{define "main"}}list{{ end }}`) + writeSource(t, fs, filepath.Join("layouts", "_default", "baseof.html"), `Base: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "_default", "baseof.html"), `Base Theme: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "_default", "list.html"), `{{define "main"}}list{{ end }}`) }, func(t *testing.T) { - assertFileContent(t, filepath.Join("public", "sect", "index.html"), false, "Base: list") + assertFileContent(t, fs, filepath.Join("public", "sect", "index.html"), false, "Base: list") }, }, { // Variant 4, theme, use themes's base func(t *testing.T) { viper.Set("theme", "mytheme") - writeSource(t, filepath.Join("themes", "mytheme", "layouts", "_default", "baseof.html"), `Base Theme: {{block "main" .}}block{{end}}`) - writeSource(t, filepath.Join("themes", "mytheme", "layouts", "_default", "list.html"), `{{define "main"}}list{{ end }}`) + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "_default", "baseof.html"), `Base Theme: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "_default", "list.html"), `{{define "main"}}list{{ end }}`) }, func(t *testing.T) { - assertFileContent(t, filepath.Join("public", "sect", "index.html"), false, "Base Theme: list") + assertFileContent(t, fs, filepath.Join("public", "sect", "index.html"), false, "Base Theme: list") }, }, } { testCommonResetState() - writeSource(t, filepath.Join("content", "sect", "page.md"), `--- + fs = hugofs.NewMem() + + writeSource(t, fs, filepath.Join("content", "sect", "page.md"), `--- title: Template test --- Some content `) this.setup(t) - if err := buildAndRenderSite(NewSiteDefaultLang()); err != nil { - t.Fatalf("[%d] Failed to build site: %s", i, err) - } + buildSingleSite(t, deps.DepsCfg{Fs: fs}, BuildCfg{}) this.assert(t) diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go new file mode 100644 index 00000000..1d775aca --- /dev/null +++ b/hugolib/testhelpers_test.go @@ -0,0 +1,53 @@ +package hugolib + +import ( + "path/filepath" + "testing" + + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/helpers" + "github.com/spf13/hugo/hugofs" + "github.com/spf13/hugo/source" + "github.com/spf13/hugo/tplapi" + "github.com/spf13/viper" + + "github.com/stretchr/testify/require" +) + +func newTestDepsConfig() deps.DepsCfg { + return deps.DepsCfg{Fs: hugofs.NewMem()} +} + +func newTestPathSpec() *helpers.PathSpec { + return helpers.NewPathSpec(hugofs.NewMem(), viper.GetViper()) +} + +func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tplapi.Template) error { + + return func(templ tplapi.Template) error { + for i := 0; i < len(additionalTemplates); i += 2 { + err := templ.AddTemplate(additionalTemplates[i], additionalTemplates[i+1]) + if err != nil { + return err + } + } + return nil + } +} + +func buildSingleSite(t *testing.T, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { + h, err := NewHugoSitesFromConfiguration(depsCfg) + + require.NoError(t, err) + require.Len(t, h.Sites, 1) + + require.NoError(t, h.Build(buildCfg)) + + return h.Sites[0] +} + +func writeSourcesToSource(t *testing.T, base string, fs *hugofs.Fs, sources ...source.ByteSource) { + for _, src := range sources { + writeSource(t, fs, filepath.Join(base, src.Name), string(src.Content)) + } +} diff --git a/source/filesystem.go b/source/filesystem.go index 7873b47f..6089824a 100644 --- a/source/filesystem.go +++ b/source/filesystem.go @@ -38,6 +38,12 @@ type Filesystem struct { files []*File Base string AvoidPaths []string + + fs *hugofs.Fs +} + +func NewFilesystem(fs *hugofs.Fs, base string, avoidPaths ...string) *Filesystem { + return &Filesystem{fs: fs, Base: base, AvoidPaths: avoidPaths} } func (f *Filesystem) FilesByExts(exts ...string) []*File { @@ -92,7 +98,7 @@ func (f *Filesystem) captureFiles() { return err } if b { - rd, err := NewLazyFileReader(hugofs.Source(), filePath) + rd, err := NewLazyFileReader(f.fs.Source, filePath) if err != nil { return err } @@ -101,7 +107,10 @@ func (f *Filesystem) captureFiles() { return err } - err := helpers.SymbolicWalk(hugofs.Source(), f.Base, walker) + if f.fs == nil { + panic("Must have a fs") + } + err := helpers.SymbolicWalk(f.fs.Source, f.Base, walker) if err != nil { jww.ERROR.Println(err) @@ -119,7 +128,7 @@ func (f *Filesystem) shouldRead(filePath string, fi os.FileInfo) (bool, error) { jww.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", filePath, err) return false, nil } - linkfi, err := hugofs.Source().Stat(link) + linkfi, err := f.fs.Source.Stat(link) if err != nil { jww.ERROR.Printf("Cannot stat '%s', error was: %s", link, err) return false, nil diff --git a/source/filesystem_test.go b/source/filesystem_test.go index a1e111d2..598a1b81 100644 --- a/source/filesystem_test.go +++ b/source/filesystem_test.go @@ -19,10 +19,12 @@ import ( "runtime" "strings" "testing" + + "github.com/spf13/hugo/hugofs" ) func TestEmptySourceFilesystem(t *testing.T) { - src := &Filesystem{Base: "Empty"} + src := NewFilesystem(hugofs.NewMem(), "Empty") if len(src.Files()) != 0 { t.Errorf("new filesystem should contain 0 files.") } @@ -37,13 +39,12 @@ type TestPath struct { } func TestAddFile(t *testing.T) { + fs := hugofs.NewMem() tests := platformPaths for _, test := range tests { base := platformBase - srcDefault := new(Filesystem) - srcWithBase := &Filesystem{ - Base: base, - } + srcDefault := NewFilesystem(fs, "") + srcWithBase := NewFilesystem(fs, base) for _, src := range []*Filesystem{srcDefault, srcWithBase} { @@ -99,8 +100,10 @@ func TestUnicodeNorm(t *testing.T) { {NFC: "é", NFD: "\x65\xcc\x81"}, } + fs := hugofs.NewMem() + for _, path := range paths { - src := new(Filesystem) + src := NewFilesystem(fs, "") _ = src.add(path.NFD, strings.NewReader("")) f := src.Files()[0] if f.BaseFileName() != path.NFC { diff --git a/target/file.go b/target/file.go index 740741bb..6bf27ba0 100644 --- a/target/file.go +++ b/target/file.go @@ -41,6 +41,8 @@ type Output interface { type Filesystem struct { PublishDir string + + Fs *hugofs.Fs } func (fs *Filesystem) Publish(path string, r io.Reader) (err error) { @@ -49,7 +51,7 @@ func (fs *Filesystem) Publish(path string, r io.Reader) (err error) { return } - return helpers.WriteToDisk(translated, r, hugofs.Destination()) + return helpers.WriteToDisk(translated, r, fs.Fs.Destination) } func (fs *Filesystem) Translate(src string) (dest string, err error) { diff --git a/target/htmlredirect.go b/target/htmlredirect.go index 7444dd42..00f5d71d 100644 --- a/target/htmlredirect.go +++ b/target/htmlredirect.go @@ -46,6 +46,8 @@ type HTMLRedirectAlias struct { PublishDir string Templates *template.Template AllowRoot bool // for the language redirects + + Fs *hugofs.Fs } func (h *HTMLRedirectAlias) Translate(alias string) (aliasPath string, err error) { @@ -145,5 +147,5 @@ func (h *HTMLRedirectAlias) Publish(path string, permalink string, page interfac return } - return helpers.WriteToDisk(path, buffer, hugofs.Destination()) + return helpers.WriteToDisk(path, buffer, h.Fs.Destination) } diff --git a/target/page.go b/target/page.go index ab38ded5..bfa431aa 100644 --- a/target/page.go +++ b/target/page.go @@ -35,6 +35,8 @@ type PagePub struct { // LangDir will contain the subdir for the language, i.e. "en", "de" etc. // It will be empty if the site is rendered in root. LangDir string + + Fs *hugofs.Fs } func (pp *PagePub) Publish(path string, r io.Reader) (err error) { @@ -44,7 +46,7 @@ func (pp *PagePub) Publish(path string, r io.Reader) (err error) { return } - return helpers.WriteToDisk(translated, r, hugofs.Destination()) + return helpers.WriteToDisk(translated, r, pp.Fs.Destination) } func (pp *PagePub) Translate(src string) (dest string, err error) { diff --git a/target/page_test.go b/target/page_test.go index b55726af..84412004 100644 --- a/target/page_test.go +++ b/target/page_test.go @@ -16,9 +16,13 @@ package target import ( "path/filepath" "testing" + + "github.com/spf13/hugo/hugofs" ) func TestPageTranslator(t *testing.T) { + fs := hugofs.NewMem() + tests := []struct { content string expected string @@ -37,7 +41,7 @@ func TestPageTranslator(t *testing.T) { } for _, test := range tests { - f := new(PagePub) + f := &PagePub{Fs: fs} dest, err := f.Translate(filepath.FromSlash(test.content)) expected := filepath.FromSlash(test.expected) if err != nil { diff --git a/tpl/amber_compiler.go b/tpl/amber_compiler.go new file mode 100644 index 00000000..4477f6ac --- /dev/null +++ b/tpl/amber_compiler.go @@ -0,0 +1,42 @@ +// Copyright 2017 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 ( + "html/template" + + "github.com/eknkc/amber" +) + +func (gt *GoHTMLTemplate) CompileAmberWithTemplate(b []byte, path string, t *template.Template) (*template.Template, error) { + c := amber.New() + + if err := c.ParseData(b, path); err != nil { + return nil, err + } + + data, err := c.CompileString() + + if err != nil { + return nil, err + } + + tpl, err := t.Funcs(gt.amberFuncMap).Parse(data) + + if err != nil { + return nil, err + } + + return tpl, nil +} diff --git a/tpl/template.go b/tpl/template.go index 867c0a2e..1c71989f 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -24,33 +24,12 @@ import ( "github.com/eknkc/amber" "github.com/spf13/afero" bp "github.com/spf13/hugo/bufferpool" + "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" - "github.com/spf13/hugo/hugofs" - jww "github.com/spf13/jwalterweatherman" "github.com/yosssi/ace" ) // TODO(bep) globals get rid of the rest of the jww.ERR etc. -//var tmpl *GoHTMLTemplate - -// TODO(bep) an interface with hundreds of methods ... remove it. -// And unexport most of these methods. -type Template interface { - ExecuteTemplate(wr io.Writer, name string, data interface{}) error - Lookup(name string) *template.Template - Templates() []*template.Template - New(name string) *template.Template - GetClone() *template.Template - LoadTemplates(absPath string) - LoadTemplatesWithPrefix(absPath, prefix string) - AddTemplate(name, tpl string) error - AddTemplateFileWithMaster(name, overlayFilename, masterFilename string) error - AddAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error - AddInternalTemplate(prefix, name, tpl string) error - AddInternalShortcode(name, tpl string) error - PrintErrors() - Funcs(funcMap template.FuncMap) -} type templateErr struct { name string @@ -70,52 +49,105 @@ type GoHTMLTemplate struct { funcster *templateFuncster - // TODO(bep) globals template - log *jww.Notepad + amberFuncMap template.FuncMap + + *deps.Deps } -// New returns a new Hugo Template System +type TemplateProvider struct{} + +var DefaultTemplateProvider *TemplateProvider + +// Update updates the Hugo Template System in the provided Deps. // with all the additional features, templates & functions -func New(logger *jww.Notepad, withTemplate ...func(templ Template) error) *GoHTMLTemplate { +func (*TemplateProvider) Update(deps *deps.Deps) error { + // TODO(bep) check that this isn't called too many times. tmpl := &GoHTMLTemplate{ Template: template.New(""), overlays: make(map[string]*template.Template), errors: make([]*templateErr, 0), - log: logger, + Deps: deps, } - tmpl.funcster = newTemplateFuncster(tmpl) + deps.Tmpl = tmpl - // The URL funcs in the funcMap is somewhat language dependent, - // so we need to wait until the language and site config is loaded. - // TODO(bep) globals - tmpl.funcster.initFuncMap() - - // TODO(bep) globals - for k, v := range tmpl.funcster.funcMap { - amber.FuncMap[k] = v - } + tmpl.initFuncs(deps) tmpl.LoadEmbedded() - for _, wt := range withTemplate { - err := wt(tmpl) + if deps.WithTemplate != nil { + err := deps.WithTemplate(tmpl) if err != nil { tmpl.errors = append(tmpl.errors, &templateErr{"init", err}) } } - tmpl.markReady() + tmpl.MarkReady() + + return nil + +} + +// Clone clones +func (*TemplateProvider) Clone(d *deps.Deps) error { + + t := d.Tmpl.(*GoHTMLTemplate) + + // 1. Clone the clone with new template funcs + // 2. Clone any overlays with new template funcs + + tmpl := &GoHTMLTemplate{ + Template: template.Must(t.Template.Clone()), + overlays: make(map[string]*template.Template), + errors: make([]*templateErr, 0), + Deps: d, + } + + d.Tmpl = tmpl + tmpl.initFuncs(d) + + for k, v := range t.overlays { + vc := template.Must(v.Clone()) + vc.Funcs(tmpl.funcster.funcMap) + tmpl.overlays[k] = vc + } + + tmpl.MarkReady() + + return nil +} + +func (t *GoHTMLTemplate) initFuncs(d *deps.Deps) { + + t.funcster = newTemplateFuncster(d) + + // The URL funcs in the funcMap is somewhat language dependent, + // so we need to wait until the language and site config is loaded. + t.funcster.initFuncMap() + + t.amberFuncMap = template.FuncMap{} + + for k, v := range amber.FuncMap { + t.amberFuncMap[k] = v + } + + for k, v := range t.funcster.funcMap { + t.amberFuncMap[k] = v + // Hacky, but we need to make sure that the func names are in the global map. + amber.FuncMap[k] = func() string { + panic("should never be invoked") + return "" + } + } - return tmpl } func (t *GoHTMLTemplate) Funcs(funcMap template.FuncMap) { t.Template.Funcs(funcMap) } -func (t *GoHTMLTemplate) partial(name string, contextList ...interface{}) template.HTML { +func (t *GoHTMLTemplate) Partial(name string, contextList ...interface{}) template.HTML { if strings.HasPrefix("partials/", name) { name = name[8:] } @@ -147,8 +179,8 @@ func (t *GoHTMLTemplate) executeTemplate(context interface{}, w io.Writer, layou } } if !worked { - t.log.ERROR.Println("Unable to render", layouts) - t.log.ERROR.Println("Expecting to find a template in either the theme/layouts or /layouts in one of the following relative locations", layouts) + t.Log.ERROR.Println("Unable to render", layouts) + t.Log.ERROR.Println("Expecting to find a template in either the theme/layouts or /layouts in one of the following relative locations", layouts) } } @@ -186,9 +218,9 @@ func (t *GoHTMLTemplate) LoadEmbedded() { t.EmbedTemplates() } -// markReady marks the template as "ready for execution". No changes allowed +// MarkReady marks the template as "ready for execution". No changes allowed // after this is set. -func (t *GoHTMLTemplate) markReady() { +func (t *GoHTMLTemplate) MarkReady() { if t.clone == nil { t.clone = template.Must(t.Template.Clone()) } @@ -244,7 +276,7 @@ func (t *GoHTMLTemplate) AddTemplateFileWithMaster(name, overlayFilename, master masterTpl := t.Lookup(masterFilename) if masterTpl == nil { - b, err := afero.ReadFile(hugofs.Source(), masterFilename) + b, err := afero.ReadFile(t.Fs.Source, masterFilename) if err != nil { return err } @@ -257,7 +289,7 @@ func (t *GoHTMLTemplate) AddTemplateFileWithMaster(name, overlayFilename, master } } - b, err := afero.ReadFile(hugofs.Source(), overlayFilename) + b, err := afero.ReadFile(t.Fs.Source, overlayFilename) if err != nil { return err } @@ -315,19 +347,13 @@ func (t *GoHTMLTemplate) AddTemplateFile(name, baseTemplatePath, path string) er switch ext { case ".amber": templateName := strings.TrimSuffix(name, filepath.Ext(name)) + ".html" - compiler := amber.New() - b, err := afero.ReadFile(hugofs.Source(), path) + b, err := afero.ReadFile(t.Fs.Source, path) if err != nil { return err } - // Parse the input data - if err := compiler.ParseData(b, path); err != nil { - return err - } - - templ, err := compiler.CompileWithTemplate(t.New(templateName)) + templ, err := t.CompileAmberWithTemplate(b, path, t.New(templateName)) if err != nil { return err } @@ -335,14 +361,14 @@ func (t *GoHTMLTemplate) AddTemplateFile(name, baseTemplatePath, path string) er return applyTemplateTransformers(templ) case ".ace": var innerContent, baseContent []byte - innerContent, err := afero.ReadFile(hugofs.Source(), path) + innerContent, err := afero.ReadFile(t.Fs.Source, path) if err != nil { return err } if baseTemplatePath != "" { - baseContent, err = afero.ReadFile(hugofs.Source(), baseTemplatePath) + baseContent, err = afero.ReadFile(t.Fs.Source, baseTemplatePath) if err != nil { return err } @@ -355,13 +381,13 @@ func (t *GoHTMLTemplate) AddTemplateFile(name, baseTemplatePath, path string) er return t.AddTemplateFileWithMaster(name, path, baseTemplatePath) } - b, err := afero.ReadFile(hugofs.Source(), path) + b, err := afero.ReadFile(t.Fs.Source, path) if err != nil { return err } - t.log.DEBUG.Printf("Add template file from path %s", path) + t.Log.DEBUG.Printf("Add template file from path %s", path) return t.AddTemplate(name, string(b)) } @@ -391,25 +417,25 @@ func isBaseTemplate(path string) bool { } func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) { - t.log.DEBUG.Printf("Load templates from path %q prefix %q", absPath, prefix) + t.Log.DEBUG.Printf("Load templates from path %q prefix %q", absPath, prefix) walker := func(path string, fi os.FileInfo, err error) error { if err != nil { return nil } - t.log.DEBUG.Println("Template path", path) + t.Log.DEBUG.Println("Template path", path) if fi.Mode()&os.ModeSymlink == os.ModeSymlink { link, err := filepath.EvalSymlinks(absPath) if err != nil { - t.log.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", absPath, err) + t.Log.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", absPath, err) return nil } - linkfi, err := hugofs.Source().Stat(link) + linkfi, err := t.Fs.Source.Stat(link) if err != nil { - t.log.ERROR.Printf("Cannot stat '%s', error was: %s", link, err) + t.Log.ERROR.Printf("Cannot stat '%s', error was: %s", link, err) return nil } if !linkfi.Mode().IsRegular() { - t.log.ERROR.Printf("Symbolic links for directories not supported, skipping '%s'", absPath) + t.Log.ERROR.Printf("Symbolic links for directories not supported, skipping '%s'", absPath) } return nil } @@ -441,7 +467,7 @@ func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) { // This may be a view that shouldn't have base template // Have to look inside it to make sure - needsBase, err := helpers.FileContainsAny(path, innerMarkers, hugofs.Source()) + needsBase, err := helpers.FileContainsAny(path, innerMarkers, t.Fs.Source) if err != nil { return err } @@ -482,7 +508,7 @@ func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) { for _, pair := range pairsToCheck { pathsToCheck := basePathsToCheck(pair, layoutDir, themeDir) for _, pathToCheck := range pathsToCheck { - if ok, err := helpers.Exists(pathToCheck, hugofs.Source()); err == nil && ok { + if ok, err := helpers.Exists(pathToCheck, t.Fs.Source); err == nil && ok { baseTemplatePath = pathToCheck break Loop } @@ -492,14 +518,14 @@ func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) { } if err := t.AddTemplateFile(tplName, baseTemplatePath, path); err != nil { - t.log.ERROR.Printf("Failed to add template %s in path %s: %s", tplName, path, err) + t.Log.ERROR.Printf("Failed to add template %s in path %s: %s", tplName, path, err) } } return nil } - if err := helpers.SymbolicWalk(hugofs.Source(), absPath, walker); err != nil { - t.log.ERROR.Printf("Failed to load templates: %s", err) + if err := helpers.SymbolicWalk(t.Fs.Source, absPath, walker); err != nil { + t.Log.ERROR.Printf("Failed to load templates: %s", err) } } @@ -526,6 +552,6 @@ func (t *GoHTMLTemplate) LoadTemplates(absPath string) { func (t *GoHTMLTemplate) PrintErrors() { for i, e := range t.errors { - t.log.ERROR.Println(i, ":", e.err) + t.Log.ERROR.Println(i, ":", e.err) } } diff --git a/tpl/template_funcs.go b/tpl/template_funcs.go index 8f653808..5db5e54a 100644 --- a/tpl/template_funcs.go +++ b/tpl/template_funcs.go @@ -43,8 +43,8 @@ import ( "github.com/bep/inflect" "github.com/spf13/afero" "github.com/spf13/cast" + "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" - "github.com/spf13/hugo/hugofs" jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" @@ -56,14 +56,15 @@ import ( // Some of the template funcs are'nt entirely stateless. type templateFuncster struct { - t *GoHTMLTemplate funcMap template.FuncMap cachedPartials partialCache + + *deps.Deps } -func newTemplateFuncster(t *GoHTMLTemplate) *templateFuncster { +func newTemplateFuncster(deps *deps.Deps) *templateFuncster { return &templateFuncster{ - t: t, + Deps: deps, cachedPartials: partialCache{p: make(map[string]template.HTML)}, } } @@ -424,7 +425,7 @@ func resetImageConfigCache() { // imageConfig returns the image.Config for the specified path relative to the // working directory. resetImageConfigCache must be run beforehand. -func imageConfig(path interface{}) (image.Config, error) { +func (t *templateFuncster) imageConfig(path interface{}) (image.Config, error) { filename, err := cast.ToStringE(path) if err != nil { return image.Config{}, err @@ -443,7 +444,7 @@ func imageConfig(path interface{}) (image.Config, error) { return config, nil } - f, err := hugofs.WorkingDir().Open(filename) + f, err := t.Fs.WorkingDir.Open(filename) if err != nil { return image.Config{}, err } @@ -1013,7 +1014,7 @@ func where(seq, key interface{}, args ...interface{}) (interface{}, error) { } // apply takes a map, array, or slice and returns a new slice with the function fname applied over it. -func (tf *templateFuncster) apply(seq interface{}, fname string, args ...interface{}) (interface{}, error) { +func (t *templateFuncster) apply(seq interface{}, fname string, args ...interface{}) (interface{}, error) { if seq == nil { return make([]interface{}, 0), nil } @@ -1028,7 +1029,7 @@ func (tf *templateFuncster) apply(seq interface{}, fname string, args ...interfa return nil, errors.New("can't iterate over a nil value") } - fn, found := tf.funcMap[fname] + fn, found := t.funcMap[fname] if !found { return nil, errors.New("can't find function " + fname) } @@ -1528,26 +1529,27 @@ type partialCache struct { // Get retrieves partial output from the cache based upon the partial name. // If the partial is not found in the cache, the partial is rendered and added // to the cache. -func (tf *templateFuncster) Get(key, name string, context interface{}) (p template.HTML) { +func (t *templateFuncster) Get(key, name string, context interface{}) (p template.HTML) { var ok bool - tf.cachedPartials.RLock() - p, ok = tf.cachedPartials.p[key] - tf.cachedPartials.RUnlock() + t.cachedPartials.RLock() + p, ok = t.cachedPartials.p[key] + t.cachedPartials.RUnlock() if ok { return p } - tf.cachedPartials.Lock() - if p, ok = tf.cachedPartials.p[key]; !ok { - tf.cachedPartials.Unlock() - p = tf.t.partial(name, context) + t.cachedPartials.Lock() + if p, ok = t.cachedPartials.p[key]; !ok { + t.cachedPartials.Unlock() + p = t.Tmpl.Partial(name, context) + + t.cachedPartials.Lock() + t.cachedPartials.p[key] = p - tf.cachedPartials.Lock() - tf.cachedPartials.p[key] = p } - tf.cachedPartials.Unlock() + t.cachedPartials.Unlock() return p } @@ -1556,14 +1558,14 @@ func (tf *templateFuncster) Get(key, name string, context interface{}) (p templa // string parameter (a string slice actually, but be only use a variadic // argument to make it optional) can be passed so that a given partial can have // multiple uses. The cache is created with name+variant as the key. -func (tf *templateFuncster) partialCached(name string, context interface{}, variant ...string) template.HTML { +func (t *templateFuncster) partialCached(name string, context interface{}, variant ...string) template.HTML { key := name if len(variant) > 0 { for i := 0; i < len(variant); i++ { key += variant[i] } } - return tf.Get(key, name, context) + return t.Get(key, name, context) } // regexpCache represents a cache of regexp objects protected by a mutex. @@ -1814,23 +1816,23 @@ func readFile(fs *afero.BasePathFs, filename string) (string, error) { // configured WorkingDir. // It returns the contents as a string. // There is a upper size limit set at 1 megabytes. -func readFileFromWorkingDir(i interface{}) (string, error) { +func (t *templateFuncster) readFileFromWorkingDir(i interface{}) (string, error) { s, err := cast.ToStringE(i) if err != nil { return "", err } - return readFile(hugofs.WorkingDir(), s) + return readFile(t.Fs.WorkingDir, s) } // readDirFromWorkingDir listst the directory content relative to the // configured WorkingDir. -func readDirFromWorkingDir(i interface{}) ([]os.FileInfo, error) { +func (t *templateFuncster) readDirFromWorkingDir(i interface{}) ([]os.FileInfo, error) { path, err := cast.ToStringE(i) if err != nil { return nil, err } - list, err := afero.ReadDir(hugofs.WorkingDir(), path) + list, err := afero.ReadDir(t.Fs.WorkingDir, path) if err != nil { return nil, fmt.Errorf("Failed to read Directory %s with error message %s", path, err) @@ -2074,20 +2076,20 @@ func htmlUnescape(in interface{}) (string, error) { return html.UnescapeString(conv), nil } -func absURL(a interface{}) (template.HTML, error) { +func (t *templateFuncster) absURL(a interface{}) (template.HTML, error) { s, err := cast.ToStringE(a) if err != nil { return "", nil } - return template.HTML(helpers.CurrentPathSpec().AbsURL(s, false)), nil + return template.HTML(t.PathSpec.AbsURL(s, false)), nil } -func relURL(a interface{}) (template.HTML, error) { +func (t *templateFuncster) relURL(a interface{}) (template.HTML, error) { s, err := cast.ToStringE(a) if err != nil { return "", nil } - return template.HTML(helpers.CurrentPathSpec().RelURL(s, false)), nil + return template.HTML(t.PathSpec.RelURL(s, false)), nil } // getenv retrieves the value of the environment variable named by the key. @@ -2101,19 +2103,19 @@ func getenv(key interface{}) (string, error) { return os.Getenv(skey), nil } -func (tf *templateFuncster) initFuncMap() { +func (t *templateFuncster) initFuncMap() { funcMap := template.FuncMap{ - "absURL": absURL, + "absURL": t.absURL, "absLangURL": func(i interface{}) (template.HTML, error) { s, err := cast.ToStringE(i) if err != nil { return "", err } - return template.HTML(helpers.CurrentPathSpec().AbsURL(s, true)), nil + return template.HTML(t.PathSpec.AbsURL(s, true)), nil }, "add": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '+') }, "after": after, - "apply": tf.apply, + "apply": t.apply, "base64Decode": base64Decode, "base64Encode": base64Encode, "chomp": chomp, @@ -2130,8 +2132,8 @@ func (tf *templateFuncster) initFuncMap() { "findRE": findRE, "first": first, "ge": ge, - "getCSV": getCSV, - "getJSON": getJSON, + "getCSV": t.getCSV, + "getJSON": t.getJSON, "getenv": getenv, "gt": gt, "hasPrefix": hasPrefix, @@ -2139,7 +2141,7 @@ func (tf *templateFuncster) initFuncMap() { "htmlEscape": htmlEscape, "htmlUnescape": htmlUnescape, "humanize": humanize, - "imageConfig": imageConfig, + "imageConfig": t.imageConfig, "in": in, "index": index, "int": func(v interface{}) (int, error) { return cast.ToIntE(v) }, @@ -2158,21 +2160,21 @@ func (tf *templateFuncster) initFuncMap() { "mul": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '*') }, "ne": ne, "now": func() time.Time { return time.Now() }, - "partial": tf.t.partial, - "partialCached": tf.partialCached, + "partial": t.Tmpl.Partial, + "partialCached": t.partialCached, "plainify": plainify, "pluralize": pluralize, "querify": querify, - "readDir": readDirFromWorkingDir, - "readFile": readFileFromWorkingDir, + "readDir": t.readDirFromWorkingDir, + "readFile": t.readFileFromWorkingDir, "ref": ref, - "relURL": relURL, + "relURL": t.relURL, "relLangURL": func(i interface{}) (template.HTML, error) { s, err := cast.ToStringE(i) if err != nil { return "", err } - return template.HTML(helpers.CurrentPathSpec().RelURL(s, true)), nil + return template.HTML(t.PathSpec.RelURL(s, true)), nil }, "relref": relRef, "replace": replace, @@ -2201,12 +2203,12 @@ func (tf *templateFuncster) initFuncMap() { "trim": trim, "truncate": truncate, "upper": upper, - "urlize": helpers.CurrentPathSpec().URLize, + "urlize": t.PathSpec.URLize, "where": where, "i18n": i18nTranslate, "T": i18nTranslate, } - tf.funcMap = funcMap - tf.t.Funcs(funcMap) + t.funcMap = funcMap + t.Tmpl.Funcs(funcMap) } diff --git a/tpl/template_funcs_test.go b/tpl/template_funcs_test.go index e0c18509..e5d0193a 100644 --- a/tpl/template_funcs_test.go +++ b/tpl/template_funcs_test.go @@ -31,6 +31,9 @@ import ( "testing" "time" + "github.com/spf13/hugo/tplapi" + + "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" "io/ioutil" @@ -43,9 +46,17 @@ import ( jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -var logger = jww.NewNotepad(jww.LevelFatal, jww.LevelFatal, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) +var ( + logger = jww.NewNotepad(jww.LevelFatal, jww.LevelFatal, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) + defaultDepsConfig = deps.DepsCfg{ + Language: helpers.NewLanguage("en"), + Logger: logger, + TemplateProvider: DefaultTemplateProvider, + } +) type tstNoStringer struct { } @@ -80,8 +91,7 @@ func tstInitTemplates() { func TestFuncsInTemplate(t *testing.T) { - viper.Reset() - defer viper.Reset() + testReset() workingDir := "/home/hugo" @@ -89,10 +99,9 @@ func TestFuncsInTemplate(t *testing.T) { viper.Set("currentContentLanguage", helpers.NewDefaultLanguage()) viper.Set("multilingual", true) - fs := &afero.MemMapFs{} - hugofs.InitFs(fs) + fs := hugofs.NewMem() - afero.WriteFile(fs, filepath.Join(workingDir, "README.txt"), []byte("Hugo Rocks!"), 0755) + afero.WriteFile(fs.Source, filepath.Join(workingDir, "README.txt"), []byte("Hugo Rocks!"), 0755) // Add the examples from the docs: As a smoke test and to make sure the examples work. // TODO(bep): docs: fix title example @@ -244,7 +253,7 @@ urlize: bat-man ` var b bytes.Buffer - templ, err := New(logger).New("test").Parse(in) + var data struct { Title string Section string @@ -259,11 +268,21 @@ urlize: bat-man tstInitTemplates() - if err != nil { - t.Fatal("Got error on parse", err) + config := defaultDepsConfig + config.WithTemplate = func(templ tplapi.Template) error { + if _, err := templ.New("test").Parse(in); err != nil { + t.Fatal("Got error on parse", err) + } + return nil + } + config.Fs = fs + + d := deps.New(config) + if err := d.LoadTemplates(); err != nil { + t.Fatal(err) } - err = templ.Execute(&b, &data) + err := d.Tmpl.Lookup("test").Execute(&b, &data) if err != nil { t.Fatal("Got error on execute", err) @@ -624,15 +643,13 @@ func blankImage(width, height int) []byte { } func TestImageConfig(t *testing.T) { - viper.Reset() - defer viper.Reset() + testReset() workingDir := "/home/hugo" viper.Set("workingDir", workingDir) - fs := &afero.MemMapFs{} - hugofs.InitFs(fs) + f := newTestFuncster() for i, this := range []struct { resetCache bool @@ -692,13 +709,13 @@ func TestImageConfig(t *testing.T) { }, }, } { - afero.WriteFile(fs, filepath.Join(workingDir, this.path), this.input, 0755) + afero.WriteFile(f.Fs.Source, filepath.Join(workingDir, this.path), this.input, 0755) if this.resetCache { resetImageConfigCache() } - result, err := imageConfig(this.path) + result, err := f.imageConfig(this.path) if err != nil { t.Errorf("imageConfig returned error: %s", err) } @@ -712,15 +729,15 @@ func TestImageConfig(t *testing.T) { } } - if _, err := imageConfig(t); err == nil { + if _, err := f.imageConfig(t); err == nil { t.Error("Expected error from imageConfig when passed invalid path") } - if _, err := imageConfig("non-existent.png"); err == nil { + if _, err := f.imageConfig("non-existent.png"); err == nil { t.Error("Expected error from imageConfig when passed non-existent file") } - if _, err := imageConfig(""); err == nil { + if _, err := f.imageConfig(""); err == nil { t.Error("Expected error from imageConfig when passed empty path") } @@ -2381,14 +2398,11 @@ func TestDefault(t *testing.T) { {map[string]string{"foo": "dog"}, `{{ default "nope" .foo "extra" }}`, ``, false}, {map[string]interface{}{"images": []string{}}, `{{ default "default.jpg" (index .images 0) }}`, `default.jpg`, true}, } { - tmpl, err := New(logger).New("test").Parse(this.tpl) - if err != nil { - t.Errorf("[%d] unable to create new html template %q: %s", i, this.tpl, err) - continue - } + + tmpl := newTestTemplate(t, "test", this.tpl) buf := new(bytes.Buffer) - err = tmpl.Execute(buf, this.input) + err := tmpl.Execute(buf, this.input) if (err == nil) != this.ok { t.Errorf("[%d] execute template returned unexpected error: %s", i, err) continue @@ -2520,6 +2534,7 @@ func TestSafeCSS(t *testing.T) { } } +// TODO(bep) what is this? Also look above. func TestSafeJS(t *testing.T) { for i, this := range []struct { str string @@ -2560,6 +2575,7 @@ func TestSafeJS(t *testing.T) { } } +// TODO(bep) what is this? func TestSafeURL(t *testing.T) { for i, this := range []struct { str string @@ -2716,18 +2732,16 @@ func TestSHA256(t *testing.T) { } func TestReadFile(t *testing.T) { - viper.Reset() - defer viper.Reset() + testReset() workingDir := "/home/hugo" viper.Set("workingDir", workingDir) - fs := &afero.MemMapFs{} - hugofs.InitFs(fs) + f := newTestFuncster() - afero.WriteFile(fs, filepath.Join(workingDir, "/f/f1.txt"), []byte("f1-content"), 0755) - afero.WriteFile(fs, filepath.Join("/home", "f2.txt"), []byte("f2-content"), 0755) + afero.WriteFile(f.Fs.Source, filepath.Join(workingDir, "/f/f1.txt"), []byte("f1-content"), 0755) + afero.WriteFile(f.Fs.Source, filepath.Join("/home", "f2.txt"), []byte("f2-content"), 0755) for i, this := range []struct { filename string @@ -2739,7 +2753,7 @@ func TestReadFile(t *testing.T) { {filepath.FromSlash("f/f1.txt"), "f1-content"}, {filepath.FromSlash("../f2.txt"), false}, } { - result, err := readFileFromWorkingDir(this.filename) + result, err := f.readFileFromWorkingDir(this.filename) if b, ok := this.expect.(bool); ok && !b { if err == nil { t.Errorf("[%d] readFile didn't return an expected error", i) @@ -2770,8 +2784,6 @@ func TestPartialCached(t *testing.T) { {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . "%s" }}`, "header"}, } - results := make(map[string]string, len(testCases)) - var data struct { Title string Section string @@ -2791,26 +2803,32 @@ func TestPartialCached(t *testing.T) { tmp = tc.tmpl } - tmpl, err := New(logger).New("testroot").Parse(tmp) - if err != nil { - t.Fatalf("[%d] unable to create new html template: %s", i, err) + defaultDepsConfig.WithTemplate = func(templ tplapi.Template) error { + err := templ.AddTemplate("testroot", tmp) + if err != nil { + return err + } + err = templ.AddTemplate("partials/"+tc.name, tc.partial) + if err != nil { + return err + } + + return nil } - if tmpl == nil { - t.Fatalf("[%d] tmpl should not be nil!", i) - } - - tmpl.New("partials/" + tc.name).Parse(tc.partial) + de := deps.New(defaultDepsConfig) + require.NoError(t, de.LoadTemplates()) buf := new(bytes.Buffer) - err = tmpl.Execute(buf, &data) + templ := de.Tmpl.Lookup("testroot") + err := templ.Execute(buf, &data) if err != nil { t.Fatalf("[%d] error executing template: %s", i, err) } for j := 0; j < 10; j++ { buf2 := new(bytes.Buffer) - err = tmpl.Execute(buf2, nil) + err := templ.Execute(buf2, nil) if err != nil { t.Fatalf("[%d] error executing template 2nd time: %s", i, err) } @@ -2819,33 +2837,33 @@ func TestPartialCached(t *testing.T) { t.Fatalf("[%d] cached results do not match:\nResult 1:\n%q\nResult 2:\n%q", i, buf, buf2) } } - - // double-check against previous test cases of the same variant - previous, ok := results[tc.name+tc.variant] - if !ok { - results[tc.name+tc.variant] = buf.String() - } else { - if previous != buf.String() { - t.Errorf("[%d] cached variant differs from previous rendering; got:\n%q\nwant:\n%q", i, buf.String(), previous) - } - } } } func BenchmarkPartial(b *testing.B) { - tstInitTemplates() - tmpl, err := New(logger).New("testroot").Parse(`{{ partial "bench1" . }}`) - if err != nil { - b.Fatalf("unable to create new html template: %s", err) + defaultDepsConfig.WithTemplate = func(templ tplapi.Template) error { + err := templ.AddTemplate("testroot", `{{ partial "bench1" . }}`) + if err != nil { + return err + } + err = templ.AddTemplate("partials/bench1", `{{ shuffle (seq 1 10) }}`) + if err != nil { + return err + } + + return nil } - tmpl.New("partials/bench1").Parse(`{{ shuffle (seq 1 10) }}`) + de := deps.New(defaultDepsConfig) + require.NoError(b, de.LoadTemplates()) + buf := new(bytes.Buffer) + tmpl := de.Tmpl.Lookup("testroot") b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - if err = tmpl.Execute(buf, nil); err != nil { + if err := tmpl.Execute(buf, nil); err != nil { b.Fatalf("error executing template: %s", err) } buf.Reset() @@ -2853,38 +2871,29 @@ func BenchmarkPartial(b *testing.B) { } func BenchmarkPartialCached(b *testing.B) { - tstInitTemplates() - tmpl, err := New(logger).New("testroot").Parse(`{{ partialCached "bench1" . }}`) - if err != nil { - b.Fatalf("unable to create new html template: %s", err) - } - - tmpl.New("partials/bench1").Parse(`{{ shuffle (seq 1 10) }}`) - buf := new(bytes.Buffer) - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - if err = tmpl.Execute(buf, nil); err != nil { - b.Fatalf("error executing template: %s", err) + defaultDepsConfig.WithTemplate = func(templ tplapi.Template) error { + err := templ.AddTemplate("testroot", `{{ partialCached "bench1" . }}`) + if err != nil { + return err + } + err = templ.AddTemplate("partials/bench1", `{{ shuffle (seq 1 10) }}`) + if err != nil { + return err } - buf.Reset() - } -} -func BenchmarkPartialCachedVariants(b *testing.B) { - tmpl, err := New(logger).New("testroot").Parse(`{{ partialCached "bench1" . "header" }}`) - if err != nil { - b.Fatalf("unable to create new html template: %s", err) + return nil } - tmpl.New("partials/bench1").Parse(`{{ shuffle (seq 1 10) }}`) + de := deps.New(defaultDepsConfig) + require.NoError(b, de.LoadTemplates()) + buf := new(bytes.Buffer) + tmpl := de.Tmpl.Lookup("testroot") b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - if err = tmpl.Execute(buf, nil); err != nil { + if err := tmpl.Execute(buf, nil); err != nil { b.Fatalf("error executing template: %s", err) } buf.Reset() @@ -2892,5 +2901,25 @@ func BenchmarkPartialCachedVariants(b *testing.B) { } func newTestFuncster() *templateFuncster { - return New(logger).funcster + d := deps.New(defaultDepsConfig) + if err := d.LoadTemplates(); err != nil { + panic(err) + } + + return d.Tmpl.(*GoHTMLTemplate).funcster +} + +func newTestTemplate(t *testing.T, name, template string) *template.Template { + defaultDepsConfig.WithTemplate = func(templ tplapi.Template) error { + err := templ.AddTemplate(name, template) + if err != nil { + return err + } + return nil + } + + de := deps.New(defaultDepsConfig) + require.NoError(t, de.LoadTemplates()) + + return de.Tmpl.Lookup(name) } diff --git a/tpl/template_i18n.go b/tpl/template_i18n.go index a725856e..a7ec0df9 100644 --- a/tpl/template_i18n.go +++ b/tpl/template_i18n.go @@ -33,6 +33,7 @@ type translate struct { current bundle.TranslateFunc } +// TODO(bep) global translator var translator *translate // SetTranslateLang sets the translations language to use during template processing. diff --git a/tpl/template_resources.go b/tpl/template_resources.go index ee630579..13ebfb69 100644 --- a/tpl/template_resources.go +++ b/tpl/template_resources.go @@ -28,7 +28,6 @@ import ( "github.com/spf13/afero" "github.com/spf13/hugo/helpers" - "github.com/spf13/hugo/hugofs" jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" ) @@ -165,25 +164,25 @@ func resGetLocal(url string, fs afero.Fs) ([]byte, error) { } // resGetResource loads the content of a local or remote file -func resGetResource(url string) ([]byte, error) { +func (t *templateFuncster) resGetResource(url string) ([]byte, error) { if url == "" { return nil, nil } if strings.Contains(url, "://") { - return resGetRemote(url, hugofs.Source(), http.DefaultClient) + return resGetRemote(url, t.Fs.Source, http.DefaultClient) } - return resGetLocal(url, hugofs.Source()) + return resGetLocal(url, t.Fs.Source) } // getJSON expects one or n-parts of a URL to a resource which can either be a local or a remote one. // If you provide multiple parts they will be joined together to the final URL. // GetJSON returns nil or parsed JSON to use in a short code. -func getJSON(urlParts ...string) interface{} { +func (t *templateFuncster) getJSON(urlParts ...string) interface{} { var v interface{} url := strings.Join(urlParts, "") for i := 0; i <= resRetries; i++ { - c, err := resGetResource(url) + c, err := t.resGetResource(url) if err != nil { jww.ERROR.Printf("Failed to get json resource %s with error message %s", url, err) return nil @@ -194,7 +193,7 @@ func getJSON(urlParts ...string) interface{} { jww.ERROR.Printf("Cannot read json from resource %s with error message %s", url, err) jww.ERROR.Printf("Retry #%d for %s and sleeping for %s", i, url, resSleep) time.Sleep(resSleep) - resDeleteCache(url, hugofs.Source()) + resDeleteCache(url, t.Fs.Source) continue } break @@ -220,18 +219,18 @@ func parseCSV(c []byte, sep string) ([][]string, error) { // The data separator can be a comma, semi-colon, pipe, etc, but only one character. // If you provide multiple parts for the URL they will be joined together to the final URL. // GetCSV returns nil or a slice slice to use in a short code. -func getCSV(sep string, urlParts ...string) [][]string { +func (t *templateFuncster) getCSV(sep string, urlParts ...string) [][]string { var d [][]string url := strings.Join(urlParts, "") var clearCacheSleep = func(i int, u string) { jww.ERROR.Printf("Retry #%d for %s and sleeping for %s", i, url, resSleep) time.Sleep(resSleep) - resDeleteCache(url, hugofs.Source()) + resDeleteCache(url, t.Fs.Source) } for i := 0; i <= resRetries; i++ { - c, err := resGetResource(url) + c, err := t.resGetResource(url) if err == nil && !bytes.Contains(c, []byte(sep)) { err = errors.New("Cannot find separator " + sep + " in CSV.") diff --git a/tpl/template_resources_test.go b/tpl/template_resources_test.go index 3385c5ef..5a92f98d 100644 --- a/tpl/template_resources_test.go +++ b/tpl/template_resources_test.go @@ -80,8 +80,10 @@ func TestScpCache(t *testing.T) { } func TestScpGetLocal(t *testing.T) { - fs := new(afero.MemMapFs) + testReset() + fs := hugofs.NewMem() ps := helpers.FilePathSeparator + tests := []struct { path string content []byte @@ -95,12 +97,12 @@ func TestScpGetLocal(t *testing.T) { for _, test := range tests { r := bytes.NewReader(test.content) - err := helpers.WriteToDisk(test.path, r, fs) + err := helpers.WriteToDisk(test.path, r, fs.Source) if err != nil { t.Error(err) } - c, err := resGetLocal(test.path, fs) + c, err := resGetLocal(test.path, fs.Source) if err != nil { t.Errorf("Error getting resource content: %s", err) } @@ -212,9 +214,9 @@ type wd struct { Reset func() } -func testRetryWhenDone() wd { +func testRetryWhenDone(f *templateFuncster) wd { cd := viper.GetString("cacheDir") - viper.Set("cacheDir", helpers.GetTempDir("", hugofs.Source())) + viper.Set("cacheDir", helpers.GetTempDir("", f.Fs.Source)) var tmpSleep time.Duration tmpSleep, resSleep = resSleep, time.Millisecond return wd{func() { @@ -224,7 +226,10 @@ func testRetryWhenDone() wd { } func TestGetJSONFailParse(t *testing.T) { - defer testRetryWhenDone().Reset() + + f := newTestFuncster() + + defer testRetryWhenDone(f).Reset() reqCount := 0 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -242,7 +247,7 @@ func TestGetJSONFailParse(t *testing.T) { defer os.Remove(getCacheFileID(url)) want := map[string]interface{}{"gomeetup": []interface{}{"Sydney", "San Francisco", "Stockholm"}} - have := getJSON(url) + have := f.getJSON(url) assert.NotNil(t, have) if have != nil { assert.EqualValues(t, want, have) @@ -250,7 +255,9 @@ func TestGetJSONFailParse(t *testing.T) { } func TestGetCSVFailParseSep(t *testing.T) { - defer testRetryWhenDone().Reset() + f := newTestFuncster() + + defer testRetryWhenDone(f).Reset() reqCount := 0 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -271,7 +278,7 @@ func TestGetCSVFailParseSep(t *testing.T) { defer os.Remove(getCacheFileID(url)) want := [][]string{{"gomeetup", "city"}, {"yes", "Sydney"}, {"yes", "San Francisco"}, {"yes", "Stockholm"}} - have := getCSV(",", url) + have := f.getCSV(",", url) assert.NotNil(t, have) if have != nil { assert.EqualValues(t, want, have) @@ -279,7 +286,10 @@ func TestGetCSVFailParseSep(t *testing.T) { } func TestGetCSVFailParse(t *testing.T) { - defer testRetryWhenDone().Reset() + + f := newTestFuncster() + + defer testRetryWhenDone(f).Reset() reqCount := 0 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -302,7 +312,7 @@ func TestGetCSVFailParse(t *testing.T) { defer os.Remove(getCacheFileID(url)) want := [][]string{{"gomeetup", "city"}, {"yes", "Sydney"}, {"yes", "San Francisco"}, {"yes", "Stockholm"}} - have := getCSV(",", url) + have := f.getCSV(",", url) assert.NotNil(t, have) if have != nil { assert.EqualValues(t, want, have) diff --git a/tpl/template_test.go b/tpl/template_test.go index cf691858..f22eb78e 100644 --- a/tpl/template_test.go +++ b/tpl/template_test.go @@ -25,9 +25,21 @@ import ( "testing" "github.com/spf13/afero" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/hugofs" + "github.com/spf13/hugo/tplapi" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" ) +func testReset() { + viper.Reset() + + // TODO(bep) viper-globals + viper.Set("currentContentLanguage", helpers.NewLanguage("en")) +} + // Some tests for Issue #1178 -- Ace func TestAceTemplates(t *testing.T) { @@ -68,11 +80,19 @@ html lang=en d := "DATA" - templ := New(logger, func(templ Template) error { + config := defaultDepsConfig + config.WithTemplate = func(templ tplapi.Template) error { return templ.AddAceTemplate("mytemplate.ace", basePath, innerPath, []byte(this.baseContent), []byte(this.innerContent)) + } - }) + a := deps.New(config) + + if err := a.LoadTemplates(); err != nil { + t.Fatal(err) + } + + templ := a.Tmpl.(*GoHTMLTemplate) if len(templ.errors) > 0 && this.expectErr == 0 { t.Errorf("Test %d with root '%s' errored: %v", i, root, templ.errors) @@ -81,7 +101,7 @@ html lang=en } var buff bytes.Buffer - err := templ.ExecuteTemplate(&buff, "mytemplate.html", d) + err := a.Tmpl.ExecuteTemplate(&buff, "mytemplate.html", d) if err != nil && this.expectErr == 0 { t.Errorf("Test %d with root '%s' errored: %s", i, root, err) @@ -93,6 +113,7 @@ html lang=en t.Errorf("Test %d with root '%s' got\n%s\nexpected\n%s", i, root, result, this.expect) } } + } } @@ -124,52 +145,59 @@ func TestAddTemplateFileWithMaster(t *testing.T) { {`tpl`, `{{.0.E}}`, 0, false}, } { - hugofs.InitMemFs() - templ := New(logger) overlayTplName := "ot" masterTplName := "mt" finalTplName := "tp" + defaultDepsConfig.WithTemplate = func(templ tplapi.Template) error { + + err := templ.AddTemplateFileWithMaster(finalTplName, overlayTplName, masterTplName) + + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] AddTemplateFileWithMaster didn't return an expected error", i) + } + } else { + + if err != nil { + t.Errorf("[%d] AddTemplateFileWithMaster failed: %s", i, err) + return nil + } + + resultTpl := templ.Lookup(finalTplName) + + if resultTpl == nil { + t.Errorf("[%d] AddTemplateFileWithMaster: Result template not found", i) + return nil + } + + var b bytes.Buffer + err := resultTpl.Execute(&b, nil) + + if err != nil { + t.Errorf("[%d] AddTemplateFileWithMaster execute failed: %s", i, err) + return nil + } + resultContent := b.String() + + if resultContent != this.expect { + t.Errorf("[%d] AddTemplateFileWithMaster got \n%s but expected \n%v", i, resultContent, this.expect) + } + } + + return nil + } + + defaultDepsConfig.Fs = hugofs.NewMem() + if this.writeSkipper != 1 { - afero.WriteFile(hugofs.Source(), masterTplName, []byte(this.masterTplContent), 0644) + afero.WriteFile(defaultDepsConfig.Fs.Source, masterTplName, []byte(this.masterTplContent), 0644) } if this.writeSkipper != 2 { - afero.WriteFile(hugofs.Source(), overlayTplName, []byte(this.overlayTplContent), 0644) + afero.WriteFile(defaultDepsConfig.Fs.Source, overlayTplName, []byte(this.overlayTplContent), 0644) } - err := templ.AddTemplateFileWithMaster(finalTplName, overlayTplName, masterTplName) - - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] AddTemplateFileWithMaster didn't return an expected error", i) - } - } else { - - if err != nil { - t.Errorf("[%d] AddTemplateFileWithMaster failed: %s", i, err) - continue - } - - resultTpl := templ.Lookup(finalTplName) - - if resultTpl == nil { - t.Errorf("[%d] AddTemplateFileWithMaster: Result template not found", i) - continue - } - - var b bytes.Buffer - err := resultTpl.Execute(&b, nil) - - if err != nil { - t.Errorf("[%d] AddTemplateFileWithMaster execute failed: %s", i, err) - continue - } - resultContent := b.String() - - if resultContent != this.expect { - t.Errorf("[%d] AddTemplateFileWithMaster got \n%s but expected \n%v", i, resultContent, this.expect) - } - } + deps.New(defaultDepsConfig) } @@ -258,23 +286,29 @@ func TestTplGoFuzzReports(t *testing.T) { H: "a,b,c,d,e,f", } - templ := New(logger, func(templ Template) error { + defaultDepsConfig.WithTemplate = func(templ tplapi.Template) error { return templ.AddTemplate("fuzz", this.data) + } - }) + de := deps.New(defaultDepsConfig) + require.NoError(t, de.LoadTemplates()) + + templ := de.Tmpl.(*GoHTMLTemplate) if len(templ.errors) > 0 && this.expectErr == 0 { t.Errorf("Test %d errored: %v", i, templ.errors) } else if len(templ.errors) == 0 && this.expectErr == 1 { t.Errorf("#1 Test %d should have errored", i) } - err := templ.ExecuteTemplate(ioutil.Discard, "fuzz", d) + + err := de.Tmpl.ExecuteTemplate(ioutil.Discard, "fuzz", d) if err != nil && this.expectErr == 0 { t.Fatalf("Test %d errored: %s", i, err) } else if err == nil && this.expectErr == 2 { t.Fatalf("#2 Test %d should have errored", i) } + } } diff --git a/tplapi/template.go b/tplapi/template.go new file mode 100644 index 00000000..58bc5ecf --- /dev/null +++ b/tplapi/template.go @@ -0,0 +1,28 @@ +package tplapi + +import ( + "html/template" + "io" +) + +// TODO(bep) make smaller +// TODO(bep) consider putting this into /tpl and the implementation in /tpl/tplimpl or something +type Template interface { + ExecuteTemplate(wr io.Writer, name string, data interface{}) error + ExecuteTemplateToHTML(context interface{}, layouts ...string) template.HTML + Lookup(name string) *template.Template + Templates() []*template.Template + New(name string) *template.Template + GetClone() *template.Template + LoadTemplates(absPath string) + LoadTemplatesWithPrefix(absPath, prefix string) + AddTemplate(name, tpl string) error + AddTemplateFileWithMaster(name, overlayFilename, masterFilename string) error + AddAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error + AddInternalTemplate(prefix, name, tpl string) error + AddInternalShortcode(name, tpl string) error + Partial(name string, contextList ...interface{}) template.HTML + PrintErrors() + Funcs(funcMap template.FuncMap) + MarkReady() +}