github.com/kristoff-it/hugo@v0.47.1/hugolib/pagemeta/page_frontmatter.go (about)

     1  // Copyright 2018 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package pagemeta
    15  
    16  import (
    17  	"io/ioutil"
    18  	"log"
    19  	"os"
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/gohugoio/hugo/helpers"
    24  
    25  	"github.com/gohugoio/hugo/config"
    26  	"github.com/spf13/cast"
    27  	jww "github.com/spf13/jwalterweatherman"
    28  )
    29  
    30  // FrontMatterHandler maps front matter into Page fields and .Params.
    31  // Note that we currently have only extracted the date logic.
    32  type FrontMatterHandler struct {
    33  	fmConfig frontmatterConfig
    34  
    35  	dateHandler        frontMatterFieldHandler
    36  	lastModHandler     frontMatterFieldHandler
    37  	publishDateHandler frontMatterFieldHandler
    38  	expiryDateHandler  frontMatterFieldHandler
    39  
    40  	// A map of all date keys configured, including any custom.
    41  	allDateKeys map[string]bool
    42  
    43  	logger *jww.Notepad
    44  }
    45  
    46  // FrontMatterDescriptor describes how to handle front matter for a given Page.
    47  // It has pointers to values in the receiving page which gets updated.
    48  type FrontMatterDescriptor struct {
    49  
    50  	// This the Page's front matter.
    51  	Frontmatter map[string]interface{}
    52  
    53  	// This is the Page's base filename, e.g. page.md.
    54  	BaseFilename string
    55  
    56  	// The content file's mod time.
    57  	ModTime time.Time
    58  
    59  	// May be set from the author date in Git.
    60  	GitAuthorDate time.Time
    61  
    62  	// The below are pointers to values on Page and will be modified.
    63  
    64  	// This is the Page's params.
    65  	Params map[string]interface{}
    66  
    67  	// This is the Page's dates.
    68  	Dates *PageDates
    69  
    70  	// This is the Page's Slug etc.
    71  	PageURLs *URLPath
    72  }
    73  
    74  var (
    75  	dateFieldAliases = map[string][]string{
    76  		fmDate:       []string{},
    77  		fmLastmod:    []string{"modified"},
    78  		fmPubDate:    []string{"pubdate", "published"},
    79  		fmExpiryDate: []string{"unpublishdate"},
    80  	}
    81  )
    82  
    83  // HandleDates updates all the dates given the current configuration and the
    84  // supplied front matter params. Note that this requires all lower-case keys
    85  // in the params map.
    86  func (f FrontMatterHandler) HandleDates(d *FrontMatterDescriptor) error {
    87  	if d.Dates == nil {
    88  		panic("missing dates")
    89  	}
    90  
    91  	if f.dateHandler == nil {
    92  		panic("missing date handler")
    93  	}
    94  
    95  	if _, err := f.dateHandler(d); err != nil {
    96  		return err
    97  	}
    98  
    99  	if _, err := f.lastModHandler(d); err != nil {
   100  		return err
   101  	}
   102  
   103  	if _, err := f.publishDateHandler(d); err != nil {
   104  		return err
   105  	}
   106  
   107  	if _, err := f.expiryDateHandler(d); err != nil {
   108  		return err
   109  	}
   110  
   111  	return nil
   112  }
   113  
   114  // IsDateKey returns whether the given front matter key is considered a date by the current
   115  // configuration.
   116  func (f FrontMatterHandler) IsDateKey(key string) bool {
   117  	return f.allDateKeys[key]
   118  }
   119  
   120  // A Zero date is a signal that the name can not be parsed.
   121  // This follows the format as outlined in Jekyll, https://jekyllrb.com/docs/posts/:
   122  // "Where YEAR is a four-digit number, MONTH and DAY are both two-digit numbers"
   123  func dateAndSlugFromBaseFilename(name string) (time.Time, string) {
   124  	withoutExt, _ := helpers.FileAndExt(name)
   125  
   126  	if len(withoutExt) < 10 {
   127  		// This can not be a date.
   128  		return time.Time{}, ""
   129  	}
   130  
   131  	// Note: Hugo currently have no custom timezone support.
   132  	// We will have to revisit this when that is in place.
   133  	d, err := time.Parse("2006-01-02", withoutExt[:10])
   134  	if err != nil {
   135  		return time.Time{}, ""
   136  	}
   137  
   138  	// Be a little lenient with the format here.
   139  	slug := strings.Trim(withoutExt[10:], " -_")
   140  
   141  	return d, slug
   142  }
   143  
   144  type frontMatterFieldHandler func(d *FrontMatterDescriptor) (bool, error)
   145  
   146  func (f FrontMatterHandler) newChainedFrontMatterFieldHandler(handlers ...frontMatterFieldHandler) frontMatterFieldHandler {
   147  	return func(d *FrontMatterDescriptor) (bool, error) {
   148  		for _, h := range handlers {
   149  			// First successful handler wins.
   150  			success, err := h(d)
   151  			if err != nil {
   152  				f.logger.ERROR.Println(err)
   153  			} else if success {
   154  				return true, nil
   155  			}
   156  		}
   157  		return false, nil
   158  	}
   159  }
   160  
   161  type frontmatterConfig struct {
   162  	date        []string
   163  	lastmod     []string
   164  	publishDate []string
   165  	expiryDate  []string
   166  }
   167  
   168  const (
   169  	// These are all the date handler identifiers
   170  	// All identifiers not starting with a ":" maps to a front matter parameter.
   171  	fmDate       = "date"
   172  	fmPubDate    = "publishdate"
   173  	fmLastmod    = "lastmod"
   174  	fmExpiryDate = "expirydate"
   175  
   176  	// Gets date from filename, e.g 218-02-22-mypage.md
   177  	fmFilename = ":filename"
   178  
   179  	// Gets date from file OS mod time.
   180  	fmModTime = ":filemodtime"
   181  
   182  	// Gets date from Git
   183  	fmGitAuthorDate = ":git"
   184  )
   185  
   186  // This is the config you get when doing nothing.
   187  func newDefaultFrontmatterConfig() frontmatterConfig {
   188  	return frontmatterConfig{
   189  		date:        []string{fmDate, fmPubDate, fmLastmod},
   190  		lastmod:     []string{fmGitAuthorDate, fmLastmod, fmDate, fmPubDate},
   191  		publishDate: []string{fmPubDate, fmDate},
   192  		expiryDate:  []string{fmExpiryDate},
   193  	}
   194  }
   195  
   196  func newFrontmatterConfig(cfg config.Provider) (frontmatterConfig, error) {
   197  	c := newDefaultFrontmatterConfig()
   198  	defaultConfig := c
   199  
   200  	if cfg.IsSet("frontmatter") {
   201  		fm := cfg.GetStringMap("frontmatter")
   202  		if fm != nil {
   203  			for k, v := range fm {
   204  				loki := strings.ToLower(k)
   205  				switch loki {
   206  				case fmDate:
   207  					c.date = toLowerSlice(v)
   208  				case fmPubDate:
   209  					c.publishDate = toLowerSlice(v)
   210  				case fmLastmod:
   211  					c.lastmod = toLowerSlice(v)
   212  				case fmExpiryDate:
   213  					c.expiryDate = toLowerSlice(v)
   214  				}
   215  			}
   216  		}
   217  	}
   218  
   219  	expander := func(c, d []string) []string {
   220  		out := expandDefaultValues(c, d)
   221  		out = addDateFieldAliases(out)
   222  		return out
   223  	}
   224  
   225  	c.date = expander(c.date, defaultConfig.date)
   226  	c.publishDate = expander(c.publishDate, defaultConfig.publishDate)
   227  	c.lastmod = expander(c.lastmod, defaultConfig.lastmod)
   228  	c.expiryDate = expander(c.expiryDate, defaultConfig.expiryDate)
   229  
   230  	return c, nil
   231  }
   232  
   233  func addDateFieldAliases(values []string) []string {
   234  	var complete []string
   235  
   236  	for _, v := range values {
   237  		complete = append(complete, v)
   238  		if aliases, found := dateFieldAliases[v]; found {
   239  			complete = append(complete, aliases...)
   240  		}
   241  	}
   242  	return helpers.UniqueStrings(complete)
   243  }
   244  
   245  func expandDefaultValues(values []string, defaults []string) []string {
   246  	var out []string
   247  	for _, v := range values {
   248  		if v == ":default" {
   249  			out = append(out, defaults...)
   250  		} else {
   251  			out = append(out, v)
   252  		}
   253  	}
   254  	return out
   255  }
   256  
   257  func toLowerSlice(in interface{}) []string {
   258  	out := cast.ToStringSlice(in)
   259  	for i := 0; i < len(out); i++ {
   260  		out[i] = strings.ToLower(out[i])
   261  	}
   262  
   263  	return out
   264  }
   265  
   266  // NewFrontmatterHandler creates a new FrontMatterHandler with the given logger and configuration.
   267  // If no logger is provided, one will be created.
   268  func NewFrontmatterHandler(logger *jww.Notepad, cfg config.Provider) (FrontMatterHandler, error) {
   269  
   270  	if logger == nil {
   271  		logger = jww.NewNotepad(jww.LevelWarn, jww.LevelWarn, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime)
   272  	}
   273  
   274  	frontMatterConfig, err := newFrontmatterConfig(cfg)
   275  	if err != nil {
   276  		return FrontMatterHandler{}, err
   277  	}
   278  
   279  	allDateKeys := make(map[string]bool)
   280  	addKeys := func(vals []string) {
   281  		for _, k := range vals {
   282  			if !strings.HasPrefix(k, ":") {
   283  				allDateKeys[k] = true
   284  			}
   285  		}
   286  	}
   287  
   288  	addKeys(frontMatterConfig.date)
   289  	addKeys(frontMatterConfig.expiryDate)
   290  	addKeys(frontMatterConfig.lastmod)
   291  	addKeys(frontMatterConfig.publishDate)
   292  
   293  	f := FrontMatterHandler{logger: logger, fmConfig: frontMatterConfig, allDateKeys: allDateKeys}
   294  
   295  	if err := f.createHandlers(); err != nil {
   296  		return f, err
   297  	}
   298  
   299  	return f, nil
   300  }
   301  
   302  func (f *FrontMatterHandler) createHandlers() error {
   303  	var err error
   304  
   305  	if f.dateHandler, err = f.createDateHandler(f.fmConfig.date,
   306  		func(d *FrontMatterDescriptor, t time.Time) {
   307  			d.Dates.Date = t
   308  			setParamIfNotSet(fmDate, t, d)
   309  		}); err != nil {
   310  		return err
   311  	}
   312  
   313  	if f.lastModHandler, err = f.createDateHandler(f.fmConfig.lastmod,
   314  		func(d *FrontMatterDescriptor, t time.Time) {
   315  			setParamIfNotSet(fmLastmod, t, d)
   316  			d.Dates.Lastmod = t
   317  		}); err != nil {
   318  		return err
   319  	}
   320  
   321  	if f.publishDateHandler, err = f.createDateHandler(f.fmConfig.publishDate,
   322  		func(d *FrontMatterDescriptor, t time.Time) {
   323  			setParamIfNotSet(fmPubDate, t, d)
   324  			d.Dates.PublishDate = t
   325  		}); err != nil {
   326  		return err
   327  	}
   328  
   329  	if f.expiryDateHandler, err = f.createDateHandler(f.fmConfig.expiryDate,
   330  		func(d *FrontMatterDescriptor, t time.Time) {
   331  			setParamIfNotSet(fmExpiryDate, t, d)
   332  			d.Dates.ExpiryDate = t
   333  		}); err != nil {
   334  		return err
   335  	}
   336  
   337  	return nil
   338  }
   339  
   340  func setParamIfNotSet(key string, value interface{}, d *FrontMatterDescriptor) {
   341  	if _, found := d.Params[key]; found {
   342  		return
   343  	}
   344  	d.Params[key] = value
   345  }
   346  
   347  func (f FrontMatterHandler) createDateHandler(identifiers []string, setter func(d *FrontMatterDescriptor, t time.Time)) (frontMatterFieldHandler, error) {
   348  	var h *frontmatterFieldHandlers
   349  	var handlers []frontMatterFieldHandler
   350  
   351  	for _, identifier := range identifiers {
   352  		switch identifier {
   353  		case fmFilename:
   354  			handlers = append(handlers, h.newDateFilenameHandler(setter))
   355  		case fmModTime:
   356  			handlers = append(handlers, h.newDateModTimeHandler(setter))
   357  		case fmGitAuthorDate:
   358  			handlers = append(handlers, h.newDateGitAuthorDateHandler(setter))
   359  		default:
   360  			handlers = append(handlers, h.newDateFieldHandler(identifier, setter))
   361  		}
   362  	}
   363  
   364  	return f.newChainedFrontMatterFieldHandler(handlers...), nil
   365  
   366  }
   367  
   368  type frontmatterFieldHandlers int
   369  
   370  func (f *frontmatterFieldHandlers) newDateFieldHandler(key string, setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
   371  	return func(d *FrontMatterDescriptor) (bool, error) {
   372  		v, found := d.Frontmatter[key]
   373  
   374  		if !found {
   375  			return false, nil
   376  		}
   377  
   378  		date, err := cast.ToTimeE(v)
   379  		if err != nil {
   380  			return false, nil
   381  		}
   382  
   383  		// We map several date keys to one, so, for example,
   384  		// "expirydate", "unpublishdate" will all set .ExpiryDate (first found).
   385  		setter(d, date)
   386  
   387  		// This is the params key as set in front matter.
   388  		d.Params[key] = date
   389  
   390  		return true, nil
   391  	}
   392  }
   393  
   394  func (f *frontmatterFieldHandlers) newDateFilenameHandler(setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
   395  	return func(d *FrontMatterDescriptor) (bool, error) {
   396  		date, slug := dateAndSlugFromBaseFilename(d.BaseFilename)
   397  		if date.IsZero() {
   398  			return false, nil
   399  		}
   400  
   401  		setter(d, date)
   402  
   403  		if _, found := d.Frontmatter["slug"]; !found {
   404  			// Use slug from filename
   405  			d.PageURLs.Slug = slug
   406  		}
   407  
   408  		return true, nil
   409  	}
   410  }
   411  
   412  func (f *frontmatterFieldHandlers) newDateModTimeHandler(setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
   413  	return func(d *FrontMatterDescriptor) (bool, error) {
   414  		if d.ModTime.IsZero() {
   415  			return false, nil
   416  		}
   417  		setter(d, d.ModTime)
   418  		return true, nil
   419  	}
   420  }
   421  
   422  func (f *frontmatterFieldHandlers) newDateGitAuthorDateHandler(setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
   423  	return func(d *FrontMatterDescriptor) (bool, error) {
   424  		if d.GitAuthorDate.IsZero() {
   425  			return false, nil
   426  		}
   427  		setter(d, d.GitAuthorDate)
   428  		return true, nil
   429  	}
   430  }