page_frontmatter.go - hugo - [fork] hugo port for 9front
HTML git clone https://git.drkhsh.at/hugo.git
DIR Log
DIR Files
DIR Refs
DIR Submodules
DIR README
DIR LICENSE
---
page_frontmatter.go (25017B)
---
1 // Copyright 2024 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 "errors"
18 "fmt"
19 "path"
20 "strings"
21 "time"
22
23 "github.com/gohugoio/hugo/common/hreflect"
24 "github.com/gohugoio/hugo/common/htime"
25 "github.com/gohugoio/hugo/common/hugio"
26 "github.com/gohugoio/hugo/common/loggers"
27 "github.com/gohugoio/hugo/common/maps"
28 "github.com/gohugoio/hugo/common/paths"
29 "github.com/gohugoio/hugo/hugofs/files"
30 "github.com/gohugoio/hugo/markup"
31 "github.com/gohugoio/hugo/media"
32 "github.com/gohugoio/hugo/output"
33 "github.com/gohugoio/hugo/resources/kinds"
34 "github.com/gohugoio/hugo/resources/page"
35 "github.com/gohugoio/hugo/resources/resource"
36 "github.com/mitchellh/mapstructure"
37
38 "github.com/gohugoio/hugo/helpers"
39
40 "github.com/gohugoio/hugo/config"
41 "github.com/spf13/cast"
42 )
43
44 type DatesStrings struct {
45 Date string `json:"date"`
46 Lastmod string `json:"lastMod"`
47 PublishDate string `json:"publishDate"`
48 ExpiryDate string `json:"expiryDate"`
49 }
50
51 type Dates struct {
52 Date time.Time
53 Lastmod time.Time
54 PublishDate time.Time
55 ExpiryDate time.Time
56 }
57
58 func (d Dates) IsDateOrLastModAfter(in Dates) bool {
59 return d.Date.After(in.Date) || d.Lastmod.After(in.Lastmod)
60 }
61
62 func (d *Dates) UpdateDateAndLastmodAndPublishDateIfAfter(in Dates) {
63 if in.Date.After(d.Date) {
64 d.Date = in.Date
65 }
66 if in.Lastmod.After(d.Lastmod) {
67 d.Lastmod = in.Lastmod
68 }
69
70 if in.PublishDate.After(d.PublishDate) && in.PublishDate.Before(htime.Now()) {
71 d.PublishDate = in.PublishDate
72 }
73 }
74
75 func (d Dates) IsAllDatesZero() bool {
76 return d.Date.IsZero() && d.Lastmod.IsZero() && d.PublishDate.IsZero() && d.ExpiryDate.IsZero()
77 }
78
79 // Page config that needs to be set early. These cannot be modified by cascade.
80 type PageConfigEarly struct {
81 Kind string // The kind of page, e.g. "page", "section", "home" etc. This is usually derived from the content path.
82 Path string // The canonical path to the page, e.g. /sect/mypage. Note: Leading slash, no trailing slash, no extensions or language identifiers.
83 Lang string // The language code for this page. This is usually derived from the module mount or filename.
84 Cascade []map[string]any
85
86 // Content holds the content for this page.
87 Content Source
88 }
89
90 // PageConfig configures a Page, typically from front matter.
91 // Note that all the top level fields are reserved Hugo keywords.
92 // Any custom configuration needs to be set in the Params map.
93 type PageConfig struct {
94 Dates Dates `json:"-"` // Dates holds the four core dates for this page.
95 DatesStrings
96 PageConfigEarly `mapstructure:",squash"`
97 Title string // The title of the page.
98 LinkTitle string // The link title of the page.
99 Type string // The content type of the page.
100 Layout string // The layout to use for to render this page.
101 Weight int // The weight of the page, used in sorting if set to a non-zero value.
102 URL string // The URL to the rendered page, e.g. /sect/mypage.html.
103 Slug string // The slug for this page.
104 Description string // The description for this page.
105 Summary string // The summary for this page.
106 Draft bool // Whether or not the content is a draft.
107 Headless bool `json:"-"` // Whether or not the page should be rendered.
108 IsCJKLanguage bool // Whether or not the content is in a CJK language.
109 TranslationKey string // The translation key for this page.
110 Keywords []string // The keywords for this page.
111 Aliases []string // The aliases for this page.
112 Outputs []string // The output formats to render this page in. If not set, the site's configured output formats for this page kind will be used.
113
114 FrontMatterOnlyValues `mapstructure:"-" json:"-"`
115
116 Sitemap config.SitemapConfig
117 Build BuildConfig
118 Menus any // Can be a string, []string or map[string]any.
119
120 // User defined params.
121 Params maps.Params
122
123 // The raw data from the content adapter.
124 // TODO(bep) clean up the ContentAdapterData vs Params.
125 ContentAdapterData map[string]any `mapstructure:"-" json:"-"`
126
127 // Compiled values.
128 CascadeCompiled *maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig] `mapstructure:"-" json:"-"`
129 ContentMediaType media.Type `mapstructure:"-" json:"-"`
130 ConfiguredOutputFormats output.Formats `mapstructure:"-" json:"-"`
131 IsFromContentAdapter bool `mapstructure:"-" json:"-"`
132 }
133
134 func ClonePageConfigForRebuild(p *PageConfig, params map[string]any) *PageConfig {
135 pp := &PageConfig{
136 PageConfigEarly: p.PageConfigEarly,
137 IsFromContentAdapter: p.IsFromContentAdapter,
138 }
139 if pp.IsFromContentAdapter {
140 pp.ContentAdapterData = params
141 } else {
142 pp.Params = params
143 }
144
145 return pp
146 }
147
148 var DefaultPageConfig = PageConfig{
149 Build: DefaultBuildConfig,
150 }
151
152 func (p *PageConfig) Init(pagesFromData bool) error {
153 if pagesFromData {
154 p.Path = strings.TrimPrefix(p.Path, "/")
155
156 if p.Path == "" && p.Kind != kinds.KindHome {
157 return fmt.Errorf("empty path is reserved for the home page")
158 }
159 if p.Lang != "" {
160 return errors.New("lang must not be set")
161 }
162
163 if p.Content.Markup != "" {
164 return errors.New("markup must not be set, use mediaType")
165 }
166 }
167
168 if p.Cascade != nil {
169 if !kinds.IsBranch(p.Kind) {
170 return errors.New("cascade is only supported for branch nodes")
171 }
172 }
173
174 return nil
175 }
176
177 func (p *PageConfig) CompileForPagesFromDataPre(basePath string, logger loggers.Logger, mediaTypes media.Types) error {
178 // In content adapters, we always get relative paths.
179 if basePath != "" {
180 p.Path = path.Join(basePath, p.Path)
181 }
182
183 if p.Params == nil {
184 p.Params = make(maps.Params)
185 } else {
186 p.Params = maps.PrepareParamsClone(p.Params)
187 }
188
189 if p.Kind == "" {
190 p.Kind = kinds.KindPage
191 }
192
193 if p.Cascade != nil {
194 cascade, err := page.DecodeCascade(logger, false, p.Cascade)
195 if err != nil {
196 return fmt.Errorf("failed to decode cascade: %w", err)
197 }
198 p.CascadeCompiled = cascade
199 }
200
201 // Note that NormalizePathStringBasic will make sure that we don't preserve the unnormalized path.
202 // We do that when we create pages from the file system; mostly for backward compatibility,
203 // but also because people tend to use use the filename to name their resources (with spaces and all),
204 // and this isn't relevant when creating resources from an API where it's easy to add textual meta data.
205 p.Path = paths.NormalizePathStringBasic(p.Path)
206
207 return p.compilePrePost("", mediaTypes)
208 }
209
210 func (p *PageConfig) compilePrePost(ext string, mediaTypes media.Types) error {
211 if p.Content.Markup == "" && p.Content.MediaType == "" {
212 if ext == "" {
213 ext = "md"
214 }
215 p.ContentMediaType = MarkupToMediaType(ext, mediaTypes)
216 if p.ContentMediaType.IsZero() {
217 return fmt.Errorf("failed to resolve media type for suffix %q", ext)
218 }
219 }
220
221 var s string
222 if p.ContentMediaType.IsZero() {
223 if p.Content.MediaType != "" {
224 s = p.Content.MediaType
225 p.ContentMediaType, _ = mediaTypes.GetByType(s)
226 }
227
228 if p.ContentMediaType.IsZero() && p.Content.Markup != "" {
229 s = p.Content.Markup
230 p.ContentMediaType = MarkupToMediaType(s, mediaTypes)
231 }
232 }
233
234 if p.ContentMediaType.IsZero() {
235 return fmt.Errorf("failed to resolve media type for %q", s)
236 }
237
238 if p.Content.Markup == "" {
239 p.Content.Markup = p.ContentMediaType.SubType
240 }
241 return nil
242 }
243
244 // Compile sets up the page configuration after all fields have been set.
245 func (p *PageConfig) Compile(ext string, logger loggers.Logger, outputFormats output.Formats, mediaTypes media.Types) error {
246 if p.IsFromContentAdapter {
247 if err := mapstructure.WeakDecode(p.ContentAdapterData, p); err != nil {
248 err = fmt.Errorf("failed to decode page map: %w", err)
249 return err
250 }
251 // Not needed anymore.
252 p.ContentAdapterData = nil
253 }
254
255 if p.Params == nil {
256 p.Params = make(maps.Params)
257 } else {
258 maps.PrepareParams(p.Params)
259 }
260
261 if err := p.compilePrePost(ext, mediaTypes); err != nil {
262 return err
263 }
264
265 if len(p.Outputs) > 0 {
266 outFormats, err := outputFormats.GetByNames(p.Outputs...)
267 if err != nil {
268 return fmt.Errorf("failed to resolve output formats %v: %w", p.Outputs, err)
269 } else {
270 p.ConfiguredOutputFormats = outFormats
271 }
272 }
273
274 return nil
275 }
276
277 // MarkupToMediaType converts a markup string to a media type.
278 func MarkupToMediaType(s string, mediaTypes media.Types) media.Type {
279 s = strings.ToLower(s)
280 mt, _ := mediaTypes.GetBestMatch(markup.ResolveMarkup(s))
281 return mt
282 }
283
284 type ResourceConfig struct {
285 Path string
286 Name string
287 Title string
288 Params maps.Params
289 Content Source
290
291 // Compiled values.
292 PathInfo *paths.Path `mapstructure:"-" json:"-"`
293 ContentMediaType media.Type
294 }
295
296 func (rc *ResourceConfig) Validate() error {
297 if rc.Content.Markup != "" {
298 return errors.New("markup must not be set, use mediaType")
299 }
300 return nil
301 }
302
303 func (rc *ResourceConfig) Compile(basePath string, pathParser *paths.PathParser, mediaTypes media.Types) error {
304 if rc.Params != nil {
305 maps.PrepareParams(rc.Params)
306 }
307
308 // Note that NormalizePathStringBasic will make sure that we don't preserve the unnormalized path.
309 // We do that when we create resources from the file system; mostly for backward compatibility,
310 // but also because people tend to use use the filename to name their resources (with spaces and all),
311 // and this isn't relevant when creating resources from an API where it's easy to add textual meta data.
312 rc.Path = paths.NormalizePathStringBasic(path.Join(basePath, rc.Path))
313 rc.PathInfo = pathParser.Parse(files.ComponentFolderContent, rc.Path)
314 if rc.Content.MediaType != "" {
315 var found bool
316 rc.ContentMediaType, found = mediaTypes.GetByType(rc.Content.MediaType)
317 if !found {
318 return fmt.Errorf("media type %q not found", rc.Content.MediaType)
319 }
320 }
321 return nil
322 }
323
324 type Source struct {
325 // MediaType is the media type of the content.
326 MediaType string
327
328 // The markup used in Value. Only used in front matter.
329 Markup string
330
331 // The content.
332 Value any
333 }
334
335 func (s Source) IsZero() bool {
336 return !hreflect.IsTruthful(s.Value)
337 }
338
339 func (s Source) IsResourceValue() bool {
340 _, ok := s.Value.(resource.Resource)
341 return ok
342 }
343
344 func (s Source) ValueAsString() string {
345 if s.Value == nil {
346 return ""
347 }
348 ss, err := cast.ToStringE(s.Value)
349 if err != nil {
350 panic(fmt.Errorf("content source: failed to convert %T to string: %s", s.Value, err))
351 }
352 return ss
353 }
354
355 func (s Source) ValueAsOpenReadSeekCloser() hugio.OpenReadSeekCloser {
356 return hugio.NewOpenReadSeekCloser(hugio.NewReadSeekerNoOpCloserFromString(s.ValueAsString()))
357 }
358
359 // FrontMatterOnlyValues holds values that can only be set via front matter.
360 type FrontMatterOnlyValues struct {
361 ResourcesMeta []map[string]any
362 }
363
364 // FrontMatterHandler maps front matter into Page fields and .Params.
365 // Note that we currently have only extracted the date logic.
366 type FrontMatterHandler struct {
367 fmConfig FrontmatterConfig
368
369 contentAdapterDatesHandler func(d *FrontMatterDescriptor) error
370
371 dateHandler frontMatterFieldHandler
372 lastModHandler frontMatterFieldHandler
373 publishDateHandler frontMatterFieldHandler
374 expiryDateHandler frontMatterFieldHandler
375
376 // A map of all date keys configured, including any custom.
377 allDateKeys map[string]bool
378
379 logger loggers.Logger
380 }
381
382 // FrontMatterDescriptor describes how to handle front matter for a given Page.
383 // It has pointers to values in the receiving page which gets updated.
384 type FrontMatterDescriptor struct {
385 // This is the Page's base filename (BaseFilename), e.g. page.md., or
386 // if page is a leaf bundle, the bundle folder name (ContentBaseName).
387 BaseFilename string
388
389 // The Page's path if the page is backed by a file, else its title.
390 PathOrTitle string
391
392 // The content file's mod time.
393 ModTime time.Time
394
395 // May be set from the author date in Git.
396 GitAuthorDate time.Time
397
398 // The below will be modified.
399 PageConfig *PageConfig
400
401 // The Location to use to parse dates without time zone info.
402 Location *time.Location
403 }
404
405 var dateFieldAliases = map[string][]string{
406 fmDate: {},
407 fmLastmod: {"modified"},
408 fmPubDate: {"pubdate", "published"},
409 fmExpiryDate: {"unpublishdate"},
410 }
411
412 // HandleDates updates all the dates given the current configuration and the
413 // supplied front matter params. Note that this requires all lower-case keys
414 // in the params map.
415 func (f FrontMatterHandler) HandleDates(d *FrontMatterDescriptor) error {
416 if d.PageConfig == nil {
417 panic("missing pageConfig")
418 }
419
420 if d.PageConfig.IsFromContentAdapter {
421 if f.contentAdapterDatesHandler == nil {
422 panic("missing content adapter date handler")
423 }
424 return f.contentAdapterDatesHandler(d)
425 }
426
427 if f.dateHandler == nil {
428 panic("missing date handler")
429 }
430
431 if _, err := f.dateHandler(d); err != nil {
432 return err
433 }
434
435 if _, err := f.lastModHandler(d); err != nil {
436 return err
437 }
438
439 if _, err := f.publishDateHandler(d); err != nil {
440 return err
441 }
442
443 if _, err := f.expiryDateHandler(d); err != nil {
444 return err
445 }
446
447 return nil
448 }
449
450 // IsDateKey returns whether the given front matter key is considered a date by the current
451 // configuration.
452 func (f FrontMatterHandler) IsDateKey(key string) bool {
453 return f.allDateKeys[key]
454 }
455
456 // dateAndSlugFromBaseFilename returns a time.Time value (resolved to the
457 // default system location) and a slug, extracted by parsing the provided path.
458 // Parsing supports YYYY-MM-DD-HH-MM-SS and YYYY-MM-DD date/time formats.
459 // Within the YYYY-MM-DD-HH-MM-SS format, the date and time values may be
460 // separated by any character including a space (e.g., YYYY-MM-DD HH-MM-SS).
461 func dateAndSlugFromBaseFilename(location *time.Location, path string) (time.Time, string) {
462 base, _ := paths.FileAndExt(path)
463
464 if len(base) < 10 {
465 // Not long enough to start with a YYYY-MM-DD date.
466 return time.Time{}, ""
467 }
468
469 // Delimiters allowed between the date and the slug.
470 delimiters := " -_"
471
472 if len(base) >= 19 {
473 // Attempt to parse a YYYY-MM-DD-HH-MM-SS date-time prefix.
474 ds := base[:10]
475 ts := strings.ReplaceAll(base[11:19], "-", ":")
476
477 d, err := htime.ToTimeInDefaultLocationE(ds+"T"+ts, location)
478 if err == nil {
479 return d, strings.Trim(base[19:], delimiters)
480 }
481 }
482
483 // Attempt to parse a YYYY-MM-DD date prefix.
484 ds := base[:10]
485
486 d, err := htime.ToTimeInDefaultLocationE(ds, location)
487 if err == nil {
488 return d, strings.Trim(base[10:], delimiters)
489 }
490
491 // If no date is defined, return the zero time instant.
492 return time.Time{}, ""
493 }
494
495 type frontMatterFieldHandler func(d *FrontMatterDescriptor) (bool, error)
496
497 func (f FrontMatterHandler) newChainedFrontMatterFieldHandler(handlers ...frontMatterFieldHandler) frontMatterFieldHandler {
498 return func(d *FrontMatterDescriptor) (bool, error) {
499 for _, h := range handlers {
500 // First successful handler wins.
501 success, err := h(d)
502 if err != nil {
503 f.logger.Errorln(err)
504 } else if success {
505 return true, nil
506 }
507 }
508 return false, nil
509 }
510 }
511
512 type FrontmatterConfig struct {
513 // Controls how the Date is set from front matter.
514 Date []string
515 // Controls how the Lastmod is set from front matter.
516 Lastmod []string
517 // Controls how the PublishDate is set from front matter.
518 PublishDate []string
519 // Controls how the ExpiryDate is set from front matter.
520 ExpiryDate []string
521 }
522
523 const (
524 // These are all the date handler identifiers
525 // All identifiers not starting with a ":" maps to a front matter parameter.
526 fmDate = "date"
527 fmPubDate = "publishdate"
528 fmLastmod = "lastmod"
529 fmExpiryDate = "expirydate"
530
531 // Gets date from filename, e.g 218-02-22-mypage.md
532 fmFilename = ":filename"
533
534 // Gets date from file OS mod time.
535 fmModTime = ":filemodtime"
536
537 // Gets date from Git
538 fmGitAuthorDate = ":git"
539 )
540
541 // This is the config you get when doing nothing.
542 func newDefaultFrontmatterConfig() FrontmatterConfig {
543 return FrontmatterConfig{
544 Date: []string{fmDate, fmPubDate, fmLastmod},
545 Lastmod: []string{fmGitAuthorDate, fmLastmod, fmDate, fmPubDate},
546 PublishDate: []string{fmPubDate, fmDate},
547 ExpiryDate: []string{fmExpiryDate},
548 }
549 }
550
551 func DecodeFrontMatterConfig(cfg config.Provider) (FrontmatterConfig, error) {
552 c := newDefaultFrontmatterConfig()
553 defaultConfig := c
554
555 if cfg.IsSet("frontmatter") {
556 fm := cfg.GetStringMap("frontmatter")
557 for k, v := range fm {
558 loki := strings.ToLower(k)
559 switch loki {
560 case fmDate:
561 c.Date = toLowerSlice(v)
562 case fmPubDate:
563 c.PublishDate = toLowerSlice(v)
564 case fmLastmod:
565 c.Lastmod = toLowerSlice(v)
566 case fmExpiryDate:
567 c.ExpiryDate = toLowerSlice(v)
568 }
569 }
570 }
571
572 expander := func(c, d []string) []string {
573 out := expandDefaultValues(c, d)
574 out = addDateFieldAliases(out)
575 return out
576 }
577
578 c.Date = expander(c.Date, defaultConfig.Date)
579 c.PublishDate = expander(c.PublishDate, defaultConfig.PublishDate)
580 c.Lastmod = expander(c.Lastmod, defaultConfig.Lastmod)
581 c.ExpiryDate = expander(c.ExpiryDate, defaultConfig.ExpiryDate)
582
583 return c, nil
584 }
585
586 func addDateFieldAliases(values []string) []string {
587 var complete []string
588
589 for _, v := range values {
590 complete = append(complete, v)
591 if aliases, found := dateFieldAliases[v]; found {
592 complete = append(complete, aliases...)
593 }
594 }
595 return helpers.UniqueStringsReuse(complete)
596 }
597
598 func expandDefaultValues(values []string, defaults []string) []string {
599 var out []string
600 for _, v := range values {
601 if v == ":default" {
602 out = append(out, defaults...)
603 } else {
604 out = append(out, v)
605 }
606 }
607 return out
608 }
609
610 func toLowerSlice(in any) []string {
611 out := cast.ToStringSlice(in)
612 for i := range out {
613 out[i] = strings.ToLower(out[i])
614 }
615
616 return out
617 }
618
619 // NewFrontmatterHandler creates a new FrontMatterHandler with the given logger and configuration.
620 // If no logger is provided, one will be created.
621 func NewFrontmatterHandler(logger loggers.Logger, frontMatterConfig FrontmatterConfig) (FrontMatterHandler, error) {
622 if logger == nil {
623 logger = loggers.NewDefault()
624 }
625
626 allDateKeys := make(map[string]bool)
627 addKeys := func(vals []string) {
628 for _, k := range vals {
629 if !strings.HasPrefix(k, ":") {
630 allDateKeys[k] = true
631 }
632 }
633 }
634
635 addKeys(frontMatterConfig.Date)
636 addKeys(frontMatterConfig.ExpiryDate)
637 addKeys(frontMatterConfig.Lastmod)
638 addKeys(frontMatterConfig.PublishDate)
639
640 f := FrontMatterHandler{logger: logger, fmConfig: frontMatterConfig, allDateKeys: allDateKeys}
641
642 if err := f.createHandlers(); err != nil {
643 return f, err
644 }
645
646 return f, nil
647 }
648
649 func (f *FrontMatterHandler) createHandlers() error {
650 var err error
651
652 if f.contentAdapterDatesHandler, err = f.createContentAdapterDatesHandler(f.fmConfig); err != nil {
653 return err
654 }
655
656 if f.dateHandler, err = f.createDateHandler(f.fmConfig.Date,
657 func(d *FrontMatterDescriptor, t time.Time) {
658 d.PageConfig.Dates.Date = t
659 setParamIfNotSet(fmDate, t, d)
660 }); err != nil {
661 return err
662 }
663
664 if f.lastModHandler, err = f.createDateHandler(f.fmConfig.Lastmod,
665 func(d *FrontMatterDescriptor, t time.Time) {
666 setParamIfNotSet(fmLastmod, t, d)
667 d.PageConfig.Dates.Lastmod = t
668 }); err != nil {
669 return err
670 }
671
672 if f.publishDateHandler, err = f.createDateHandler(f.fmConfig.PublishDate,
673 func(d *FrontMatterDescriptor, t time.Time) {
674 setParamIfNotSet(fmPubDate, t, d)
675 d.PageConfig.Dates.PublishDate = t
676 }); err != nil {
677 return err
678 }
679
680 if f.expiryDateHandler, err = f.createDateHandler(f.fmConfig.ExpiryDate,
681 func(d *FrontMatterDescriptor, t time.Time) {
682 setParamIfNotSet(fmExpiryDate, t, d)
683 d.PageConfig.Dates.ExpiryDate = t
684 }); err != nil {
685 return err
686 }
687
688 return nil
689 }
690
691 func setParamIfNotSet(key string, value any, d *FrontMatterDescriptor) {
692 if _, found := d.PageConfig.Params[key]; found {
693 return
694 }
695 d.PageConfig.Params[key] = value
696 }
697
698 func (f FrontMatterHandler) createContentAdapterDatesHandler(fmcfg FrontmatterConfig) (func(d *FrontMatterDescriptor) error, error) {
699 setTime := func(key string, value time.Time, in *PageConfig) {
700 switch key {
701 case fmDate:
702 in.Dates.Date = value
703 case fmLastmod:
704 in.Dates.Lastmod = value
705 case fmPubDate:
706 in.Dates.PublishDate = value
707 case fmExpiryDate:
708 in.Dates.ExpiryDate = value
709 }
710 }
711
712 getTime := func(key string, in *PageConfig) time.Time {
713 switch key {
714 case fmDate:
715 return in.Dates.Date
716 case fmLastmod:
717 return in.Dates.Lastmod
718 case fmPubDate:
719 return in.Dates.PublishDate
720 case fmExpiryDate:
721 return in.Dates.ExpiryDate
722 }
723 return time.Time{}
724 }
725
726 createSetter := func(identifiers []string, date string) func(pcfg *PageConfig) {
727 var getTimes []func(in *PageConfig) time.Time
728 for _, identifier := range identifiers {
729 if strings.HasPrefix(identifier, ":") {
730 continue
731 }
732 switch identifier {
733 case fmDate:
734 getTimes = append(getTimes, func(in *PageConfig) time.Time {
735 return getTime(fmDate, in)
736 })
737 case fmLastmod:
738 getTimes = append(getTimes, func(in *PageConfig) time.Time {
739 return getTime(fmLastmod, in)
740 })
741 case fmPubDate:
742 getTimes = append(getTimes, func(in *PageConfig) time.Time {
743 return getTime(fmPubDate, in)
744 })
745 case fmExpiryDate:
746 getTimes = append(getTimes, func(in *PageConfig) time.Time {
747 return getTime(fmExpiryDate, in)
748 })
749 }
750 }
751
752 return func(pcfg *PageConfig) {
753 for _, get := range getTimes {
754 if t := get(pcfg); !t.IsZero() {
755 setTime(date, t, pcfg)
756 return
757 }
758 }
759 }
760 }
761
762 setDate := createSetter(fmcfg.Date, fmDate)
763 setLastmod := createSetter(fmcfg.Lastmod, fmLastmod)
764 setPublishDate := createSetter(fmcfg.PublishDate, fmPubDate)
765 setExpiryDate := createSetter(fmcfg.ExpiryDate, fmExpiryDate)
766
767 fn := func(d *FrontMatterDescriptor) error {
768 pcfg := d.PageConfig
769 setDate(pcfg)
770 setLastmod(pcfg)
771 setPublishDate(pcfg)
772 setExpiryDate(pcfg)
773 return nil
774 }
775 return fn, nil
776 }
777
778 func (f FrontMatterHandler) createDateHandler(identifiers []string, setter func(d *FrontMatterDescriptor, t time.Time)) (frontMatterFieldHandler, error) {
779 var h *frontmatterFieldHandlers
780 var handlers []frontMatterFieldHandler
781
782 for _, identifier := range identifiers {
783 switch identifier {
784 case fmFilename:
785 handlers = append(handlers, h.newDateFilenameHandler(setter))
786 case fmModTime:
787 handlers = append(handlers, h.newDateModTimeHandler(setter))
788 case fmGitAuthorDate:
789 handlers = append(handlers, h.newDateGitAuthorDateHandler(setter))
790 default:
791 handlers = append(handlers, h.newDateFieldHandler(identifier, setter))
792 }
793 }
794
795 return f.newChainedFrontMatterFieldHandler(handlers...), nil
796 }
797
798 type frontmatterFieldHandlers int
799
800 func (f *frontmatterFieldHandlers) newDateFieldHandler(key string, setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
801 return func(d *FrontMatterDescriptor) (bool, error) {
802 v, found := d.PageConfig.Params[key]
803
804 if !found || v == "" || v == nil {
805 return false, nil
806 }
807
808 var date time.Time
809 if vt, ok := v.(time.Time); ok && vt.Location() == d.Location {
810 date = vt
811 } else {
812 var err error
813 date, err = htime.ToTimeInDefaultLocationE(v, d.Location)
814 if err != nil {
815 return false, fmt.Errorf("the %q front matter field is not a parsable date: see %s", key, d.PathOrTitle)
816 }
817 d.PageConfig.Params[key] = date
818 }
819
820 // We map several date keys to one, so, for example,
821 // "expirydate", "unpublishdate" will all set .ExpiryDate (first found).
822 setter(d, date)
823
824 return true, nil
825 }
826 }
827
828 func (f *frontmatterFieldHandlers) newDateFilenameHandler(setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
829 return func(d *FrontMatterDescriptor) (bool, error) {
830 date, slug := dateAndSlugFromBaseFilename(d.Location, d.BaseFilename)
831 if date.IsZero() {
832 return false, nil
833 }
834
835 setter(d, date)
836
837 if _, found := d.PageConfig.Params["slug"]; !found {
838 // Use slug from filename
839 d.PageConfig.Slug = slug
840 }
841
842 return true, nil
843 }
844 }
845
846 func (f *frontmatterFieldHandlers) newDateModTimeHandler(setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
847 return func(d *FrontMatterDescriptor) (bool, error) {
848 if d.ModTime.IsZero() {
849 return false, nil
850 }
851 setter(d, d.ModTime)
852 return true, nil
853 }
854 }
855
856 func (f *frontmatterFieldHandlers) newDateGitAuthorDateHandler(setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
857 return func(d *FrontMatterDescriptor) (bool, error) {
858 if d.GitAuthorDate.IsZero() {
859 return false, nil
860 }
861 setter(d, d.GitAuthorDate)
862 return true, nil
863 }
864 }