mirror of
				https://github.com/gohugoio/hugo.git
				synced 2024-05-11 05:54:58 +00:00 
			
		
		
		
	The main motivation of this commit is to add a `page.Page` interface to replace the very file-oriented `hugolib.Page` struct. This is all a preparation step for issue #5074, "pages from other data sources". But this also fixes a set of annoying limitations, especially related to custom output formats, and shortcodes. Most notable changes: * The inner content of shortcodes using the `{{%` as the outer-most delimiter will now be sent to the content renderer, e.g. Blackfriday. This means that any markdown will partake in the global ToC and footnote context etc. * The Custom Output formats are now "fully virtualized". This removes many of the current limitations. * The taxonomy list type now has a reference to the `Page` object. This improves the taxonomy template `.Title` situation and make common template constructs much simpler. See #5074 Fixes #5763 Fixes #5758 Fixes #5090 Fixes #5204 Fixes #4695 Fixes #5607 Fixes #5707 Fixes #5719 Fixes #3113 Fixes #5706 Fixes #5767 Fixes #5723 Fixes #5769 Fixes #5770 Fixes #5771 Fixes #5759 Fixes #5776 Fixes #5777 Fixes #5778
		
			
				
	
	
		
			409 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			409 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2019 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 commands
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"errors"
 | 
						|
 | 
						|
	"io/ioutil"
 | 
						|
 | 
						|
	"github.com/gohugoio/hugo/common/herrors"
 | 
						|
	"github.com/gohugoio/hugo/common/hugo"
 | 
						|
 | 
						|
	jww "github.com/spf13/jwalterweatherman"
 | 
						|
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"regexp"
 | 
						|
	"strings"
 | 
						|
	"sync"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/gohugoio/hugo/common/loggers"
 | 
						|
	"github.com/gohugoio/hugo/config"
 | 
						|
 | 
						|
	"github.com/spf13/cobra"
 | 
						|
 | 
						|
	"github.com/gohugoio/hugo/hugolib"
 | 
						|
	"github.com/spf13/afero"
 | 
						|
 | 
						|
	"github.com/bep/debounce"
 | 
						|
	"github.com/gohugoio/hugo/common/types"
 | 
						|
	"github.com/gohugoio/hugo/deps"
 | 
						|
	"github.com/gohugoio/hugo/helpers"
 | 
						|
	"github.com/gohugoio/hugo/hugofs"
 | 
						|
	"github.com/gohugoio/hugo/langs"
 | 
						|
)
 | 
						|
 | 
						|
type commandeerHugoState struct {
 | 
						|
	*deps.DepsCfg
 | 
						|
	hugo     *hugolib.HugoSites
 | 
						|
	fsCreate sync.Once
 | 
						|
}
 | 
						|
 | 
						|
type commandeer struct {
 | 
						|
	*commandeerHugoState
 | 
						|
 | 
						|
	logger *loggers.Logger
 | 
						|
 | 
						|
	// Currently only set when in "fast render mode". But it seems to
 | 
						|
	// be fast enough that we could maybe just add it for all server modes.
 | 
						|
	changeDetector *fileChangeDetector
 | 
						|
 | 
						|
	// We need to reuse this on server rebuilds.
 | 
						|
	destinationFs afero.Fs
 | 
						|
 | 
						|
	h    *hugoBuilderCommon
 | 
						|
	ftch flagsToConfigHandler
 | 
						|
 | 
						|
	visitedURLs *types.EvictingStringQueue
 | 
						|
 | 
						|
	doWithCommandeer func(c *commandeer) error
 | 
						|
 | 
						|
	// We watch these for changes.
 | 
						|
	configFiles []string
 | 
						|
 | 
						|
	// Used in cases where we get flooded with events in server mode.
 | 
						|
	debounce func(f func())
 | 
						|
 | 
						|
	serverPorts         []int
 | 
						|
	languagesConfigured bool
 | 
						|
	languages           langs.Languages
 | 
						|
	doLiveReload        bool
 | 
						|
	fastRenderMode      bool
 | 
						|
	showErrorInBrowser  bool
 | 
						|
 | 
						|
	configured bool
 | 
						|
	paused     bool
 | 
						|
 | 
						|
	// Any error from the last build.
 | 
						|
	buildErr error
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) errCount() int {
 | 
						|
	return int(c.logger.ErrorCounter.Count())
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) getErrorWithContext() interface{} {
 | 
						|
	errCount := c.errCount()
 | 
						|
 | 
						|
	if errCount == 0 {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	m := make(map[string]interface{})
 | 
						|
 | 
						|
	m["Error"] = errors.New(removeErrorPrefixFromLog(c.logger.Errors()))
 | 
						|
	m["Version"] = hugo.BuildVersionString()
 | 
						|
 | 
						|
	fe := herrors.UnwrapErrorWithFileContext(c.buildErr)
 | 
						|
	if fe != nil {
 | 
						|
		m["File"] = fe
 | 
						|
	}
 | 
						|
 | 
						|
	if c.h.verbose {
 | 
						|
		var b bytes.Buffer
 | 
						|
		herrors.FprintStackTrace(&b, c.buildErr)
 | 
						|
		m["StackTrace"] = b.String()
 | 
						|
	}
 | 
						|
 | 
						|
	return m
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) Set(key string, value interface{}) {
 | 
						|
	if c.configured {
 | 
						|
		panic("commandeer cannot be changed")
 | 
						|
	}
 | 
						|
	c.Cfg.Set(key, value)
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) initFs(fs *hugofs.Fs) error {
 | 
						|
	c.destinationFs = fs.Destination
 | 
						|
	c.DepsCfg.Fs = fs
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func newCommandeer(mustHaveConfigFile, running bool, h *hugoBuilderCommon, f flagsToConfigHandler, doWithCommandeer func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
 | 
						|
 | 
						|
	var rebuildDebouncer func(f func())
 | 
						|
	if running {
 | 
						|
		// The time value used is tested with mass content replacements in a fairly big Hugo site.
 | 
						|
		// It is better to wait for some seconds in those cases rather than get flooded
 | 
						|
		// with rebuilds.
 | 
						|
		rebuildDebouncer = debounce.New(4 * time.Second)
 | 
						|
	}
 | 
						|
 | 
						|
	c := &commandeer{
 | 
						|
		h:                   h,
 | 
						|
		ftch:                f,
 | 
						|
		commandeerHugoState: &commandeerHugoState{},
 | 
						|
		doWithCommandeer:    doWithCommandeer,
 | 
						|
		visitedURLs:         types.NewEvictingStringQueue(10),
 | 
						|
		debounce:            rebuildDebouncer,
 | 
						|
		// This will be replaced later, but we need something to log to before the configuration is read.
 | 
						|
		logger: loggers.NewLogger(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, running),
 | 
						|
	}
 | 
						|
 | 
						|
	return c, c.loadConfig(mustHaveConfigFile, running)
 | 
						|
}
 | 
						|
 | 
						|
type fileChangeDetector struct {
 | 
						|
	sync.Mutex
 | 
						|
	current map[string]string
 | 
						|
	prev    map[string]string
 | 
						|
 | 
						|
	irrelevantRe *regexp.Regexp
 | 
						|
}
 | 
						|
 | 
						|
func (f *fileChangeDetector) OnFileClose(name, md5sum string) {
 | 
						|
	f.Lock()
 | 
						|
	defer f.Unlock()
 | 
						|
	f.current[name] = md5sum
 | 
						|
}
 | 
						|
 | 
						|
func (f *fileChangeDetector) changed() []string {
 | 
						|
	if f == nil {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	f.Lock()
 | 
						|
	defer f.Unlock()
 | 
						|
	var c []string
 | 
						|
	for k, v := range f.current {
 | 
						|
		vv, found := f.prev[k]
 | 
						|
		if !found || v != vv {
 | 
						|
			c = append(c, k)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return f.filterIrrelevant(c)
 | 
						|
}
 | 
						|
 | 
						|
func (f *fileChangeDetector) filterIrrelevant(in []string) []string {
 | 
						|
	var filtered []string
 | 
						|
	for _, v := range in {
 | 
						|
		if !f.irrelevantRe.MatchString(v) {
 | 
						|
			filtered = append(filtered, v)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return filtered
 | 
						|
}
 | 
						|
 | 
						|
func (f *fileChangeDetector) PrepareNew() {
 | 
						|
	if f == nil {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	f.Lock()
 | 
						|
	defer f.Unlock()
 | 
						|
 | 
						|
	if f.current == nil {
 | 
						|
		f.current = make(map[string]string)
 | 
						|
		f.prev = make(map[string]string)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	f.prev = make(map[string]string)
 | 
						|
	for k, v := range f.current {
 | 
						|
		f.prev[k] = v
 | 
						|
	}
 | 
						|
	f.current = make(map[string]string)
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
 | 
						|
 | 
						|
	if c.DepsCfg == nil {
 | 
						|
		c.DepsCfg = &deps.DepsCfg{}
 | 
						|
	}
 | 
						|
 | 
						|
	if c.logger != nil {
 | 
						|
		// Truncate the error log if this is a reload.
 | 
						|
		c.logger.Reset()
 | 
						|
	}
 | 
						|
 | 
						|
	cfg := c.DepsCfg
 | 
						|
	c.configured = false
 | 
						|
	cfg.Running = running
 | 
						|
 | 
						|
	var dir string
 | 
						|
	if c.h.source != "" {
 | 
						|
		dir, _ = filepath.Abs(c.h.source)
 | 
						|
	} else {
 | 
						|
		dir, _ = os.Getwd()
 | 
						|
	}
 | 
						|
 | 
						|
	var sourceFs afero.Fs = hugofs.Os
 | 
						|
	if c.DepsCfg.Fs != nil {
 | 
						|
		sourceFs = c.DepsCfg.Fs.Source
 | 
						|
	}
 | 
						|
 | 
						|
	environment := c.h.getEnvironment(running)
 | 
						|
 | 
						|
	doWithConfig := func(cfg config.Provider) error {
 | 
						|
 | 
						|
		if c.ftch != nil {
 | 
						|
			c.ftch.flagsToConfig(cfg)
 | 
						|
		}
 | 
						|
 | 
						|
		cfg.Set("workingDir", dir)
 | 
						|
		cfg.Set("environment", environment)
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	doWithCommandeer := func(cfg config.Provider) error {
 | 
						|
		c.Cfg = cfg
 | 
						|
		if c.doWithCommandeer == nil {
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
		err := c.doWithCommandeer(c)
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	configPath := c.h.source
 | 
						|
	if configPath == "" {
 | 
						|
		configPath = dir
 | 
						|
	}
 | 
						|
	config, configFiles, err := hugolib.LoadConfig(
 | 
						|
		hugolib.ConfigSourceDescriptor{
 | 
						|
			Fs:           sourceFs,
 | 
						|
			Path:         configPath,
 | 
						|
			WorkingDir:   dir,
 | 
						|
			Filename:     c.h.cfgFile,
 | 
						|
			AbsConfigDir: c.h.getConfigDir(dir),
 | 
						|
			Environment:  environment},
 | 
						|
		doWithCommandeer,
 | 
						|
		doWithConfig)
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		if mustHaveConfigFile {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		if err != hugolib.ErrNoConfigFile {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	c.configFiles = configFiles
 | 
						|
 | 
						|
	if l, ok := c.Cfg.Get("languagesSorted").(langs.Languages); ok {
 | 
						|
		c.languagesConfigured = true
 | 
						|
		c.languages = l
 | 
						|
	}
 | 
						|
 | 
						|
	// Set some commonly used flags
 | 
						|
	c.doLiveReload = running && !c.Cfg.GetBool("disableLiveReload")
 | 
						|
	c.fastRenderMode = c.doLiveReload && !c.Cfg.GetBool("disableFastRender")
 | 
						|
	c.showErrorInBrowser = c.doLiveReload && !c.Cfg.GetBool("disableBrowserError")
 | 
						|
 | 
						|
	// This is potentially double work, but we need to do this one more time now
 | 
						|
	// that all the languages have been configured.
 | 
						|
	if c.doWithCommandeer != nil {
 | 
						|
		if err := c.doWithCommandeer(c); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	logger, err := c.createLogger(config, running)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	cfg.Logger = logger
 | 
						|
	c.logger = logger
 | 
						|
 | 
						|
	createMemFs := config.GetBool("renderToMemory")
 | 
						|
 | 
						|
	if createMemFs {
 | 
						|
		// Rendering to memoryFS, publish to Root regardless of publishDir.
 | 
						|
		config.Set("publishDir", "/")
 | 
						|
	}
 | 
						|
 | 
						|
	c.fsCreate.Do(func() {
 | 
						|
		fs := hugofs.NewFrom(sourceFs, config)
 | 
						|
 | 
						|
		if c.destinationFs != nil {
 | 
						|
			// Need to reuse the destination on server rebuilds.
 | 
						|
			fs.Destination = c.destinationFs
 | 
						|
		} else if createMemFs {
 | 
						|
			// Hugo writes the output to memory instead of the disk.
 | 
						|
			fs.Destination = new(afero.MemMapFs)
 | 
						|
		}
 | 
						|
 | 
						|
		if c.fastRenderMode {
 | 
						|
			// For now, fast render mode only. It should, however, be fast enough
 | 
						|
			// for the full variant, too.
 | 
						|
			changeDetector := &fileChangeDetector{
 | 
						|
				// We use this detector to decide to do a Hot reload of a single path or not.
 | 
						|
				// We need to filter out source maps and possibly some other to be able
 | 
						|
				// to make that decision.
 | 
						|
				irrelevantRe: regexp.MustCompile(`\.map$`),
 | 
						|
			}
 | 
						|
 | 
						|
			changeDetector.PrepareNew()
 | 
						|
			fs.Destination = hugofs.NewHashingFs(fs.Destination, changeDetector)
 | 
						|
			c.changeDetector = changeDetector
 | 
						|
		}
 | 
						|
 | 
						|
		if c.Cfg.GetBool("logPathWarnings") {
 | 
						|
			fs.Destination = hugofs.NewCreateCountingFs(fs.Destination)
 | 
						|
		}
 | 
						|
 | 
						|
		// To debug hard-to-find path issues.
 | 
						|
		//fs.Destination = hugofs.NewStacktracerFs(fs.Destination, `fr/fr`)
 | 
						|
 | 
						|
		err = c.initFs(fs)
 | 
						|
		if err != nil {
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		var h *hugolib.HugoSites
 | 
						|
 | 
						|
		h, err = hugolib.NewHugoSites(*c.DepsCfg)
 | 
						|
		c.hugo = h
 | 
						|
 | 
						|
	})
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	cacheDir, err := helpers.GetCacheDir(sourceFs, config)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	config.Set("cacheDir", cacheDir)
 | 
						|
 | 
						|
	cfg.Logger.INFO.Println("Using config file:", config.ConfigFileUsed())
 | 
						|
 | 
						|
	themeDir := c.hugo.PathSpec.GetFirstThemeDir()
 | 
						|
	if themeDir != "" {
 | 
						|
		if _, err := sourceFs.Stat(themeDir); os.IsNotExist(err) {
 | 
						|
			return newSystemError("Unable to find theme Directory:", themeDir)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	dir, themeVersionMismatch, minVersion := c.isThemeVsHugoVersionMismatch(sourceFs)
 | 
						|
 | 
						|
	if themeVersionMismatch {
 | 
						|
		name := filepath.Base(dir)
 | 
						|
		cfg.Logger.ERROR.Printf("%s theme does not support Hugo version %s. Minimum version required is %s\n",
 | 
						|
			strings.ToUpper(name), hugo.CurrentVersion.ReleaseVersion(), minVersion)
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
 | 
						|
}
 |