github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/resources/page/pagemeta/page_frontmatter.go (about) 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/loggers" 22 "github.com/gohugoio/hugo/common/paths" 23 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 // Controls how the Date is set from front matter. 164 Date []string 165 // Controls how the Lastmod is set from front matter. 166 Lastmod []string 167 // Controls how the PublishDate is set from front matter. 168 PublishDate []string 169 // Controls how the ExpiryDate is set from front matter. 170 ExpiryDate []string 171 } 172 173 const ( 174 // These are all the date handler identifiers 175 // All identifiers not starting with a ":" maps to a front matter parameter. 176 fmDate = "date" 177 fmPubDate = "publishdate" 178 fmLastmod = "lastmod" 179 fmExpiryDate = "expirydate" 180 181 // Gets date from filename, e.g 218-02-22-mypage.md 182 fmFilename = ":filename" 183 184 // Gets date from file OS mod time. 185 fmModTime = ":filemodtime" 186 187 // Gets date from Git 188 fmGitAuthorDate = ":git" 189 ) 190 191 // This is the config you get when doing nothing. 192 func newDefaultFrontmatterConfig() FrontmatterConfig { 193 return FrontmatterConfig{ 194 Date: []string{fmDate, fmPubDate, fmLastmod}, 195 Lastmod: []string{fmGitAuthorDate, fmLastmod, fmDate, fmPubDate}, 196 PublishDate: []string{fmPubDate, fmDate}, 197 ExpiryDate: []string{fmExpiryDate}, 198 } 199 } 200 201 func DecodeFrontMatterConfig(cfg config.Provider) (FrontmatterConfig, error) { 202 c := newDefaultFrontmatterConfig() 203 defaultConfig := c 204 205 if cfg.IsSet("frontmatter") { 206 fm := cfg.GetStringMap("frontmatter") 207 for k, v := range fm { 208 loki := strings.ToLower(k) 209 switch loki { 210 case fmDate: 211 c.Date = toLowerSlice(v) 212 case fmPubDate: 213 c.PublishDate = toLowerSlice(v) 214 case fmLastmod: 215 c.Lastmod = toLowerSlice(v) 216 case fmExpiryDate: 217 c.ExpiryDate = toLowerSlice(v) 218 } 219 } 220 } 221 222 expander := func(c, d []string) []string { 223 out := expandDefaultValues(c, d) 224 out = addDateFieldAliases(out) 225 return out 226 } 227 228 c.Date = expander(c.Date, defaultConfig.Date) 229 c.PublishDate = expander(c.PublishDate, defaultConfig.PublishDate) 230 c.Lastmod = expander(c.Lastmod, defaultConfig.Lastmod) 231 c.ExpiryDate = expander(c.ExpiryDate, defaultConfig.ExpiryDate) 232 233 return c, nil 234 } 235 236 func addDateFieldAliases(values []string) []string { 237 var complete []string 238 239 for _, v := range values { 240 complete = append(complete, v) 241 if aliases, found := dateFieldAliases[v]; found { 242 complete = append(complete, aliases...) 243 } 244 } 245 return helpers.UniqueStringsReuse(complete) 246 } 247 248 func expandDefaultValues(values []string, defaults []string) []string { 249 var out []string 250 for _, v := range values { 251 if v == ":default" { 252 out = append(out, defaults...) 253 } else { 254 out = append(out, v) 255 } 256 } 257 return out 258 } 259 260 func toLowerSlice(in any) []string { 261 out := cast.ToStringSlice(in) 262 for i := 0; i < len(out); i++ { 263 out[i] = strings.ToLower(out[i]) 264 } 265 266 return out 267 } 268 269 // NewFrontmatterHandler creates a new FrontMatterHandler with the given logger and configuration. 270 // If no logger is provided, one will be created. 271 func NewFrontmatterHandler(logger loggers.Logger, frontMatterConfig FrontmatterConfig) (FrontMatterHandler, error) { 272 if logger == nil { 273 logger = loggers.NewDefault() 274 } 275 276 allDateKeys := make(map[string]bool) 277 addKeys := func(vals []string) { 278 for _, k := range vals { 279 if !strings.HasPrefix(k, ":") { 280 allDateKeys[k] = true 281 } 282 } 283 } 284 285 addKeys(frontMatterConfig.Date) 286 addKeys(frontMatterConfig.ExpiryDate) 287 addKeys(frontMatterConfig.Lastmod) 288 addKeys(frontMatterConfig.PublishDate) 289 290 f := FrontMatterHandler{logger: logger, fmConfig: frontMatterConfig, allDateKeys: allDateKeys} 291 292 if err := f.createHandlers(); err != nil { 293 return f, err 294 } 295 296 return f, nil 297 } 298 299 func (f *FrontMatterHandler) createHandlers() error { 300 var err error 301 302 if f.dateHandler, err = f.createDateHandler(f.fmConfig.Date, 303 func(d *FrontMatterDescriptor, t time.Time) { 304 d.Dates.FDate = t 305 setParamIfNotSet(fmDate, t, d) 306 }); err != nil { 307 return err 308 } 309 310 if f.lastModHandler, err = f.createDateHandler(f.fmConfig.Lastmod, 311 func(d *FrontMatterDescriptor, t time.Time) { 312 setParamIfNotSet(fmLastmod, t, d) 313 d.Dates.FLastmod = t 314 }); err != nil { 315 return err 316 } 317 318 if f.publishDateHandler, err = f.createDateHandler(f.fmConfig.PublishDate, 319 func(d *FrontMatterDescriptor, t time.Time) { 320 setParamIfNotSet(fmPubDate, t, d) 321 d.Dates.FPublishDate = t 322 }); err != nil { 323 return err 324 } 325 326 if f.expiryDateHandler, err = f.createDateHandler(f.fmConfig.ExpiryDate, 327 func(d *FrontMatterDescriptor, t time.Time) { 328 setParamIfNotSet(fmExpiryDate, t, d) 329 d.Dates.FExpiryDate = t 330 }); err != nil { 331 return err 332 } 333 334 return nil 335 } 336 337 func setParamIfNotSet(key string, value any, d *FrontMatterDescriptor) { 338 if _, found := d.Params[key]; found { 339 return 340 } 341 d.Params[key] = value 342 } 343 344 func (f FrontMatterHandler) createDateHandler(identifiers []string, setter func(d *FrontMatterDescriptor, t time.Time)) (frontMatterFieldHandler, error) { 345 var h *frontmatterFieldHandlers 346 var handlers []frontMatterFieldHandler 347 348 for _, identifier := range identifiers { 349 switch identifier { 350 case fmFilename: 351 handlers = append(handlers, h.newDateFilenameHandler(setter)) 352 case fmModTime: 353 handlers = append(handlers, h.newDateModTimeHandler(setter)) 354 case fmGitAuthorDate: 355 handlers = append(handlers, h.newDateGitAuthorDateHandler(setter)) 356 default: 357 handlers = append(handlers, h.newDateFieldHandler(identifier, setter)) 358 } 359 } 360 361 return f.newChainedFrontMatterFieldHandler(handlers...), nil 362 } 363 364 type frontmatterFieldHandlers int 365 366 func (f *frontmatterFieldHandlers) newDateFieldHandler(key string, setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler { 367 return func(d *FrontMatterDescriptor) (bool, error) { 368 v, found := d.Frontmatter[key] 369 370 if !found { 371 return false, nil 372 } 373 374 date, err := htime.ToTimeInDefaultLocationE(v, d.Location) 375 if err != nil { 376 return false, nil 377 } 378 379 // We map several date keys to one, so, for example, 380 // "expirydate", "unpublishdate" will all set .ExpiryDate (first found). 381 setter(d, date) 382 383 // This is the params key as set in front matter. 384 d.Params[key] = date 385 386 return true, nil 387 } 388 } 389 390 func (f *frontmatterFieldHandlers) newDateFilenameHandler(setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler { 391 return func(d *FrontMatterDescriptor) (bool, error) { 392 date, slug := dateAndSlugFromBaseFilename(d.Location, d.BaseFilename) 393 if date.IsZero() { 394 return false, nil 395 } 396 397 setter(d, date) 398 399 if _, found := d.Frontmatter["slug"]; !found { 400 // Use slug from filename 401 d.PageURLs.Slug = slug 402 } 403 404 return true, nil 405 } 406 } 407 408 func (f *frontmatterFieldHandlers) newDateModTimeHandler(setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler { 409 return func(d *FrontMatterDescriptor) (bool, error) { 410 if d.ModTime.IsZero() { 411 return false, nil 412 } 413 setter(d, d.ModTime) 414 return true, nil 415 } 416 } 417 418 func (f *frontmatterFieldHandlers) newDateGitAuthorDateHandler(setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler { 419 return func(d *FrontMatterDescriptor) (bool, error) { 420 if d.GitAuthorDate.IsZero() { 421 return false, nil 422 } 423 setter(d, d.GitAuthorDate) 424 return true, nil 425 } 426 }