github.com/fighterlyt/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 }