diff --git a/resources/page/pagemeta/page_frontmatter.go b/resources/page/pagemeta/page_frontmatter.go index fd4f7759b..9018acb71 100644 --- a/resources/page/pagemeta/page_frontmatter.go +++ b/resources/page/pagemeta/page_frontmatter.go @@ -457,26 +457,43 @@ func (f FrontMatterHandler) IsDateKey(key string) bool { return f.allDateKeys[key] } -// A Zero date is a signal that the name can not be parsed. -// This follows the format as outlined in Jekyll, https://jekyllrb.com/docs/posts/: -// "Where YEAR is a four-digit number, MONTH and DAY are both two-digit numbers" -func dateAndSlugFromBaseFilename(location *time.Location, name string) (time.Time, string) { - withoutExt, _ := paths.FileAndExt(name) +// dateAndSlugFromBaseFilename returns a time.Time value (resolved to the +// default system location) and a slug, extracted by parsing the provided path. +// Parsing supports YYYY-MM-DD-HH-MM-SS and YYYY-MM-DD date/time formats. +// Within the YYYY-MM-DD-HH-MM-SS format, the date and time values may be +// separated by any character including a space (e.g., YYYY-MM-DD HH-MM-SS). +func dateAndSlugFromBaseFilename(location *time.Location, path string) (time.Time, string) { + base, _ := paths.FileAndExt(path) - if len(withoutExt) < 10 { - // This can not be a date. + if len(base) < 10 { + // Not long enough to start with a YYYY-MM-DD date. return time.Time{}, "" } - d, err := htime.ToTimeInDefaultLocationE(withoutExt[:10], location) - if err != nil { - return time.Time{}, "" + // Delimiters allowed between the date and the slug. + delimiters := " -_" + + if len(base) >= 19 { + // Attempt to parse a YYYY-MM-DD-HH-MM-SS date-time prefix. + ds := base[:10] + ts := strings.ReplaceAll(base[11:19], "-", ":") + + d, err := htime.ToTimeInDefaultLocationE(ds+"T"+ts, location) + if err == nil { + return d, strings.Trim(base[19:], delimiters) + } } - // Be a little lenient with the format here. - slug := strings.Trim(withoutExt[10:], " -_") + // Attempt to parse a YYYY-MM-DD date prefix. + ds := base[:10] - return d, slug + d, err := htime.ToTimeInDefaultLocationE(ds, location) + if err == nil { + return d, strings.Trim(base[10:], delimiters) + } + + // If no date is defined, return the zero time instant. + return time.Time{}, "" } type frontMatterFieldHandler func(d *FrontMatterDescriptor) (bool, error) diff --git a/resources/page/pagemeta/pagemeta_test.go b/resources/page/pagemeta/pagemeta_test.go index f205c74f8..80c19d07f 100644 --- a/resources/page/pagemeta/pagemeta_test.go +++ b/resources/page/pagemeta/pagemeta_test.go @@ -102,27 +102,46 @@ func TestDateAndSlugFromBaseFilename(t *testing.T) { date string slug string }{ - {"page.md", "0001-01-01", ""}, - {"2012-09-12-page.md", "2012-09-12", "page"}, - {"2018-02-28-page.md", "2018-02-28", "page"}, - {"2018-02-28_page.md", "2018-02-28", "page"}, - {"2018-02-28 page.md", "2018-02-28", "page"}, - {"2018-02-28page.md", "2018-02-28", "page"}, - {"2018-02-28-.md", "2018-02-28", ""}, - {"2018-02-28-.md", "2018-02-28", ""}, - {"2018-02-28.md", "2018-02-28", ""}, - {"2018-02-28-page", "2018-02-28", "page"}, - {"2012-9-12-page.md", "0001-01-01", ""}, - {"asdfasdf.md", "0001-01-01", ""}, + // date + {"2025-07-04 page.md", "2025-07-04T00:00:00+02:00", "page"}, + {"2025-07-04-page.md", "2025-07-04T00:00:00+02:00", "page"}, + {"2025-07-04_page.md", "2025-07-04T00:00:00+02:00", "page"}, + {"2025-07-04page.md", "2025-07-04T00:00:00+02:00", "page"}, + {"2025-07-04", "2025-07-04T00:00:00+02:00", ""}, + {"2025-07-04-.md", "2025-07-04T00:00:00+02:00", ""}, + {"2025-07-04.md", "2025-07-04T00:00:00+02:00", ""}, + // date and time + {"2025-07-04-22-17-13 page.md", "2025-07-04T22:17:13+02:00", "page"}, + {"2025-07-04-22-17-13-page.md", "2025-07-04T22:17:13+02:00", "page"}, + {"2025-07-04-22-17-13_page.md", "2025-07-04T22:17:13+02:00", "page"}, + {"2025-07-04-22-17-13page.md", "2025-07-04T22:17:13+02:00", "page"}, + {"2025-07-04-22-17-13", "2025-07-04T22:17:13+02:00", ""}, + {"2025-07-04-22-17-13-.md", "2025-07-04T22:17:13+02:00", ""}, + {"2025-07-04-22-17-13.md", "2025-07-04T22:17:13+02:00", ""}, + // date and time with other separators between the two + {"2025-07-04T22-17-13.md", "2025-07-04T22:17:13+02:00", ""}, + {"2025-07-04 22-17-13.md", "2025-07-04T22:17:13+02:00", ""}, + // no date or time + {"something.md", "0001-01-01T00:00:00+00:00", ""}, // 9 chars + {"some-thing-.md", "0001-01-01T00:00:00+00:00", ""}, // 10 chars + {"somethingsomething.md", "0001-01-01T00:00:00+00:00", ""}, // 18 chars + {"something-something.md", "0001-01-01T00:00:00+00:00", ""}, // 19 chars + {"something-something-else.md", "0001-01-01T00:00:00+00:00", ""}, // 27 chars + // invalid + {"2025-07-4-page.md", "0001-01-01T00:00:00+00:00", ""}, + {"2025-07-4-22-17-13-page.md", "0001-01-01T00:00:00+00:00", ""}, + {"asdfasdf.md", "0001-01-01T00:00:00+00:00", ""}, } + location, err := time.LoadLocation("Europe/Oslo") + if err != nil { + t.Error("Unable to determine location from given time zone") + } for _, test := range tests { - expecteFDate, err := time.Parse("2006-01-02", test.date) - c.Assert(err, qt.IsNil) - gotDate, gotSlug := dateAndSlugFromBaseFilename(time.UTC, test.name) + gotDate, gotSlug := dateAndSlugFromBaseFilename(location, test.name) - c.Assert(gotDate, qt.Equals, expecteFDate) + c.Assert(gotDate.Format("2006-01-02T15:04:05-07:00"), qt.Equals, test.date) c.Assert(gotSlug, qt.Equals, test.slug) }