URI: 
       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 }