hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

page_frontmatter.go (11348B)

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